lat.md 0.5.0 → 0.7.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 (39) hide show
  1. package/dist/src/cli/check.d.ts +7 -5
  2. package/dist/src/cli/check.js +243 -74
  3. package/dist/src/cli/context.d.ts +3 -7
  4. package/dist/src/cli/context.js +15 -1
  5. package/dist/src/cli/expand.d.ts +7 -0
  6. package/dist/src/cli/expand.js +92 -0
  7. package/dist/src/cli/gen.js +11 -4
  8. package/dist/src/cli/hook.d.ts +1 -0
  9. package/dist/src/cli/hook.js +147 -0
  10. package/dist/src/cli/index.js +77 -28
  11. package/dist/src/cli/init.js +148 -120
  12. package/dist/src/cli/locate.d.ts +2 -2
  13. package/dist/src/cli/locate.js +9 -4
  14. package/dist/src/cli/refs.d.ts +20 -4
  15. package/dist/src/cli/refs.js +64 -42
  16. package/dist/src/cli/search.d.ts +25 -3
  17. package/dist/src/cli/search.js +87 -47
  18. package/dist/src/cli/section.d.ts +26 -0
  19. package/dist/src/cli/section.js +133 -0
  20. package/dist/src/code-refs.js +2 -1
  21. package/dist/src/config.js +3 -2
  22. package/dist/src/context.d.ts +21 -0
  23. package/dist/src/context.js +11 -0
  24. package/dist/src/format.d.ts +4 -3
  25. package/dist/src/format.js +16 -20
  26. package/dist/src/init-version.d.ts +10 -0
  27. package/dist/src/init-version.js +49 -0
  28. package/dist/src/lattice.d.ts +11 -5
  29. package/dist/src/lattice.js +87 -38
  30. package/dist/src/mcp/server.js +27 -279
  31. package/dist/src/search/index.js +5 -4
  32. package/dist/src/source-parser.d.ts +23 -0
  33. package/dist/src/source-parser.js +720 -0
  34. package/package.json +3 -1
  35. package/templates/AGENTS.md +38 -6
  36. package/templates/cursor-rules.md +11 -5
  37. package/templates/lat-prompt-hook.sh +2 -2
  38. package/dist/src/cli/prompt.d.ts +0 -2
  39. package/dist/src/cli/prompt.js +0 -60
