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.
- package/dist/src/cli/check.d.ts +7 -5
- package/dist/src/cli/check.js +243 -74
- package/dist/src/cli/context.d.ts +3 -7
- package/dist/src/cli/context.js +15 -1
- package/dist/src/cli/expand.d.ts +7 -0
- package/dist/src/cli/expand.js +92 -0
- 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 +64 -42
- package/dist/src/cli/search.d.ts +25 -3
- package/dist/src/cli/search.js +87 -47
- package/dist/src/cli/section.d.ts +26 -0
- package/dist/src/cli/section.js +133 -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 +4 -3
- package/dist/src/format.js +16 -20
- package/dist/src/init-version.d.ts +10 -0
- package/dist/src/init-version.js +49 -0
- package/dist/src/lattice.d.ts +11 -5
- package/dist/src/lattice.js +87 -38
- package/dist/src/mcp/server.js +27 -279
- package/dist/src/search/index.js +5 -4
- package/dist/src/source-parser.d.ts +23 -0
- package/dist/src/source-parser.js +720 -0
- package/package.json +3 -1
- package/templates/AGENTS.md +38 -6
- package/templates/cursor-rules.md +11 -5
- package/templates/lat-prompt-hook.sh +2 -2
- package/dist/src/cli/prompt.d.ts +0 -2
- package/dist/src/cli/prompt.js +0 -60
package/dist/src/cli/check.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
23
|
-
export declare function
|
|
24
|
-
export declare function
|
|
25
|
-
export declare function
|
|
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>;
|
package/dist/src/cli/check.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
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,
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const loc =
|
|
243
|
-
const [first, ...rest] =
|
|
244
|
-
|
|
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
|
-
|
|
362
|
+
lines.push(` ${s.red(line)}`);
|
|
247
363
|
}
|
|
248
364
|
}
|
|
365
|
+
return lines;
|
|
249
366
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
256
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export type
|
|
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
|
-
}):
|
|
6
|
+
}): CmdContext;
|
package/dist/src/cli/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/src/cli/gen.js
CHANGED
|
@@ -9,9 +9,16 @@ export function readCursorRulesTemplate() {
|
|
|
9
9
|
}
|
|
10
10
|
export async function genCmd(target) {
|
|
11
11
|
const normalized = target.toLowerCase();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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>;
|