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.
- package/README.md +3 -2
- package/dist/src/cli/check.d.ts +7 -5
- package/dist/src/cli/check.js +186 -65
- package/dist/src/cli/context.d.ts +3 -8
- package/dist/src/cli/context.js +13 -1
- package/dist/src/cli/expand.d.ts +7 -0
- package/dist/src/cli/{prompt.js → expand.js} +44 -13
- package/dist/src/cli/gen.js +11 -4
- package/dist/src/cli/hook.d.ts +1 -0
- package/dist/src/cli/hook.js +147 -0
- package/dist/src/cli/index.js +77 -28
- package/dist/src/cli/init.js +148 -120
- package/dist/src/cli/locate.d.ts +2 -2
- package/dist/src/cli/locate.js +9 -4
- package/dist/src/cli/refs.d.ts +20 -4
- package/dist/src/cli/refs.js +63 -39
- package/dist/src/cli/search.d.ts +25 -3
- package/dist/src/cli/search.js +82 -48
- package/dist/src/cli/section.d.ts +26 -0
- package/dist/src/cli/section.js +138 -0
- package/dist/src/code-refs.js +2 -1
- package/dist/src/config.js +3 -2
- package/dist/src/context.d.ts +21 -0
- package/dist/src/context.js +11 -0
- package/dist/src/format.d.ts +5 -3
- package/dist/src/format.js +24 -19
- package/dist/src/init-version.d.ts +10 -0
- package/dist/src/init-version.js +49 -0
- package/dist/src/lattice.d.ts +1 -2
- package/dist/src/lattice.js +5 -8
- package/dist/src/mcp/server.js +26 -279
- package/dist/src/parser.js +2 -0
- package/dist/src/source-parser.js +389 -2
- package/package.json +2 -1
- package/templates/AGENTS.md +36 -5
- package/templates/cursor-rules.md +9 -4
- package/templates/lat-prompt-hook.sh +2 -2
- package/dist/src/cli/prompt.d.ts +0 -2
package/dist/src/cli/refs.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
const
|
|
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
|
-
|
|
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:
|
|
58
|
-
for (const ref of
|
|
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
|
-
|
|
61
|
+
codeRefs.push(`${ref.file}:${ref.line}`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
}
|
package/dist/src/cli/search.d.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
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<
|
|
27
|
+
}, progress?: IndexProgress): Promise<CmdResult>;
|
package/dist/src/cli/search.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const results = await searchSections(db, query, provider, key,
|
|
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
|
-
|
|
52
|
-
return;
|
|
31
|
+
return { query, matches: [] };
|
|
53
32
|
}
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
+
}
|
package/dist/src/code-refs.js
CHANGED
|
@@ -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');
|
package/dist/src/config.js
CHANGED
|
@@ -16,8 +16,9 @@ export function readConfig() {
|
|
|
16
16
|
try {
|
|
17
17
|
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
18
18
|
}
|
|
19
|
-
catch {
|
|
20
|
-
|
|
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
|
+
};
|
package/dist/src/format.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Section, SectionMatch } from './lattice.js';
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
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[]
|
|
7
|
+
export declare function formatResultList(ctx: CmdContext, header: string, matches: SectionMatch[]): string;
|
|
8
|
+
export declare function formatNavHints(ctx: CmdContext): string;
|
package/dist/src/format.js
CHANGED
|
@@ -1,38 +1,43 @@
|
|
|
1
1
|
import { join, relative } from 'node:path';
|
|
2
|
-
|
|
3
|
-
export function formatSectionId(id) {
|
|
2
|
+
export function formatSectionId(id, s) {
|
|
4
3
|
const parts = id.split('#');
|
|
5
4
|
return parts.length === 1
|
|
6
|
-
?
|
|
7
|
-
:
|
|
8
|
-
|
|
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(
|
|
11
|
-
const
|
|
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 ? ' ' +
|
|
13
|
+
const reasonSuffix = opts?.reason ? ' ' + s.dim(`(${opts.reason})`) : '';
|
|
14
14
|
const lines = [
|
|
15
|
-
`${
|
|
16
|
-
` ${
|
|
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.
|
|
19
|
-
|
|
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
|
|
28
|
-
const lines = ['',
|
|
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,
|
|
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;
|