ucn 5.4.1 → 5.4.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/cli/index.js +2 -2
- package/core/output/analysis-ext.js +6 -6
- package/core/output/find.js +4 -0
- package/core/output/lines.js +8 -2
- package/core/output-budget.js +24 -13
- package/mcp/server.js +3 -3
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -933,9 +933,9 @@ Common flags:
|
|
|
933
933
|
--all --compact --no-compact --json --include-tests --class-name=X --line=N
|
|
934
934
|
--range=N-M (source with --file=PATH)
|
|
935
935
|
--base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
|
|
936
|
-
--max-chars=N (text
|
|
936
|
+
--max-chars=N (text characters, not UTF-8 bytes; default 10K targeted / 3K broad, ceiling 100K)
|
|
937
937
|
--lines find/usages/search/show/impact: grep -n shape, one path:line:text
|
|
938
|
-
record per output line; usages
|
|
938
|
+
record per output line; usages combines occurrences on each source line
|
|
939
939
|
(tags after a tab: # unverified: <reason>, # import,
|
|
940
940
|
# callee); accounting and notes go to stderr as "# " lines; exit 1
|
|
941
941
|
when nothing matched; exit 2 on errors. No default result cap.
|
|
@@ -204,18 +204,18 @@ function formatDiffImpact(result, options = {}) {
|
|
|
204
204
|
|
|
205
205
|
const s = result.summary || {};
|
|
206
206
|
const parts = [];
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
parts.push(`${
|
|
207
|
+
for (const [label, suffix] of [['Functions', 'Functions'], ['Declarations', 'Symbols']]) {
|
|
208
|
+
const counts = ['modified', 'new', 'deleted']
|
|
209
|
+
.filter(kind => s[kind + suffix] > 0)
|
|
210
|
+
.map(kind => `${s[kind + suffix]} ${kind}`);
|
|
211
|
+
if (counts.length) parts.push(`${label}: ${counts.join(', ')}`);
|
|
212
212
|
}
|
|
213
213
|
parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
|
|
214
214
|
if (s.unverifiedCallSites > 0) parts.push(`${s.unverifiedCallSites} unverified`);
|
|
215
215
|
if (s.totalDependencySites || s.unverifiedDependencySites) {
|
|
216
216
|
parts.push(`${s.totalDependencySites || 0} confirmed + ${s.unverifiedDependencySites || 0} unverified non-call dependency sites across ${s.dependencyFiles || 0} files`);
|
|
217
217
|
}
|
|
218
|
-
lines.push(parts.join('
|
|
218
|
+
lines.push(parts.join('; '));
|
|
219
219
|
// fix #283: changed paths outside supported source are invisible to the
|
|
220
220
|
// symbol analysis — disclose instead of silently narrowing the diff.
|
|
221
221
|
if (result.nonSourcePaths > 0) {
|
package/core/output/find.js
CHANGED
|
@@ -158,10 +158,13 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
158
158
|
const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
|
|
159
159
|
const handle = formatSymbolHandle(s);
|
|
160
160
|
const loc = handle || (s.relativePath + ':' + s.startLine);
|
|
161
|
+
const nameLocation = s.nameLine && s.nameLine !== s.startLine
|
|
162
|
+
? `name token at ${s.relativePath || s.file}:${s.nameLine}` : '';
|
|
161
163
|
|
|
162
164
|
if (compact) {
|
|
163
165
|
// One line per result: "<handle> <sig> <usages?> <doc snippet?>"
|
|
164
166
|
const parts = [`${loc} ${sig}${confStr}`];
|
|
167
|
+
if (nameLocation) parts.push(`[${nameLocation}]`);
|
|
165
168
|
if (s.usageCounts !== undefined && s.usageCounts.total > 0) {
|
|
166
169
|
const scope = sameNameDefinitionCounts.get(s.name) > 1 ? ' name-wide' : '';
|
|
167
170
|
const label = s.usageCounts.complete === false
|
|
@@ -186,6 +189,7 @@ function formatFindDetailed(symbols, query, options = {}) {
|
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
lines.push(`${loc} ${sig}${confStr}`);
|
|
192
|
+
if (nameLocation) lines.push(` ${nameLocation} (handle starts at declaration line ${s.startLine})`);
|
|
189
193
|
if (s.docstring) {
|
|
190
194
|
const snip = firstSentenceShort(s.docstring);
|
|
191
195
|
if (snip) lines.push(` "${snip}"`);
|
package/core/output/lines.js
CHANGED
|
@@ -64,14 +64,20 @@ function accountComments(account) {
|
|
|
64
64
|
|
|
65
65
|
function findRecords(result) {
|
|
66
66
|
const out = [];
|
|
67
|
+
const notes = [];
|
|
67
68
|
if (Array.isArray(result)) {
|
|
68
|
-
for (const symbol of result)
|
|
69
|
+
for (const symbol of result) {
|
|
70
|
+
out.push(record(pathOf(symbol), symbol.startLine, signatureOf(symbol), symbol.type));
|
|
71
|
+
if (symbol.nameLine && symbol.nameLine !== symbol.startLine) {
|
|
72
|
+
notes.push(`# ${record(pathOf(symbol), symbol.startLine, symbol.name)} starts at the declaration; name token at line ${symbol.nameLine} (usages reports the token line).`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
69
75
|
} else if (result && Array.isArray(result.types)) {
|
|
70
76
|
for (const type of result.types) {
|
|
71
77
|
out.push(record(pathOf(type), type.startLine ?? type.line, type.name, type.type || type.kind));
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
|
-
return { records: out, notes
|
|
80
|
+
return { records: out, notes };
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
function usagesRecords(result) {
|
package/core/output-budget.js
CHANGED
|
@@ -15,7 +15,7 @@ const BROAD_COMMANDS = new Set([
|
|
|
15
15
|
...[...BROAD_CANONICAL].map(toMcpName),
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
-
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):|\d+ test-file usage\(s\) hidden\b|
|
|
18
|
+
const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|Note):|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
|
|
19
19
|
const MAX_PRESERVED_CONTRACT_LINES = 24;
|
|
20
20
|
const MAX_PRESERVED_CONTRACT_CHARS = 8000;
|
|
21
21
|
|
|
@@ -134,23 +134,23 @@ function applyOutputBudget(text, {
|
|
|
134
134
|
params = {},
|
|
135
135
|
trailingChars = 0,
|
|
136
136
|
} = {}) {
|
|
137
|
+
const defaultLimit = BROAD_COMMANDS.has(command)
|
|
138
|
+
? BROAD_OUTPUT_CHARS
|
|
139
|
+
: DEFAULT_OUTPUT_CHARS;
|
|
140
|
+
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
141
|
+
const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
142
|
+
const limit = Math.max(0, hardLimit - trailingChars);
|
|
137
143
|
if (!text) {
|
|
138
144
|
return {
|
|
139
|
-
text: '(no output)',
|
|
140
|
-
truncated:
|
|
145
|
+
text: '(no output)'.slice(0, limit),
|
|
146
|
+
truncated: '(no output)'.length > limit,
|
|
141
147
|
fullChars: 0,
|
|
142
|
-
requestedLimit:
|
|
148
|
+
requestedLimit: hardLimit,
|
|
143
149
|
contractMetadata: [],
|
|
144
150
|
contractMetadataComplete: true,
|
|
145
151
|
};
|
|
146
152
|
}
|
|
147
153
|
|
|
148
|
-
const defaultLimit = BROAD_COMMANDS.has(command)
|
|
149
|
-
? BROAD_OUTPUT_CHARS
|
|
150
|
-
: DEFAULT_OUTPUT_CHARS;
|
|
151
|
-
const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
|
|
152
|
-
const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
|
|
153
|
-
const limit = Math.max(0, hardLimit - trailingChars);
|
|
154
154
|
if (text.length <= limit) {
|
|
155
155
|
return {
|
|
156
156
|
text,
|
|
@@ -196,10 +196,21 @@ function applyOutputBudget(text, {
|
|
|
196
196
|
// can consume most of the transport. Trust/account lines take precedence
|
|
197
197
|
// over body detail and are appended directly after a compact notice.
|
|
198
198
|
if (compactBudget) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
maxChars: metadataCapacity,
|
|
199
|
+
let candidate = preservedContractMetadata(text, '', {
|
|
200
|
+
maxChars: Math.max(0, limit - notice.length - 1),
|
|
202
201
|
});
|
|
202
|
+
if (candidate.omitted > 0) {
|
|
203
|
+
// Spend less on generic guidance when that lets a complete scope
|
|
204
|
+
// or parameter warning survive even a tiny transport ceiling.
|
|
205
|
+
const shorterNotice = `... OUTPUT TRUNCATED. Raise ${raiseHint}.`;
|
|
206
|
+
const shorterCandidate = preservedContractMetadata(text, '', {
|
|
207
|
+
maxChars: Math.max(0, limit - shorterNotice.length - 1),
|
|
208
|
+
});
|
|
209
|
+
if (shorterNotice.length < notice.length && shorterCandidate.lines.length > candidate.lines.length) {
|
|
210
|
+
notice = shorterNotice;
|
|
211
|
+
candidate = shorterCandidate;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
203
214
|
const candidateText = candidate.lines.join('\n');
|
|
204
215
|
const separatorChars = candidateText ? 2 : 1;
|
|
205
216
|
const bodyBudget = Math.max(0,
|
package/mcp/server.js
CHANGED
|
@@ -119,7 +119,7 @@ const server = new StdioMcpServer({
|
|
|
119
119
|
|
|
120
120
|
function toolResult(text, command, maxChars, suffixNote, params = {}) {
|
|
121
121
|
const suffix = suffixNote || '';
|
|
122
|
-
const budget = applyOutputBudget(text, {
|
|
122
|
+
const budget = applyOutputBudget(text + suffix, {
|
|
123
123
|
command,
|
|
124
124
|
maxChars,
|
|
125
125
|
surface: 'mcp',
|
|
@@ -130,7 +130,7 @@ function toolResult(text, command, maxChars, suffixNote, params = {}) {
|
|
|
130
130
|
// narrowing hints; preserved contract lines follow). A structuredContent
|
|
131
131
|
// side-channel is rendered INSTEAD of content by MCP clients that prefer
|
|
132
132
|
// structured results, which discards the entire answer (fix #284).
|
|
133
|
-
return { content: [{ type: 'text', text: budget.text
|
|
133
|
+
return { content: [{ type: 'text', text: budget.text }] };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
function toolError(message) {
|
|
@@ -307,7 +307,7 @@ const INPUT_SHAPE = {
|
|
|
307
307
|
line: integerParam('Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.', { exclusiveMinimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
|
308
308
|
limit: integerParam('Max results to return (default: 500; structural search: 50; usages and lines: uncapped). Caps find, usages, search, deadcode, api, and repo files. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
|
|
309
309
|
max_files: integerParam('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000000 }),
|
|
310
|
-
max_chars: integerParam('Max output
|
|
310
|
+
max_chars: integerParam('Max output characters, not UTF-8 bytes, including notes and preserved metadata. Broad sweep commands (repo, entrypoints, endpoints, deadcode, deps, check, audit_async) default to 3K; all other commands default to 10K. Maximum: 100K. all=true lifts formatter caps but keeps the 100K transport ceiling.', { exclusiveMinimum: 0, maximum: 100000 }),
|
|
311
311
|
type: stringParam('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
|
|
312
312
|
param: stringParam('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
|
|
313
313
|
receiver: stringParam('Filter calls by receiver (structural search, type=call). E.g. "db", "http".'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.2",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|