@mrclrchtr/supi-claude-md 0.1.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.
Files changed (34) hide show
  1. package/README.md +73 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/package.json +44 -0
  19. package/skills/claude-md-improver/SKILL.md +253 -0
  20. package/skills/claude-md-improver/references/quality-criteria.md +114 -0
  21. package/skills/claude-md-improver/references/templates.md +300 -0
  22. package/skills/claude-md-improver/references/update-guidelines.md +218 -0
  23. package/skills/claude-md-revision/SKILL.md +126 -0
  24. package/skills/claude-md-revision/evals/evals.json +43 -0
  25. package/skills/claude-md-revision/references/quality-criteria.md +114 -0
  26. package/skills/claude-md-revision/references/templates.md +300 -0
  27. package/skills/claude-md-revision/references/update-guidelines.md +218 -0
  28. package/src/claude-md.ts +163 -0
  29. package/src/config.ts +33 -0
  30. package/src/discovery.ts +133 -0
  31. package/src/index.ts +1 -0
  32. package/src/settings-registration.ts +119 -0
  33. package/src/state.ts +94 -0
  34. package/src/subdirectory.ts +84 -0
@@ -0,0 +1,163 @@
1
+ // supi-claude-md — subdirectory context injection for pi.
2
+ //
3
+ // Subdirectory discovery: inject CLAUDE.md/AGENTS.md from subdirectories
4
+ // below cwd when the agent accesses files there (via tool_result augmentation).
5
+ // Root/ancestor context files are owned by pi's native system prompt and are
6
+ // never re-injected by this extension.
7
+
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import type {
11
+ BeforeAgentStartEvent,
12
+ ExtensionAPI,
13
+ ExtensionContext,
14
+ SessionCompactEvent,
15
+ SessionStartEvent,
16
+ TurnEndEvent,
17
+ } from "@earendil-works/pi-coding-agent";
18
+ import { loadClaudeMdConfig } from "./config.ts";
19
+ import {
20
+ extractPathFromToolEvent,
21
+ filterAlreadyLoaded,
22
+ findSubdirContextFiles,
23
+ } from "./discovery.ts";
24
+ import { registerClaudeMdSettings } from "./settings-registration.ts";
25
+ import { type ClaudeMdState, createInitialState, reconstructState } from "./state.ts";
26
+ import type { ContextUsage, InjectionCheckOptions } from "./subdirectory.ts";
27
+ import { formatSubdirContext, shouldInjectSubdir } from "./subdirectory.ts";
28
+
29
+ const baseDir = dirname(dirname(fileURLToPath(import.meta.url)));
30
+
31
+ export default function claudeMdExtension(pi: ExtensionAPI) {
32
+ registerClaudeMdSettings();
33
+ const state: ClaudeMdState = createInitialState();
34
+
35
+ // ── Session lifecycle ──────────────────────────────────────
36
+
37
+ pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
38
+ Object.assign(state, createInitialState());
39
+
40
+ try {
41
+ const branch = ctx.sessionManager.getBranch();
42
+
43
+ if (branch.length > 0) {
44
+ const reconstructed = reconstructState(branch);
45
+ state.completedTurns = reconstructed.completedTurns;
46
+ state.injectedDirs = reconstructed.injectedDirs;
47
+ }
48
+ } catch {
49
+ // Reconstruction failed — start fresh
50
+ }
51
+ });
52
+
53
+ // ── Turn tracking ──────────────────────────────────────────
54
+
55
+ pi.on("turn_end", async (event: TurnEndEvent, _ctx: ExtensionContext) => {
56
+ const msg = event.message as { stopReason?: string };
57
+ if (msg?.stopReason === "stop") {
58
+ state.completedTurns++;
59
+ }
60
+ });
61
+
62
+ // ── Compaction ─────────────────────────────────────────────
63
+
64
+ pi.on("session_compact", async (_event: SessionCompactEvent, _ctx: ExtensionContext) => {
65
+ state.injectedDirs.clear();
66
+ });
67
+
68
+ // ── Native context path capture (before_agent_start) ───────
69
+
70
+ pi.on("before_agent_start", async (event: BeforeAgentStartEvent, _ctx: ExtensionContext) => {
71
+ const eventWithOpts = event as BeforeAgentStartEvent & {
72
+ systemPromptOptions?: { contextFiles?: Array<{ path?: string; content?: string }> };
73
+ };
74
+
75
+ captureNativePaths(state, eventWithOpts);
76
+ // Root/ancestor context files are owned by pi's system prompt.
77
+ // SuPi never re-injects them; subdirectory injection handles directories below cwd.
78
+ });
79
+
80
+ // ── Subdirectory injection (tool_result) ───────────────────
81
+
82
+ pi.on("tool_result", async (event, _ctx) => {
83
+ const config = loadClaudeMdConfig(_ctx.cwd);
84
+ if (!config.subdirs) return;
85
+ if (event.isError) return;
86
+
87
+ const filePath = extractPathFromToolEvent(
88
+ event.toolName,
89
+ event.input as Record<string, unknown>,
90
+ );
91
+ if (!filePath) return;
92
+
93
+ const found = filterAlreadyLoaded(
94
+ findSubdirContextFiles(filePath, _ctx.cwd, config.fileNames),
95
+ state.nativeContextPaths,
96
+ );
97
+ if (found.length === 0) return;
98
+
99
+ const dirsToInject = collectStaleDirs(found, {
100
+ injectedDirs: state.injectedDirs,
101
+ currentTurn: state.completedTurns,
102
+ rereadInterval: config.rereadInterval,
103
+ contextThreshold: config.contextThreshold,
104
+ contextUsage: _ctx.getContextUsage() as ContextUsage | undefined,
105
+ });
106
+ if (dirsToInject.size === 0) return;
107
+
108
+ const filesToInject = Array.from(dirsToInject.values()).flat();
109
+ const contextText = formatSubdirContext(filesToInject, state.completedTurns);
110
+ if (!contextText) return;
111
+
112
+ updateInjectedDirTracking(state, dirsToInject);
113
+
114
+ return {
115
+ content: [...event.content, { type: "text" as const, text: contextText }],
116
+ };
117
+ });
118
+
119
+ pi.on("resources_discover", () => ({
120
+ skillPaths: [join(baseDir, "skills")],
121
+ }));
122
+ }
123
+
124
+ function captureNativePaths(
125
+ state: ClaudeMdState,
126
+ opts: { systemPromptOptions?: { contextFiles?: Array<{ path?: string; content?: string }> } },
127
+ ): void {
128
+ if (!state.firstAgentStart) return;
129
+ state.firstAgentStart = false;
130
+ const contextFiles = opts.systemPromptOptions?.contextFiles ?? [];
131
+ for (const file of contextFiles) {
132
+ if (file.path) {
133
+ state.nativeContextPaths.add(file.path);
134
+ }
135
+ }
136
+ }
137
+
138
+ function collectStaleDirs(
139
+ found: ReturnType<typeof findSubdirContextFiles>,
140
+ injectionOpts: InjectionCheckOptions,
141
+ ): Map<string, typeof found> {
142
+ const dirsToInject = new Map<string, typeof found>();
143
+ for (const file of found) {
144
+ if (shouldInjectSubdir(file.dir, injectionOpts)) {
145
+ const existing = dirsToInject.get(file.dir) ?? [];
146
+ existing.push(file);
147
+ dirsToInject.set(file.dir, existing);
148
+ }
149
+ }
150
+ return dirsToInject;
151
+ }
152
+
153
+ function updateInjectedDirTracking(
154
+ state: ClaudeMdState,
155
+ dirsToInject: Map<string, Array<{ dir: string; relativePath: string }>>,
156
+ ): void {
157
+ for (const [dir, files] of dirsToInject) {
158
+ const firstFile = files[0];
159
+ if (firstFile) {
160
+ state.injectedDirs.set(dir, { turn: state.completedTurns, file: firstFile.relativePath });
161
+ }
162
+ }
163
+ }
package/src/config.ts ADDED
@@ -0,0 +1,33 @@
1
+ // Configuration for supi-claude-md.
2
+ //
3
+ // Config shape (in supi shared config, "claude-md" section):
4
+ // {
5
+ // "rereadInterval": 3, // turns between subdirectory re-reads (0 = off)
6
+ // "contextThreshold": 80, // skip injection when context % >= threshold
7
+ // "subdirs": true, // enable subdirectory context discovery
8
+ // "fileNames": ["CLAUDE.md", "AGENTS.md"] // context file names to look for
9
+ // }
10
+
11
+ import { loadSupiConfig } from "@mrclrchtr/supi-core";
12
+
13
+ export interface ClaudeMdConfig {
14
+ /** Turns between re-reading previously injected subdirectory context. 0 = disabled. Default: 3 */
15
+ rereadInterval: number;
16
+ /** Skip injection when context window usage % >= threshold. 0 = always skip, 100 = never skip. Default: 80 */
17
+ contextThreshold: number;
18
+ /** Enable subdirectory context discovery. Default: true */
19
+ subdirs: boolean;
20
+ /** Context file names to look for (first match per directory). Default: ["CLAUDE.md", "AGENTS.md"] */
21
+ fileNames: string[];
22
+ }
23
+
24
+ export const CLAUDE_MD_DEFAULTS: ClaudeMdConfig = {
25
+ rereadInterval: 3,
26
+ contextThreshold: 80,
27
+ subdirs: true,
28
+ fileNames: ["CLAUDE.md", "AGENTS.md"],
29
+ };
30
+
31
+ export function loadClaudeMdConfig(cwd: string, homeDir?: string): ClaudeMdConfig {
32
+ return loadSupiConfig("claude-md", cwd, CLAUDE_MD_DEFAULTS, { homeDir });
33
+ }
@@ -0,0 +1,133 @@
1
+ // Context file discovery — walk up from a file's directory toward cwd,
2
+ // finding CLAUDE.md / AGENTS.md (or configured file names) along the way.
3
+
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+
7
+ export interface DiscoveredContextFile {
8
+ /** Absolute path of the context file */
9
+ absolutePath: string;
10
+ /** Path relative to cwd */
11
+ relativePath: string;
12
+ /** The directory containing this file */
13
+ dir: string;
14
+ }
15
+
16
+ /**
17
+ * Walk up from a file's directory toward cwd, collecting context files.
18
+ * Stops at cwd (does not walk above).
19
+ * Returns files ordered from nearest ancestor to farthest (closest to cwd).
20
+ */
21
+ export function findSubdirContextFiles(
22
+ filePath: string,
23
+ cwd: string,
24
+ fileNames: string[],
25
+ ): DiscoveredContextFile[] {
26
+ const absFilePath = path.resolve(cwd, filePath);
27
+ const absCwd = path.resolve(cwd);
28
+
29
+ if (!isPathWithinCwd(absFilePath, absCwd)) {
30
+ return [];
31
+ }
32
+
33
+ const startDir = resolveStartDir(absFilePath);
34
+ if (!startDir) return [];
35
+
36
+ return walkUpForContextFiles(startDir, absCwd, fileNames);
37
+ }
38
+
39
+ function isPathWithinCwd(absFilePath: string, absCwd: string): boolean {
40
+ const relativeToFile = path.relative(absCwd, absFilePath);
41
+ return !relativeToFile.startsWith("..") && !path.isAbsolute(relativeToFile);
42
+ }
43
+
44
+ function resolveStartDir(absFilePath: string): string | null {
45
+ try {
46
+ return fs.statSync(absFilePath).isDirectory() ? absFilePath : path.dirname(absFilePath);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function walkUpForContextFiles(
53
+ startDir: string,
54
+ absCwd: string,
55
+ fileNames: string[],
56
+ ): DiscoveredContextFile[] {
57
+ const results: DiscoveredContextFile[] = [];
58
+ let currentDir = startDir;
59
+
60
+ while (true) {
61
+ if (!isWithinCwd(currentDir, absCwd)) break;
62
+
63
+ const found = findFirstContextFile(currentDir, fileNames, absCwd);
64
+ if (found) results.push(found);
65
+
66
+ if (currentDir === absCwd) break;
67
+
68
+ const parent = path.dirname(currentDir);
69
+ if (parent === currentDir) break;
70
+ currentDir = parent;
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ function isWithinCwd(dir: string, absCwd: string): boolean {
77
+ const relDir = path.relative(absCwd, dir);
78
+ return !relDir.startsWith("..") && (!path.isAbsolute(relDir) || relDir === "");
79
+ }
80
+
81
+ function findFirstContextFile(
82
+ dir: string,
83
+ fileNames: string[],
84
+ absCwd: string,
85
+ ): DiscoveredContextFile | null {
86
+ for (const fileName of fileNames) {
87
+ const candidate = path.join(dir, fileName);
88
+ if (fs.existsSync(candidate)) {
89
+ return {
90
+ absolutePath: candidate,
91
+ relativePath: path.relative(absCwd, candidate),
92
+ dir,
93
+ };
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Filter out context files already loaded by pi natively.
101
+ */
102
+ export function filterAlreadyLoaded(
103
+ found: DiscoveredContextFile[],
104
+ nativeContextPaths: Set<string>,
105
+ ): DiscoveredContextFile[] {
106
+ return found.filter((f) => !nativeContextPaths.has(f.absolutePath));
107
+ }
108
+
109
+ /**
110
+ * Extract file path from a tool event input.
111
+ * Returns null for unsupported tools.
112
+ */
113
+ export function extractPathFromToolEvent(
114
+ toolName: string,
115
+ input: Record<string, unknown>,
116
+ ): string | null {
117
+ switch (toolName) {
118
+ case "read":
119
+ case "write":
120
+ case "edit":
121
+ case "ls": {
122
+ const p = input.path;
123
+ return typeof p === "string" ? p : null;
124
+ }
125
+ case "lsp":
126
+ case "tree_sitter": {
127
+ const f = input.file;
128
+ return typeof f === "string" ? f : null;
129
+ }
130
+ default:
131
+ return null;
132
+ }
133
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./claude-md.ts";
@@ -0,0 +1,119 @@
1
+ // Claude-MD settings registration for the supi settings registry.
2
+
3
+ import type { SettingItem } from "@earendil-works/pi-tui";
4
+ import {
5
+ type ConfigSettingsHelpers,
6
+ createInputSubmenu,
7
+ registerConfigSettings,
8
+ } from "@mrclrchtr/supi-core";
9
+ import { CLAUDE_MD_DEFAULTS, type ClaudeMdConfig } from "./config.ts";
10
+
11
+ const THRESHOLD_VALUES = [
12
+ "0",
13
+ "5",
14
+ "10",
15
+ "15",
16
+ "20",
17
+ "25",
18
+ "30",
19
+ "35",
20
+ "40",
21
+ "45",
22
+ "50",
23
+ "55",
24
+ "60",
25
+ "65",
26
+ "70",
27
+ "75",
28
+ "80",
29
+ "85",
30
+ "90",
31
+ "95",
32
+ "100",
33
+ ];
34
+
35
+ // ── Settings registration ────────────────────────────────────
36
+
37
+ export function registerClaudeMdSettings(): void {
38
+ registerConfigSettings({
39
+ id: "claude-md",
40
+ label: "Claude-MD",
41
+ section: "claude-md",
42
+ defaults: CLAUDE_MD_DEFAULTS,
43
+ buildItems: (_settings) => buildClaudeMdSettingItems(_settings),
44
+ // biome-ignore lint/complexity/useMaxParams: ConfigSettingsOptions interface callback
45
+ persistChange: (_scope, _cwd, settingId, value, helpers) => {
46
+ handleSettingChange(settingId, value, helpers);
47
+ },
48
+ });
49
+ }
50
+
51
+ function handleSettingChange(
52
+ settingId: string,
53
+ value: string,
54
+ helpers: ConfigSettingsHelpers,
55
+ ): void {
56
+ switch (settingId) {
57
+ case "subdirs": {
58
+ helpers.set("subdirs", value === "on");
59
+ break;
60
+ }
61
+ case "rereadInterval": {
62
+ const num = Number.parseInt(value, 10);
63
+ helpers.set("rereadInterval", Number.isNaN(num) ? 0 : num);
64
+ break;
65
+ }
66
+ case "contextThreshold": {
67
+ const num = Number.parseInt(value, 10);
68
+ helpers.set("contextThreshold", Number.isNaN(num) ? 80 : num);
69
+ break;
70
+ }
71
+ case "fileNames": {
72
+ const names = value
73
+ .split(",")
74
+ .map((s) => s.trim())
75
+ .filter((s) => s.length > 0);
76
+ if (names.length > 0) {
77
+ helpers.set("fileNames", names);
78
+ } else {
79
+ helpers.unset("fileNames");
80
+ }
81
+ break;
82
+ }
83
+ }
84
+ }
85
+
86
+ function buildClaudeMdSettingItems(settings: ClaudeMdConfig): SettingItem[] {
87
+ return [
88
+ {
89
+ id: "subdirs",
90
+ label: "Subdirectory Discovery",
91
+ description: "Inject CLAUDE.md/AGENTS.md from subdirectories when browsing files",
92
+ currentValue: settings.subdirs ? "on" : "off",
93
+ values: ["on", "off"],
94
+ },
95
+ {
96
+ id: "rereadInterval",
97
+ label: "Subdirectory Re-read Interval",
98
+ description: "Turns between re-reading previously injected subdirectory context (0 = off)",
99
+ currentValue: String(settings.rereadInterval),
100
+ submenu: (currentValue, done) =>
101
+ createInputSubmenu(currentValue, "Interval (0 = off):", done),
102
+ },
103
+ {
104
+ id: "contextThreshold",
105
+ label: "Context Threshold",
106
+ description: "Skip injection when context window usage % ≥ threshold (100 = never skip)",
107
+ currentValue: String(settings.contextThreshold),
108
+ values: THRESHOLD_VALUES,
109
+ },
110
+ {
111
+ id: "fileNames",
112
+ label: "Context File Names",
113
+ description: "File names to look for in each directory (comma-separated)",
114
+ currentValue: settings.fileNames.join(", "),
115
+ submenu: (currentValue, done) =>
116
+ createInputSubmenu(currentValue, "File names (comma-separated):", done),
117
+ },
118
+ ];
119
+ }
package/src/state.ts ADDED
@@ -0,0 +1,94 @@
1
+ // Internal state types for supi-claude-md.
2
+ //
3
+ // State is reconstructed from session history on session_start
4
+ // and mutated in-place during the session lifecycle.
5
+
6
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
7
+
8
+ export interface InjectedDir {
9
+ /** Turn number when this directory's context was last injected */
10
+ turn: number;
11
+ /** Relative path of the context file that was injected */
12
+ file: string;
13
+ }
14
+
15
+ export interface ClaudeMdState {
16
+ /** Count of completed assistant turns (stopReason: "stop") */
17
+ completedTurns: number;
18
+ /** Map of directory path → injection info */
19
+ injectedDirs: Map<string, InjectedDir>;
20
+ /** Set of paths already loaded by pi natively (dedup) */
21
+ nativeContextPaths: Set<string>;
22
+ /** Whether this is the first before_agent_start (for native path capture) */
23
+ firstAgentStart: boolean;
24
+ }
25
+
26
+ export function createInitialState(): ClaudeMdState {
27
+ return {
28
+ completedTurns: 0,
29
+ injectedDirs: new Map(),
30
+ nativeContextPaths: new Set(),
31
+ firstAgentStart: true,
32
+ };
33
+ }
34
+
35
+ const CONTEXT_TAG_REGEX =
36
+ /<extension-context\s+source="supi-claude-md"\s+file="([^"]+)"\s+turn="(\d+)">/g;
37
+
38
+ export function reconstructState(branch: SessionEntry[]): {
39
+ completedTurns: number;
40
+ injectedDirs: Map<string, InjectedDir>;
41
+ } {
42
+ let completedTurns = 0;
43
+ const injectedDirs = new Map<string, InjectedDir>();
44
+
45
+ for (const entry of branch) {
46
+ if (isCompletedAssistantTurn(entry)) completedTurns++;
47
+
48
+ const toolResultContent = getToolResultContent(entry);
49
+ if (toolResultContent) {
50
+ extractInjectedDirs(toolResultContent, injectedDirs);
51
+ }
52
+ }
53
+
54
+ return { completedTurns, injectedDirs };
55
+ }
56
+
57
+ function isCompletedAssistantTurn(entry: SessionEntry): boolean {
58
+ return (
59
+ entry.type === "message" &&
60
+ entry.message.role === "assistant" &&
61
+ entry.message.stopReason === "stop"
62
+ );
63
+ }
64
+
65
+ function getToolResultContent(entry: SessionEntry): unknown {
66
+ if (entry.type !== "message" || entry.message.role !== "toolResult") {
67
+ return undefined;
68
+ }
69
+ return entry.message.content;
70
+ }
71
+
72
+ function extractInjectedDirs(content: unknown, injectedDirs: Map<string, InjectedDir>): void {
73
+ const parts = content as Array<{ type?: string; text?: string }> | undefined;
74
+ if (!parts) return;
75
+
76
+ for (const part of parts) {
77
+ if (part.type === "text" && part.text) {
78
+ parseContextTags(part.text, injectedDirs);
79
+ }
80
+ }
81
+ }
82
+
83
+ function parseContextTags(text: string, injectedDirs: Map<string, InjectedDir>): void {
84
+ const matches = text.matchAll(CONTEXT_TAG_REGEX);
85
+ for (const match of matches) {
86
+ const file = match[1];
87
+ const turn = Number.parseInt(match[2] ?? "0", 10);
88
+ if (file) {
89
+ const lastSlash = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"));
90
+ const dir = lastSlash >= 0 ? file.substring(0, lastSlash) : ".";
91
+ injectedDirs.set(dir, { turn, file });
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,84 @@
1
+ // Subdirectory context injection logic.
2
+ //
3
+ // Handles formatting discovered context files into <extension-context> blocks
4
+ // and determining whether injection should occur based on staleness.
5
+
6
+ import * as fs from "node:fs";
7
+ import { wrapExtensionContext } from "@mrclrchtr/supi-core";
8
+ import type { DiscoveredContextFile } from "./discovery.ts";
9
+ import type { InjectedDir } from "./state.ts";
10
+
11
+ /**
12
+ * Context usage info from pi's ctx.getContextUsage().
13
+ */
14
+ export interface ContextUsage {
15
+ tokens: number | null;
16
+ contextWindow: number;
17
+ percent: number | null;
18
+ }
19
+
20
+ /**
21
+ * Format discovered context files into <extension-context> blocks.
22
+ * Each file is read and wrapped individually.
23
+ */
24
+ export function formatSubdirContext(files: DiscoveredContextFile[], turn: number): string {
25
+ const parts: string[] = [];
26
+
27
+ for (const file of files) {
28
+ try {
29
+ const content = fs.readFileSync(file.absolutePath, "utf-8").trim();
30
+ if (content) {
31
+ parts.push(
32
+ wrapExtensionContext("supi-claude-md", content, {
33
+ file: file.relativePath,
34
+ turn,
35
+ }),
36
+ );
37
+ }
38
+ } catch {
39
+ // File may have been deleted between discovery and read
40
+ }
41
+ }
42
+
43
+ return parts.join("\n\n");
44
+ }
45
+
46
+ export interface InjectionCheckOptions {
47
+ injectedDirs: Map<string, InjectedDir>;
48
+ currentTurn: number;
49
+ rereadInterval: number;
50
+ contextThreshold: number;
51
+ contextUsage?: ContextUsage;
52
+ }
53
+
54
+ /**
55
+ * Determine if subdirectory context should be injected.
56
+ * Returns true if:
57
+ * - The directory has not been injected yet (always, even under context pressure)
58
+ * - The directory was injected but is stale (turn delta >= rereadInterval)
59
+ * AND context usage is below the threshold
60
+ * - rereadInterval is 0 (disabled — always false for re-injections)
61
+ */
62
+ export function shouldInjectSubdir(dir: string, options: InjectionCheckOptions): boolean {
63
+ const { injectedDirs, currentTurn, rereadInterval, contextThreshold, contextUsage } = options;
64
+
65
+ // Never-injected directory: always inject (even when rereadInterval is 0)
66
+ // First-time discovery is always allowed regardless of context pressure
67
+ const injected = injectedDirs.get(dir);
68
+ if (!injected) return true;
69
+
70
+ // Already-injected directory: skip if reread is disabled
71
+ if (rereadInterval === 0) return false;
72
+
73
+ // Re-injection: skip when context usage is at or above threshold
74
+ if (
75
+ contextThreshold < 100 &&
76
+ contextUsage &&
77
+ contextUsage.percent != null &&
78
+ contextUsage.percent >= contextThreshold
79
+ ) {
80
+ return false;
81
+ }
82
+
83
+ return currentTurn - injected.turn >= rereadInterval;
84
+ }