@@ -1,4 +1,4 @@
1
- import type { CliContext } from './context.js';
1
+ import type { CmdContext, CmdResult } from '../context.js';
2
2
  export type CheckError = {
3
3
  file: string;
4
4
  line: number;
@@ -19,7 +19,9 @@ export type IndexError = {
19
19
  snippet?: string;
20
20
  };
21
21
  export declare function checkIndex(latticeDir: string): Promise<IndexError[]>;
22
- export declare function checkMdCmd(ctx: CliContext): Promise<void>;
23
- export declare function checkCodeRefsCmd(ctx: CliContext): Promise<void>;
24
- export declare function checkIndexCmd(ctx: CliContext): Promise<void>;
25
- export declare function checkAllCmd(ctx: CliContext): Promise<void>;
22
+ export declare function checkSections(latticeDir: string): Promise<CheckError[]>;
23
+ export declare function checkAllCommand(ctx: CmdContext): Promise<CmdResult>;
24
+ export declare function checkMdCommand(ctx: CmdContext): Promise<CmdResult>;
25
+ export declare function checkCodeRefsCommand(ctx: CmdContext): Promise<CmdResult>;
26
+ export declare function checkIndexCommand(ctx: CmdContext): Promise<CmdResult>;
27
+ export declare function checkSectionsCommand(ctx: CmdContext): Promise<CmdResult>;
@@ -1,8 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { basename, extname, join, relative } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { basename, dirname, extname, join, relative } from 'node:path';
3
4
  import { listLatticeFiles, loadAllSections, extractRefs, flattenSections, parseFrontmatter, parseSections, buildFileIndex, resolveRef, } from '../lattice.js';
4
5
  import { scanCodeRefs } from '../code-refs.js';
5
6
  import { walkEntries } from '../walk.js';
7
+ import { INIT_VERSION, readInitVersion } from '../init-version.js';
6
8
  function filePart(id) {
7
9
  const h = id.indexOf('#');
8
10
  return h === -1 ? id : id.slice(0, h);
@@ -30,7 +32,68 @@ function countByExt(paths) {
30
32
  }
31
33
  return stats;
32
34
  }
35
+ /** Source file extensions recognized for code wiki links. */
36
+ const SOURCE_EXTS = new Set([
37
+ '.ts',
38
+ '.tsx',
39
+ '.js',
40
+ '.jsx',
41
+ '.py',
42
+ '.rs',
43
+ '.go',
44
+ '.c',
45
+ '.h',
46
+ ]);
47
+ function isSourcePath(target) {
48
+ const hashIdx = target.indexOf('#');
49
+ const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
50
+ const ext = extname(filePart);
51
+ return SOURCE_EXTS.has(ext);
52
+ }
53
+ /**
54
+ * Try resolving a wiki link target as a source code reference.
55
+ * Returns null if the reference is valid, or an error message string.
56
+ */
57
+ async function tryResolveSourceRef(target, projectRoot) {
58
+ if (!isSourcePath(target)) {
59
+ // Check if it looks like a file path with an unsupported extension
60
+ const hashIdx = target.indexOf('#');
61
+ const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
62
+ const ext = extname(filePart);
63
+ if (ext && hashIdx !== -1) {
64
+ const supported = [...SOURCE_EXTS].sort().join(', ');
65
+ return `broken link [[${target}]] — unsupported file extension "${ext}". Supported: ${supported}`;
66
+ }
67
+ return `broken link [[${target}]] — no matching section found`;
68
+ }
69
+ const hashIdx = target.indexOf('#');
70
+ const filePart = hashIdx === -1 ? target : target.slice(0, hashIdx);
71
+ const symbolPart = hashIdx === -1 ? '' : target.slice(hashIdx + 1);
72
+ const absPath = join(projectRoot, filePart);
73
+ if (!existsSync(absPath)) {
74
+ return `broken link [[${target}]] — file "${filePart}" not found`;
75
+ }
76
+ if (!symbolPart) {
77
+ // File-only link with no symbol — valid as long as file exists
78
+ return null;
79
+ }
80
+ try {
81
+ const { resolveSourceSymbol } = await import('../source-parser.js');
82
+ const { found, error } = await resolveSourceSymbol(filePart, symbolPart, projectRoot);
83
+ if (error) {
84
+ return `broken link [[${target}]] — ${error}`;
85
+ }
86
+ if (!found) {
87
+ return `broken link [[${target}]] — symbol "${symbolPart}" not found in "${filePart}"`;
88
+ }
89
+ return null;
90
+ }
91
+ catch (err) {
92
+ return `broken link [[${target}]] — failed to parse "${filePart}": ${err instanceof Error ? err.message : String(err)}`;
93
+ }
94
+ }
33
95
  export async function checkMd(latticeDir) {
96
+ const projectRoot = dirname(latticeDir);
34
97
  const files = await listLatticeFiles(latticeDir);
35
98
  const allSections = await loadAllSections(latticeDir);
36
99
  const flat = flattenSections(allSections);
@@ -39,7 +102,7 @@ export async function checkMd(latticeDir) {
39
102
  const errors = [];
40
103
  for (const file of files) {
41
104
  const content = await readFile(file, 'utf-8');
42
- const refs = extractRefs(file, content, latticeDir);
105
+ const refs = extractRefs(file, content, projectRoot);
43
106
  const relPath = relative(process.cwd(), file);
44
107
  for (const ref of refs) {
45
108
  const { resolved, ambiguous, suggested } = resolveRef(ref.target, sectionIds, fileIndex);
@@ -52,19 +115,23 @@ export async function checkMd(latticeDir) {
52
115
  });
53
116
  }
54
117
  else if (!sectionIds.has(resolved.toLowerCase())) {
55
- errors.push({
56
- file: relPath,
57
- line: ref.line,
58
- target: ref.target,
59
- message: `broken link [[${ref.target}]] — no matching section found`,
60
- });
118
+ // Try resolving as a source code reference (e.g. [[src/foo.ts#bar]])
119
+ const sourceErr = await tryResolveSourceRef(ref.target, projectRoot);
120
+ if (sourceErr !== null) {
121
+ errors.push({
122
+ file: relPath,
123
+ line: ref.line,
124
+ target: ref.target,
125
+ message: sourceErr,
126
+ });
127
+ }
61
128
  }
62
129
  }
63
130
  }
64
131
  return { errors, files: countByExt(files) };
65
132
  }
