nogrep 1.0.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 (48) hide show
  1. package/README.md +91 -0
  2. package/commands/init.md +241 -0
  3. package/commands/off.md +11 -0
  4. package/commands/on.md +21 -0
  5. package/commands/query.md +13 -0
  6. package/commands/status.md +15 -0
  7. package/commands/update.md +89 -0
  8. package/dist/chunk-SMUAF6SM.js +12 -0
  9. package/dist/chunk-SMUAF6SM.js.map +1 -0
  10. package/dist/query.d.ts +12 -0
  11. package/dist/query.js +272 -0
  12. package/dist/query.js.map +1 -0
  13. package/dist/settings.d.ts +6 -0
  14. package/dist/settings.js +75 -0
  15. package/dist/settings.js.map +1 -0
  16. package/dist/signals.d.ts +9 -0
  17. package/dist/signals.js +174 -0
  18. package/dist/signals.js.map +1 -0
  19. package/dist/trim.d.ts +3 -0
  20. package/dist/trim.js +266 -0
  21. package/dist/trim.js.map +1 -0
  22. package/dist/types.d.ts +141 -0
  23. package/dist/types.js +7 -0
  24. package/dist/types.js.map +1 -0
  25. package/dist/validate.d.ts +10 -0
  26. package/dist/validate.js +143 -0
  27. package/dist/validate.js.map +1 -0
  28. package/dist/write.d.ts +8 -0
  29. package/dist/write.js +267 -0
  30. package/dist/write.js.map +1 -0
  31. package/docs/ARCHITECTURE.md +239 -0
  32. package/docs/CLAUDE.md +161 -0
  33. package/docs/CONVENTIONS.md +162 -0
  34. package/docs/SPEC.md +803 -0
  35. package/docs/TASKS.md +216 -0
  36. package/hooks/hooks.json +35 -0
  37. package/hooks/pre-tool-use.sh +37 -0
  38. package/hooks/prompt-submit.sh +26 -0
  39. package/hooks/session-start.sh +21 -0
  40. package/package.json +24 -0
  41. package/scripts/query.ts +290 -0
  42. package/scripts/settings.ts +98 -0
  43. package/scripts/signals.ts +237 -0
  44. package/scripts/trim.ts +379 -0
  45. package/scripts/types.ts +186 -0
  46. package/scripts/validate.ts +181 -0
  47. package/scripts/write.ts +346 -0
  48. package/templates/claude-md-patch.md +8 -0
