@mrclrchtr/supi-context 1.3.0 → 1.4.0
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 +39 -24
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +13 -13
- package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
- package/package.json +4 -3
- package/src/analysis.ts +223 -19
- package/src/context.ts +4 -3
- package/src/format.ts +527 -116
- package/src/prompt-inference.ts +37 -13
- package/src/renderer.ts +30 -13
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
package/src/analysis.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// biome-ignore lint/nursery/noExcessiveLinesPerFile: analysis file is inherently large
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
type BuildSystemPromptOptions,
|
|
4
5
|
buildSessionContext,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
SettingsManager,
|
|
11
12
|
} from "@earendil-works/pi-coding-agent";
|
|
12
13
|
import { getRegisteredContextProviders } from "@mrclrchtr/supi-core/api";
|
|
14
|
+
|
|
13
15
|
import { deriveOptionsFromSystemPrompt, extractGuidelinesSection } from "./prompt-inference.ts";
|
|
14
16
|
|
|
15
17
|
type AgentMessage = Parameters<typeof estimateTokens>[0];
|
|
@@ -26,12 +28,15 @@ export interface CategoryTokens {
|
|
|
26
28
|
export interface ContextFileInfo {
|
|
27
29
|
path: string;
|
|
28
30
|
tokens: number;
|
|
31
|
+
lines: number;
|
|
32
|
+
origin: "global" | "project";
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface InjectedFileInfo {
|
|
32
36
|
file: string;
|
|
33
37
|
turn: number;
|
|
34
38
|
tokens: number;
|
|
39
|
+
lines: number;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export interface SkillInfo {
|
|
@@ -39,6 +44,25 @@ export interface SkillInfo {
|
|
|
39
44
|
tokens: number;
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
export interface ToolInfo {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
tokens: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Per-tool breakdown of the one-line tool snippet shown in "Available tools". */
|
|
54
|
+
export interface ToolSnippetInfo {
|
|
55
|
+
name: string;
|
|
56
|
+
tokens: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Source-attributed guideline info. */
|
|
60
|
+
export interface GuidelineSourceInfo {
|
|
61
|
+
source: string; // "default" | tool name | "other"
|
|
62
|
+
tokens: number;
|
|
63
|
+
bulletCount: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
export interface ContextProviderSection {
|
|
43
67
|
id: string;
|
|
44
68
|
label: string;
|
|
@@ -51,22 +75,29 @@ export interface ContextAnalysis {
|
|
|
51
75
|
totalTokens: number | null;
|
|
52
76
|
scaled: boolean;
|
|
53
77
|
approximationNote: string | null;
|
|
78
|
+
full: boolean;
|
|
54
79
|
categories: CategoryTokens & {
|
|
55
80
|
autocompactBuffer: number;
|
|
56
81
|
freeSpace: number;
|
|
57
82
|
};
|
|
58
83
|
systemPromptBreakdown: {
|
|
59
84
|
base: number;
|
|
85
|
+
instructionFiles: ContextFileInfo[];
|
|
60
86
|
contextFiles: ContextFileInfo[];
|
|
61
87
|
skills: SkillInfo[];
|
|
62
88
|
guidelines: number;
|
|
63
89
|
toolSnippets: number;
|
|
90
|
+
toolSnippetDetails: ToolSnippetInfo[];
|
|
91
|
+
guidelineSources: GuidelineSourceInfo[];
|
|
64
92
|
appendText: number;
|
|
65
93
|
};
|
|
66
94
|
injectedFiles: InjectedFileInfo[];
|
|
67
95
|
skills: SkillInfo[];
|
|
68
96
|
guidelines: number;
|
|
69
|
-
|
|
97
|
+
guidelineBullets: string[];
|
|
98
|
+
guidelineSources: GuidelineSourceInfo[];
|
|
99
|
+
toolSnippetDetails: ToolSnippetInfo[];
|
|
100
|
+
toolDefinitions: { count: number; tokens: number; tools: ToolInfo[] };
|
|
70
101
|
compaction: { summarizedTurns: number } | null;
|
|
71
102
|
providerSections: ContextProviderSection[];
|
|
72
103
|
}
|
|
@@ -239,16 +270,50 @@ function collectProviderData(): ContextProviderSection[] {
|
|
|
239
270
|
return sections;
|
|
240
271
|
}
|
|
241
272
|
|
|
273
|
+
const INSTRUCTION_FILE_PATTERN = /^(AGENTS|CLAUDE|\.claude\.local)\.md$/i;
|
|
274
|
+
|
|
275
|
+
function isInstructionFile(path: string): boolean {
|
|
276
|
+
const basename = path.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
277
|
+
return INSTRUCTION_FILE_PATTERN.test(basename);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function determineOrigin(filePath: string, cwd: string): "global" | "project" {
|
|
281
|
+
const resolvedPath = resolve(cwd, filePath);
|
|
282
|
+
const fileDir = dirname(resolvedPath);
|
|
283
|
+
let current = cwd;
|
|
284
|
+
const root = resolve("/");
|
|
285
|
+
while (true) {
|
|
286
|
+
if (fileDir === current) return "project";
|
|
287
|
+
if (current === root) break;
|
|
288
|
+
const parent = resolve(current, "..");
|
|
289
|
+
if (parent === current) break;
|
|
290
|
+
current = parent;
|
|
291
|
+
}
|
|
292
|
+
return "global";
|
|
293
|
+
}
|
|
294
|
+
|
|
242
295
|
function computeContextFiles(
|
|
243
296
|
promptOptions: BuildSystemPromptOptions | undefined,
|
|
244
|
-
|
|
245
|
-
|
|
297
|
+
cwd: string,
|
|
298
|
+
): { contextFiles: ContextFileInfo[]; instructionFiles: ContextFileInfo[] } {
|
|
299
|
+
const contextFiles: ContextFileInfo[] = [];
|
|
300
|
+
const instructionFiles: ContextFileInfo[] = [];
|
|
246
301
|
if (promptOptions?.contextFiles) {
|
|
247
302
|
for (const cf of promptOptions.contextFiles) {
|
|
248
|
-
|
|
303
|
+
const info: ContextFileInfo = {
|
|
304
|
+
path: cf.path,
|
|
305
|
+
tokens: estimateTextTokens(cf.content),
|
|
306
|
+
lines: cf.content.split("\n").length,
|
|
307
|
+
origin: determineOrigin(cf.path, cwd),
|
|
308
|
+
};
|
|
309
|
+
if (isInstructionFile(cf.path)) {
|
|
310
|
+
instructionFiles.push(info);
|
|
311
|
+
} else {
|
|
312
|
+
contextFiles.push(info);
|
|
313
|
+
}
|
|
249
314
|
}
|
|
250
315
|
}
|
|
251
|
-
return
|
|
316
|
+
return { contextFiles, instructionFiles };
|
|
252
317
|
}
|
|
253
318
|
|
|
254
319
|
function computeSkills(promptOptions: BuildSystemPromptOptions | undefined): SkillInfo[] {
|
|
@@ -262,12 +327,118 @@ function computeSkills(promptOptions: BuildSystemPromptOptions | undefined): Ski
|
|
|
262
327
|
return skills;
|
|
263
328
|
}
|
|
264
329
|
|
|
330
|
+
function extractGuidelineBullets(guidelinesText: string | null): string[] {
|
|
331
|
+
if (!guidelinesText) return [];
|
|
332
|
+
return guidelinesText
|
|
333
|
+
.split("\n")
|
|
334
|
+
.map((line) => line.trim())
|
|
335
|
+
.filter((line) => line.startsWith("- "))
|
|
336
|
+
.map((line) => line.slice(2).trim());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Known texts of PI built-in default guidelines.
|
|
341
|
+
* These are generated by buildSystemPrompt() in the system prompt builder.
|
|
342
|
+
*/
|
|
343
|
+
const DEFAULT_GUIDELINE_TEXTS = new Set([
|
|
344
|
+
"Use bash for file operations like ls, rg, find",
|
|
345
|
+
"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)",
|
|
346
|
+
"Be concise in your responses",
|
|
347
|
+
"Show file paths clearly when working with files",
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Known promptGuidelines from PI's built-in tools.
|
|
352
|
+
* These are hardcoded in the tool definition modules (read, write, edit).
|
|
353
|
+
*/
|
|
354
|
+
const BUILTIN_TOOL_GUIDELINES: Record<string, string[]> = {
|
|
355
|
+
read: ["Use read to examine files instead of cat or sed."],
|
|
356
|
+
write: ["Use write only for new files or complete rewrites."],
|
|
357
|
+
edit: [
|
|
358
|
+
"Use edit for precise changes (edits[].oldText must match exactly)",
|
|
359
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
|
360
|
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
|
|
361
|
+
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
|
362
|
+
],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Build a reverse map from guideline text → tool name so we can look up
|
|
367
|
+
* which built-in tool (if any) contributed each guideline bullet.
|
|
368
|
+
*/
|
|
369
|
+
function buildGuidelineToToolMap(): Map<string, string> {
|
|
370
|
+
const map = new Map<string, string>();
|
|
371
|
+
for (const [tool, guidelines] of Object.entries(BUILTIN_TOOL_GUIDELINES)) {
|
|
372
|
+
for (const guideline of guidelines) {
|
|
373
|
+
map.set(guideline, tool);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return map;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const GUIDELINE_TO_TOOL = buildGuidelineToToolMap();
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Given guideline bullets extracted from the system prompt, classify each bullet
|
|
383
|
+
* by source and compute per-source token counts.
|
|
384
|
+
*/
|
|
385
|
+
function classifyGuidelines(bullets: string[], _activeToolNames: string[]): GuidelineSourceInfo[] {
|
|
386
|
+
const sources = new Map<string, { chars: number; count: number }>();
|
|
387
|
+
|
|
388
|
+
for (const bullet of bullets) {
|
|
389
|
+
let source: string;
|
|
390
|
+
if (DEFAULT_GUIDELINE_TEXTS.has(bullet)) {
|
|
391
|
+
source = "default";
|
|
392
|
+
} else {
|
|
393
|
+
const toolName = GUIDELINE_TO_TOOL.get(bullet);
|
|
394
|
+
source = toolName ?? "other";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const entry = sources.get(source) ?? { chars: 0, count: 0 };
|
|
398
|
+
entry.chars += bullet.length;
|
|
399
|
+
entry.count += 1;
|
|
400
|
+
sources.set(source, entry);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return Array.from(sources.entries())
|
|
404
|
+
.map(([source, { chars, count }]) => ({
|
|
405
|
+
source,
|
|
406
|
+
tokens: Math.ceil(chars / 4),
|
|
407
|
+
bulletCount: count,
|
|
408
|
+
}))
|
|
409
|
+
.sort((a, b) => {
|
|
410
|
+
// Default first, then tool sources (alphabetical), then "other" last
|
|
411
|
+
if (a.source === "default") return -1;
|
|
412
|
+
if (b.source === "default") return 1;
|
|
413
|
+
if (a.source === "other") return 1;
|
|
414
|
+
if (b.source === "other") return -1;
|
|
415
|
+
return a.source.localeCompare(b.source);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Build per-tool snippet breakdown from the toolSnippets record.
|
|
421
|
+
*/
|
|
422
|
+
function buildToolSnippetDetails(
|
|
423
|
+
toolSnippets: Record<string, string> | undefined,
|
|
424
|
+
): ToolSnippetInfo[] {
|
|
425
|
+
if (!toolSnippets) return [];
|
|
426
|
+
|
|
427
|
+
return Object.entries(toolSnippets)
|
|
428
|
+
.map(([name, snippet]) => ({
|
|
429
|
+
name,
|
|
430
|
+
tokens: estimateTextTokens(snippet),
|
|
431
|
+
}))
|
|
432
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
433
|
+
}
|
|
434
|
+
|
|
265
435
|
function computeSystemPromptBreakdown(
|
|
266
436
|
promptOptions: BuildSystemPromptOptions | undefined,
|
|
267
437
|
systemPromptText: string,
|
|
268
438
|
systemPromptTokens: number,
|
|
439
|
+
cwd: string,
|
|
269
440
|
): ContextAnalysis["systemPromptBreakdown"] {
|
|
270
|
-
const contextFiles = computeContextFiles(promptOptions);
|
|
441
|
+
const { contextFiles, instructionFiles } = computeContextFiles(promptOptions, cwd);
|
|
271
442
|
const skills = computeSkills(promptOptions);
|
|
272
443
|
|
|
273
444
|
const skillsTotal = skills.reduce((s, c) => s + c.tokens, 0);
|
|
@@ -277,9 +448,13 @@ function computeSystemPromptBreakdown(
|
|
|
277
448
|
: promptOptions?.promptGuidelines
|
|
278
449
|
? estimateTextTokens(promptOptions.promptGuidelines.join("\n"))
|
|
279
450
|
: 0;
|
|
280
|
-
const
|
|
451
|
+
const toolSnippetsTotal = promptOptions?.toolSnippets
|
|
281
452
|
? estimateTextTokens(Object.values(promptOptions.toolSnippets).join("\n"))
|
|
282
453
|
: 0;
|
|
454
|
+
const toolSnippetDetails = buildToolSnippetDetails(promptOptions?.toolSnippets);
|
|
455
|
+
const guidelineBullets = extractGuidelineBullets(inferredGuidelines);
|
|
456
|
+
const activeToolNames = promptOptions?.selectedTools ?? [];
|
|
457
|
+
const guidelineSources = classifyGuidelines(guidelineBullets, activeToolNames);
|
|
283
458
|
const appendText = promptOptions?.appendSystemPrompt
|
|
284
459
|
? estimateTextTokens(promptOptions.appendSystemPrompt)
|
|
285
460
|
: 0;
|
|
@@ -289,31 +464,47 @@ function computeSystemPromptBreakdown(
|
|
|
289
464
|
|
|
290
465
|
const knownSubtotal =
|
|
291
466
|
contextFiles.reduce((s, c) => s + c.tokens, 0) +
|
|
467
|
+
instructionFiles.reduce((s, c) => s + c.tokens, 0) +
|
|
292
468
|
skillsTotal +
|
|
293
469
|
guidelines +
|
|
294
|
-
|
|
470
|
+
toolSnippetsTotal +
|
|
295
471
|
appendText +
|
|
296
472
|
customTokens;
|
|
297
473
|
|
|
298
474
|
const base = Math.max(0, systemPromptTokens - knownSubtotal);
|
|
299
475
|
|
|
300
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
base,
|
|
478
|
+
instructionFiles,
|
|
479
|
+
contextFiles,
|
|
480
|
+
skills,
|
|
481
|
+
guidelines,
|
|
482
|
+
toolSnippets: toolSnippetsTotal,
|
|
483
|
+
toolSnippetDetails,
|
|
484
|
+
guidelineSources,
|
|
485
|
+
appendText,
|
|
486
|
+
};
|
|
301
487
|
}
|
|
302
488
|
|
|
303
|
-
function computeToolDefinitions(pi: ExtensionAPI): {
|
|
489
|
+
function computeToolDefinitions(pi: ExtensionAPI): {
|
|
490
|
+
count: number;
|
|
491
|
+
tokens: number;
|
|
492
|
+
tools: ToolInfo[];
|
|
493
|
+
} {
|
|
304
494
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
305
495
|
const allTools = pi.getAllTools();
|
|
306
496
|
const activeTools = allTools.filter((t) => activeToolNames.has(t.name));
|
|
497
|
+
const tools = activeTools.map((t) => ({
|
|
498
|
+
name: t.name,
|
|
499
|
+
description: t.description,
|
|
500
|
+
tokens: estimateTextTokens(
|
|
501
|
+
JSON.stringify({ name: t.name, description: t.description, parameters: t.parameters }),
|
|
502
|
+
),
|
|
503
|
+
}));
|
|
307
504
|
return {
|
|
308
505
|
count: activeTools.length,
|
|
309
|
-
tokens:
|
|
310
|
-
|
|
311
|
-
sum +
|
|
312
|
-
estimateTextTokens(
|
|
313
|
-
JSON.stringify({ name: t.name, description: t.description, parameters: t.parameters }),
|
|
314
|
-
),
|
|
315
|
-
0,
|
|
316
|
-
),
|
|
506
|
+
tokens: tools.reduce((sum, t) => sum + t.tokens, 0),
|
|
507
|
+
tools,
|
|
317
508
|
};
|
|
318
509
|
}
|
|
319
510
|
|
|
@@ -353,7 +544,12 @@ export function extractInjectedContextFiles(messages: AgentMessage[]): InjectedF
|
|
|
353
544
|
const innerContent = match[3];
|
|
354
545
|
const key = `${file}::${turn}`;
|
|
355
546
|
if (!seen.has(key)) {
|
|
356
|
-
seen.set(key, {
|
|
547
|
+
seen.set(key, {
|
|
548
|
+
file,
|
|
549
|
+
turn,
|
|
550
|
+
tokens: estimateTextTokens(innerContent),
|
|
551
|
+
lines: innerContent.split("\n").length,
|
|
552
|
+
});
|
|
357
553
|
}
|
|
358
554
|
match = regex.exec(content);
|
|
359
555
|
}
|
|
@@ -366,6 +562,7 @@ export function analyzeContext(
|
|
|
366
562
|
ctx: ExtensionCommandContext,
|
|
367
563
|
pi: ExtensionAPI,
|
|
368
564
|
cachedOptions: BuildSystemPromptOptions | undefined,
|
|
565
|
+
full = false,
|
|
369
566
|
): ContextAnalysis {
|
|
370
567
|
const branch = ctx.sessionManager.getBranch();
|
|
371
568
|
const apiView = buildSessionContext(branch);
|
|
@@ -403,17 +600,21 @@ export function analyzeContext(
|
|
|
403
600
|
promptOptions,
|
|
404
601
|
systemPromptText,
|
|
405
602
|
scaling.categories.systemPrompt,
|
|
603
|
+
ctx.cwd,
|
|
406
604
|
);
|
|
407
605
|
const injectedFiles = extractInjectedContextFiles(apiView.messages);
|
|
408
606
|
const toolDefinitions = computeToolDefinitions(pi);
|
|
409
607
|
const compaction = detectCompaction(branch);
|
|
410
608
|
|
|
609
|
+
const guidelineBullets = extractGuidelineBullets(extractGuidelinesSection(systemPromptText));
|
|
610
|
+
|
|
411
611
|
return {
|
|
412
612
|
modelName: ctx.model?.name ?? ctx.model?.id ?? "No model selected",
|
|
413
613
|
contextWindow,
|
|
414
614
|
totalTokens: scaling.totalTokens,
|
|
415
615
|
scaled: scaling.scaled,
|
|
416
616
|
approximationNote: scaling.approximationNote,
|
|
617
|
+
full,
|
|
417
618
|
categories: {
|
|
418
619
|
...scaling.categories,
|
|
419
620
|
autocompactBuffer,
|
|
@@ -423,6 +624,9 @@ export function analyzeContext(
|
|
|
423
624
|
injectedFiles,
|
|
424
625
|
skills: breakdown.skills,
|
|
425
626
|
guidelines: breakdown.guidelines,
|
|
627
|
+
guidelineBullets,
|
|
628
|
+
guidelineSources: breakdown.guidelineSources,
|
|
629
|
+
toolSnippetDetails: breakdown.toolSnippetDetails,
|
|
426
630
|
toolDefinitions,
|
|
427
631
|
compaction,
|
|
428
632
|
providerSections: collectProviderData(),
|
package/src/context.ts
CHANGED
|
@@ -15,9 +15,10 @@ export default function contextExtension(pi: ExtensionAPI) {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
pi.registerCommand("supi-context", {
|
|
18
|
-
description: "Show detailed context usage",
|
|
19
|
-
handler: async (
|
|
20
|
-
const
|
|
18
|
+
description: "Show detailed context usage. Pass 'full' to show all guideline bullets.",
|
|
19
|
+
handler: async (args, ctx) => {
|
|
20
|
+
const full = args.trim() === "full";
|
|
21
|
+
const analysis = analyzeContext(ctx, pi, cachedOptions, full);
|
|
21
22
|
const shortContent = `${formatTokens(analysis.totalTokens ?? 0)} / ${formatTokens(analysis.contextWindow)} tokens`;
|
|
22
23
|
|
|
23
24
|
pi.sendMessage({
|