lat.md 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +58 -8
- package/dist/src/cli/check.d.ts +18 -0
- package/dist/src/cli/check.js +122 -0
- package/dist/src/cli/context.d.ts +10 -0
- package/dist/src/cli/context.js +14 -0
- package/dist/src/cli/gen.d.ts +2 -0
- package/dist/src/cli/gen.js +14 -0
- package/dist/src/cli/index.js +126 -17
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +67 -0
- package/dist/src/cli/locate.d.ts +2 -1
- package/dist/src/cli/locate.js +8 -20
- package/dist/src/cli/prompt.d.ts +2 -0
- package/dist/src/cli/prompt.js +62 -0
- package/dist/src/cli/refs.d.ts +4 -1
- package/dist/src/cli/refs.js +36 -109
- package/dist/src/cli/search.d.ts +5 -0
- package/dist/src/cli/search.js +55 -0
- package/dist/src/cli/templates.d.ts +1 -0
- package/dist/src/cli/templates.js +15 -0
- package/dist/src/code-refs.d.ts +13 -0
- package/dist/src/code-refs.js +63 -0
- package/dist/src/format.d.ts +7 -1
- package/dist/src/format.js +26 -4
- package/dist/src/lattice.d.ts +6 -0
- package/dist/src/lattice.js +98 -11
- package/dist/src/search/db.d.ts +4 -0
- package/dist/src/search/db.js +31 -0
- package/dist/src/search/embeddings.d.ts +2 -0
- package/dist/src/search/embeddings.js +25 -0
- package/dist/src/search/index.d.ts +9 -0
- package/dist/src/search/index.js +66 -0
- package/dist/src/search/provider.d.ts +8 -0
- package/dist/src/search/provider.js +40 -0
- package/dist/src/search/search.d.ts +9 -0
- package/dist/src/search/search.js +17 -0
- package/package.json +28 -4
- package/templates/AGENTS.md +56 -0
- package/templates/README +1 -0
- package/templates/init/README.md +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yury Selivanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,16 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# lat.md
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://github.com/1st1/lat.md/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A knowledge graph for your codebase, written in markdown.
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
`AGENTS.md` doesn't scale. A single flat file can describe a small project, but as a codebase grows, maintaining one monolithic document becomes impractical. Key design decisions get buried, business logic goes undocumented, and agents hallucinate context they should be able to look up.
|
|
10
|
+
|
|
11
|
+
## The idea
|
|
12
|
+
|
|
13
|
+
Compress the knowledge about your program domain into a **graph** — a set of interconnected markdown files that live in a `lat.md/` directory at the root of your project. Sections link to each other with `[[wiki links]]`, source files link back with `// @lat:` comments, and `lat check` ensures nothing drifts out of sync.
|
|
14
|
+
|
|
15
|
+
The result is a structured knowledge base that:
|
|
16
|
+
|
|
17
|
+
- 📈 **Scales** — split knowledge across as many files and sections as you need
|
|
18
|
+
- 🔗 **Cross-references** — wiki links (`[[cli#search#Indexing]]`) connect concepts into a navigable graph
|
|
19
|
+
- ✅ **Stays in sync** — `lat check` validates that all links resolve and that required code references exist
|
|
20
|
+
- 🔍 **Is searchable** — exact, fuzzy, and semantic (vector) search across all sections
|
|
21
|
+
- 🤝 **Works for humans and machines** — readable in any editor (or Obsidian), queryable by agents via the `lat` CLI
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g lat.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or use directly with `npx lat.md <command>`.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
Run `lat init` to scaffold a `lat.md/` directory, then write markdown files describing your architecture, business logic, test specs — whatever matters. Link between sections using `[[file#Section#Subsection]]` syntax. Annotate source code with `// @lat: [[section-id]]` (or `# @lat: [[section-id]]` in Python) comments to tie implementation back to concepts.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
my-project/
|
|
37
|
+
├── lat.md/
|
|
38
|
+
│ ├── architecture.md # system design, key decisions
|
|
39
|
+
│ ├── auth.md # authentication & authorization logic
|
|
40
|
+
│ └── tests.md # test specs (require-code-mention: true)
|
|
41
|
+
├── src/
|
|
42
|
+
│ ├── auth.ts # // @lat: [[auth#OAuth Flow]]
|
|
43
|
+
│ └── server.ts # // @lat: [[architecture#Request Pipeline]]
|
|
44
|
+
└── ...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx lat.md init # scaffold a lat.md/ directory
|
|
51
|
+
npx lat.md check # validate all wiki links and code refs
|
|
52
|
+
npx lat.md locate "OAuth Flow" # find sections by name (exact, fuzzy)
|
|
53
|
+
npx lat.md refs "auth#OAuth Flow" # find what references a section
|
|
54
|
+
npx lat.md search "how do we auth?" # semantic search via embeddings
|
|
55
|
+
npx lat.md prompt "fix [[OAuth Flow]]" # expand [[refs]] in a prompt for agents
|
|
56
|
+
```
|
|
5
57
|
|
|
6
58
|
## Development
|
|
7
59
|
|
|
8
|
-
Requires Node.js 22
|
|
60
|
+
Requires Node.js 22+ and pnpm.
|
|
9
61
|
|
|
10
62
|
```bash
|
|
11
63
|
pnpm install
|
|
12
|
-
pnpm
|
|
13
|
-
pnpm test
|
|
14
|
-
pnpm typecheck # tsc --noEmit
|
|
15
|
-
pnpm format # prettier
|
|
64
|
+
pnpm build
|
|
65
|
+
pnpm test
|
|
16
66
|
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CliContext } from './context.js';
|
|
2
|
+
export type CheckError = {
|
|
3
|
+
file: string;
|
|
4
|
+
line: number;
|
|
5
|
+
target: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
/** File counts grouped by extension (e.g. { ".ts": 5, ".py": 2 }). */
|
|
9
|
+
export type FileStats = Record<string, number>;
|
|
10
|
+
export type CheckResult = {
|
|
11
|
+
errors: CheckError[];
|
|
12
|
+
files: FileStats;
|
|
13
|
+
};
|
|
14
|
+
export declare function checkMd(latticeDir: string): Promise<CheckResult>;
|
|
15
|
+
export declare function checkCodeRefs(latticeDir: string): Promise<CheckResult>;
|
|
16
|
+
export declare function checkMdCmd(ctx: CliContext): Promise<void>;
|
|
17
|
+
export declare function checkCodeRefsCmd(ctx: CliContext): Promise<void>;
|
|
18
|
+
export declare function checkAllCmd(ctx: CliContext): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, join, relative } from 'node:path';
|
|
3
|
+
import { listLatticeFiles, loadAllSections, extractRefs, flattenSections, parseFrontmatter, parseSections, } from '../lattice.js';
|
|
4
|
+
import { scanCodeRefs } from '../code-refs.js';
|
|
5
|
+
function countByExt(paths) {
|
|
6
|
+
const stats = {};
|
|
7
|
+
for (const p of paths) {
|
|
8
|
+
const ext = extname(p) || '(no ext)';
|
|
9
|
+
stats[ext] = (stats[ext] || 0) + 1;
|
|
10
|
+
}
|
|
11
|
+
return stats;
|
|
12
|
+
}
|
|
13
|
+
export async function checkMd(latticeDir) {
|
|
14
|
+
const files = await listLatticeFiles(latticeDir);
|
|
15
|
+
const allSections = await loadAllSections(latticeDir);
|
|
16
|
+
const flat = flattenSections(allSections);
|
|
17
|
+
const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
|
|
18
|
+
const errors = [];
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const content = await readFile(file, 'utf-8');
|
|
21
|
+
const refs = extractRefs(file, content);
|
|
22
|
+
const relPath = relative(process.cwd(), file);
|
|
23
|
+
for (const ref of refs) {
|
|
24
|
+
const target = ref.target.toLowerCase();
|
|
25
|
+
if (!sectionIds.has(target)) {
|
|
26
|
+
errors.push({
|
|
27
|
+
file: relPath,
|
|
28
|
+
line: ref.line,
|
|
29
|
+
target: ref.target,
|
|
30
|
+
message: `broken link [[${ref.target}]] — no matching section found`,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { errors, files: countByExt(files) };
|
|
36
|
+
}
|
|
37
|
+
export async function checkCodeRefs(latticeDir) {
|
|
38
|
+
const projectRoot = join(latticeDir, '..');
|
|
39
|
+
const allSections = await loadAllSections(latticeDir);
|
|
40
|
+
const flat = flattenSections(allSections);
|
|
41
|
+
const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
|
|
42
|
+
const scan = await scanCodeRefs(projectRoot);
|
|
43
|
+
const errors = [];
|
|
44
|
+
const mentionedSections = new Set();
|
|
45
|
+
for (const ref of scan.refs) {
|
|
46
|
+
const target = ref.target.toLowerCase();
|
|
47
|
+
mentionedSections.add(target);
|
|
48
|
+
if (!sectionIds.has(target)) {
|
|
49
|
+
errors.push({
|
|
50
|
+
file: ref.file,
|
|
51
|
+
line: ref.line,
|
|
52
|
+
target: ref.target,
|
|
53
|
+
message: `@lat: [[${ref.target}]] — no matching section found`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const files = await listLatticeFiles(latticeDir);
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const content = await readFile(file, 'utf-8');
|
|
60
|
+
const fm = parseFrontmatter(content);
|
|
61
|
+
if (!fm.requireCodeMention)
|
|
62
|
+
continue;
|
|
63
|
+
const sections = parseSections(file, content);
|
|
64
|
+
const fileSections = flattenSections(sections);
|
|
65
|
+
const leafSections = fileSections.filter((s) => s.children.length === 0);
|
|
66
|
+
const relPath = relative(process.cwd(), file);
|
|
67
|
+
for (const leaf of leafSections) {
|
|
68
|
+
if (!mentionedSections.has(leaf.id.toLowerCase())) {
|
|
69
|
+
errors.push({
|
|
70
|
+
file: relPath,
|
|
71
|
+
line: leaf.startLine,
|
|
72
|
+
target: leaf.id,
|
|
73
|
+
message: `section "${leaf.id}" requires a code mention but none found`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { errors, files: countByExt(scan.files) };
|
|
79
|
+
}
|
|
80
|
+
function formatErrors(ctx, errors) {
|
|
81
|
+
for (const err of errors) {
|
|
82
|
+
console.error(`${ctx.chalk.cyan(err.file + ':' + err.line)}: ${ctx.chalk.red(err.message)}`);
|
|
83
|
+
}
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
console.error(ctx.chalk.red(`\n${errors.length} error${errors.length === 1 ? '' : 's'} found`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function formatStats(ctx, stats) {
|
|
89
|
+
const entries = Object.entries(stats).sort(([a], [b]) => a.localeCompare(b));
|
|
90
|
+
const parts = entries.map(([ext, n]) => `${n} ${ext}`);
|
|
91
|
+
console.log(ctx.chalk.dim(`Scanned ${parts.join(', ')}`));
|
|
92
|
+
}
|
|
93
|
+
export async function checkMdCmd(ctx) {
|
|
94
|
+
const { errors, files } = await checkMd(ctx.latDir);
|
|
95
|
+
formatStats(ctx, files);
|
|
96
|
+
formatErrors(ctx, errors);
|
|
97
|
+
if (errors.length > 0)
|
|
98
|
+
process.exit(1);
|
|
99
|
+
console.log(ctx.chalk.green('md: All links OK'));
|
|
100
|
+
}
|
|
101
|
+
export async function checkCodeRefsCmd(ctx) {
|
|
102
|
+
const { errors, files } = await checkCodeRefs(ctx.latDir);
|
|
103
|
+
formatStats(ctx, files);
|
|
104
|
+
formatErrors(ctx, errors);
|
|
105
|
+
if (errors.length > 0)
|
|
106
|
+
process.exit(1);
|
|
107
|
+
console.log(ctx.chalk.green('code-refs: All references OK'));
|
|
108
|
+
}
|
|
109
|
+
export async function checkAllCmd(ctx) {
|
|
110
|
+
const md = await checkMd(ctx.latDir);
|
|
111
|
+
const code = await checkCodeRefs(ctx.latDir);
|
|
112
|
+
const allErrors = [...md.errors, ...code.errors];
|
|
113
|
+
const allFiles = { ...md.files };
|
|
114
|
+
for (const [ext, n] of Object.entries(code.files)) {
|
|
115
|
+
allFiles[ext] = (allFiles[ext] || 0) + n;
|
|
116
|
+
}
|
|
117
|
+
formatStats(ctx, allFiles);
|
|
118
|
+
formatErrors(ctx, allErrors);
|
|
119
|
+
if (allErrors.length > 0)
|
|
120
|
+
process.exit(1);
|
|
121
|
+
console.log(ctx.chalk.green('All checks passed'));
|
|
122
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { findLatticeDir } from '../lattice.js';
|
|
3
|
+
export function resolveContext(opts) {
|
|
4
|
+
const color = opts.color !== false;
|
|
5
|
+
if (!color) {
|
|
6
|
+
chalk.level = 0;
|
|
7
|
+
}
|
|
8
|
+
const latDir = findLatticeDir(opts.dir) ?? '';
|
|
9
|
+
if (!latDir) {
|
|
10
|
+
console.error(chalk.red('No lat.md directory found'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
return { latDir, color, chalk };
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { findTemplatesDir } from './templates.js';
|
|
4
|
+
export function readAgentsTemplate() {
|
|
5
|
+
return readFileSync(join(findTemplatesDir(), 'AGENTS.md'), 'utf-8');
|
|
6
|
+
}
|
|
7
|
+
export async function genCmd(target) {
|
|
8
|
+
const normalized = target.toLowerCase();
|
|
9
|
+
if (normalized !== 'agents.md' && normalized !== 'claude.md') {
|
|
10
|
+
console.error(`Unknown target: ${target}. Supported: agents.md, claude.md`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
process.stdout.write(readAgentsTemplate());
|
|
14
|
+
}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -1,19 +1,128 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { resolveContext } from './context.js';
|
|
7
|
+
import { locateCmd } from './locate.js';
|
|
8
|
+
import { refsCmd } from './refs.js';
|
|
9
|
+
function findPackageJson() {
|
|
10
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
while (true) {
|
|
12
|
+
const candidate = join(dir, 'package.json');
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(candidate, 'utf-8')).version;
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
const parent = dirname(dir);
|
|
18
|
+
if (parent === dir)
|
|
19
|
+
return '0.0.0';
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
|
-
|
|
23
|
+
const version = findPackageJson();
|
|
24
|
+
const program = new Command();
|
|
25
|
+
program
|
|
26
|
+
.name('lat')
|
|
27
|
+
.description('Anchor source code to high-level concepts defined in markdown')
|
|
28
|
+
.version(version)
|
|
29
|
+
.option('--dir <path>', 'project root to look for lat.md in (default: cwd)')
|
|
30
|
+
.option('--no-color', 'disable color output');
|
|
31
|
+
program
|
|
32
|
+
.command('locate')
|
|
33
|
+
.description('Find sections by id')
|
|
34
|
+
.argument('<query>', 'section id to search for')
|
|
35
|
+
.action(async (query) => {
|
|
36
|
+
const ctx = resolveContext(program.opts());
|
|
37
|
+
await locateCmd(ctx, query);
|
|
38
|
+
});
|
|
39
|
+
program
|
|
40
|
+
.command('refs')
|
|
41
|
+
.description('Find references to a section')
|
|
42
|
+
.argument('<query>', 'section id to find references for')
|
|
43
|
+
.option('--scope <scope>', 'where to search: md, code, or md+code', 'md')
|
|
44
|
+
.action(async (query, opts) => {
|
|
45
|
+
const scope = opts.scope;
|
|
46
|
+
if (scope !== 'md' && scope !== 'code' && scope !== 'md+code') {
|
|
47
|
+
console.error(`Unknown scope: ${scope}. Use md, code, or md+code.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const ctx = resolveContext(program.opts());
|
|
51
|
+
await refsCmd(ctx, query, scope);
|
|
52
|
+
});
|
|
53
|
+
const check = program
|
|
54
|
+
.command('check')
|
|
55
|
+
.description('Validate links and code references')
|
|
56
|
+
.action(async () => {
|
|
57
|
+
const ctx = resolveContext(program.opts());
|
|
58
|
+
const { checkAllCmd } = await import('./check.js');
|
|
59
|
+
await checkAllCmd(ctx);
|
|
60
|
+
});
|
|
61
|
+
check
|
|
62
|
+
.command('md')
|
|
63
|
+
.description('Validate wiki links in lat.md markdown files')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const ctx = resolveContext(program.opts());
|
|
66
|
+
const { checkMdCmd } = await import('./check.js');
|
|
67
|
+
await checkMdCmd(ctx);
|
|
68
|
+
});
|
|
69
|
+
check
|
|
70
|
+
.command('code-refs')
|
|
71
|
+
.description('Validate @lat code references and coverage')
|
|
72
|
+
.action(async () => {
|
|
73
|
+
const ctx = resolveContext(program.opts());
|
|
74
|
+
const { checkCodeRefsCmd } = await import('./check.js');
|
|
75
|
+
await checkCodeRefsCmd(ctx);
|
|
76
|
+
});
|
|
77
|
+
program
|
|
78
|
+
.command('prompt')
|
|
79
|
+
.description('Expand [[refs]] in a prompt to lat.md section locations')
|
|
80
|
+
.argument('[text]', 'prompt text')
|
|
81
|
+
.option('--stdin', 'read prompt from stdin')
|
|
82
|
+
.action(async (text, opts) => {
|
|
83
|
+
if (opts.stdin) {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
for await (const chunk of process.stdin) {
|
|
86
|
+
chunks.push(chunk);
|
|
87
|
+
}
|
|
88
|
+
text = Buffer.concat(chunks).toString('utf-8');
|
|
89
|
+
}
|
|
90
|
+
if (!text) {
|
|
91
|
+
console.error('Provide prompt text as an argument or use --stdin');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const ctx = resolveContext(program.opts());
|
|
95
|
+
const { promptCmd } = await import('./prompt.js');
|
|
96
|
+
await promptCmd(ctx, text);
|
|
97
|
+
});
|
|
98
|
+
program
|
|
99
|
+
.command('search')
|
|
100
|
+
.description('Semantic search across lat.md sections')
|
|
101
|
+
.argument('[query]', 'search query in plain English')
|
|
102
|
+
.option('--limit <n>', 'max results', '5')
|
|
103
|
+
.option('--reindex', 'force full re-indexing')
|
|
104
|
+
.action(async (query, opts) => {
|
|
105
|
+
const ctx = resolveContext(program.opts());
|
|
106
|
+
const { searchCmd } = await import('./search.js');
|
|
107
|
+
await searchCmd(ctx, query, {
|
|
108
|
+
limit: parseInt(opts.limit),
|
|
109
|
+
reindex: opts.reindex,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
program
|
|
113
|
+
.command('gen')
|
|
114
|
+
.description('Generate a file to stdout (agents.md, claude.md)')
|
|
115
|
+
.argument('<target>', 'file to generate: agents.md or claude.md')
|
|
116
|
+
.action(async (target) => {
|
|
117
|
+
const { genCmd } = await import('./gen.js');
|
|
118
|
+
await genCmd(target);
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command('init')
|
|
122
|
+
.description('Initialize a lat.md directory')
|
|
123
|
+
.argument('[dir]', 'target directory (default: cwd)')
|
|
124
|
+
.action(async (dir) => {
|
|
125
|
+
const { initCmd } = await import('./init.js');
|
|
126
|
+
await initCmd(dir);
|
|
127
|
+
});
|
|
128
|
+
await program.parseAsync();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCmd(targetDir?: string): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, cpSync, mkdirSync, writeFileSync, symlinkSync, } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { findTemplatesDir } from './templates.js';
|
|
6
|
+
import { readAgentsTemplate } from './gen.js';
|
|
7
|
+
async function confirm(rl, message) {
|
|
8
|
+
try {
|
|
9
|
+
const answer = await rl.question(`${message} ${chalk.dim('[Y/n]')} `);
|
|
10
|
+
return answer.trim().toLowerCase() !== 'n';
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function initCmd(targetDir) {
|
|
17
|
+
const root = resolve(targetDir ?? process.cwd());
|
|
18
|
+
const latDir = join(root, 'lat.md');
|
|
19
|
+
const interactive = process.stdin.isTTY ?? false;
|
|
20
|
+
const rl = interactive
|
|
21
|
+
? createInterface({ input: process.stdin, output: process.stdout })
|
|
22
|
+
: null;
|
|
23
|
+
const ask = async (message) => {
|
|
24
|
+
if (!rl)
|
|
25
|
+
return true;
|
|
26
|
+
return confirm(rl, message);
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
// Step 1: lat.md/ directory
|
|
30
|
+
if (existsSync(latDir)) {
|
|
31
|
+
console.log(chalk.green('lat.md/') + ' already exists');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
if (!(await ask('Create lat.md/ directory?'))) {
|
|
35
|
+
console.log('Aborted.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const templateDir = join(findTemplatesDir(), 'init');
|
|
39
|
+
mkdirSync(latDir, { recursive: true });
|
|
40
|
+
cpSync(templateDir, latDir, { recursive: true });
|
|
41
|
+
console.log(chalk.green('Created lat.md/'));
|
|
42
|
+
}
|
|
43
|
+
// Step 2: AGENTS.md / CLAUDE.md
|
|
44
|
+
const agentsPath = join(root, 'AGENTS.md');
|
|
45
|
+
const claudePath = join(root, 'CLAUDE.md');
|
|
46
|
+
const hasAgents = existsSync(agentsPath);
|
|
47
|
+
const hasClaude = existsSync(claudePath);
|
|
48
|
+
if (!hasAgents && !hasClaude) {
|
|
49
|
+
if (await ask('Generate AGENTS.md and CLAUDE.md with lat.md instructions for coding agents?')) {
|
|
50
|
+
const template = readAgentsTemplate();
|
|
51
|
+
writeFileSync(agentsPath, template);
|
|
52
|
+
symlinkSync('AGENTS.md', claudePath);
|
|
53
|
+
console.log(chalk.green('Created AGENTS.md and CLAUDE.md → AGENTS.md'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const existing = [hasAgents && 'AGENTS.md', hasClaude && 'CLAUDE.md']
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join(' and ');
|
|
60
|
+
console.log(`\n${existing} already exists. Run ${chalk.cyan('lat gen agents.md')} to preview the template,` +
|
|
61
|
+
` then incorporate its content or overwrite as needed.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
rl?.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/src/cli/locate.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CliContext } from './context.js';
|
|
2
|
+
export declare function locateCmd(ctx: CliContext, query: string): Promise<void>;
|
package/dist/src/cli/locate.js
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export async function
|
|
4
|
-
|
|
5
|
-
console.error('Usage: lat locate <query>');
|
|
6
|
-
process.exit(1);
|
|
7
|
-
}
|
|
8
|
-
const query = args[0];
|
|
9
|
-
const latticeDir = findLatticeDir();
|
|
10
|
-
if (!latticeDir) {
|
|
11
|
-
console.error('No .lattice directory found');
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
const sections = await loadAllSections(latticeDir);
|
|
1
|
+
import { loadAllSections, findSections } from '../lattice.js';
|
|
2
|
+
import { formatResultList } from '../format.js';
|
|
3
|
+
export async function locateCmd(ctx, query) {
|
|
4
|
+
const sections = await loadAllSections(ctx.latDir);
|
|
15
5
|
const matches = findSections(sections, query);
|
|
16
6
|
if (matches.length === 0) {
|
|
17
|
-
console.error(`No sections matching "${query}"`);
|
|
7
|
+
console.error(ctx.chalk.red(`No sections matching "${query}" (no exact, substring, or fuzzy matches)`));
|
|
18
8
|
process.exit(1);
|
|
19
9
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log(formatSectionPreview(matches[i], latticeDir));
|
|
24
|
-
}
|
|
10
|
+
console.log(formatResultList(`Sections matching "${query}":`, matches, ctx.latDir, {
|
|
11
|
+
numbered: true,
|
|
12
|
+
}));
|
|
25
13
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { relative } from 'node:path';
|
|
2
|
+
import { loadAllSections, findSections, flattenSections, } from '../lattice.js';
|
|
3
|
+
const WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
4
|
+
function formatContext(section, latDir) {
|
|
5
|
+
const relPath = relative(process.cwd(), latDir + '/' + section.file + '.md');
|
|
6
|
+
const loc = `${relPath}:${section.startLine}-${section.endLine}`;
|
|
7
|
+
let text = `[${section.id}](${loc})`;
|
|
8
|
+
if (section.body) {
|
|
9
|
+
text += `: ${section.body}`;
|
|
10
|
+
}
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
export async function promptCmd(ctx, text) {
|
|
14
|
+
const allSections = await loadAllSections(ctx.latDir);
|
|
15
|
+
const flat = flattenSections(allSections);
|
|
16
|
+
const refs = [...text.matchAll(WIKI_LINK_RE)];
|
|
17
|
+
if (refs.length === 0) {
|
|
18
|
+
process.stdout.write(text);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const resolved = new Map();
|
|
22
|
+
for (const match of refs) {
|
|
23
|
+
const target = match[1];
|
|
24
|
+
if (resolved.has(target))
|
|
25
|
+
continue;
|
|
26
|
+
const q = target.toLowerCase();
|
|
27
|
+
const exact = flat.find((s) => s.id.toLowerCase() === q);
|
|
28
|
+
if (exact) {
|
|
29
|
+
resolved.set(target, exact);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const fuzzy = findSections(allSections, target);
|
|
33
|
+
if (fuzzy.length === 1) {
|
|
34
|
+
resolved.set(target, fuzzy[0]);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (fuzzy.length > 1) {
|
|
38
|
+
console.error(ctx.chalk.red(`Ambiguous reference [[${target}]].`));
|
|
39
|
+
console.error(ctx.chalk.dim('\nCould match:\n'));
|
|
40
|
+
for (const m of fuzzy) {
|
|
41
|
+
console.error(' ' + m.id);
|
|
42
|
+
}
|
|
43
|
+
console.error(ctx.chalk.dim('\nAsk the user which section they meant.'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
console.error(ctx.chalk.red(`No section found for [[${target}]] (no exact, substring, or fuzzy matches).`));
|
|
47
|
+
console.error(ctx.chalk.dim('Ask the user to correct the reference.'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
// Replace [[refs]] inline
|
|
51
|
+
let output = text.replace(WIKI_LINK_RE, (_match, target) => {
|
|
52
|
+
const section = resolved.get(target);
|
|
53
|
+
return `[[${section.id}]]`;
|
|
54
|
+
});
|
|
55
|
+
// Append context block
|
|
56
|
+
output += '\n\n<lat-context>\n';
|
|
57
|
+
for (const section of resolved.values()) {
|
|
58
|
+
output += formatContext(section, ctx.latDir) + '\n';
|
|
59
|
+
}
|
|
60
|
+
output += '</lat-context>\n';
|
|
61
|
+
process.stdout.write(output);
|
|
62
|
+
}
|
package/dist/src/cli/refs.d.ts
CHANGED