66
133
  export async function checkCodeRefs(latticeDir) {
67
- const projectRoot = join(latticeDir, '..');
134
+ const projectRoot = dirname(latticeDir);
68
135
  const allSections = await loadAllSections(latticeDir);
69
136
  const flat = flattenSections(allSections);
70
137
  const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
@@ -98,7 +165,7 @@ export async function checkCodeRefs(latticeDir) {
98
165
  const fm = parseFrontmatter(content);
99
166
  if (!fm.requireCodeMention)
100
167
  continue;
101
- const sections = parseSections(file, content, latticeDir);
168
+ const sections = parseSections(file, content, projectRoot);
102
169
  const fileSections = flattenSections(sections);
103
170
  const leafSections = fileSections.filter((s) => s.children.length === 0);
104
171
  const relPath = relative(process.cwd(), file);
@@ -151,9 +218,22 @@ function indexSnippet(entries) {
151
218
  export async function checkIndex(latticeDir) {
152
219
  const errors = [];
153
220
  const allPaths = await walkEntries(latticeDir);
221
+ // Flag non-.md files — only markdown belongs in lat.md/
222
+ for (const p of allPaths) {
223
+ const name = p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
224
+ if (!name.endsWith('.md')) {
225
+ const relDir = basename(latticeDir) + '/';
226
+ errors.push({
227
+ dir: relDir,
228
+ message: `"${p}" is not a .md file — only markdown files belong in lat.md/`,
229
+ });
230
+ }
231
+ }
232
+ // Only .md files participate in index validation
233
+ const mdPaths = allPaths.filter((p) => p.endsWith('.md'));
154
234
  // Collect all directories to check (including root, represented as '')
155
235
  const dirs = new Set(['']);
156
- for (const p of allPaths) {
236
+ for (const p of mdPaths) {
157
237
  const parts = p.split('/');
158
238
  // Add every directory prefix
159
239
  for (let i = 1; i < parts.length; i++) {
@@ -169,7 +249,7 @@ export async function checkIndex(latticeDir) {
169
249
  const indexRelPath = dir === '' ? indexFileName : dir + '/' + indexFileName;
170
250
  // Get the immediate children of this directory
171
251
  const prefix = dir === '' ? '' : dir + '/';
172
- const childPaths = allPaths
252
+ const childPaths = mdPaths
173
253
  .filter((p) => p.startsWith(prefix) && p !== indexRelPath)
174
254
  .map((p) => p.slice(prefix.length));
175
255
  const children = immediateEntries(childPaths);
@@ -222,84 +302,124 @@ export async function checkIndex(latticeDir) {
222
302
  }
223
303
  return errors;
224
304
  }
225
- function formatErrors(ctx, errors, startIdx = 0) {
226
- for (let i = 0; i < errors.length; i++) {
227
- const err = errors[i];
228
- if (i > 0 || startIdx > 0)
229
- console.error('');
230
- const loc = ctx.chalk.cyan(err.file + ':' + err.line);
231
- const [first, ...rest] = err.message.split('\n');
232
- console.error(`- ${loc}: ${ctx.chalk.red(first)}`);
233
- for (const line of rest) {
234
- console.error(` ${ctx.chalk.red(line)}`);
305
+ // --- Section structure validation ---
306
+ /** Max characters for the first paragraph of a section (excluding [[wiki links]]). */
307
+ const MAX_BODY_LENGTH = 250;
308
+ /** Count body text length excluding `[[...]]` wiki link markers and content. */
309
+ function bodyTextLength(body) {
310
+ return body.replace(/\[\[[^\]]*\]\]/g, '').length;
311
+ }
312
+ export async function checkSections(latticeDir) {
313
+ const projectRoot = dirname(latticeDir);
314
+ const files = await listLatticeFiles(latticeDir);
315
+ const errors = [];
316
+ for (const file of files) {
317
+ const content = await readFile(file, 'utf-8');
318
+ const sections = parseSections(file, content, projectRoot);
319
+ const flat = flattenSections(sections);
320
+ const relPath = relative(process.cwd(), file);
321
+ for (const section of flat) {
322
+ if (!section.firstParagraph) {
323
+ errors.push({
324
+ file: relPath,
325
+ line: section.startLine,
326
+ target: section.id,
327
+ message: `section "${section.id}" has no leading paragraph. ` +
328
+ `Every section must start with a brief overview (≤${MAX_BODY_LENGTH} chars) ` +
329
+ `summarizing what it documents — this powers search snippets and command output.`,
330
+ });
331
+ continue;
332
+ }
333
+ const len = bodyTextLength(section.firstParagraph);
334
+ if (len > MAX_BODY_LENGTH) {
335
+ errors.push({
336
+ file: relPath,
337
+ line: section.startLine,
338
+ target: section.id,
339
+ message: `section "${section.id}" leading paragraph is ${len} characters ` +
340
+ `(max ${MAX_BODY_LENGTH}, excluding [[wiki links]]). ` +
341
+ `Keep the first paragraph brief — it serves as the section's summary ` +
342
+ `in search results and command output. Use subsequent paragraphs for details.`,
343
+ });
344
+ }
235
345
  }
236
346
  }
347
+ return errors;
348
+ }
349
+ // --- Formatting helpers (shared by all check commands) ---
350
+ function formatFileStats(files, s) {
351
+ const entries = Object.entries(files).sort(([a], [b]) => a.localeCompare(b));
352
+ return s.dim(`Scanned ${entries.map(([ext, n]) => `${n} ${ext}`).join(', ')}`);
237
353
  }
238
- function formatIndexErrors(ctx, errors, startIdx = 0) {
239
- for (let i = 0; i < errors.length; i++) {
240
- if (i > 0 || startIdx > 0)
241
- console.error('');
242
- const loc = ctx.chalk.cyan(errors[i].dir);
243
- const [first, ...rest] = errors[i].message.split('\n');
244
- console.error(`- ${loc}: ${ctx.chalk.red(first)}`);
354
+ function formatCheckErrors(errors, s) {
355
+ const lines = [];
356
+ for (const err of errors) {
357
+ lines.push('');
358
+ const loc = s.cyan(err.file + ':' + err.line);
359
+ const [first, ...rest] = err.message.split('\n');
360
+ lines.push(`- ${loc}: ${s.red(first)}`);
245
361
  for (const line of rest) {
246
- console.error(` ${ctx.chalk.red(line)}`);
362
+ lines.push(` ${s.red(line)}`);
247
363
  }
248
364
  }
365
+ return lines;
249
366
  }
250
- function formatErrorCount(ctx, count) {
251
- if (count > 0) {
252
- console.error(ctx.chalk.red(`\n${count} error${count === 1 ? '' : 's'} found`));
367
+ function formatCheckIndexErrors(errors, s) {
368
+ const lines = [];
369
+ for (const err of errors) {
370
+ lines.push('');
371
+ const loc = s.cyan(err.dir);
372
+ const [first, ...rest] = err.message.split('\n');
373
+ lines.push(`- ${loc}: ${s.red(first)}`);
374
+ for (const line of rest) {
375
+ lines.push(` ${s.red(line)}`);
376
+ }
253
377
  }
378
+ return lines;
254
379
  }
255
- function formatStats(ctx, stats) {
256
- const entries = Object.entries(stats).sort(([a], [b]) => a.localeCompare(b));
257
- const parts = entries.map(([ext, n]) => `${n} ${ext}`);
258
- console.log(ctx.chalk.dim(`Scanned ${parts.join(', ')}`));
380
+ function formatErrorCount(count, s) {
381
+ return s.red(`\n${count} error${count === 1 ? '' : 's'} found`);
259
382
  }
260
- export async function checkMdCmd(ctx) {
261
- const { errors, files } = await checkMd(ctx.latDir);
262
- formatStats(ctx, files);
263
- formatErrors(ctx, errors);
264
- formatErrorCount(ctx, errors.length);
265
- if (errors.length > 0)
266
- process.exit(1);
267
- console.log(ctx.chalk.green('md: All links OK'));
268
- }
269
- export async function checkCodeRefsCmd(ctx) {
270
- const { errors, files } = await checkCodeRefs(ctx.latDir);
271
- formatStats(ctx, files);
272
- formatErrors(ctx, errors);
273
- formatErrorCount(ctx, errors.length);
274
- if (errors.length > 0)
275
- process.exit(1);
276
- console.log(ctx.chalk.green('code-refs: All references OK'));
277
- }
278
- export async function checkIndexCmd(ctx) {
279
- const errors = await checkIndex(ctx.latDir);
280
- formatIndexErrors(ctx, errors);
281
- formatErrorCount(ctx, errors.length);
282
- if (errors.length > 0)
283
- process.exit(1);
284
- console.log(ctx.chalk.green('index: All directory index files OK'));
285
- }
286
- export async function checkAllCmd(ctx) {
383
+ // --- Unified command functions ---
384
+ export async function checkAllCommand(ctx) {
287
385
  const md = await checkMd(ctx.latDir);
288
386
  const code = await checkCodeRefs(ctx.latDir);
289
387
  const indexErrors = await checkIndex(ctx.latDir);
388
+ const sectionErrors = await checkSections(ctx.latDir);
290
389
  const allErrors = [...md.errors, ...code.errors];
291
390
  const allFiles = { ...md.files };
292
391
  for (const [ext, n] of Object.entries(code.files)) {
293
392
  allFiles[ext] = (allFiles[ext] || 0) + n;
294
393
  }
295
- formatStats(ctx, allFiles);
296
- formatErrors(ctx, allErrors);
297
- formatIndexErrors(ctx, indexErrors, allErrors.length);
298
- const totalErrors = allErrors.length + indexErrors.length;
299
- formatErrorCount(ctx, totalErrors);
300
- if (totalErrors > 0)
301
- process.exit(1);
302
- console.log(ctx.chalk.green('All checks passed'));
394
+ const s = ctx.styler;
395
+ const lines = [formatFileStats(allFiles, s)];
396
+ // Init version warning first — user should fix setup before addressing errors
397
+ const storedVersion = readInitVersion(ctx.latDir);
398
+ if (storedVersion === null) {
399
+ lines.push('', s.yellow('Warning:') +
400
+ ' No init version recorded — run ' +
401
+ s.cyan('lat init') +
402
+ ' to set up agent hooks and configuration.');
403
+ }
404
+ else if (storedVersion < INIT_VERSION) {
405
+ lines.push('', s.yellow('Warning:') +
406
+ ' Your setup is outdated (v' +
407
+ storedVersion +
408
+ ' → v' +
409
+ INIT_VERSION +
410
+ '). Re-run ' +
411
+ s.cyan('lat init') +
412
+ ' to update agent hooks and configuration.');
413
+ }
414
+ lines.push(...formatCheckErrors(allErrors, s));
415
+ lines.push(...formatCheckIndexErrors(indexErrors, s));
416
+ lines.push(...formatCheckErrors(sectionErrors, s));
417
+ const totalErrors = allErrors.length + indexErrors.length + sectionErrors.length;
418
+ if (totalErrors > 0) {
419
+ lines.push(formatErrorCount(totalErrors, s));
420
+ return { output: lines.join('\n'), isError: true };
421
+ }
422
+ lines.push(s.green('All checks passed'));
303
423
  const { getLlmKey } = await import('../config.js');
304
424
  let hasKey = false;
305
425
  try {
@@ -309,10 +429,59 @@ export async function checkAllCmd(ctx) {
309
429
  // key resolution failed (e.g. empty file) — treat as missing
310
430
  }
311
431
  if (!hasKey) {
312
- console.log(ctx.chalk.yellow('Warning:') +
432
+ lines.push(s.yellow('Warning:') +
313
433
  ' No LLM key found — semantic search (lat search) will not work.' +
314
434
  ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
315
- ctx.chalk.cyan('lat init') +
435
+ s.cyan('lat init') +
316
436
  ' to configure.');
317
437
  }
438
+ return { output: lines.join('\n') };
439
+ }
440
+ export async function checkMdCommand(ctx) {
441
+ const { errors, files } = await checkMd(ctx.latDir);
442
+ const s = ctx.styler;
443
+ const lines = [formatFileStats(files, s)];
444
+ lines.push(...formatCheckErrors(errors, s));
445
+ if (errors.length > 0) {
446
+ lines.push(formatErrorCount(errors.length, s));
447
+ return { output: lines.join('\n'), isError: true };
448
+ }
449
+ lines.push(s.green('md: All links OK'));
450
+ return { output: lines.join('\n') };
451
+ }
452
+ export async function checkCodeRefsCommand(ctx) {
453
+ const { errors, files } = await checkCodeRefs(ctx.latDir);
454
+ const s = ctx.styler;
455
+ const lines = [formatFileStats(files, s)];
456
+ lines.push(...formatCheckErrors(errors, s));
457
+ if (errors.length > 0) {
458
+ lines.push(formatErrorCount(errors.length, s));
459
+ return { output: lines.join('\n'), isError: true };
460
+ }
461
+ lines.push(s.green('code-refs: All references OK'));
462
+ return { output: lines.join('\n') };
463
+ }
464
+ export async function checkIndexCommand(ctx) {
465
+ const errors = await checkIndex(ctx.latDir);
466
+ const s = ctx.styler;
467
+ const lines = [];
468
+ lines.push(...formatCheckIndexErrors(errors, s));
469
+ if (errors.length > 0) {
470
+ lines.push(formatErrorCount(errors.length, s));
471
+ return { output: lines.join('\n'), isError: true };
472
+ }
473
+ lines.push(s.green('index: All directory index files OK'));
474
+ return { output: lines.join('\n') };
475
+ }
476
+ export async function checkSectionsCommand(ctx) {
477
+ const errors = await checkSections(ctx.latDir);
478
+ const s = ctx.styler;
479
+ const lines = [];
480
+ lines.push(...formatCheckErrors(errors, s));
481
+ if (errors.length > 0) {
482
+ lines.push(formatErrorCount(errors.length, s));
483
+ return { output: lines.join('\n'), isError: true };
484
+ }
485
+ lines.push(s.green('sections: All sections have valid leading paragraphs'));
486
+ return { output: lines.join('\n') };
318
487
  }
@@ -1,10 +1,6 @@
1
- import { type ChalkInstance } from 'chalk';
2
- export type CliContext = {
3
- latDir: string;
4
- color: boolean;
5
- chalk: ChalkInstance;
6
- };
1
+ import type { CmdContext } from '../context.js';
2
+ export type { CmdContext };
7
3
  export declare function resolveContext(opts: {
8
4
  dir?: string;
9
5
  color?: boolean;
10
- }): CliContext;
6
+ }): CmdContext;
@@ -1,5 +1,18 @@
1
+ import { dirname } from 'node:path';
1
2
  import chalk from 'chalk';
2
3
  import { findLatticeDir } from '../lattice.js';
4
+ function makeChalkStyler() {
5
+ return {
6
+ bold: (s) => chalk.bold(s),
7
+ dim: (s) => chalk.dim(s),
8
+ red: (s) => chalk.red(s),
9
+ cyan: (s) => chalk.cyan(s),
10
+ white: (s) => chalk.white(s),
11
+ green: (s) => chalk.green(s),
12
+ yellow: (s) => chalk.yellow(s),
13
+ boldWhite: (s) => chalk.bold.white(s),
14
+ };
15
+ }
3
16
  export function resolveContext(opts) {
4
17
  const color = opts.color !== false;
5
18
  if (!color) {
@@ -11,5 +24,6 @@ export function resolveContext(opts) {
11
24
  console.error(chalk.dim('Run `lat init` to create one.'));
12
25
  process.exit(1);
13
26
  }
14
- return { latDir, color, chalk };
27
+ const projectRoot = dirname(latDir);
28
+ return { latDir, projectRoot, styler: makeChalkStyler(), mode: 'cli' };
15
29
  }
@@ -0,0 +1,7 @@
1
+ import type { CmdContext, CmdResult } from '../context.js';
2
+ /**
3
+ * Resolve [[refs]] in text and return the expanded output.
4
+ * Returns null if there are no wiki links, or if resolution fails.
5
+ */
6
+ export declare function expandPrompt(ctx: CmdContext, text: string): Promise<string | null>;
7
+ export declare function expandCommand(ctx: CmdContext, text: string): Promise<CmdResult>;
@@ -0,0 +1,92 @@
1
+ import { join, relative } from 'node:path';
2
+ import { loadAllSections, findSections, } from '../lattice.js';
3
+ const WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
4
+ function formatLocation(section, projectRoot) {
5
+ const relPath = relative(process.cwd(), join(projectRoot, section.filePath));
6
+ return `${relPath}:${section.startLine}-${section.endLine}`;
7
+ }
8
+ /**
9
+ * Resolve [[refs]] in text and return the expanded output.
10
+ * Returns null if there are no wiki links, or if resolution fails.
11
+ */
12
+ export async function expandPrompt(ctx, text) {
13
+ const refs = [...text.matchAll(WIKI_LINK_RE)];
14
+ if (refs.length === 0)
15
+ return null;
16
+ const allSections = await loadAllSections(ctx.latDir);
17
+ const resolved = new Map();
18
+ const errors = [];
19
+ for (const match of refs) {
20
+ const target = match[1];
21
+ if (resolved.has(target))
22
+ continue;
23
+ const matches = findSections(allSections, target);
24
+ if (matches.length >= 1) {
25
+ resolved.set(target, {
26
+ target,
27
+ best: matches[0],
28
+ alternatives: matches.slice(1),
29
+ });
30
+ }
31
+ else {
32
+ errors.push(`No section found for [[${target}]]`);
33
+ }
34
+ }
35
+ if (errors.length > 0)
36
+ return null;
37
+ // Replace [[refs]] inline
38
+ let output = text.replace(WIKI_LINK_RE, (_match, target) => {
39
+ const ref = resolved.get(target);
40
+ return `[[${ref.best.section.id}]]`;
41
+ });
42
+ // Append context block as nested outliner
43
+ output += '\n\n<lat-context>\n';
44
+ for (const ref of resolved.values()) {
45
+ const isExact = ref.best.reason === 'exact match' ||
46
+ ref.best.reason.startsWith('file stem expanded');
47
+ const all = isExact ? [ref.best] : [ref.best, ...ref.alternatives];
48
+ if (isExact) {
49
+ output += `* \`[[${ref.target}]]\` is referring to:\n`;
50
+ }
51
+ else {
52
+ output += `* \`[[${ref.target}]]\` might be referring to either of the following:\n`;
53
+ }
54
+ for (const m of all) {
55
+ const reason = isExact ? '' : ` (${m.reason})`;
56
+ output += ` * [[${m.section.id}]]${reason}\n`;
57
+ output += ` * ${formatLocation(m.section, ctx.projectRoot)}\n`;
58
+ if (m.section.firstParagraph) {
59
+ output += ` * ${m.section.firstParagraph}\n`;
60
+ }
61
+ }
62
+ }
63
+ output += '</lat-context>\n';
64
+ return output;
65
+ }
66
+ export async function expandCommand(ctx, text) {
67
+ const result = await expandPrompt(ctx, text);
68
+ if (result === null) {
69
+ const refs = [...text.matchAll(WIKI_LINK_RE)];
70
+ if (refs.length === 0) {
71
+ return { output: text };
72
+ }
73
+ // Resolution failed — find which ref is broken
74
+ const allSections = await loadAllSections(ctx.latDir);
75
+ for (const match of refs) {
76
+ const target = match[1];
77
+ const matches = findSections(allSections, target);
78
+ if (matches.length === 0) {
79
+ const s = ctx.styler;
80
+ return {
81
+ output: s.red(`No section found for [[${target}]]`) +
82
+ ' (no exact, substring, or fuzzy matches).\n' +
83
+ s.dim('Ask the user to correct the reference.'),
84
+ isError: true,
85
+ };
86
+ }
87
+ }
88
+ // All refs matched individually but expansion still failed — shouldn't happen
89
+ return { output: text };
90
+ }
91
+ return { output: result };
92
+ }
@@ -9,9 +9,16 @@ export function readCursorRulesTemplate() {
9
9
  }
10
10
  export async function genCmd(target) {
11
11
  const normalized = target.toLowerCase();
12
- if (normalized !== 'agents.md' && normalized !== 'claude.md') {
13
- console.error(`Unknown target: ${target}. Supported: agents.md, claude.md`);
14
- process.exit(1);
12
+ switch (normalized) {
13
+ case 'agents.md':
14
+ case 'claude.md':
15
+ process.stdout.write(readAgentsTemplate());
16
+ break;
17
+ case 'cursor-rules.md':
18
+ process.stdout.write(readCursorRulesTemplate());
19
+ break;
20
+ default:
21
+ console.error(`Unknown target: ${target}. Supported: agents.md, claude.md, cursor-rules.md`);
22
+ process.exit(1);
15
23
  }
16
- process.stdout.write(readAgentsTemplate());
17
24
  }
@@ -0,0 +1 @@
1
+ export declare function hookCmd(agent: string, event: string): Promise<void>;