@mrclrchtr/supi-claude-md 1.1.2 → 1.2.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 CHANGED
@@ -1,73 +1,42 @@
1
1
  # @mrclrchtr/supi-claude-md
2
2
 
3
- Automatic `CLAUDE.md` / `AGENTS.md` context management for the [pi coding agent](https://github.com/earendil-works/pi).
3
+ Subdirectory context for PI your project's conventions follow the agent wherever it goes.
4
4
 
5
- ## Install
6
-
7
- ```bash
8
- pi install npm:@mrclrchtr/supi-claude-md
9
- ```
5
+ Pi loads your root `CLAUDE.md` by default. Claude-MD extends that downward: when the agent reaches into `src/auth/`, it picks up `src/auth/CLAUDE.md` too. Conventions are where the code is, not just at the project root.
10
6
 
11
- ## What it adds
7
+ Then it helps you keep those files in shape — audit quality, flag stale sections, and capture session learnings with your approval.
12
8
 
13
- This extension adds **subdirectory context discovery**: it injects `CLAUDE.md` / `AGENTS.md` from subdirectories when the agent touches files there.
14
-
15
- Pi loads root and ancestor instruction files natively into the system prompt on every turn. This package only handles subdirectories below `cwd`. To pick up root instruction file edits mid-session, use pi's `/reload` command or restart the session.
16
-
17
- If your install surface includes the shared SuPi settings command (for example via `@mrclrchtr/supi`), this package contributes a Claude-MD section there:
18
-
19
- ```text
20
- /supi-settings
21
- ```
9
+ ## What you get
22
10
 
23
- Inside `/supi-settings`, Claude-MD contributes:
11
+ ### Context that travels
24
12
 
25
- - `Subdirectory Discovery`: on/off toggle
26
- - `Subdirectory Re-read Interval`: text input; enter a number of turns or `0` to disable subdirectory re-reads
27
- - `Context Threshold`: common percentage values from `0` to `100`
28
- - `Context File Names`: comma-separated text input; empty input restores the default filenames
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.
29
14
 
30
- This package bundles two skills:
15
+ ### CLAUDE.md maintenance
31
16
 
32
- - `claude-md-improver`: audit CLAUDE.md files, evaluate quality, and propose targeted updates. SuPi-aware: compares CLAUDE.md sections against a synthesized context baseline from `supi-code-intelligence` and `supi-claude-md`, then flags redundant content and suggests compression when only part of a section should stay
33
- - `claude-md-revision`: capture session learnings into CLAUDE.md with user approval
17
+ Two bundled skills:
34
18
 
35
- ## Configuration
19
+ - **claude-md-improver** — audit every CLAUDE.md in your repo. Flags redundancy, stale sections, and content already covered by SuPi's auto-injected context. Suggests targeted updates.
20
+ - **claude-md-revision** — capture what you learned this session into CLAUDE.md. Ask the agent to remember a pattern, convention, or gotcha — it proposes the edit, you approve.
36
21
 
37
- Configuration uses the shared SuPi config system.
22
+ ## Install
38
23
 
39
- Config file locations:
24
+ ```bash
25
+ pi install npm:@mrclrchtr/supi-claude-md
26
+ ```
40
27
 
41
- - global: `~/.pi/agent/supi/config.json`
42
- - project: `.pi/supi/config.json`
28
+ ## Settings
43
29
 
44
- Use the `claude-md` section:
30
+ Configure via `/supi-settings` or directly in config:
45
31
 
46
32
  ```json
47
33
  {
48
34
  "claude-md": {
49
- "rereadInterval": 3,
50
- "contextThreshold": 80,
51
35
  "subdirs": true,
52
36
  "fileNames": ["CLAUDE.md", "AGENTS.md"]
53
37
  }
54
38
  }
55
39
  ```
56
40
 
57
- Options:
58
-
59
- - `rereadInterval`: turns between re-reading previously injected subdirectory context; `0` disables subdirectory re-reads (first-time discovery is unaffected)
60
- - `contextThreshold`: skip subdirectory re-injection when context usage is at or above this percent; `100` disables context gating; first-time discovery is always allowed
61
- - `subdirs`: enable or disable subdirectory discovery
62
- - `fileNames`: ordered list of context filenames to search for
63
-
64
- ## Requirements
65
-
66
- - `@earendil-works/pi-coding-agent`
67
- - `@earendil-works/pi-tui`
68
- - `@mrclrchtr/supi-core`
69
-
70
- ## Source
71
-
72
- - Entrypoint: `src/claude-md.ts`
73
- - Skills: `skills/claude-md-improver/`, `skills/claude-md-revision/`
41
+ - `subdirs` — toggle subdirectory discovery on/off
42
+ - `fileNames` — which filenames to look for (comma-separated)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,5 +22,13 @@
22
22
  "@earendil-works/pi-coding-agent": "*",
23
23
  "@earendil-works/pi-tui": "*"
24
24
  },
