@mrclrchtr/supi-claude-md 1.1.3 → 1.3.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 +1 -9
- package/node_modules/@mrclrchtr/supi-core/README.md +9 -3
- package/node_modules/@mrclrchtr/supi-core/package.json +12 -2
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -0
- package/package.json +9 -4
- package/src/api.ts +1 -0
- package/src/claude-md.ts +14 -34
- package/src/config.ts +1 -9
- package/src/extension.ts +1 -0
- package/src/settings-registration.ts +1 -50
- package/src/state.ts +10 -34
- package/src/subdirectory.ts +6 -49
package/README.md
CHANGED
|
@@ -10,11 +10,7 @@ Then it helps you keep those files in shape — audit quality, flag stale sectio
|
|
|
10
10
|
|
|
11
11
|
### Context that travels
|
|
12
12
|
|
|
13
|
-
Reads, writes, edits, LSP operations — any time the agent touches a file, it picks up the nearest `CLAUDE.md` or `AGENTS.md` in that directory.
|
|
14
|
-
|
|
15
|
-
### Smart about when to refresh
|
|
16
|
-
|
|
17
|
-
First-time discovery always injects. Re-reads wait a configurable number of turns and skip when the context window is too full. No flooding.
|
|
13
|
+
Reads, writes, edits, LSP operations — any time the agent touches a file, it picks up the nearest `CLAUDE.md` or `AGENTS.md` in that directory. Each subdirectory's context is injected once (on first discovery) and available for the rest of the session.
|
|
18
14
|
|
|
19
15
|
### CLAUDE.md maintenance
|
|
20
16
|
|
|
@@ -37,14 +33,10 @@ Configure via `/supi-settings` or directly in config:
|
|
|
37
33
|
{
|
|
38
34
|
"claude-md": {
|
|
39
35
|
"subdirs": true,
|
|
40
|
-
"rereadInterval": 3,
|
|
41
|
-
"contextThreshold": 80,
|
|
42
36
|
"fileNames": ["CLAUDE.md", "AGENTS.md"]
|
|
43
37
|
}
|
|
44
38
|
}
|
|
45
39
|
```
|
|
46
40
|
|
|
47
41
|
- `subdirs` — toggle subdirectory discovery on/off
|
|
48
|
-
- `rereadInterval` — turns between re-reading a directory's context (0 = never re-read)
|
|
49
|
-
- `contextThreshold` — skip re-reads when context usage is above this percent
|
|
50
42
|
- `fileNames` — which filenames to look for (comma-separated)
|
|
@@ -12,7 +12,12 @@ pnpm add @mrclrchtr/supi-core
|
|
|
12
12
|
|
|
13
13
|
## Package role
|
|
14
14
|
|
|
15
|
-
`@mrclrchtr/supi-core`
|
|
15
|
+
`@mrclrchtr/supi-core` now has two explicit surfaces:
|
|
16
|
+
|
|
17
|
+
- `@mrclrchtr/supi-core/api` — shared library helpers for other SuPi packages
|
|
18
|
+
- `@mrclrchtr/supi-core/extension` — a minimal pi extension that registers `/supi-settings`
|
|
19
|
+
|
|
20
|
+
`pi.extensions` still points at the real file path `./src/extension.ts` inside the package. The `/api` and `/extension` paths are consumer-facing package exports, not manifest aliases.
|
|
16
21
|
|
|
17
22
|
## What it provides
|
|
18
23
|
|
|
@@ -59,7 +64,7 @@ Main helpers:
|
|
|
59
64
|
## Example
|
|
60
65
|
|
|
61
66
|
```ts
|
|
62
|
-
import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core";
|
|
67
|
+
import { loadSupiConfig, registerConfigSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
|
|
63
68
|
|
|
64
69
|
const config = loadSupiConfig("my-extension", process.cwd(), {
|
|
65
70
|
enabled: true,
|
|
@@ -87,4 +92,5 @@ const message = wrapExtensionContext("my-extension", "hello", {
|
|
|
87
92
|
|
|
88
93
|
## Source
|
|
89
94
|
|
|
90
|
-
-
|
|
95
|
+
- Library surface: `src/api.ts`
|
|
96
|
+
- Extension surface: `src/extension.ts`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,5 +30,15 @@
|
|
|
30
30
|
"optional": true
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
"main": "src/
|
|
33
|
+
"main": "src/api.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
"./api": "./src/api.ts",
|
|
36
|
+
"./extension": "./src/extension.ts",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./src/extension.ts"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
34
44
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// supi-core — shared infrastructure for SuPi extensions.
|
|
2
|
+
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
|
+
// and settings registry for supi-wide TUI settings.
|
|
4
|
+
|
|
5
|
+
export type { SupiConfigLocation, SupiConfigOptions } from "./config.ts";
|
|
6
|
+
export {
|
|
7
|
+
loadSupiConfig,
|
|
8
|
+
loadSupiConfigForScope,
|
|
9
|
+
removeSupiConfigKey,
|
|
10
|
+
writeSupiConfig,
|
|
11
|
+
} from "./config.ts";
|
|
12
|
+
export type { ConfigSettingsHelpers, ConfigSettingsOptions } from "./config-settings.ts";
|
|
13
|
+
export { registerConfigSettings } from "./config-settings.ts";
|
|
14
|
+
export type { ContextMessageLike } from "./context-messages.ts";
|
|
15
|
+
export {
|
|
16
|
+
findLastUserMessageIndex,
|
|
17
|
+
getContextToken,
|
|
18
|
+
getPromptContent,
|
|
19
|
+
pruneAndReorderContextMessages,
|
|
20
|
+
restorePromptContent,
|
|
21
|
+
} from "./context-messages.ts";
|
|
22
|
+
export type { ContextProvider } from "./context-provider-registry.ts";
|
|
23
|
+
export {
|
|
24
|
+
clearRegisteredContextProviders,
|
|
25
|
+
getRegisteredContextProviders,
|
|
26
|
+
registerContextProvider,
|
|
27
|
+
} from "./context-provider-registry.ts";
|
|
28
|
+
export { wrapExtensionContext } from "./context-tag.ts";
|
|
29
|
+
export type {
|
|
30
|
+
DebugAgentAccess,
|
|
31
|
+
DebugEvent,
|
|
32
|
+
DebugEventInput,
|
|
33
|
+
DebugEventQuery,
|
|
34
|
+
DebugEventQueryResult,
|
|
35
|
+
DebugEventView,
|
|
36
|
+
DebugLevel,
|
|
37
|
+
DebugNotifyLevel,
|
|
38
|
+
DebugRegistryConfig,
|
|
39
|
+
DebugSummary,
|
|
40
|
+
} from "./debug-registry.ts";
|
|
41
|
+
export {
|
|
42
|
+
clearDebugEvents,
|
|
43
|
+
configureDebugRegistry,
|
|
44
|
+
DEBUG_REGISTRY_DEFAULTS,
|
|
45
|
+
getDebugEvents,
|
|
46
|
+
getDebugRegistryConfig,
|
|
47
|
+
getDebugSummary,
|
|
48
|
+
recordDebugEvent,
|
|
49
|
+
redactDebugData,
|
|
50
|
+
resetDebugRegistry,
|
|
51
|
+
} from "./debug-registry.ts";
|
|
52
|
+
export type { KnownRootEntry } from "./project-roots.ts";
|
|
53
|
+
export {
|
|
54
|
+
buildKnownRootsMap,
|
|
55
|
+
byPathDepth,
|
|
56
|
+
dedupeTopmostRoots,
|
|
57
|
+
findProjectRoot,
|
|
58
|
+
isWithin,
|
|
59
|
+
isWithinOrEqual,
|
|
60
|
+
mergeKnownRoots,
|
|
61
|
+
resolveKnownRoot,
|
|
62
|
+
segmentCount,
|
|
63
|
+
sortRootsBySpecificity,
|
|
64
|
+
walkProject,
|
|
65
|
+
} from "./project-roots.ts";
|
|
66
|
+
export { getActiveBranchEntries } from "./session-utils.ts";
|
|
67
|
+
export { registerSettingsCommand } from "./settings-command.ts";
|
|
68
|
+
export type { SettingsScope, SettingsSection } from "./settings-registry.ts";
|
|
69
|
+
export {
|
|
70
|
+
clearRegisteredSettings,
|
|
71
|
+
getRegisteredSettings,
|
|
72
|
+
registerSettings,
|
|
73
|
+
} from "./settings-registry.ts";
|
|
74
|
+
export { createInputSubmenu, openSettingsOverlay } from "./settings-ui.ts";
|
|
75
|
+
export type { TitleTarget } from "./terminal.ts";
|
|
76
|
+
export {
|
|
77
|
+
DONE_SYMBOL,
|
|
78
|
+
formatTitle,
|
|
79
|
+
signalBell,
|
|
80
|
+
signalDone,
|
|
81
|
+
signalWaiting,
|
|
82
|
+
WAITING_SYMBOL,
|
|
83
|
+
} from "./terminal.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerSettingsCommand as default } from "./settings-command.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-claude-md",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "SuPi claude-md extension — automatic subdirectory context injection for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"!__tests__"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mrclrchtr/supi-core": "1.
|
|
24
|
+
"@mrclrchtr/supi-core": "1.3.0"
|
|
25
25
|
},
|
|
26
26
|
"bundledDependencies": [
|
|
27
27
|
"@mrclrchtr/supi-core"
|
|
@@ -40,8 +40,13 @@
|
|
|
40
40
|
},
|
|
41
41
|
"pi": {
|
|
42
42
|
"extensions": [
|
|
43
|
-
"./src/
|
|
43
|
+
"./src/extension.ts"
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
|
-
"main": "src/
|
|
46
|
+
"main": "src/api.ts",
|
|
47
|
+
"exports": {
|
|
48
|
+
"./api": "./src/api.ts",
|
|
49
|
+
"./extension": "./src/extension.ts",
|
|
50
|
+
"./package.json": "./package.json"
|
|
51
|
+
}
|
|
47
52
|
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./claude-md.ts";
|
package/src/claude-md.ts
CHANGED
|
@@ -13,17 +13,17 @@ import type {
|
|
|
13
13
|
ExtensionContext,
|
|
14
14
|
SessionCompactEvent,
|
|
15
15
|
SessionStartEvent,
|
|
16
|
-
TurnEndEvent,
|
|
17
16
|
} from "@earendil-works/pi-coding-agent";
|
|
18
17
|
import { loadClaudeMdConfig } from "./config.ts";
|
|
18
|
+
import type { DiscoveredContextFile } from "./discovery.ts";
|
|
19
19
|
import {
|
|
20
20
|
extractPathFromToolEvent,
|
|
21
21
|
filterAlreadyLoaded,
|
|
22
22
|
findSubdirContextFiles,
|
|
23
23
|
} from "./discovery.ts";
|
|
24
24
|
import { registerClaudeMdSettings } from "./settings-registration.ts";
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
25
|
+
import type { ClaudeMdState } from "./state.ts";
|
|
26
|
+
import { createInitialState, reconstructState } from "./state.ts";
|
|
27
27
|
import { formatSubdirContext, shouldInjectSubdir } from "./subdirectory.ts";
|
|
28
28
|
|
|
29
29
|
const baseDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -39,10 +39,8 @@ export default function claudeMdExtension(pi: ExtensionAPI) {
|
|
|
39
39
|
|
|
40
40
|
try {
|
|
41
41
|
const branch = ctx.sessionManager.getBranch();
|
|
42
|
-
|
|
43
42
|
if (branch.length > 0) {
|
|
44
43
|
const reconstructed = reconstructState(branch);
|
|
45
|
-
state.completedTurns = reconstructed.completedTurns;
|
|
46
44
|
state.injectedDirs = reconstructed.injectedDirs;
|
|
47
45
|
}
|
|
48
46
|
} catch {
|
|
@@ -50,15 +48,6 @@ export default function claudeMdExtension(pi: ExtensionAPI) {
|
|
|
50
48
|
}
|
|
51
49
|
});
|
|
52
50
|
|
|
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
51
|
// ── Compaction ─────────────────────────────────────────────
|
|
63
52
|
|
|
64
53
|
pi.on("session_compact", async (_event: SessionCompactEvent, _ctx: ExtensionContext) => {
|
|
@@ -96,17 +85,11 @@ export default function claudeMdExtension(pi: ExtensionAPI) {
|
|
|
96
85
|
);
|
|
97
86
|
if (found.length === 0) return;
|
|
98
87
|
|
|
99
|
-
const dirsToInject =
|
|
100
|
-
injectedDirs: state.injectedDirs,
|
|
101
|
-
currentTurn: state.completedTurns,
|
|
102
|
-
rereadInterval: config.rereadInterval,
|
|
103
|
-
contextThreshold: config.contextThreshold,
|
|
104
|
-
contextUsage: _ctx.getContextUsage() as ContextUsage | undefined,
|
|
105
|
-
});
|
|
88
|
+
const dirsToInject = collectFreshDirs(found, state.injectedDirs);
|
|
106
89
|
if (dirsToInject.size === 0) return;
|
|
107
90
|
|
|
108
91
|
const filesToInject = Array.from(dirsToInject.values()).flat();
|
|
109
|
-
const contextText = formatSubdirContext(filesToInject
|
|
92
|
+
const contextText = formatSubdirContext(filesToInject);
|
|
110
93
|
if (!contextText) return;
|
|
111
94
|
|
|
112
95
|
updateInjectedDirTracking(state, dirsToInject);
|
|
@@ -135,13 +118,13 @@ function captureNativePaths(
|
|
|
135
118
|
}
|
|
136
119
|
}
|
|
137
120
|
|
|
138
|
-
function
|
|
139
|
-
found:
|
|
140
|
-
|
|
141
|
-
): Map<string,
|
|
142
|
-
const dirsToInject = new Map<string,
|
|
121
|
+
function collectFreshDirs(
|
|
122
|
+
found: DiscoveredContextFile[],
|
|
123
|
+
injectedDirs: Set<string>,
|
|
124
|
+
): Map<string, DiscoveredContextFile[]> {
|
|
125
|
+
const dirsToInject = new Map<string, DiscoveredContextFile[]>();
|
|
143
126
|
for (const file of found) {
|
|
144
|
-
if (shouldInjectSubdir(file.dir,
|
|
127
|
+
if (shouldInjectSubdir(file.dir, injectedDirs)) {
|
|
145
128
|
const existing = dirsToInject.get(file.dir) ?? [];
|
|
146
129
|
existing.push(file);
|
|
147
130
|
dirsToInject.set(file.dir, existing);
|
|
@@ -152,12 +135,9 @@ function collectStaleDirs(
|
|
|
152
135
|
|
|
153
136
|
function updateInjectedDirTracking(
|
|
154
137
|
state: ClaudeMdState,
|
|
155
|
-
dirsToInject: Map<string,
|
|
138
|
+
dirsToInject: Map<string, DiscoveredContextFile[]>,
|
|
156
139
|
): void {
|
|
157
|
-
for (const
|
|
158
|
-
|
|
159
|
-
if (firstFile) {
|
|
160
|
-
state.injectedDirs.set(dir, { turn: state.completedTurns, file: firstFile.relativePath });
|
|
161
|
-
}
|
|
140
|
+
for (const dir of dirsToInject.keys()) {
|
|
141
|
+
state.injectedDirs.add(dir);
|
|
162
142
|
}
|
|
163
143
|
}
|
package/src/config.ts
CHANGED
|
@@ -2,19 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Config shape (in supi shared config, "claude-md" section):
|
|
4
4
|
// {
|
|
5
|
-
// "rereadInterval": 3, // turns between subdirectory re-reads (0 = off)
|
|
6
|
-
// "contextThreshold": 80, // skip injection when context % >= threshold
|
|
7
5
|
// "subdirs": true, // enable subdirectory context discovery
|
|
8
6
|
// "fileNames": ["CLAUDE.md", "AGENTS.md"] // context file names to look for
|
|
9
7
|
// }
|
|
10
8
|
|
|
11
|
-
import { loadSupiConfig } from "@mrclrchtr/supi-core";
|
|
9
|
+
import { loadSupiConfig } from "@mrclrchtr/supi-core/api";
|
|
12
10
|
|
|
13
11
|
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
12
|
/** Enable subdirectory context discovery. Default: true */
|
|
19
13
|
subdirs: boolean;
|
|
20
14
|
/** Context file names to look for (first match per directory). Default: ["CLAUDE.md", "AGENTS.md"] */
|
|
@@ -22,8 +16,6 @@ export interface ClaudeMdConfig {
|
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
export const CLAUDE_MD_DEFAULTS: ClaudeMdConfig = {
|
|
25
|
-
rereadInterval: 3,
|
|
26
|
-
contextThreshold: 80,
|
|
27
19
|
subdirs: true,
|
|
28
20
|
fileNames: ["CLAUDE.md", "AGENTS.md"],
|
|
29
21
|
};
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./claude-md.ts";
|
|
@@ -5,33 +5,9 @@ import {
|
|
|
5
5
|
type ConfigSettingsHelpers,
|
|
6
6
|
createInputSubmenu,
|
|
7
7
|
registerConfigSettings,
|
|
8
|
-
} from "@mrclrchtr/supi-core";
|
|
8
|
+
} from "@mrclrchtr/supi-core/api";
|
|
9
9
|
import { CLAUDE_MD_DEFAULTS, type ClaudeMdConfig } from "./config.ts";
|
|
10
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
11
|
// ── Settings registration ────────────────────────────────────
|
|
36
12
|
|
|
37
13
|
export function registerClaudeMdSettings(): void {
|
|
@@ -58,16 +34,6 @@ function handleSettingChange(
|
|
|
58
34
|
helpers.set("subdirs", value === "on");
|
|
59
35
|
break;
|
|
60
36
|
}
|
|
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
37
|
case "fileNames": {
|
|
72
38
|
const names = value
|
|
73
39
|
.split(",")
|
|
@@ -92,21 +58,6 @@ function buildClaudeMdSettingItems(settings: ClaudeMdConfig): SettingItem[] {
|
|
|
92
58
|
currentValue: settings.subdirs ? "on" : "off",
|
|
93
59
|
values: ["on", "off"],
|
|
94
60
|
},
|
|
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
61
|
{
|
|
111
62
|
id: "fileNames",
|
|
112
63
|
label: "Context File Names",
|
package/src/state.ts
CHANGED
|
@@ -5,18 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
7
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
8
|
export interface ClaudeMdState {
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
/** Map of directory path → injection info */
|
|
19
|
-
injectedDirs: Map<string, InjectedDir>;
|
|
9
|
+
/** Set of directory paths whose context has already been injected */
|
|
10
|
+
injectedDirs: Set<string>;
|
|
20
11
|
/** Set of paths already loaded by pi natively (dedup) */
|
|
21
12
|
nativeContextPaths: Set<string>;
|
|
22
13
|
/** Whether this is the first before_agent_start (for native path capture) */
|
|
@@ -25,41 +16,27 @@ export interface ClaudeMdState {
|
|
|
25
16
|
|
|
26
17
|
export function createInitialState(): ClaudeMdState {
|
|
27
18
|
return {
|
|
28
|
-
|
|
29
|
-
injectedDirs: new Map(),
|
|
19
|
+
injectedDirs: new Set(),
|
|
30
20
|
nativeContextPaths: new Set(),
|
|
31
21
|
firstAgentStart: true,
|
|
32
22
|
};
|
|
33
23
|
}
|
|
34
24
|
|
|
35
|
-
const CONTEXT_TAG_REGEX =
|
|
36
|
-
/<extension-context\s+source="supi-claude-md"\s+file="([^"]+)"\s+turn="(\d+)">/g;
|
|
25
|
+
const CONTEXT_TAG_REGEX = /<extension-context\s+source="supi-claude-md"\s+file="([^"]+)"[^>]*>/g;
|
|
37
26
|
|
|
38
27
|
export function reconstructState(branch: SessionEntry[]): {
|
|
39
|
-
|
|
40
|
-
injectedDirs: Map<string, InjectedDir>;
|
|
28
|
+
injectedDirs: Set<string>;
|
|
41
29
|
} {
|
|
42
|
-
|
|
43
|
-
const injectedDirs = new Map<string, InjectedDir>();
|
|
30
|
+
const injectedDirs = new Set<string>();
|
|
44
31
|
|
|
45
32
|
for (const entry of branch) {
|
|
46
|
-
if (isCompletedAssistantTurn(entry)) completedTurns++;
|
|
47
|
-
|
|
48
33
|
const toolResultContent = getToolResultContent(entry);
|
|
49
34
|
if (toolResultContent) {
|
|
50
35
|
extractInjectedDirs(toolResultContent, injectedDirs);
|
|
51
36
|
}
|
|
52
37
|
}
|
|
53
38
|
|
|
54
|
-
return {
|
|
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
|
-
);
|
|
39
|
+
return { injectedDirs };
|
|
63
40
|
}
|
|
64
41
|
|
|
65
42
|
function getToolResultContent(entry: SessionEntry): unknown {
|
|
@@ -69,7 +46,7 @@ function getToolResultContent(entry: SessionEntry): unknown {
|
|
|
69
46
|
return entry.message.content;
|
|
70
47
|
}
|
|
71
48
|
|
|
72
|
-
function extractInjectedDirs(content: unknown, injectedDirs:
|
|
49
|
+
function extractInjectedDirs(content: unknown, injectedDirs: Set<string>): void {
|
|
73
50
|
const parts = content as Array<{ type?: string; text?: string }> | undefined;
|
|
74
51
|
if (!parts) return;
|
|
75
52
|
|
|
@@ -80,15 +57,14 @@ function extractInjectedDirs(content: unknown, injectedDirs: Map<string, Injecte
|
|
|
80
57
|
}
|
|
81
58
|
}
|
|
82
59
|
|
|
83
|
-
function parseContextTags(text: string, injectedDirs:
|
|
60
|
+
function parseContextTags(text: string, injectedDirs: Set<string>): void {
|
|
84
61
|
const matches = text.matchAll(CONTEXT_TAG_REGEX);
|
|
85
62
|
for (const match of matches) {
|
|
86
63
|
const file = match[1];
|
|
87
|
-
const turn = Number.parseInt(match[2] ?? "0", 10);
|
|
88
64
|
if (file) {
|
|
89
65
|
const lastSlash = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\"));
|
|
90
66
|
const dir = lastSlash >= 0 ? file.substring(0, lastSlash) : ".";
|
|
91
|
-
injectedDirs.
|
|
67
|
+
injectedDirs.add(dir);
|
|
92
68
|
}
|
|
93
69
|
}
|
|
94
70
|
}
|
package/src/subdirectory.ts
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
// Subdirectory context injection logic.
|
|
2
2
|
//
|
|
3
3
|
// Handles formatting discovered context files into <extension-context> blocks
|
|
4
|
-
// and determining whether injection should occur
|
|
4
|
+
// and determining whether injection should occur.
|
|
5
5
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
|
-
import { wrapExtensionContext } from "@mrclrchtr/supi-core";
|
|
7
|
+
import { wrapExtensionContext } from "@mrclrchtr/supi-core/api";
|
|
8
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
9
|
|
|
20
10
|
/**
|
|
21
11
|
* Format discovered context files into <extension-context> blocks.
|
|
22
12
|
* Each file is read and wrapped individually.
|
|
23
13
|
*/
|
|
24
|
-
export function formatSubdirContext(files: DiscoveredContextFile[]
|
|
14
|
+
export function formatSubdirContext(files: DiscoveredContextFile[]): string {
|
|
25
15
|
const parts: string[] = [];
|
|
26
16
|
|
|
27
17
|
for (const file of files) {
|
|
@@ -31,7 +21,6 @@ export function formatSubdirContext(files: DiscoveredContextFile[], turn: number
|
|
|
31
21
|
parts.push(
|
|
32
22
|
wrapExtensionContext("supi-claude-md", content, {
|
|
33
23
|
file: file.relativePath,
|
|
34
|
-
turn,
|
|
35
24
|
}),
|
|
36
25
|
);
|
|
37
26
|
}
|
|
@@ -43,42 +32,10 @@ export function formatSubdirContext(files: DiscoveredContextFile[], turn: number
|
|
|
43
32
|
return parts.join("\n\n");
|
|
44
33
|
}
|
|
45
34
|
|
|
46
|
-
export interface InjectionCheckOptions {
|
|
47
|
-
injectedDirs: Map<string, InjectedDir>;
|
|
48
|
-
currentTurn: number;
|
|
49
|
-
rereadInterval: number;
|
|
50
|
-
contextThreshold: number;
|
|
51
|
-
contextUsage?: ContextUsage;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
35
|
/**
|
|
55
36
|
* 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)
|
|
37
|
+
* Returns true only if the directory has not been injected yet.
|
|
61
38
|
*/
|
|
62
|
-
export function shouldInjectSubdir(dir: string,
|
|
63
|
-
|
|
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;
|
|
39
|
+
export function shouldInjectSubdir(dir: string, injectedDirs: Set<string>): boolean {
|
|
40
|
+
return !injectedDirs.has(dir);
|
|
84
41
|
}
|