@@ -0,0 +1,186 @@
1
+ // --- Directory / File types ---
2
+
3
+ export interface DirectoryNode {
4
+ name: string
5
+ path: string
6
+ type: 'file' | 'directory'
7
+ children?: DirectoryNode[]
8
+ }
9
+
10
+ export interface ManifestFile {
11
+ path: string
12
+ type: string
13
+ depth: number
14
+ }
15
+
16
+ export interface ChurnEntry {
17
+ path: string
18
+ changes: number
19
+ }
20
+
21
+ export interface FileSize {
22
+ path: string
23
+ bytes: number
24
+ }
25
+
26
+ // --- Signal collection ---
27
+
28
+ export interface SignalResult {
29
+ directoryTree: DirectoryNode[]
30
+ extensionMap: Record<string, number>
31
+ manifests: ManifestFile[]
32
+ entryPoints: string[]
33
+ gitChurn: ChurnEntry[]
34
+ largeFiles: FileSize[]
35
+ envFiles: string[]
36
+ testFiles: string[]
37
+ }
38
+
39
+ // --- Stack detection ---
40
+
41
+ export interface StackConventions {
42
+ entryPattern: string
43
+ testPattern: string
44
+ configLocation: string
45
+ }
46
+
47
+ export interface DomainCluster {
48
+ name: string
49
+ path: string
50
+ confidence: number
51
+ }
52
+
53
+ export interface StackResult {
54
+ primaryLanguage: string
55
+ frameworks: string[]
56
+ architecture: 'monolith' | 'monorepo' | 'multi-repo' | 'microservice' | 'library'
57
+ domainClusters: DomainCluster[]
58
+ conventions: StackConventions
59
+ stackHints: string
60
+ dynamicTaxonomy: { domain: string[]; tech: string[] }
61
+ }
62
+
63
+ // --- Tags ---
64
+
65
+ export interface TagSet {
66
+ domain: string[]
67
+ layer: string[]
68
+ tech: string[]
69
+ concern: string[]
70
+ type: string[]
71
+ }
72
+
73
+ export interface Taxonomy {
74
+ static: {
75
+ layer: string[]
76
+ concern: string[]
77
+ type: string[]
78
+ }
79
+ dynamic: {
80
+ domain: string[]
81
+ tech: string[]
82
+ }
83
+ custom: Record<string, string[]>
84
+ }
85
+
86
+ // --- Relations ---
87
+
88
+ export interface Relation {
89
+ id: string
90
+ reason: string
91
+ }
92
+
93
+ export interface ExternalDep {
94
+ name: string
95
+ usage: string
96
+ }
97
+
98
+ export interface SyncMeta {
99
+ commit: string
100
+ timestamp: string
101
+ srcHash: string
102
+ }
103
+
104
+ // --- Context nodes ---
105
+
106
+ export interface NodeResult {
107
+ id: string
108
+ title: string
109
+ category: 'domain' | 'architecture' | 'flow' | 'entity'
110
+ tags: TagSet
111
+ relatesTo: Relation[]
112
+ inverseRelations: Relation[]
113
+ srcPaths: string[]
114
+ keywords: string[]
115
+ lastSynced: SyncMeta
116
+ purpose: string
117
+ publicSurface: string[]
118
+ doesNotOwn: string[]
119
+ externalDeps: ExternalDep[]
120
+ gotchas: string[]
121
+ }
122
+
123
+ // --- Index ---
124
+
125
+ export interface PathEntry {
126
+ context: string
127
+ tags: string[]
128
+ }
129
+
130
+ export interface IndexJson {
131
+ version: string
132
+ generatedAt: string
133
+ commit: string
134
+ stack: Pick<StackResult, 'primaryLanguage' | 'frameworks' | 'architecture'>
135
+ tags: Record<string, string[]>
136
+ keywords: Record<string, string[]>
137
+ paths: Record<string, PathEntry>
138
+ }
139
+
140
+ // --- Registry ---
141
+
142
+ export interface RegistryMapping {
143
+ glob: string
144
+ contextFile: string
145
+ watch: boolean
146
+ }
147
+
148
+ export interface RegistryJson {
149
+ mappings: RegistryMapping[]
150
+ }
151
+
152
+ // --- Query ---
153
+
154
+ export interface RankedResult {
155
+ contextFile: string
156
+ score: number
157
+ matchedOn: string[]
158
+ summary: string
159
+ }
160
+
161
+ // --- Validation ---
162
+
163
+ export interface StaleResult {
164
+ file: string
165
+ isStale: boolean
166
+ reason?: string
167
+ }
168
+
169
+ // --- Settings ---
170
+
171
+ export interface NogrepSettings {
172
+ enabled: boolean
173
+ }
174
+
175
+ // --- Errors ---
176
+
177
+ export type NogrepErrorCode = 'NO_INDEX' | 'NO_GIT' | 'IO_ERROR' | 'STALE'
178
+
179
+ export class NogrepError extends Error {
180
+ constructor(
181
+ message: string,
182
+ public code: NogrepErrorCode,
183
+ ) {
184
+ super(message)
185
+ }
186
+ }
@@ -0,0 +1,181 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join, resolve as resolvePath } from 'node:path'
3
+ import { createHash } from 'node:crypto'
4
+ import { parseArgs } from 'node:util'
5
+ import { glob } from 'glob'
6
+ import matter from 'gray-matter'
7
+ import type { StaleResult } from './types.js'
8
+ import { NogrepError } from './types.js'
9
+
10
+ // --- Freshness check ---
11
+
12
+ export async function checkFreshness(
13
+ nodeFile: string,
14
+ projectRoot: string,
15
+ ): Promise<StaleResult> {
16
+ let content: string
17
+ try {
18
+ content = await readFile(join(projectRoot, nodeFile), 'utf-8')
19
+ } catch {
20
+ return { file: nodeFile, isStale: true, reason: 'context file not found' }
21
+ }
22
+
23
+ const parsed = matter(content)
24
+ const srcPaths: string[] = parsed.data.src_paths ?? []
25
+ const lastSynced = parsed.data.last_synced as
26
+ | { src_hash?: string; commit?: string; timestamp?: string }
27
+ | undefined
28
+
29
+ if (!lastSynced?.src_hash) {
30
+ return { file: nodeFile, isStale: true, reason: 'no src_hash in frontmatter' }
31
+ }
32
+
33
+ if (srcPaths.length === 0) {
34
+ return { file: nodeFile, isStale: false }
35
+ }
36
+
37
+ // Glob all matching source files
38
+ const allFiles: string[] = []
39
+ for (const pattern of srcPaths) {
40
+ const matches = await glob(pattern, {
41
+ cwd: projectRoot,
42
+ nodir: true,
43
+ ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**', 'coverage/**'],
44
+ })
45
+ allFiles.push(...matches)
46
+ }
47
+
48
+ allFiles.sort()
49
+
50
+ if (allFiles.length === 0) {
51
+ return { file: nodeFile, isStale: true, reason: 'no source files match src_paths' }
52
+ }
53
+
54
+ // Compute SHA256 of all file contents concatenated
55
+ const hash = createHash('sha256')
56
+ for (const file of allFiles) {
57
+ try {
58
+ const fileContent = await readFile(join(projectRoot, file))
59
+ hash.update(fileContent)
60
+ } catch {
61
+ // File unreadable — skip
62
+ }
63
+ }
64
+ const currentHash = `sha256:${hash.digest('hex').slice(0, 12)}`
65
+
66
+ if (currentHash !== lastSynced.src_hash) {
67
+ return {
68
+ file: nodeFile,
69
+ isStale: true,
70
+ reason: `hash mismatch: expected ${lastSynced.src_hash}, got ${currentHash}`,
71
+ }
72
+ }
73
+
74
+ return { file: nodeFile, isStale: false }
75
+ }
76
+
77
+ // --- Discover all context nodes ---
78
+
79
+ async function discoverNodes(projectRoot: string): Promise<string[]> {
80
+ const nogrepDir = join(projectRoot, '.nogrep')
81
+ const patterns = [
82
+ 'domains/*.md',
83
+ 'architecture/*.md',
84
+ 'flows/*.md',
85
+ 'entities/*.md',
86
+ ]
87
+
88
+ const files: string[] = []
89
+ for (const pattern of patterns) {
90
+ const matches = await glob(pattern, { cwd: nogrepDir, nodir: true })
91
+ files.push(...matches.map(m => `.nogrep/${m}`))
92
+ }
93
+
94
+ return files.sort()
95
+ }
96
+
97
+ // --- Validate all nodes ---
98
+
99
+ export async function validateAll(
100
+ projectRoot: string,
101
+ ): Promise<{ total: number; fresh: StaleResult[]; stale: StaleResult[] }> {
102
+ const indexPath = join(projectRoot, '.nogrep', '_index.json')
103
+ try {
104
+ await readFile(indexPath, 'utf-8')
105
+ } catch {
106
+ throw new NogrepError(
107
+ 'No .nogrep/_index.json found. Run /nogrep:init first.',
108
+ 'NO_INDEX',
109
+ )
110
+ }
111
+
112
+ const nodeFiles = await discoverNodes(projectRoot)
113
+ const results = await Promise.all(
114
+ nodeFiles.map(f => checkFreshness(f, projectRoot)),
115
+ )
116
+
117
+ const fresh = results.filter(r => !r.isStale)
118
+ const stale = results.filter(r => r.isStale)
119
+
120
+ return { total: results.length, fresh, stale }
121
+ }
122
+
123
+ // --- Formatting ---
124
+
125
+ function formatText(result: { total: number; fresh: StaleResult[]; stale: StaleResult[] }): string {
126
+ const lines: string[] = []
127
+ lines.push(`nogrep index: ${result.total} nodes`)
128
+ lines.push(` Fresh: ${result.fresh.length}`)
129
+ lines.push(` Stale: ${result.stale.length}`)
130
+
131
+ if (result.stale.length > 0) {
132
+ lines.push('')
133
+ lines.push('Stale nodes:')
134
+ for (const s of result.stale) {
135
+ lines.push(` - ${s.file}: ${s.reason}`)
136
+ }
137
+ }
138
+
139
+ return lines.join('\n')
140
+ }
141
+
142
+ function formatJson(result: { total: number; fresh: StaleResult[]; stale: StaleResult[] }): string {
143
+ return JSON.stringify(result, null, 2)
144
+ }
145
+
146
+ // --- CLI ---
147
+
148
+ async function main(): Promise<void> {
149
+ const { values } = parseArgs({
150
+ options: {
151
+ format: { type: 'string', default: 'text' },
152
+ root: { type: 'string', default: process.cwd() },
153
+ },
154
+ strict: true,
155
+ })
156
+
157
+ const root = resolvePath(values.root ?? process.cwd())
158
+ const format = values.format ?? 'text'
159
+
160
+ const result = await validateAll(root)
161
+
162
+ switch (format) {
163
+ case 'json':
164
+ process.stdout.write(formatJson(result) + '\n')
165
+ break
166
+ case 'text':
167
+ default:
168
+ process.stdout.write(formatText(result) + '\n')
169
+ break
170
+ }
171
+ }
172
+
173
+ main().catch((err: unknown) => {
174
+ if (err instanceof NogrepError) {
175
+ process.stderr.write(JSON.stringify({ error: err.message, code: err.code }) + '\n')
176
+ } else {
177
+ const message = err instanceof Error ? err.message : String(err)
178
+ process.stderr.write(JSON.stringify({ error: message }) + '\n')
179
+ }
180
+ process.exitCode = 1
181
+ })
@@ -0,0 +1,346 @@
1
+ import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'
2
+ import { join, resolve, dirname } from 'node:path'
3
+ import { execFile } from 'node:child_process'
4
+ import { promisify } from 'node:util'
5
+ import { createHash } from 'node:crypto'
6
+ import { glob } from 'glob'
7
+ import matter from 'gray-matter'
8
+ import yaml from 'js-yaml'
9
+ import type {
10
+ NodeResult,
11
+ StackResult,
12
+ IndexJson,
13
+ RegistryJson,
14
+ PathEntry,
15
+ NogrepError,
16
+ } from './types.js'
17
+
18
+ const execFileAsync = promisify(execFile)
19
+
20
+ // --- Manual Notes preservation ---
21
+
22
+ function extractManualNotes(content: string): string {
23
+ const match = content.match(
24
+ /## Manual Notes\n([\s\S]*?)(?=\n## |\n---|\s*$)/,
25
+ )
26
+ return match ? match[1]!.trim() : ''
27
+ }
28
+
29
+ // --- Context node markdown generation ---
30
+
31
+ function buildNodeFrontmatter(node: NodeResult): Record<string, unknown> {
32
+ return {
33
+ id: node.id,
34
+ title: node.title,
35
+ category: node.category,
36
+ tags: {
37
+ domain: node.tags.domain,
38
+ layer: node.tags.layer,
39
+ tech: node.tags.tech,
40
+ concern: node.tags.concern,
41
+ type: node.tags.type,
42
+ },
43
+ relates_to: node.relatesTo.map(r => ({ id: r.id, reason: r.reason })),
44
+ inverse_relations: node.inverseRelations.map(r => ({ id: r.id, reason: r.reason })),
45
+ src_paths: node.srcPaths,
46
+ keywords: node.keywords,
47
+ last_synced: {
48
+ commit: node.lastSynced.commit,
49
+ timestamp: node.lastSynced.timestamp,
50
+ src_hash: node.lastSynced.srcHash,
51
+ },
52
+ }
53
+ }
54
+
55
+ function buildNodeMarkdown(node: NodeResult, manualNotes: string): string {
56
+ const fm = buildNodeFrontmatter(node)
57
+ const yamlStr = yaml.dump(fm, { lineWidth: -1, quotingType: '"', forceQuotes: false })
58
+
59
+ const sections: string[] = []
60
+ sections.push(`---\n${yamlStr.trimEnd()}\n---`)
61
+
62
+ sections.push(`\n## Purpose\n${node.purpose}`)
63
+
64
+ if (node.publicSurface.length > 0) {
65
+ sections.push(`\n## Public Surface\n\n\`\`\`\n${node.publicSurface.join('\n')}\n\`\`\``)
66
+ }
67
+
68
+ if (node.doesNotOwn.length > 0) {
69
+ sections.push(`\n## Does Not Own\n${node.doesNotOwn.map(d => `- ${d}`).join('\n')}`)
70
+ }
71
+
72
+ if (node.gotchas.length > 0) {
73
+ sections.push(`\n## Gotchas\n${node.gotchas.map(g => `- ${g}`).join('\n')}`)
74
+ }
75
+
76
+ const notesContent = manualNotes || '_Human annotations. Never overwritten by nogrep update._'
77
+ sections.push(`\n## Manual Notes\n${notesContent}`)
78
+
79
+ return sections.join('\n') + '\n'
80
+ }
81
+
82
+ // --- Write context files ---
83
+
84
+ function categoryDir(category: NodeResult['category']): string {
85
+ switch (category) {
86
+ case 'domain': return 'domains'
87
+ case 'architecture': return 'architecture'
88
+ case 'flow': return 'flows'
89
+ case 'entity': return 'entities'
90
+ }
91
+ }
92
+
93
+ export async function writeContextNodes(
94
+ nodes: NodeResult[],
95
+ outputDir: string,
96
+ ): Promise<void> {
97
+ for (const node of nodes) {
98
+ const dir = join(outputDir, categoryDir(node.category))
99
+ await mkdir(dir, { recursive: true })
100
+
101
+ const filePath = join(dir, `${node.id}.md`)
102
+
103
+ // Preserve existing manual notes
104
+ let manualNotes = ''
105
+ try {
106
+ const existing = await readFile(filePath, 'utf-8')
107
+ manualNotes = extractManualNotes(existing)
108
+ } catch {
109
+ // File doesn't exist yet
110
+ }
111
+
112
+ const content = buildNodeMarkdown(node, manualNotes)
113
+ await writeFile(filePath, content, 'utf-8')
114
+ }
115
+ }
116
+
117
+ // --- Build index ---
118
+
119
+ export function buildIndex(
120
+ nodes: NodeResult[],
121
+ stack: Pick<StackResult, 'primaryLanguage' | 'frameworks' | 'architecture'>,
122
+ ): IndexJson {
123
+ const tags: Record<string, string[]> = {}
124
+ const keywords: Record<string, string[]> = {}
125
+ const paths: Record<string, PathEntry> = {}
126
+
127
+ // Populate inverse relations
128
+ const inverseMap = new Map<string, Array<{ fromId: string; reason: string }>>()
129
+ for (const node of nodes) {
130
+ for (const rel of node.relatesTo) {
131
+ const existing = inverseMap.get(rel.id) ?? []
132
+ existing.push({ fromId: node.id, reason: rel.reason })
133
+ inverseMap.set(rel.id, existing)
134
+ }
135
+ }
136
+
137
+ for (const node of nodes) {
138
+ const contextFile = `.nogrep/${categoryDir(node.category)}/${node.id}.md`
139
+
140
+ // Merge inverse relations from the map
141
+ const inverseEntries = inverseMap.get(node.id) ?? []
142
+ for (const inv of inverseEntries) {
143
+ if (!node.inverseRelations.some(r => r.id === inv.fromId)) {
144
+ node.inverseRelations.push({ id: inv.fromId, reason: inv.reason })
145
+ }
146
+ }
147
+
148
+ // Build tag index
149
+ const tagCategories: Array<[string, string[]]> = [
150
+ ['domain', node.tags.domain],
151
+ ['layer', node.tags.layer],
152
+ ['tech', node.tags.tech],
153
+ ['concern', node.tags.concern],
154
+ ['type', node.tags.type],
155
+ ]
156
+
157
+ const flatTags: string[] = []
158
+ for (const [cat, values] of tagCategories) {
159
+ for (const val of values) {
160
+ const tagKey = `${cat}:${val}`
161
+ flatTags.push(tagKey)
162
+ const tagList = tags[tagKey] ?? []
163
+ if (!tagList.includes(contextFile)) {
164
+ tagList.push(contextFile)
165
+ }
166
+ tags[tagKey] = tagList
167
+ }
168
+ }
169
+
170
+ // Build keyword index
171
+ for (const kw of node.keywords) {
172
+ const kwList = keywords[kw] ?? []
173
+ if (!kwList.includes(contextFile)) {
174
+ kwList.push(contextFile)
175
+ }
176
+ keywords[kw] = kwList
177
+ }
178
+
179
+ // Build path index
180
+ for (const srcPath of node.srcPaths) {
181
+ paths[srcPath] = { context: contextFile, tags: flatTags }
182
+ }
183
+ }
184
+
185
+ let commit = ''
186
+ try {
187
+ // Sync exec not ideal but this is a one-time build step
188
+ // We'll set it from the caller if available
189
+ } catch {
190
+ // no git
191
+ }
192
+
193
+ return {
194
+ version: '1.0',
195
+ generatedAt: new Date().toISOString(),
196
+ commit,
197
+ stack: {
198
+ primaryLanguage: stack.primaryLanguage,
199
+ frameworks: stack.frameworks,
200
+ architecture: stack.architecture,
201
+ },
202
+ tags,
203
+ keywords,
204
+ paths,
205
+ }
206
+ }
207
+
208
+ // --- Build registry ---
209
+
210
+ export function buildRegistry(nodes: NodeResult[]): RegistryJson {
211
+ const mappings = nodes.flatMap(node =>
212
+ node.srcPaths.map(srcPath => ({
213
+ glob: srcPath,
214
+ contextFile: `.nogrep/${categoryDir(node.category)}/${node.id}.md`,
215
+ watch: true,
216
+ })),
217
+ )
218
+
219
+ return { mappings }
220
+ }
221
+
222
+ // --- Patch CLAUDE.md ---
223
+
224
+ export async function patchClaudeMd(projectRoot: string): Promise<void> {
225
+ const claudeMdPath = join(projectRoot, 'CLAUDE.md')
226
+ const patchPath = join(dirname(import.meta.url.replace('file://', '')), '..', 'templates', 'claude-md-patch.md')
227
+
228
+ let patch: string
229
+ try {
230
+ patch = await readFile(patchPath, 'utf-8')
231
+ } catch {
232
+ // Fallback: inline the patch content
233
+ patch = [
234
+ '<!-- nogrep -->',
235
+ '## Code Navigation',
236
+ '',
237
+ 'This project uses [nogrep](https://github.com/techtulp/nogrep).',
238
+ 'Context files in `.nogrep/` are a navigable index of this codebase.',
239
+ 'When you see nogrep results injected into your context, trust them —',
240
+ 'read those files before exploring source.',
241
+ '<!-- /nogrep -->',
242
+ ].join('\n') + '\n'
243
+ }
244
+
245
+ let existing = ''
246
+ try {
247
+ existing = await readFile(claudeMdPath, 'utf-8')
248
+ } catch {
249
+ // File doesn't exist
250
+ }
251
+
252
+ // Check for existing nogrep marker
253
+ if (existing.includes('<!-- nogrep -->')) {
254
+ return
255
+ }
256
+
257
+ const newContent = existing
258
+ ? existing.trimEnd() + '\n\n' + patch
259
+ : patch
260
+
261
+ await writeFile(claudeMdPath, newContent, 'utf-8')
262
+ }
263
+
264
+ // --- Write all outputs ---
265
+
266
+ interface WriteInput {
267
+ nodes: NodeResult[]
268
+ stack: Pick<StackResult, 'primaryLanguage' | 'frameworks' | 'architecture'>
269
+ }
270
+
271
+ async function writeAll(input: WriteInput, projectRoot: string): Promise<void> {
272
+ const outputDir = join(projectRoot, '.nogrep')
273
+ await mkdir(outputDir, { recursive: true })
274
+
275
+ // Write context node files
276
+ await writeContextNodes(input.nodes, outputDir)
277
+
278
+ // Build and write index
279
+ const index = buildIndex(input.nodes, input.stack)
280
+
281
+ // Try to get current git commit
282
+ try {
283
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], {
284
+ cwd: projectRoot,
285
+ })
286
+ index.commit = stdout.trim()
287
+ } catch {
288
+ // no git
289
+ }
290
+
291
+ await writeFile(
292
+ join(outputDir, '_index.json'),
293
+ JSON.stringify(index, null, 2) + '\n',
294
+ 'utf-8',
295
+ )
296
+
297
+ // Build and write registry
298
+ const registry = buildRegistry(input.nodes)
299
+ await writeFile(
300
+ join(outputDir, '_registry.json'),
301
+ JSON.stringify(registry, null, 2) + '\n',
302
+ 'utf-8',
303
+ )
304
+
305
+ // Patch CLAUDE.md
306
+ await patchClaudeMd(projectRoot)
307
+ }
308
+
309
+ // --- CLI interface ---
310
+
311
+ async function main(): Promise<void> {
312
+ const args = process.argv.slice(2)
313
+ let inputFile: string | undefined
314
+ let projectRoot = process.cwd()
315
+
316
+ for (let i = 0; i < args.length; i++) {
317
+ if (args[i] === '--input' && args[i + 1]) {
318
+ inputFile = args[i + 1]!
319
+ i++
320
+ } else if (args[i] === '--root' && args[i + 1]) {
321
+ projectRoot = resolve(args[i + 1]!)
322
+ i++
323
+ }
324
+ }
325
+
326
+ let rawInput: string
327
+ if (inputFile) {
328
+ rawInput = await readFile(resolve(inputFile), 'utf-8')
329
+ } else {
330
+ // Read from stdin
331
+ const chunks: Buffer[] = []
332
+ for await (const chunk of process.stdin) {
333
+ chunks.push(chunk as Buffer)
334
+ }
335
+ rawInput = Buffer.concat(chunks).toString('utf-8')
336
+ }
337
+
338
+ const input = JSON.parse(rawInput) as WriteInput
339
+ await writeAll(input, projectRoot)
340
+ }
341
+
342
+ main().catch((err: unknown) => {
343
+ const message = err instanceof Error ? err.message : String(err)
344
+ process.stderr.write(JSON.stringify({ error: message }) + '\n')
345
+ process.exitCode = 1
346
+ })
@@ -0,0 +1,8 @@
1
+ <!-- nogrep -->
2
+ ## Code Navigation
3
+
4
+ This project uses [nogrep](https://github.com/techtulp/nogrep).
5
+ Context files in `.nogrep/` are a navigable index of this codebase.
6
+ When you see nogrep results injected into your context, trust them —
7
+ read those files before exploring source.
8
+ <!-- /nogrep -->