25
+ "peerDependenciesMeta": {
26
+ "@earendil-works/pi-coding-agent": {
27
+ "optional": true
28
+ },
29
+ "@earendil-works/pi-tui": {
30
+ "optional": true
31
+ }
32
+ },
25
33
  "main": "src/index.ts"
26
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-claude-md",
3
- "version": "1.1.2",
3
+ "version": "1.2.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.1.2"
24
+ "@mrclrchtr/supi-core": "1.2.0"
25
25
  },
26
26
  "bundledDependencies": [
27
27
  "@mrclrchtr/supi-core"
@@ -30,6 +30,14 @@
30
30
  "@earendil-works/pi-coding-agent": "*",
31
31
  "@earendil-works/pi-tui": "*"
32
32
  },
33
+ "peerDependenciesMeta": {
34
+ "@earendil-works/pi-coding-agent": {
35
+ "optional": true
36
+ },
37
+ "@earendil-works/pi-tui": {
38
+ "optional": true
39
+ }
40
+ },
33
41
  "pi": {
34
42
  "extensions": [
35
43
  "./src/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 { type ClaudeMdState, createInitialState, reconstructState } from "./state.ts";
26
- import type { ContextUsage, InjectionCheckOptions } from "./subdirectory.ts";
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 = 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
- });
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, state.completedTurns);
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 collectStaleDirs(
139
- found: ReturnType<typeof findSubdirContextFiles>,
140
- injectionOpts: InjectionCheckOptions,
141
- ): Map<string, typeof found> {
142
- const dirsToInject = new Map<string, typeof found>();
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, injectionOpts)) {
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, Array<{ dir: string; relativePath: string }>>,
138
+ dirsToInject: Map<string, DiscoveredContextFile[]>,
156
139
  ): 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
- }
140
+ for (const dir of dirsToInject.keys()) {
141
+ state.injectedDirs.add(dir);
162
142
  }
163
143
  }
package/src/config.ts CHANGED
@@ -2,8 +2,6 @@
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
  // }
@@ -11,10 +9,6 @@
11
9
  import { loadSupiConfig } from "@mrclrchtr/supi-core";
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
  };
@@ -8,30 +8,6 @@ import {
8
8
  } from "@mrclrchtr/supi-core";
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
- /** Count of completed assistant turns (stopReason: "stop") */
17
- completedTurns: number;
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
- completedTurns: 0,
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
- completedTurns: number;
40
- injectedDirs: Map<string, InjectedDir>;
28
+ injectedDirs: Set<string>;
41
29
  } {
42
- let completedTurns = 0;
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 { 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
- );
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: Map<string, InjectedDir>): void {
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: Map<string, InjectedDir>): void {
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.set(dir, { turn, file });
67
+ injectedDirs.add(dir);
92
68
  }
93
69
  }
94
70
  }
@@ -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 based on staleness.
4
+ // and determining whether injection should occur.
5
5
 
6
6
  import * as fs from "node:fs";
7
7
  import { wrapExtensionContext } from "@mrclrchtr/supi-core";
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[], turn: number): string {
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, 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;
39
+ export function shouldInjectSubdir(dir: string, injectedDirs: Set<string>): boolean {
40
+ return !injectedDirs.has(dir);
84
41
  }