lat.md 0.6.0 → 0.7.1

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.
@@ -2,37 +2,37 @@ import { readFile } from 'node:fs/promises';
2
2
  import { listLatticeFiles, loadAllSections, findSections, extractRefs, flattenSections, buildFileIndex, resolveRef, } from '../lattice.js';
3
3
  import { formatResultList } from '../format.js';
4
4
  import { scanCodeRefs } from '../code-refs.js';
5
- export async function refsCmd(ctx, query, scope) {
5
+ /**
6
+ * Find all sections and code locations that reference a given section.
7
+ * Accepts any valid section id (full-path, short-form, with or without brackets).
8
+ */
9
+ export async function findRefs(ctx, query, scope) {
10
+ query = query.replace(/^\[\[|\]\]$/g, '');
6
11
  const allSections = await loadAllSections(ctx.latDir);
7
- const matches = findSections(allSections, query);
8
- if (matches.length === 0) {
9
- console.error(ctx.chalk.red(`No section matching "${query}" (no exact, substring, or fuzzy matches)`));
10
- process.exit(1);
11
- }
12
- // Resolve short refs and require exact match
13
12
  const flat = flattenSections(allSections);
14
13
  const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
15
14
  const fileIndex = buildFileIndex(allSections);
16
15
  const { resolved } = resolveRef(query, sectionIds, fileIndex);
17
16
  const q = resolved.toLowerCase();
18
- const exactMatch = flat.find((s) => s.id.toLowerCase() === q);
19
- if (!exactMatch) {
20
- console.error(ctx.chalk.red(`No section "${query}" found.`));
21
- if (matches.length > 0) {
22
- console.error(ctx.chalk.dim('\nDid you mean:\n'));
23
- for (const m of matches) {
24
- console.error(ctx.chalk.dim('*') +
25
- ' ' +
26
- ctx.chalk.white(m.section.id) +
27
- ' ' +
28
- ctx.chalk.dim(`(${m.reason})`));
29
- }
17
+ let exactMatch = flat.find((s) => s.id.toLowerCase() === q);
18
+ // If resolveRef didn't land on an exact id, use findSections as fallback
19
+ const matches = !exactMatch ? findSections(allSections, query) : [];
20
+ if (!exactMatch && matches.length >= 1) {
21
+ const top = matches[0];
22
+ const isConfident = top.reason === 'exact match' ||
23
+ top.reason.startsWith('file stem expanded') ||
24
+ top.reason === 'section name match';
25
+ if (isConfident) {
26
+ exactMatch = top.section;
30
27
  }
31
- process.exit(1);
28
+ }
29
+ if (!exactMatch) {
30
+ const suggestions = matches.length > 0 ? matches : findSections(allSections, query);
31
+ return { kind: 'no-match', suggestions };
32
32
  }
33
33
  const targetId = exactMatch.id.toLowerCase();
34
- const mdMatches = [];
35
- const codeLines = [];
34
+ const mdRefs = [];
35
+ const codeRefs = [];
36
36
  if (scope === 'md' || scope === 'md+code') {
37
37
  const files = await listLatticeFiles(ctx.latDir);
38
38
  const matchingFromSections = new Set();
@@ -49,33 +49,57 @@ export async function refsCmd(ctx, query, scope) {
49
49
  if (matchingFromSections.size > 0) {
50
50
  const referrers = flat.filter((s) => matchingFromSections.has(s.id.toLowerCase()));
51
51
  for (const s of referrers) {
52
- mdMatches.push({ section: s, reason: 'wiki link' });
52
+ mdRefs.push({ section: s, reason: 'wiki link' });
53
53
  }
54
54
  }
55
55
  }
56
56
  if (scope === 'code' || scope === 'md+code') {
57
- const { refs: codeRefs } = await scanCodeRefs(ctx.projectRoot);
58
- for (const ref of codeRefs) {
57
+ const { refs: scannedRefs } = await scanCodeRefs(ctx.projectRoot);
58
+ for (const ref of scannedRefs) {
59
59
  const { resolved: codeResolved } = resolveRef(ref.target, sectionIds, fileIndex);
60
60
  if (codeResolved.toLowerCase() === targetId) {
61
- codeLines.push(`${ref.file}:${ref.line}`);
61
+ codeRefs.push(`${ref.file}:${ref.line}`);
62
62
  }
63
63
  }
64
64
  }
65
- if (mdMatches.length === 0 && codeLines.length === 0) {
66
- console.error(ctx.chalk.red(`No references to "${exactMatch.id}" found`));
67
- process.exit(1);
65
+ return { kind: 'found', target: exactMatch, mdRefs, codeRefs };
66
+ }
67
+ export async function refsCommand(ctx, query, scope) {
68
+ const result = await findRefs(ctx, query, scope);
69
+ if (result.kind === 'no-match') {
70
+ const s = ctx.styler;
71
+ if (result.suggestions.length > 0) {
72
+ const suggestions = result.suggestions
73
+ .map((m) => ` ${s.dim('*')} ${s.white(m.section.id)} ${s.dim(`(${m.reason})`)}`)
74
+ .join('\n');
75
+ return {
76
+ output: s.red(`No section "${query}" found.`) +
77
+ ' Did you mean:\n' +
78
+ suggestions,
79
+ isError: true,
80
+ };
81
+ }
82
+ return {
83
+ output: s.red(`No section matching "${query}"`),
84
+ isError: true,
85
+ };
68
86
  }
69
- if (mdMatches.length > 0) {
70
- console.log(formatResultList(`References to "${exactMatch.id}":`, mdMatches, ctx.projectRoot));
87
+ const { target, mdRefs, codeRefs } = result;
88
+ if (mdRefs.length === 0 && codeRefs.length === 0) {
89
+ return {
90
+ output: ctx.styler.yellow(`No references to "${target.id}" found`),
91
+ isError: true,
92
+ };
71
93
  }
72
- if (codeLines.length > 0) {
73
- if (mdMatches.length > 0)
74
- console.log('');
75
- console.log(ctx.chalk.bold('Code references:'));
76
- console.log('');
77
- for (const line of codeLines) {
78
- console.log(`${ctx.chalk.dim('*')} ${line}`);
79
- }
94
+ const s = ctx.styler;
95
+ const parts = [];
96
+ if (mdRefs.length > 0) {
97
+ parts.push(formatResultList(ctx, `References to "${target.id}":`, mdRefs));
98
+ }
99
+ if (codeRefs.length > 0) {
100
+ parts.push('## Code references:' +
101
+ '\n\n' +
102
+ codeRefs.map((l) => `${s.dim('*')} ${l}`).join('\n'));
80
103
  }
104
+ return { output: parts.join('\n') };
81
105
  }
@@ -1,5 +1,27 @@
1
- import type { CliContext } from './context.js';
2
- export declare function searchCmd(ctx: CliContext, query: string | undefined, opts: {
1
+ import type { CmdContext, CmdResult, Styler } from '../context.js';
2
+ import { type IndexStats } from '../search/index.js';
3
+ import { type SectionMatch } from '../lattice.js';
4
+ export type SearchResult = {
5
+ query: string;
6
+ matches: SectionMatch[];
7
+ };
8
+ export type IndexProgress = {
9
+ /** Called before indexing starts. `isEmpty` is true on first run. */
10
+ beforeIndex?: (isEmpty: boolean) => void;
11
+ /** Called after indexing completes with stats. */
12
+ afterIndex?: (stats: IndexStats, isEmpty: boolean) => void;
13
+ };
14
+ /**
15
+ * Run a semantic search across lat.md sections.
16
+ * Handles indexing (with optional progress callback). Returns matched sections.
17
+ */
18
+ export declare function runSearch(latDir: string, query: string, key: string, limit: number, progress?: IndexProgress): Promise<SearchResult>;
19
+ /**
20
+ * Index-only mode (no query). Used by `lat search --reindex`.
21
+ */
22
+ export declare function runIndex(latDir: string, key: string, progress?: IndexProgress): Promise<void>;
23
+ export declare function cliProgress(reindex: boolean, s: Styler): IndexProgress;
24
+ export declare function searchCommand(ctx: CmdContext, query: string | undefined, opts: {
3
25
  limit: number;
4
26
  reindex?: boolean;
5
- }): Promise<void>;
27
+ }, progress?: IndexProgress): Promise<CmdResult>;
@@ -1,67 +1,101 @@
1
- import chalk from 'chalk';
2
1
  import { openDb, ensureSchema, closeDb } from '../search/db.js';
3
2
  import { detectProvider } from '../search/provider.js';
4
3
  import { indexSections } from '../search/index.js';
5
4
  import { searchSections } from '../search/search.js';
6
- import { loadAllSections, flattenSections } from '../lattice.js';
7
- import { formatResultList } from '../format.js';
8
- import { getLlmKey, getConfigPath } from '../config.js';
9
- export async function searchCmd(ctx, query, opts) {
10
- let key;
11
- try {
12
- key = getLlmKey();
13
- }
14
- catch (err) {
15
- console.error(chalk.red(err.message));
16
- process.exit(1);
17
- }
18
- if (!key) {
19
- console.error(chalk.red('No API key configured.') +
20
- ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
21
- chalk.cyan('lat init') +
22
- ' to save one in ' +
23
- chalk.dim(getConfigPath()) +
24
- '.');
25
- process.exit(1);
26
- }
5
+ import { loadAllSections, flattenSections, } from '../lattice.js';
6
+ import { formatResultList, formatNavHints } from '../format.js';
7
+ async function withDb(latDir, key, progress, fn) {
27
8
  const provider = detectProvider(key);
28
- const db = openDb(ctx.latDir);
9
+ const db = openDb(latDir);
29
10
  try {
30
11
  await ensureSchema(db, provider.dimensions);
31
- // Check if index needs updating
32
12
  const countResult = await db.execute('SELECT COUNT(*) as n FROM sections');
33
13
  const isEmpty = countResult.rows[0].n === 0;
34
- if (isEmpty || opts.reindex) {
35
- const label = opts.reindex ? 'Re-indexing' : 'Building index';
36
- process.stderr.write(chalk.dim(`${label}...`));
37
- const stats = await indexSections(ctx.latDir, db, provider, key);
38
- process.stderr.write(chalk.dim(` done (${stats.added} added, ${stats.updated} updated, ${stats.removed} removed)\n`));
39
- }
40
- else {
41
- // Incremental update
42
- const stats = await indexSections(ctx.latDir, db, provider, key);
43
- if (stats.added + stats.updated + stats.removed > 0) {
44
- process.stderr.write(chalk.dim(`Index updated: ${stats.added} added, ${stats.updated} updated, ${stats.removed} removed\n`));
45
- }
46
- }
47
- if (!query)
48
- return;
49
- const results = await searchSections(db, query, provider, key, opts.limit);
14
+ progress?.beforeIndex?.(isEmpty);
15
+ const stats = await indexSections(latDir, db, provider, key);
16
+ progress?.afterIndex?.(stats, isEmpty);
17
+ return await fn(db, provider);
18
+ }
19
+ finally {
20
+ await closeDb(db);
21
+ }
22
+ }
23
+ /**
24
+ * Run a semantic search across lat.md sections.
25
+ * Handles indexing (with optional progress callback). Returns matched sections.
26
+ */
27
+ export async function runSearch(latDir, query, key, limit, progress) {
28
+ return withDb(latDir, key, progress, async (db, provider) => {
29
+ const results = await searchSections(db, query, provider, key, limit);
50
30
  if (results.length === 0) {
51
- console.log(chalk.dim('No results found.'));
52
- return;
31
+ return { query, matches: [] };
53
32
  }
54
- // Load sections for formatting with location info
55
- const allSections = await loadAllSections(ctx.latDir);
33
+ const allSections = await loadAllSections(latDir);
56
34
  const flat = flattenSections(allSections);
57
35
  const byId = new Map(flat.map((s) => [s.id, s]));
58
- const matched = results
36
+ const matches = results
59
37
  .map((r) => byId.get(r.id))
60
38
  .filter((s) => !!s)
61
39
  .map((s) => ({ section: s, reason: 'semantic match' }));
62
- console.log(formatResultList(`Search results for "${query}":`, matched, ctx.projectRoot));
40
+ return { query, matches };
41
+ });
42
+ }
43
+ /**
44
+ * Index-only mode (no query). Used by `lat search --reindex`.
45
+ */
46
+ export async function runIndex(latDir, key, progress) {
47
+ await withDb(latDir, key, progress, async () => { });
48
+ }
49
+ export function cliProgress(reindex, s) {
50
+ return {
51
+ beforeIndex(isEmpty) {
52
+ if (isEmpty || reindex) {
53
+ const label = reindex ? 'Re-indexing' : 'Building index';
54
+ process.stderr.write(s.dim(`${label}...`));
55
+ }
56
+ },
57
+ afterIndex(stats, isEmpty) {
58
+ if (isEmpty || reindex) {
59
+ process.stderr.write(s.dim(` done (${stats.added} added, ${stats.updated} updated, ${stats.removed} removed)\n`));
60
+ }
61
+ else if (stats.added + stats.updated + stats.removed > 0) {
62
+ process.stderr.write(s.dim(`Index updated: ${stats.added} added, ${stats.updated} updated, ${stats.removed} removed\n`));
63
+ }
64
+ },
65
+ };
66
+ }
67
+ export async function searchCommand(ctx, query, opts, progress) {
68
+ const { getLlmKey, getConfigPath } = await import('../config.js');
69
+ let key;
70
+ try {
71
+ key = getLlmKey();
63
72
  }
64
- finally {
65
- await closeDb(db);
73
+ catch (err) {
74
+ return { output: err.message, isError: true };
75
+ }
76
+ if (!key) {
77
+ const s = ctx.styler;
78
+ return {
79
+ output: s.red('No API key configured.') +
80
+ ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
81
+ s.cyan('lat init') +
82
+ (ctx.mode === 'cli'
83
+ ? ' to save one in ' + s.dim(getConfigPath())
84
+ : '') +
85
+ '.',
86
+ isError: true,
87
+ };
88
+ }
89
+ if (!query) {
90
+ await runIndex(ctx.latDir, key, progress);
91
+ return { output: '' };
92
+ }
93
+ const result = await runSearch(ctx.latDir, query, key, opts.limit, progress);
94
+ if (result.matches.length === 0) {
95
+ return { output: 'No results found.' };
66
96
  }
97
+ return {
98
+ output: formatResultList(ctx, `Search results for "${query}":`, result.matches) +
99
+ formatNavHints(ctx),
100
+ };
67
101
  }
@@ -0,0 +1,26 @@
1
+ import { type Section, type SectionMatch } from '../lattice.js';
2
+ import type { CmdContext, CmdResult } from '../context.js';
3
+ export type SectionFound = {
4
+ kind: 'found';
5
+ section: Section;
6
+ content: string;
7
+ outgoingRefs: {
8
+ target: string;
9
+ resolved: Section;
10
+ }[];
11
+ incomingRefs: SectionMatch[];
12
+ };
13
+ export type SectionResult = SectionFound | {
14
+ kind: 'no-match';
15
+ suggestions: SectionMatch[];
16
+ };
17
+ /**
18
+ * Look up a section by id, return its content, outgoing wiki link targets,
19
+ * and incoming references from other sections.
20
+ */
21
+ export declare function getSection(ctx: CmdContext, query: string): Promise<SectionResult>;
22
+ /**
23
+ * Format a successful section result with styling.
24
+ */
25
+ export declare function formatSectionOutput(ctx: CmdContext, result: SectionFound): string;
26
+ export declare function sectionCommand(ctx: CmdContext, query: string): Promise<CmdResult>;
@@ -0,0 +1,138 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { loadAllSections, findSections, flattenSections, extractRefs, buildFileIndex, resolveRef, listLatticeFiles, } from '../lattice.js';
4
+ import { formatSectionId, formatNavHints } from '../format.js';
5
+ /**
6
+ * Look up a section by id, return its content, outgoing wiki link targets,
7
+ * and incoming references from other sections.
8
+ */
9
+ export async function getSection(ctx, query) {
10
+ query = query.replace(/^\[\[|\]\]$/g, '');
11
+ const allSections = await loadAllSections(ctx.latDir);
12
+ const matches = findSections(allSections, query);
13
+ if (matches.length === 0) {
14
+ return { kind: 'no-match', suggestions: [] };
15
+ }
16
+ // Accept the top match if confident
17
+ const top = matches[0];
18
+ const isConfident = top.reason === 'exact match' ||
19
+ top.reason.startsWith('file stem expanded') ||
20
+ top.reason === 'section name match';
21
+ if (!isConfident) {
22
+ return { kind: 'no-match', suggestions: matches };
23
+ }
24
+ const section = top.section;
25
+ // Read raw content between startLine and endLine
26
+ const absPath = join(ctx.projectRoot, section.filePath);
27
+ const fileContent = await readFile(absPath, 'utf-8');
28
+ const lines = fileContent.split('\n');
29
+ const content = lines
30
+ .slice(section.startLine - 1, section.endLine)
31
+ .join('\n');
32
+ // Find outgoing wiki link targets within this section's content
33
+ const flat = flattenSections(allSections);
34
+ const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
35
+ const fileIndex = buildFileIndex(allSections);
36
+ const sectionRefs = extractRefs(absPath, fileContent, ctx.projectRoot);
37
+ const sectionId = section.id.toLowerCase();
38
+ const outgoingRefs = [];
39
+ const seen = new Set();
40
+ for (const ref of sectionRefs) {
41
+ if (ref.fromSection.toLowerCase() !== sectionId)
42
+ continue;
43
+ const { resolved } = resolveRef(ref.target, sectionIds, fileIndex);
44
+ const resolvedLower = resolved.toLowerCase();
45
+ if (seen.has(resolvedLower))
46
+ continue;
47
+ seen.add(resolvedLower);
48
+ const targetSection = flat.find((s) => s.id.toLowerCase() === resolvedLower);
49
+ if (targetSection) {
50
+ outgoingRefs.push({ target: ref.target, resolved: targetSection });
51
+ }
52
+ }
53
+ // Find incoming references: other sections that link to this one
54
+ const incomingRefs = [];
55
+ const files = await listLatticeFiles(ctx.latDir);
56
+ const incomingSections = new Set();
57
+ for (const file of files) {
58
+ const fc = await readFile(file, 'utf-8');
59
+ const fileRefs = extractRefs(file, fc, ctx.projectRoot);
60
+ for (const ref of fileRefs) {
61
+ const { resolved } = resolveRef(ref.target, sectionIds, fileIndex);
62
+ if (resolved.toLowerCase() === sectionId &&
63
+ ref.fromSection.toLowerCase() !== sectionId) {
64
+ if (!incomingSections.has(ref.fromSection.toLowerCase())) {
65
+ incomingSections.add(ref.fromSection.toLowerCase());
66
+ const fromSection = flat.find((s) => s.id.toLowerCase() === ref.fromSection.toLowerCase());
67
+ if (fromSection) {
68
+ incomingRefs.push({ section: fromSection, reason: 'wiki link' });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return { kind: 'found', section, content, outgoingRefs, incomingRefs };
75
+ }
76
+ function truncate(s, max) {
77
+ return s.length > max ? s.slice(0, max) + '...' : s;
78
+ }
79
+ /**
80
+ * Format a successful section result with styling.
81
+ */
82
+ export function formatSectionOutput(ctx, result) {
83
+ const s = ctx.styler;
84
+ const { section, content, outgoingRefs, incomingRefs } = result;
85
+ const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
86
+ const loc = `${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`;
87
+ const quoted = content
88
+ .split('\n')
89
+ .map((line) => (line ? `> ${line}` : '>'))
90
+ .join('\n');
91
+ const parts = [
92
+ `${s.bold('[[' + formatSectionId(section.id, s) + ']]')} (${loc})`,
93
+ '',
94
+ quoted,
95
+ ];
96
+ if (outgoingRefs.length > 0) {
97
+ parts.push('', '## This section references:', '');
98
+ for (const ref of outgoingRefs) {
99
+ const body = ref.resolved.firstParagraph
100
+ ? ` ${s.dim('—')} ${truncate(ref.resolved.firstParagraph, 120)}`
101
+ : '';
102
+ parts.push(`${s.dim('*')} [[${formatSectionId(ref.resolved.id, s)}]]${body}`);
103
+ }
104
+ }
105
+ if (incomingRefs.length > 0) {
106
+ parts.push('', '## Referenced by:', '');
107
+ for (const ref of incomingRefs) {
108
+ const body = ref.section.firstParagraph
109
+ ? ` ${s.dim('—')} ${truncate(ref.section.firstParagraph, 120)}`
110
+ : '';
111
+ parts.push(`${s.dim('*')} [[${formatSectionId(ref.section.id, s)}]]${body}`);
112
+ }
113
+ }
114
+ parts.push(formatNavHints(ctx));
115
+ return parts.join('\n');
116
+ }
117
+ export async function sectionCommand(ctx, query) {
118
+ const result = await getSection(ctx, query);
119
+ if (result.kind === 'no-match') {
120
+ const s = ctx.styler;
121
+ if (result.suggestions.length > 0) {
122
+ const suggestions = result.suggestions
123
+ .map((m) => ` ${s.dim('*')} ${s.white(m.section.id)} ${s.dim(`(${m.reason})`)}`)
124
+ .join('\n');
125
+ return {
126
+ output: s.red(`No section "${query}" found.`) +
127
+ ' Did you mean:\n' +
128
+ suggestions,
129
+ isError: true,
130
+ };
131
+ }
132
+ return {
133
+ output: s.red(`No sections matching "${query}"`),
134
+ isError: true,
135
+ };
136
+ }
137
+ return { output: formatSectionOutput(ctx, result) };
138
+ }
@@ -39,7 +39,8 @@ export async function scanCodeRefs(projectRoot) {
39
39
  try {
40
40
  content = await readFile(file, 'utf-8');
41
41
  }
42
- catch {
42
+ catch (err) {
43
+ process.stderr.write(`Error: failed to read ${file}: ${err.message}\n`);
43
44
  continue;
44
45
  }
45
46
  const lines = content.split('\n');
@@ -16,8 +16,9 @@ export function readConfig() {
16
16
  try {
17
17
  return JSON.parse(readFileSync(configPath, 'utf-8'));
18
18
  }
19
- catch {
20
- return {};
19
+ catch (err) {
20
+ process.stderr.write(`Error: failed to parse config ${configPath}: ${err.message}\n`);
21
+ process.exit(1);
21
22
  }
22
23
  }
23
24
  export function writeConfig(config) {
@@ -0,0 +1,21 @@
1
+ export type Styler = {
2
+ bold: (s: string) => string;
3
+ dim: (s: string) => string;
4
+ red: (s: string) => string;
5
+ cyan: (s: string) => string;
6
+ white: (s: string) => string;
7
+ green: (s: string) => string;
8
+ yellow: (s: string) => string;
9
+ boldWhite: (s: string) => string;
10
+ };
11
+ export declare const plainStyler: Styler;
12
+ export type CmdContext = {
13
+ latDir: string;
14
+ projectRoot: string;
15
+ styler: Styler;
16
+ mode: 'cli' | 'mcp';
17
+ };
18
+ export type CmdResult = {
19
+ output: string;
20
+ isError?: boolean;
21
+ };
@@ -0,0 +1,11 @@
1
+ const identity = (s) => s;
2
+ export const plainStyler = {
3
+ bold: identity,
4
+ dim: identity,
5
+ red: identity,
6
+ cyan: identity,
7
+ white: identity,
8
+ green: identity,
9
+ yellow: identity,
10
+ boldWhite: identity,
11
+ };
@@ -1,6 +1,8 @@
1
1
  import type { Section, SectionMatch } from './lattice.js';
2
- export declare function formatSectionId(id: string): string;
3
- export declare function formatSectionPreview(section: Section, projectRoot: string, opts?: {
2
+ import type { CmdContext, Styler } from './context.js';
3
+ export declare function formatSectionId(id: string, s: Styler): string;
4
+ export declare function formatSectionPreview(ctx: CmdContext, section: Section, opts?: {
4
5
  reason?: string;
5
6
  }): string;
6
- export declare function formatResultList(header: string, matches: SectionMatch[], projectRoot: string): string;
7
+ export declare function formatResultList(ctx: CmdContext, header: string, matches: SectionMatch[]): string;
8
+ export declare function formatNavHints(ctx: CmdContext): string;
@@ -1,38 +1,43 @@
1
1
  import { join, relative } from 'node:path';
2
- import chalk from 'chalk';
3
- export function formatSectionId(id) {
2
+ export function formatSectionId(id, s) {
4
3
  const parts = id.split('#');
5
4
  return parts.length === 1
6
- ? chalk.bold.white(parts[0])
7
- : chalk.dim(parts.slice(0, -1).join('#') + '#') +
8
- chalk.bold.white(parts[parts.length - 1]);
5
+ ? s.boldWhite(parts[0])
6
+ : s.dim(parts.slice(0, -1).join('#') + '#') +
7
+ s.boldWhite(parts[parts.length - 1]);
9
8
  }
10
- export function formatSectionPreview(section, projectRoot, opts) {
11
- const relPath = relative(process.cwd(), join(projectRoot, section.filePath));
9
+ export function formatSectionPreview(ctx, section, opts) {
10
+ const s = ctx.styler;
11
+ const relPath = relative(process.cwd(), join(ctx.projectRoot, section.filePath));
12
12
  const kind = section.id.includes('#') ? 'Section' : 'File';
13
- const reasonSuffix = opts?.reason ? ' ' + chalk.dim(`(${opts.reason})`) : '';
13
+ const reasonSuffix = opts?.reason ? ' ' + s.dim(`(${opts.reason})`) : '';
14
14
  const lines = [
15
- `${chalk.dim('*')} ${chalk.dim(kind + ':')} [[${formatSectionId(section.id)}]]${reasonSuffix}`,
16
- ` ${chalk.dim('Defined in')} ${chalk.cyan(relPath)}${chalk.dim(`:${section.startLine}-${section.endLine}`)}`,
15
+ `${s.dim('*')} ${s.dim(kind + ':')} [[${formatSectionId(section.id, s)}]]${reasonSuffix}`,
16
+ ` ${s.dim('Defined in')} ${s.cyan(relPath)}${s.dim(`:${section.startLine}-${section.endLine}`)}`,
17
17
  ];
18
- if (section.body) {
19
- const truncated = section.body.length > 200
20
- ? section.body.slice(0, 200) + '...'
21
- : section.body;
22
- lines.push('');
23
- lines.push(` ${chalk.dim('>')} ${truncated}`);
18
+ if (section.firstParagraph) {
19
+ lines.push('', ` ${s.dim('>')} ${section.firstParagraph}`);
24
20
  }
25
21
  return lines.join('\n');
26
22
  }
27
- export function formatResultList(header, matches, projectRoot) {
28
- const lines = ['', chalk.bold(header), ''];
23
+ export function formatResultList(ctx, header, matches) {
24
+ const lines = ['', `## ${header}`, ''];
29
25
  for (let i = 0; i < matches.length; i++) {
30
26
  if (i > 0)
31
27
  lines.push('');
32
- lines.push(formatSectionPreview(matches[i].section, projectRoot, {
28
+ lines.push(formatSectionPreview(ctx, matches[i].section, {
33
29
  reason: matches[i].reason,
34
30
  }));
35
31
  }
36
32
  lines.push('');
37
33
  return lines.join('\n');
38
34
  }
35
+ export function formatNavHints(ctx) {
36
+ const s = ctx.styler;
37
+ const hints = ctx.mode === 'cli'
38
+ ? `${s.dim('*')} \`lat section "section#id"\` \u2014 show full content with outgoing/incoming refs\n` +
39
+ `${s.dim('*')} \`lat search "new query"\` \u2014 search for something else`
40
+ : `${s.dim('*')} \`lat_section\` \u2014 show full content with outgoing/incoming refs\n` +
41
+ `${s.dim('*')} \`lat_search\` \u2014 search for something else`;
42
+ return `\n## To navigate further:\n\n${hints}`;
43
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Bump this number whenever `lat init` setup changes in a way that
3
+ * requires users to re-run it (e.g. new hooks, AGENTS.md changes,
4
+ * MCP config changes).
5
+ */
6
+ export declare const INIT_VERSION = 1;
7
+ export declare function readInitVersion(latDir: string): number | null;
8
+ export declare function readFileHash(latDir: string, relPath: string): string | null;
9
+ export declare function contentHash(content: string): string;
10
+ export declare function writeInitMeta(latDir: string, fileHashes: Record<string, string>): void;