pi-ui-extend 1.0.39 → 1.0.41
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/README.md +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { REPO_DISCOVERY_TOOLS } from "../tool-descriptions";
|
|
3
3
|
import { directoryExists, findIndexedProjectRoot, findProjectRoot } from "../lib/project.js";
|
|
4
|
+
import {
|
|
5
|
+
applyNativeCompactPolicy,
|
|
6
|
+
describeNativeCompactArgs,
|
|
7
|
+
loadRepoDiscoveryProfile,
|
|
8
|
+
truncateNativeCompactOutput,
|
|
9
|
+
type NativePolicyOutcome,
|
|
10
|
+
type RepoDiscoveryOutputMode,
|
|
11
|
+
type RepoDiscoveryProfile,
|
|
12
|
+
} from "./native-compact.js";
|
|
4
13
|
|
|
5
14
|
const IDX_COMMANDS = ["architecture", "structure", "ast", "search", "explain", "deps"] as const;
|
|
6
15
|
const TARGET_COMMANDS = new Set<string>(["ast", "search", "explain", "deps"]);
|
|
@@ -20,6 +29,7 @@ type RepoDiscoveryParams = {
|
|
|
20
29
|
args?: string[];
|
|
21
30
|
maxLines?: number;
|
|
22
31
|
maxBytes?: number;
|
|
32
|
+
outputMode?: RepoDiscoveryOutputMode;
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
type RepoDiscoveryWrapperParams = Omit<RepoDiscoveryParams, "command">;
|
|
@@ -82,6 +92,10 @@ function numberSchema(description: string, defaultValue: number) {
|
|
|
82
92
|
return { type: "number", description, default: defaultValue };
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
function boundedIntegerSchema(description: string, defaultValue: number, maximum: number) {
|
|
96
|
+
return { type: "integer", minimum: 1, maximum, description, default: defaultValue };
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
function validateCommand(command: string): command is IdxCommand {
|
|
86
100
|
return IDX_COMMANDS.includes(command as IdxCommand);
|
|
87
101
|
}
|
|
@@ -263,10 +277,26 @@ async function executeRepoDiscovery(
|
|
|
263
277
|
signal: AbortSignal | undefined,
|
|
264
278
|
ctx: ToolContext,
|
|
265
279
|
toolName: string,
|
|
280
|
+
profile: RepoDiscoveryProfile,
|
|
266
281
|
) {
|
|
267
282
|
if (signal?.aborted) return textResult(`${toolName} cancelled`);
|
|
268
283
|
|
|
269
|
-
|
|
284
|
+
let policyOutcome: NativePolicyOutcome | undefined;
|
|
285
|
+
let effectiveParams = params;
|
|
286
|
+
if (profile === "native-compact") {
|
|
287
|
+
const policy = applyNativeCompactPolicy({
|
|
288
|
+
command: params.command,
|
|
289
|
+
args: params.args,
|
|
290
|
+
maxLines: params.maxLines,
|
|
291
|
+
maxBytes: params.maxBytes,
|
|
292
|
+
outputMode: params.outputMode,
|
|
293
|
+
});
|
|
294
|
+
policyOutcome = policy.outcome;
|
|
295
|
+
if (policy.ok === false) return textResult(`Native Compact refused ${toolName}: ${policy.message}`, true, { nativePolicy: policy.outcome });
|
|
296
|
+
effectiveParams = { ...params, args: policy.args, maxLines: policy.maxLines, maxBytes: policy.maxBytes };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const idxArgs = buildIdxArgs(effectiveParams, toolName);
|
|
270
300
|
if (typeof idxArgs === "string") return textResult(idxArgs, true);
|
|
271
301
|
|
|
272
302
|
const indexedProject = ensureIndexedProject(ctx.cwd, toolName);
|
|
@@ -276,9 +306,11 @@ async function executeRepoDiscovery(
|
|
|
276
306
|
const exitCode = result.code ?? 0;
|
|
277
307
|
const combined = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? "\n" : "");
|
|
278
308
|
const output = combined.trim() ? combined : "No output";
|
|
279
|
-
const maxLines = positiveInteger(
|
|
280
|
-
const maxBytes = positiveInteger(
|
|
281
|
-
const truncated =
|
|
309
|
+
const maxLines = positiveInteger(effectiveParams.maxLines, DEFAULT_MAX_LINES);
|
|
310
|
+
const maxBytes = positiveInteger(effectiveParams.maxBytes, DEFAULT_MAX_BYTES);
|
|
311
|
+
const truncated = profile === "native-compact"
|
|
312
|
+
? truncateNativeCompactOutput(output, maxLines, maxBytes, effectiveParams.outputMode ?? "compact")
|
|
313
|
+
: truncateOutput(output, maxLines, maxBytes);
|
|
282
314
|
|
|
283
315
|
return textResult(truncated.text, exitCode !== 0, {
|
|
284
316
|
command: ["idx", ...idxArgs],
|
|
@@ -286,12 +318,18 @@ async function executeRepoDiscovery(
|
|
|
286
318
|
initializedProject: indexedProject.initialized,
|
|
287
319
|
exitCode,
|
|
288
320
|
truncation: truncated.truncation,
|
|
321
|
+
...(policyOutcome ? { nativePolicy: policyOutcome } : {}),
|
|
289
322
|
});
|
|
290
323
|
}
|
|
291
324
|
|
|
292
|
-
const
|
|
293
|
-
maxLines: numberSchema("
|
|
294
|
-
maxBytes: numberSchema("
|
|
325
|
+
const BASELINE_REPO_TOOL_PROPERTIES = {
|
|
326
|
+
maxLines: numberSchema("Returned line cap, keeps top lines (default 2000). Prefer native limits/cursors before raising.", DEFAULT_MAX_LINES),
|
|
327
|
+
maxBytes: numberSchema("Returned byte cap, keeps top bytes (default 50000). Narrow the query before raising.", DEFAULT_MAX_BYTES),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const NATIVE_COMPACT_REPO_TOOL_PROPERTIES = {
|
|
331
|
+
maxLines: boundedIntegerSchema("Final delivered line cap. compact default/max 400; outputMode=full permits up to 2000.", 400, 2_000),
|
|
332
|
+
maxBytes: boundedIntegerSchema("Final delivered byte cap. compact default/max 12000; outputMode=full permits up to 50000.", 12_000, 50_000),
|
|
295
333
|
};
|
|
296
334
|
|
|
297
335
|
const IDX_ARG_DESCRIPTIONS: Record<IdxCommand, string> = {
|
|
@@ -300,22 +338,41 @@ const IDX_ARG_DESCRIPTIONS: Record<IdxCommand, string> = {
|
|
|
300
338
|
"idx structure flags: [--path-prefix <area>] [--kind <kind>] [--max-depth <n>] [--max-files <n>] [--cursor <n>] [--include-internal] [--no-tests] [--include-tests-summary].",
|
|
301
339
|
ast: "idx ast flags: [--max-depth <n>] [--max-nodes <n>] [--cursor <n>] [--no-include-text].",
|
|
302
340
|
search:
|
|
303
|
-
"idx search
|
|
341
|
+
"idx search: default 3 results without code; --include-content only for narrow follow-up. Flags: [--max-files <n>] [--path-prefix <area>] [--chunk-types <types|api|impl|tests|imports>] [--mode hybrid|semantic|lexical|symbol] [--min-score <score>] [--include-content] [--include-imports] [--dedupe-file] [--dedupe-symbol] [--cluster] [--exclude-tests] [--include-tests].",
|
|
304
342
|
explain:
|
|
305
343
|
"idx explain flags: [--path-prefix <area>] [--include-body] [--body-lines <n>] [--signature-only].",
|
|
306
344
|
deps: "idx deps flags: [--mode modules|module-imports|calls|call-graph] [--direction callers|callees|both] [--depth <n>] [--show-edges] [--tests].",
|
|
307
345
|
};
|
|
308
346
|
|
|
309
|
-
function argsSchema(command: IdxCommand) {
|
|
347
|
+
function argsSchema(command: IdxCommand, profile: RepoDiscoveryProfile) {
|
|
348
|
+
const baseDescription = IDX_ARG_DESCRIPTIONS[command];
|
|
310
349
|
return {
|
|
311
350
|
type: "array",
|
|
312
|
-
items:
|
|
313
|
-
|
|
351
|
+
items: profile === "native-compact"
|
|
352
|
+
? { type: "string", minLength: 1, description: "idx argv token; --flag=value is normalized and revalidated by Native Compact" }
|
|
353
|
+
: stringSchema("idx argv token; pass flags and values as separate items"),
|
|
354
|
+
description: profile === "native-compact"
|
|
355
|
+
? `${baseDescription} ${describeNativeCompactArgs(command)}`
|
|
356
|
+
: baseDescription,
|
|
314
357
|
};
|
|
315
358
|
}
|
|
316
359
|
|
|
317
|
-
function repoToolParameters(command: IdxCommand, targetDescription
|
|
318
|
-
const
|
|
360
|
+
function repoToolParameters(command: IdxCommand, targetDescription: string | undefined, profile: RepoDiscoveryProfile) {
|
|
361
|
+
const outputProperties = profile === "native-compact"
|
|
362
|
+
? NATIVE_COMPACT_REPO_TOOL_PROPERTIES
|
|
363
|
+
: BASELINE_REPO_TOOL_PROPERTIES;
|
|
364
|
+
const properties = {
|
|
365
|
+
args: argsSchema(command, profile),
|
|
366
|
+
...outputProperties,
|
|
367
|
+
...(profile === "native-compact" ? {
|
|
368
|
+
outputMode: {
|
|
369
|
+
type: "string",
|
|
370
|
+
enum: ["compact", "full"],
|
|
371
|
+
default: "compact",
|
|
372
|
+
description: "Native Compact delivery: compact by default. Use full only on this same tool call when broader output is intentional or its compact result was actually truncated; do not switch an unrelated repo tool to full after a policy refusal.",
|
|
373
|
+
},
|
|
374
|
+
} : {}),
|
|
375
|
+
};
|
|
319
376
|
|
|
320
377
|
return {
|
|
321
378
|
type: "object",
|
|
@@ -336,6 +393,7 @@ function registerRepoCommandTool(
|
|
|
336
393
|
promptGuidelines: string[];
|
|
337
394
|
targetDescription?: string;
|
|
338
395
|
},
|
|
396
|
+
profile: RepoDiscoveryProfile,
|
|
339
397
|
) {
|
|
340
398
|
pi.registerTool({
|
|
341
399
|
name: options.name,
|
|
@@ -343,15 +401,23 @@ function registerRepoCommandTool(
|
|
|
343
401
|
description: options.description,
|
|
344
402
|
promptSnippet: options.promptSnippet,
|
|
345
403
|
promptGuidelines: options.promptGuidelines,
|
|
346
|
-
parameters: repoToolParameters(options.command, options.targetDescription),
|
|
404
|
+
parameters: repoToolParameters(options.command, options.targetDescription, profile),
|
|
347
405
|
|
|
348
406
|
async execute(_toolCallId: string, params: RepoDiscoveryWrapperParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ToolContext) {
|
|
349
|
-
return executeRepoDiscovery(pi, { ...params, command: options.command }, signal, ctx, options.name);
|
|
407
|
+
return executeRepoDiscovery(pi, { ...params, command: options.command }, signal, ctx, options.name, profile);
|
|
350
408
|
},
|
|
351
409
|
});
|
|
352
410
|
}
|
|
353
411
|
|
|
354
|
-
export
|
|
412
|
+
export type RepoDiscoveryExtensionOptions = {
|
|
413
|
+
profile?: RepoDiscoveryProfile;
|
|
414
|
+
cwd?: string;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
export default function repoDiscoveryExtension(pi: ExtensionAPI, options: RepoDiscoveryExtensionOptions = {}) {
|
|
418
|
+
const registrationCwd = options.cwd ?? process.cwd();
|
|
419
|
+
const profileConfig = options.profile ? { profile: options.profile, issues: [] } : loadRepoDiscoveryProfile(registrationCwd);
|
|
420
|
+
const profile = profileConfig.profile;
|
|
355
421
|
pi.registerCommand(INIT_COMMAND_NAME, {
|
|
356
422
|
description: "Initialize idx repository discovery for this project, then reload Pi to expose repo_* tools",
|
|
357
423
|
handler: async (_args: string, ctx: CommandContext) => {
|
|
@@ -424,8 +490,8 @@ export default function repoDiscoveryExtension(pi: ExtensionAPI) {
|
|
|
424
490
|
},
|
|
425
491
|
});
|
|
426
492
|
|
|
427
|
-
if (!findIndexedProjectRoot(
|
|
493
|
+
if (!findIndexedProjectRoot(registrationCwd)) return;
|
|
428
494
|
|
|
429
|
-
for (const tool of REPO_DISCOVERY_TOOLS) registerRepoCommandTool(pi, tool);
|
|
495
|
+
for (const tool of REPO_DISCOVERY_TOOLS) registerRepoCommandTool(pi, tool, profile);
|
|
430
496
|
|
|
431
497
|
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
4
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
5
|
+
|
|
6
|
+
import { getPiToolsSuiteUserConfigPath } from "../config.js";
|
|
7
|
+
|
|
8
|
+
export const REPO_DISCOVERY_PROFILES = ["baseline", "native-compact"] as const;
|
|
9
|
+
export type RepoDiscoveryProfile = (typeof REPO_DISCOVERY_PROFILES)[number];
|
|
10
|
+
export type RepoDiscoveryOutputMode = "compact" | "full";
|
|
11
|
+
export type RepoIdxCommand = "architecture" | "structure" | "ast" | "search" | "explain" | "deps";
|
|
12
|
+
|
|
13
|
+
export const NATIVE_COMPACT_OUTPUT_LIMITS = Object.freeze({
|
|
14
|
+
compact: { maxLines: 400, maxBytes: 12_000 },
|
|
15
|
+
full: { maxLines: 2_000, maxBytes: 50_000 },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export interface RepoDiscoveryProfileConfig {
|
|
19
|
+
profile: RepoDiscoveryProfile;
|
|
20
|
+
issues: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NativePolicyOutcome {
|
|
24
|
+
version: 1;
|
|
25
|
+
profile: "native-compact";
|
|
26
|
+
outputMode: RepoDiscoveryOutputMode;
|
|
27
|
+
maxLines: number;
|
|
28
|
+
maxBytes: number;
|
|
29
|
+
refused: boolean;
|
|
30
|
+
reason?: NativePolicyRefusalReason;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type NativePolicyRefusalReason =
|
|
34
|
+
| "invalid-wrapper-budget"
|
|
35
|
+
| "unknown-flag"
|
|
36
|
+
| "missing-flag-value"
|
|
37
|
+
| "duplicate-flag"
|
|
38
|
+
| "invalid-flag-value"
|
|
39
|
+
| "conflicting-flags"
|
|
40
|
+
| "compact-limit-exceeded"
|
|
41
|
+
| "full-limit-exceeded";
|
|
42
|
+
|
|
43
|
+
export type NativePolicyResult =
|
|
44
|
+
| { ok: true; args: string[]; maxLines: number; maxBytes: number; outcome: NativePolicyOutcome }
|
|
45
|
+
| { ok: false; message: string; outcome: NativePolicyOutcome };
|
|
46
|
+
|
|
47
|
+
export interface NativePolicyParams {
|
|
48
|
+
command: RepoIdxCommand;
|
|
49
|
+
args?: string[];
|
|
50
|
+
maxLines?: number;
|
|
51
|
+
maxBytes?: number;
|
|
52
|
+
outputMode?: RepoDiscoveryOutputMode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StrictTruncation {
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
totalLines: number;
|
|
58
|
+
outputLines: number;
|
|
59
|
+
totalBytes: number;
|
|
60
|
+
outputBytes: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type FlagKind = "boolean" | "string" | "positive-int" | "nonnegative-int" | "number";
|
|
64
|
+
type FlagSpec = { kind: FlagKind; values?: readonly string[] };
|
|
65
|
+
|
|
66
|
+
const FLAG_SPECS: Record<RepoIdxCommand, Record<string, FlagSpec>> = {
|
|
67
|
+
architecture: {
|
|
68
|
+
"--path-prefix": { kind: "string" },
|
|
69
|
+
},
|
|
70
|
+
structure: {
|
|
71
|
+
"--path-prefix": { kind: "string" },
|
|
72
|
+
"--kind": { kind: "string" },
|
|
73
|
+
"--max-depth": { kind: "positive-int" },
|
|
74
|
+
"--max-files": { kind: "positive-int" },
|
|
75
|
+
"--cursor": { kind: "nonnegative-int" },
|
|
76
|
+
"--include-internal": { kind: "boolean" },
|
|
77
|
+
"--no-tests": { kind: "boolean" },
|
|
78
|
+
"--include-tests-summary": { kind: "boolean" },
|
|
79
|
+
},
|
|
80
|
+
ast: {
|
|
81
|
+
"--max-depth": { kind: "positive-int" },
|
|
82
|
+
"--max-nodes": { kind: "positive-int" },
|
|
83
|
+
"--cursor": { kind: "nonnegative-int" },
|
|
84
|
+
"--no-include-text": { kind: "boolean" },
|
|
85
|
+
},
|
|
86
|
+
search: {
|
|
87
|
+
"--max-files": { kind: "positive-int" },
|
|
88
|
+
"--path-prefix": { kind: "string" },
|
|
89
|
+
"--chunk-types": { kind: "string" },
|
|
90
|
+
"--mode": { kind: "string", values: ["hybrid", "semantic", "lexical", "symbol"] },
|
|
91
|
+
"--include-imports": { kind: "boolean" },
|
|
92
|
+
"--min-score": { kind: "number" },
|
|
93
|
+
"--include-content": { kind: "boolean" },
|
|
94
|
+
"--dedupe-file": { kind: "boolean" },
|
|
95
|
+
"--dedupe-symbol": { kind: "boolean" },
|
|
96
|
+
"--cluster": { kind: "boolean" },
|
|
97
|
+
"--exclude-tests": { kind: "boolean" },
|
|
98
|
+
"--include-tests": { kind: "boolean" },
|
|
99
|
+
},
|
|
100
|
+
explain: {
|
|
101
|
+
"--path-prefix": { kind: "string" },
|
|
102
|
+
"--include-body": { kind: "boolean" },
|
|
103
|
+
"--body-lines": { kind: "positive-int" },
|
|
104
|
+
"--signature-only": { kind: "boolean" },
|
|
105
|
+
},
|
|
106
|
+
deps: {
|
|
107
|
+
"--mode": { kind: "string", values: ["modules", "module-imports", "calls", "call-graph"] },
|
|
108
|
+
"--direction": { kind: "string", values: ["callers", "imported-by", "callees", "imports", "both"] },
|
|
109
|
+
"--depth": { kind: "positive-int" },
|
|
110
|
+
"--show-edges": { kind: "boolean" },
|
|
111
|
+
"--tests": { kind: "boolean" },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const COMPACT_NATIVE_LIMITS: Partial<Record<RepoIdxCommand, Record<string, number>>> = {
|
|
116
|
+
structure: { "--max-depth": 2, "--max-files": 20 },
|
|
117
|
+
ast: { "--max-depth": 3, "--max-nodes": 40 },
|
|
118
|
+
search: { "--max-files": 3 },
|
|
119
|
+
explain: { "--body-lines": 20 },
|
|
120
|
+
deps: { "--depth": 1 },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const FULL_NATIVE_LIMITS: Partial<Record<RepoIdxCommand, Record<string, number>>> = {
|
|
124
|
+
structure: { "--max-depth": 8, "--max-files": 300 },
|
|
125
|
+
ast: { "--max-depth": 8, "--max-nodes": 500 },
|
|
126
|
+
search: { "--max-files": 50 },
|
|
127
|
+
explain: { "--body-lines": 200 },
|
|
128
|
+
deps: { "--depth": 6 },
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const NATIVE_COMPACT_ARG_GUIDANCE: Record<RepoIdxCommand, string> = {
|
|
132
|
+
architecture:
|
|
133
|
+
"Native Compact: keep outputMode=compact unless this same tool result is actually truncated; scope with --path-prefix before using full.",
|
|
134
|
+
structure:
|
|
135
|
+
"Native Compact: compact allows --max-files<=20 and --max-depth<=2; continue with --cursor. For an intentional broader structure call, set outputMode=full on that same call (full limits: --max-files<=300, --max-depth<=8). Do not retry a rejected compact value unchanged.",
|
|
136
|
+
ast:
|
|
137
|
+
"Native Compact: compact allows --max-nodes<=40 and --max-depth<=3 and defaults to --no-include-text; continue with --cursor. For an intentional broader AST call, set outputMode=full on that same call (full limits: --max-nodes<=500, --max-depth<=8). Do not retry a rejected compact value unchanged.",
|
|
138
|
+
search:
|
|
139
|
+
"Native Compact: compact allows --max-files<=3; with --include-content use exactly --max-files 1. For an intentional broader search, set outputMode=full on that same call (full limit: --max-files<=50). Do not retry a rejected compact value unchanged.",
|
|
140
|
+
explain:
|
|
141
|
+
"Native Compact: compact defaults to --signature-only; with --include-body use --body-lines<=20. For an intentional broader explanation, set outputMode=full on that same call (full limit: --body-lines<=200). Do not retry a rejected compact value unchanged.",
|
|
142
|
+
deps:
|
|
143
|
+
"Native Compact: compact allows --depth<=1. For an intentional deeper dependency traversal, set outputMode=full on that same call (full limit: --depth<=6). Do not retry a rejected compact value unchanged.",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export function describeNativeCompactArgs(command: RepoIdxCommand): string {
|
|
147
|
+
return NATIVE_COMPACT_ARG_GUIDANCE[command];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
151
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readJsonc(filePath: string): Record<string, unknown> {
|
|
155
|
+
if (!existsSync(filePath)) return {};
|
|
156
|
+
try {
|
|
157
|
+
const parsed = parseJsonc(readFileSync(filePath, "utf8"));
|
|
158
|
+
return isRecord(parsed) ? parsed : {};
|
|
159
|
+
} catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function findProjectConfig(startDir: string): string | undefined {
|
|
165
|
+
let dir = resolve(startDir);
|
|
166
|
+
const root = parse(dir).root;
|
|
167
|
+
while (true) {
|
|
168
|
+
const candidate = join(dir, ".pi", "pi-tools-suite.jsonc");
|
|
169
|
+
if (existsSync(candidate)) return candidate;
|
|
170
|
+
if (dir === root) return undefined;
|
|
171
|
+
const parent = dirname(dir);
|
|
172
|
+
if (parent === dir) return undefined;
|
|
173
|
+
dir = parent;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseProfile(value: unknown): RepoDiscoveryProfile | undefined {
|
|
178
|
+
if (value === "baseline" || value === "native-compact") return value;
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function loadRepoDiscoveryProfile(
|
|
183
|
+
cwd: string = process.cwd(),
|
|
184
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
185
|
+
homeDir: string = env.HOME ?? process.env.HOME ?? homedir(),
|
|
186
|
+
): RepoDiscoveryProfileConfig {
|
|
187
|
+
let profile: RepoDiscoveryProfile = "baseline";
|
|
188
|
+
const issues: string[] = [];
|
|
189
|
+
const layers = [getPiToolsSuiteUserConfigPath(homeDir)];
|
|
190
|
+
if (env.PI_CONFIG_DIR) layers.push(join(env.PI_CONFIG_DIR, "pi-tools-suite.jsonc"));
|
|
191
|
+
const projectConfig = findProjectConfig(cwd);
|
|
192
|
+
if (projectConfig) layers.push(projectConfig);
|
|
193
|
+
|
|
194
|
+
for (const filePath of layers) {
|
|
195
|
+
const section = readJsonc(filePath).repoDiscovery;
|
|
196
|
+
if (!isRecord(section) || section.profile === undefined) continue;
|
|
197
|
+
const next = parseProfile(section.profile);
|
|
198
|
+
if (next) profile = next;
|
|
199
|
+
else issues.push(`repoDiscovery.profile must be baseline or native-compact; keeping ${profile}.`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (env.PI_REPO_DISCOVERY_PROFILE !== undefined) {
|
|
203
|
+
const next = parseProfile(env.PI_REPO_DISCOVERY_PROFILE);
|
|
204
|
+
if (next) profile = next;
|
|
205
|
+
else issues.push("PI_REPO_DISCOVERY_PROFILE must be baseline or native-compact; ignoring it.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { profile, issues };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateNumeric(kind: FlagKind, value: string): boolean {
|
|
212
|
+
const numeric = Number(value);
|
|
213
|
+
if (!Number.isFinite(numeric)) return false;
|
|
214
|
+
if (kind === "number") return true;
|
|
215
|
+
if (!Number.isSafeInteger(numeric)) return false;
|
|
216
|
+
if (kind === "positive-int") return numeric > 0;
|
|
217
|
+
if (kind === "nonnegative-int") return numeric >= 0;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseArgs(command: RepoIdxCommand, args: string[]):
|
|
222
|
+
| { ok: true; normalized: string[]; values: Map<string, string | true> }
|
|
223
|
+
| { ok: false; reason: NativePolicyRefusalReason; message: string } {
|
|
224
|
+
const specs = FLAG_SPECS[command];
|
|
225
|
+
const normalized: string[] = [];
|
|
226
|
+
const values = new Map<string, string | true>();
|
|
227
|
+
|
|
228
|
+
for (let index = 0; index < args.length; index++) {
|
|
229
|
+
const token = args[index]!;
|
|
230
|
+
if (!token.startsWith("--")) {
|
|
231
|
+
return { ok: false, reason: "unknown-flag", message: `Unexpected native idx argument: ${token}. Pass only documented flags in args.` };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const equalsIndex = token.indexOf("=");
|
|
235
|
+
const flag = equalsIndex >= 0 ? token.slice(0, equalsIndex) : token;
|
|
236
|
+
const inlineValue = equalsIndex >= 0 ? token.slice(equalsIndex + 1) : undefined;
|
|
237
|
+
const spec = specs[flag];
|
|
238
|
+
if (!spec) return { ok: false, reason: "unknown-flag", message: `Unsupported ${command} flag in Native Compact: ${flag}.` };
|
|
239
|
+
if (values.has(flag)) return { ok: false, reason: "duplicate-flag", message: `Duplicate ${command} flag is not allowed: ${flag}.` };
|
|
240
|
+
|
|
241
|
+
if (spec.kind === "boolean") {
|
|
242
|
+
if (inlineValue !== undefined) {
|
|
243
|
+
return { ok: false, reason: "invalid-flag-value", message: `${flag} is a boolean flag and does not accept =value.` };
|
|
244
|
+
}
|
|
245
|
+
values.set(flag, true);
|
|
246
|
+
normalized.push(flag);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const value = inlineValue ?? args[++index];
|
|
251
|
+
if (value === undefined || value.length === 0) {
|
|
252
|
+
return { ok: false, reason: "missing-flag-value", message: `${flag} requires a value.` };
|
|
253
|
+
}
|
|
254
|
+
if (inlineValue === undefined && value.startsWith("--")) {
|
|
255
|
+
return { ok: false, reason: "missing-flag-value", message: `${flag} requires a value before the next flag.` };
|
|
256
|
+
}
|
|
257
|
+
if (spec.values && !spec.values.includes(value)) {
|
|
258
|
+
return { ok: false, reason: "invalid-flag-value", message: `${flag} must be one of: ${spec.values.join(", ")}.` };
|
|
259
|
+
}
|
|
260
|
+
if (["positive-int", "nonnegative-int", "number"].includes(spec.kind) && !validateNumeric(spec.kind, value)) {
|
|
261
|
+
return { ok: false, reason: "invalid-flag-value", message: `${flag} has an invalid numeric value.` };
|
|
262
|
+
}
|
|
263
|
+
values.set(flag, value);
|
|
264
|
+
normalized.push(flag, value);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { ok: true, normalized, values };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function numericValue(values: Map<string, string | true>, flag: string): number | undefined {
|
|
271
|
+
const raw = values.get(flag);
|
|
272
|
+
return typeof raw === "string" ? Number(raw) : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function appendValue(args: string[], values: Map<string, string | true>, flag: string, value: string): void {
|
|
276
|
+
if (values.has(flag)) return;
|
|
277
|
+
values.set(flag, value);
|
|
278
|
+
args.push(flag, value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function appendBoolean(args: string[], values: Map<string, string | true>, flag: string): void {
|
|
282
|
+
if (values.has(flag)) return;
|
|
283
|
+
values.set(flag, true);
|
|
284
|
+
args.push(flag);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function refusal(
|
|
288
|
+
outputMode: RepoDiscoveryOutputMode,
|
|
289
|
+
maxLines: number,
|
|
290
|
+
maxBytes: number,
|
|
291
|
+
reason: NativePolicyRefusalReason,
|
|
292
|
+
message: string,
|
|
293
|
+
): NativePolicyResult {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
message,
|
|
297
|
+
outcome: { version: 1, profile: "native-compact", outputMode, maxLines, maxBytes, refused: true, reason },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function validWrapperBudget(value: number | undefined): value is number {
|
|
302
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function applyNativeCompactPolicy(params: NativePolicyParams): NativePolicyResult {
|
|
306
|
+
const outputMode = params.outputMode ?? "compact";
|
|
307
|
+
if (outputMode !== "compact" && outputMode !== "full") {
|
|
308
|
+
return refusal("compact", NATIVE_COMPACT_OUTPUT_LIMITS.compact.maxLines, NATIVE_COMPACT_OUTPUT_LIMITS.compact.maxBytes, "invalid-flag-value", "Native Compact outputMode must be compact or full.");
|
|
309
|
+
}
|
|
310
|
+
const outputLimits = NATIVE_COMPACT_OUTPUT_LIMITS[outputMode];
|
|
311
|
+
const maxLines = params.maxLines === undefined ? outputLimits.maxLines : params.maxLines;
|
|
312
|
+
const maxBytes = params.maxBytes === undefined ? outputLimits.maxBytes : params.maxBytes;
|
|
313
|
+
if (!validWrapperBudget(maxLines) || !validWrapperBudget(maxBytes)) {
|
|
314
|
+
return refusal(outputMode, outputLimits.maxLines, outputLimits.maxBytes, "invalid-wrapper-budget", "Native Compact maxLines/maxBytes must be positive integers.");
|
|
315
|
+
}
|
|
316
|
+
if (maxLines > outputLimits.maxLines || maxBytes > outputLimits.maxBytes) {
|
|
317
|
+
return refusal(
|
|
318
|
+
outputMode,
|
|
319
|
+
maxLines,
|
|
320
|
+
maxBytes,
|
|
321
|
+
outputMode === "compact" ? "compact-limit-exceeded" : "full-limit-exceeded",
|
|
322
|
+
`Native Compact ${outputMode} output budget exceeded. Allowed maxLines=${outputLimits.maxLines}, maxBytes=${outputLimits.maxBytes}.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const parsed = parseArgs(params.command, params.args ?? []);
|
|
327
|
+
if (parsed.ok === false) return refusal(outputMode, maxLines, maxBytes, parsed.reason, parsed.message);
|
|
328
|
+
const args = [...parsed.normalized];
|
|
329
|
+
const values = parsed.values;
|
|
330
|
+
|
|
331
|
+
if (params.command === "search" && values.has("--include-tests") && values.has("--exclude-tests")) {
|
|
332
|
+
return refusal(outputMode, maxLines, maxBytes, "conflicting-flags", "repo_search cannot combine --include-tests with --exclude-tests.");
|
|
333
|
+
}
|
|
334
|
+
if (params.command === "structure" && values.has("--no-tests") && values.has("--include-tests-summary")) {
|
|
335
|
+
return refusal(outputMode, maxLines, maxBytes, "conflicting-flags", "repo_structure cannot combine --no-tests with --include-tests-summary.");
|
|
336
|
+
}
|
|
337
|
+
if (params.command === "explain" && values.has("--include-body") && values.has("--signature-only")) {
|
|
338
|
+
return refusal(outputMode, maxLines, maxBytes, "conflicting-flags", "repo_explain cannot combine --include-body with --signature-only.");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (outputMode === "compact") {
|
|
342
|
+
if (params.command === "structure") {
|
|
343
|
+
appendValue(args, values, "--max-files", "20");
|
|
344
|
+
appendValue(args, values, "--max-depth", "2");
|
|
345
|
+
}
|
|
346
|
+
if (params.command === "ast") {
|
|
347
|
+
appendValue(args, values, "--max-depth", "3");
|
|
348
|
+
appendValue(args, values, "--max-nodes", "40");
|
|
349
|
+
appendBoolean(args, values, "--no-include-text");
|
|
350
|
+
}
|
|
351
|
+
if (params.command === "search" && values.has("--include-content")) {
|
|
352
|
+
appendValue(args, values, "--max-files", "1");
|
|
353
|
+
}
|
|
354
|
+
if (params.command === "explain") {
|
|
355
|
+
if (values.has("--include-body")) appendValue(args, values, "--body-lines", "20");
|
|
356
|
+
else appendBoolean(args, values, "--signature-only");
|
|
357
|
+
}
|
|
358
|
+
if (params.command === "deps") appendValue(args, values, "--depth", "1");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const nativeLimits = outputMode === "compact" ? COMPACT_NATIVE_LIMITS[params.command] : FULL_NATIVE_LIMITS[params.command];
|
|
362
|
+
for (const [flag, maximum] of Object.entries(nativeLimits ?? {})) {
|
|
363
|
+
const value = numericValue(values, flag);
|
|
364
|
+
if (value !== undefined && value > maximum) {
|
|
365
|
+
const recovery = outputMode === "compact"
|
|
366
|
+
? `Retry with ${flag}<=${maximum} (prefer a native cursor/narrower scope), or set outputMode=full on this same tool call when the broader request is intentional. Do not repeat the rejected compact value unchanged.`
|
|
367
|
+
: `Retry with ${flag}<=${maximum} and narrow/page the request. Do not repeat the rejected full value unchanged.`;
|
|
368
|
+
return refusal(
|
|
369
|
+
outputMode,
|
|
370
|
+
maxLines,
|
|
371
|
+
maxBytes,
|
|
372
|
+
outputMode === "compact" ? "compact-limit-exceeded" : "full-limit-exceeded",
|
|
373
|
+
`${flag}=${value} exceeds Native Compact ${outputMode} limit ${maximum}. ${recovery}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (params.command === "search" && outputMode === "compact" && values.has("--include-content")) {
|
|
379
|
+
const maxFiles = numericValue(values, "--max-files");
|
|
380
|
+
if (maxFiles !== 1) {
|
|
381
|
+
return refusal(
|
|
382
|
+
outputMode,
|
|
383
|
+
maxLines,
|
|
384
|
+
maxBytes,
|
|
385
|
+
"compact-limit-exceeded",
|
|
386
|
+
"--include-content in compact mode requires --max-files 1. Retry with --max-files 1, or set outputMode=full on this same search call when broad inline content is intentional; do not repeat the rejected compact value unchanged.",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
args,
|
|
394
|
+
maxLines,
|
|
395
|
+
maxBytes,
|
|
396
|
+
outcome: { version: 1, profile: "native-compact", outputMode, maxLines, maxBytes, refused: false },
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function truncateUtf8Prefix(text: string, maxBytes: number): string {
|
|
401
|
+
let output = "";
|
|
402
|
+
let bytes = 0;
|
|
403
|
+
for (const char of text) {
|
|
404
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
405
|
+
if (bytes + charBytes > maxBytes) break;
|
|
406
|
+
output += char;
|
|
407
|
+
bytes += charBytes;
|
|
408
|
+
}
|
|
409
|
+
return output;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function truncateNativeCompactOutput(
|
|
413
|
+
text: string,
|
|
414
|
+
maxLines: number,
|
|
415
|
+
maxBytes: number,
|
|
416
|
+
outputMode: RepoDiscoveryOutputMode,
|
|
417
|
+
): { text: string; truncation: StrictTruncation } {
|
|
418
|
+
const totalBytes = Buffer.byteLength(text, "utf8");
|
|
419
|
+
const lines = text.split("\n");
|
|
420
|
+
if (lines.length <= maxLines && totalBytes <= maxBytes) {
|
|
421
|
+
return {
|
|
422
|
+
text,
|
|
423
|
+
truncation: { truncated: false, totalLines: lines.length, outputLines: lines.length, totalBytes, outputBytes: totalBytes },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const maxBodyLines = Math.max(0, maxLines - 1);
|
|
428
|
+
let body = lines.slice(0, maxBodyLines).join("\n");
|
|
429
|
+
let bodyBytes = Buffer.byteLength(body, "utf8");
|
|
430
|
+
let bodyLines = body.length === 0 ? 0 : body.split("\n").length;
|
|
431
|
+
const continuation = outputMode === "compact"
|
|
432
|
+
? "Use a native cursor/narrower scope or outputMode=full."
|
|
433
|
+
: "Use a native cursor or narrower scope.";
|
|
434
|
+
|
|
435
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
436
|
+
const marker = `[Output truncated: first ${bodyLines}/${lines.length} lines, ${bodyBytes}/${totalBytes} bytes. ${continuation}]`;
|
|
437
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
438
|
+
const separatorBytes = body.length > 0 ? 1 : 0;
|
|
439
|
+
const availableBodyBytes = Math.max(0, maxBytes - markerBytes - separatorBytes);
|
|
440
|
+
if (bodyBytes > availableBodyBytes) {
|
|
441
|
+
body = truncateUtf8Prefix(body, availableBodyBytes);
|
|
442
|
+
bodyBytes = Buffer.byteLength(body, "utf8");
|
|
443
|
+
bodyLines = body.length === 0 ? 0 : body.split("\n").length;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const finalText = body.length > 0 ? `${body}\n${marker}` : truncateUtf8Prefix(marker, maxBytes);
|
|
447
|
+
return {
|
|
448
|
+
text: finalText,
|
|
449
|
+
truncation: { truncated: true, totalLines: lines.length, outputLines: bodyLines, totalBytes, outputBytes: bodyBytes },
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const fallback = truncateUtf8Prefix("[Output truncated; narrow the idx query.]", maxBytes);
|
|
454
|
+
return {
|
|
455
|
+
text: fallback,
|
|
456
|
+
truncation: { truncated: true, totalLines: lines.length, outputLines: 0, totalBytes, outputBytes: 0 },
|
|
457
|
+
};
|
|
458
|
+
}
|