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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { findLatticeDir } from '../lattice.js';
|
|
3
|
+
import { plainStyler } from '../context.js';
|
|
4
|
+
import { expandPrompt } from './expand.js';
|
|
5
|
+
import { runSearch } from './search.js';
|
|
6
|
+
import { getSection, formatSectionOutput } from './section.js';
|
|
7
|
+
import { getLlmKey } from '../config.js';
|
|
8
|
+
function outputPromptSubmit(context) {
|
|
9
|
+
process.stdout.write(JSON.stringify({
|
|
10
|
+
hookSpecificOutput: {
|
|
11
|
+
hookEventName: 'UserPromptSubmit',
|
|
12
|
+
additionalContext: context,
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
function outputStop(reason) {
|
|
17
|
+
process.stdout.write(JSON.stringify({
|
|
18
|
+
decision: 'block',
|
|
19
|
+
reason,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
async function readStdin() {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
for await (const chunk of process.stdin) {
|
|
25
|
+
chunks.push(chunk);
|
|
26
|
+
}
|
|
27
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
28
|
+
}
|
|
29
|
+
function hasWikiLinks(text) {
|
|
30
|
+
return /\[\[[^\]]+\]\]/.test(text);
|
|
31
|
+
}
|
|
32
|
+
function makeHookCtx(latDir) {
|
|
33
|
+
return {
|
|
34
|
+
latDir,
|
|
35
|
+
projectRoot: dirname(latDir),
|
|
36
|
+
styler: plainStyler,
|
|
37
|
+
mode: 'cli',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function searchAndExpand(ctx, userPrompt) {
|
|
41
|
+
let key;
|
|
42
|
+
try {
|
|
43
|
+
key = getLlmKey();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (!key)
|
|
49
|
+
return null;
|
|
50
|
+
const result = await runSearch(ctx.latDir, userPrompt, key, 5);
|
|
51
|
+
if (result.matches.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const parts = [
|
|
54
|
+
`Search results for the user prompt (${result.matches.length} matches):`,
|
|
55
|
+
'',
|
|
56
|
+
];
|
|
57
|
+
for (const match of result.matches) {
|
|
58
|
+
const sectionResult = await getSection(ctx, match.section.id);
|
|
59
|
+
if (sectionResult.kind === 'found') {
|
|
60
|
+
parts.push(formatSectionOutput(ctx, sectionResult));
|
|
61
|
+
parts.push('');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return parts.join('\n');
|
|
65
|
+
}
|
|
66
|
+
async function handleUserPromptSubmit() {
|
|
67
|
+
let userPrompt = '';
|
|
68
|
+
try {
|
|
69
|
+
const raw = await readStdin();
|
|
70
|
+
const input = JSON.parse(raw);
|
|
71
|
+
userPrompt = input.user_prompt ?? '';
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// If we can't parse stdin, still emit the reminder
|
|
75
|
+
}
|
|
76
|
+
const parts = [];
|
|
77
|
+
parts.push("Before starting work, run `lat search` with one or more queries describing the user's intent.", 'ALWAYS do this, even when the task seems straightforward — search results may reveal critical design details, protocols, or constraints.', 'Use `lat section` to read the full content of relevant matches.', 'Do not read files, write code, or run commands until you have searched.');
|
|
78
|
+
const latDir = findLatticeDir();
|
|
79
|
+
if (latDir && userPrompt) {
|
|
80
|
+
const ctx = makeHookCtx(latDir);
|
|
81
|
+
// If the user prompt contains [[refs]], resolve them inline
|
|
82
|
+
if (hasWikiLinks(userPrompt)) {
|
|
83
|
+
try {
|
|
84
|
+
const expanded = await expandPrompt(ctx, userPrompt);
|
|
85
|
+
if (expanded) {
|
|
86
|
+
parts.push('', 'Expanded user prompt with resolved [[refs]]:', expanded);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
parts.push('', 'NOTE: The user prompt contains [[refs]] but they could not be resolved. Ask the user to correct them.');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
parts.push('', 'NOTE: The user prompt contains [[refs]] but resolution failed. Run `lat expand` on the prompt text manually.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Search for relevant sections and include their full content
|
|
97
|
+
try {
|
|
98
|
+
const searchContext = await searchAndExpand(ctx, userPrompt);
|
|
99
|
+
if (searchContext) {
|
|
100
|
+
parts.push('', searchContext);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Search failed (no key, index error, etc.) — agent can search manually
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
outputPromptSubmit(parts.join('\n'));
|
|
108
|
+
}
|
|
109
|
+
async function handleStop() {
|
|
110
|
+
// Only emit the reminder if we're in a project with lat.md
|
|
111
|
+
const latDir = findLatticeDir();
|
|
112
|
+
if (!latDir)
|
|
113
|
+
return;
|
|
114
|
+
// Read stdin to check if we already blocked once
|
|
115
|
+
let stopHookActive = false;
|
|
116
|
+
try {
|
|
117
|
+
const raw = await readStdin();
|
|
118
|
+
const input = JSON.parse(raw);
|
|
119
|
+
stopHookActive = input.stop_hook_active ?? false;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// If we can't parse stdin, treat as first attempt
|
|
123
|
+
}
|
|
124
|
+
// Don't block twice — avoids infinite loop
|
|
125
|
+
if (stopHookActive)
|
|
126
|
+
return;
|
|
127
|
+
const parts = [];
|
|
128
|
+
parts.push('Before finishing, verify:', '- Did you update `lat.md/`? Run `lat search` with a query describing what you changed to find relevant sections that may need updating.', '- Did you run `lat check` and confirm all links and code refs pass?', 'If you made code changes but did not update lat.md/, do that now.');
|
|
129
|
+
outputStop(parts.join('\n'));
|
|
130
|
+
}
|
|
131
|
+
export async function hookCmd(agent, event) {
|
|
132
|
+
if (agent !== 'claude') {
|
|
133
|
+
console.error(`Unknown agent: ${agent}. Supported: claude`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
switch (event) {
|
|
137
|
+
case 'UserPromptSubmit':
|
|
138
|
+
await handleUserPromptSubmit();
|
|
139
|
+
break;
|
|
140
|
+
case 'Stop':
|
|
141
|
+
await handleStop();
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
console.error(`Unknown hook event: ${event}. Supported: UserPromptSubmit, Stop`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -8,8 +8,6 @@ import { dirname, join } from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { Command } from 'commander';
|
|
10
10
|
import { resolveContext } from './context.js';
|
|
11
|
-
import { locateCmd } from './locate.js';
|
|
12
|
-
import { refsCmd } from './refs.js';
|
|
13
11
|
function findPackageJson() {
|
|
14
12
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
15
13
|
while (true) {
|
|
@@ -24,6 +22,14 @@ function findPackageJson() {
|
|
|
24
22
|
dir = parent;
|
|
25
23
|
}
|
|
26
24
|
}
|
|
25
|
+
function handleResult(result) {
|
|
26
|
+
if (result.isError) {
|
|
27
|
+
console.error(result.output);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
if (result.output)
|
|
31
|
+
console.log(result.output);
|
|
32
|
+
}
|
|
27
33
|
const version = findPackageJson();
|
|
28
34
|
const program = new Command();
|
|
29
35
|
program
|
|
@@ -39,7 +45,17 @@ program
|
|
|
39
45
|
.argument('<query>', 'section id to search for')
|
|
40
46
|
.action(async (query) => {
|
|
41
47
|
const ctx = resolveContext(program.opts());
|
|
42
|
-
await
|
|
48
|
+
const { locateCommand } = await import('./locate.js');
|
|
49
|
+
handleResult(await locateCommand(ctx, query));
|
|
50
|
+
});
|
|
51
|
+
program
|
|
52
|
+
.command('section')
|
|
53
|
+
.description('Show a section with its content, outgoing refs, and incoming refs')
|
|
54
|
+
.argument('<query>', 'section id to look up')
|
|
55
|
+
.action(async (query) => {
|
|
56
|
+
const ctx = resolveContext(program.opts());
|
|
57
|
+
const { sectionCommand } = await import('./section.js');
|
|
58
|
+
handleResult(await sectionCommand(ctx, query));
|
|
43
59
|
});
|
|
44
60
|
program
|
|
45
61
|
.command('refs')
|
|
@@ -53,46 +69,50 @@ program
|
|
|
53
69
|
process.exit(1);
|
|
54
70
|
}
|
|
55
71
|
const ctx = resolveContext(program.opts());
|
|
56
|
-
await
|
|
72
|
+
const { refsCommand } = await import('./refs.js');
|
|
73
|
+
handleResult(await refsCommand(ctx, query, scope));
|
|
57
74
|
});
|
|
58
75
|
const check = program
|
|
59
76
|
.command('check')
|
|
60
77
|
.description('Validate links and code references')
|
|
61
78
|
.action(async () => {
|
|
62
79
|
const ctx = resolveContext(program.opts());
|
|
63
|
-
const {
|
|
64
|
-
await
|
|
80
|
+
const { checkAllCommand } = await import('./check.js');
|
|
81
|
+
handleResult(await checkAllCommand(ctx));
|
|
65
82
|
});
|
|
66
83
|
check
|
|
67
84
|
.command('md')
|
|
68
85
|
.description('Validate wiki links in lat.md markdown files')
|
|
69
86
|
.action(async () => {
|
|
70
87
|
const ctx = resolveContext(program.opts());
|
|
71
|
-
const {
|
|
72
|
-
await
|
|
88
|
+
const { checkMdCommand } = await import('./check.js');
|
|
89
|
+
handleResult(await checkMdCommand(ctx));
|
|
73
90
|
});
|
|
74
91
|
check
|
|
75
92
|
.command('code-refs')
|
|
76
93
|
.description('Validate @lat code references and coverage')
|
|
77
94
|
.action(async () => {
|
|
78
95
|
const ctx = resolveContext(program.opts());
|
|
79
|
-
const {
|
|
80
|
-
await
|
|
96
|
+
const { checkCodeRefsCommand } = await import('./check.js');
|
|
97
|
+
handleResult(await checkCodeRefsCommand(ctx));
|
|
81
98
|
});
|
|
82
99
|
check
|
|
83
100
|
.command('index')
|
|
84
101
|
.description('Validate directory index files in lat.md')
|
|
85
102
|
.action(async () => {
|
|
86
103
|
const ctx = resolveContext(program.opts());
|
|
87
|
-
const {
|
|
88
|
-
await
|
|
104
|
+
const { checkIndexCommand } = await import('./check.js');
|
|
105
|
+
handleResult(await checkIndexCommand(ctx));
|
|
89
106
|
});
|
|
90
|
-
|
|
91
|
-
.command('
|
|
92
|
-
.description('
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
check
|
|
108
|
+
.command('sections')
|
|
109
|
+
.description('Validate section leading paragraphs in lat.md')
|
|
110
|
+
.action(async () => {
|
|
111
|
+
const ctx = resolveContext(program.opts());
|
|
112
|
+
const { checkSectionsCommand } = await import('./check.js');
|
|
113
|
+
handleResult(await checkSectionsCommand(ctx));
|
|
114
|
+
});
|
|
115
|
+
async function runExpand(text, opts) {
|
|
96
116
|
if (opts.stdin) {
|
|
97
117
|
const chunks = [];
|
|
98
118
|
for await (const chunk of process.stdin) {
|
|
@@ -101,12 +121,33 @@ program
|
|
|
101
121
|
text = Buffer.concat(chunks).toString('utf-8');
|
|
102
122
|
}
|
|
103
123
|
if (!text) {
|
|
104
|
-
console.error('Provide
|
|
124
|
+
console.error('Provide text as an argument or use --stdin');
|
|
105
125
|
process.exit(1);
|
|
106
126
|
}
|
|
107
127
|
const ctx = resolveContext(program.opts());
|
|
108
|
-
const {
|
|
109
|
-
await
|
|
128
|
+
const { expandCommand } = await import('./expand.js');
|
|
129
|
+
const result = await expandCommand(ctx, text);
|
|
130
|
+
if (result.isError) {
|
|
131
|
+
console.error(result.output);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
// Use stdout.write (no trailing newline) for piping
|
|
135
|
+
process.stdout.write(result.output);
|
|
136
|
+
}
|
|
137
|
+
program
|
|
138
|
+
.command('expand')
|
|
139
|
+
.description('Expand [[refs]] in text to lat.md section locations')
|
|
140
|
+
.argument('[text]', 'text containing [[refs]]')
|
|
141
|
+
.option('--stdin', 'read text from stdin')
|
|
142
|
+
.action(runExpand);
|
|
143
|
+
// Deprecated alias — hidden from --help
|
|
144
|
+
program
|
|
145
|
+
.command('prompt', { hidden: true })
|
|
146
|
+
.argument('[text]')
|
|
147
|
+
.option('--stdin')
|
|
148
|
+
.action(async (text, opts) => {
|
|
149
|
+
console.error('Warning: `lat prompt` is deprecated, use `lat expand` instead.');
|
|
150
|
+
await runExpand(text, opts);
|
|
110
151
|
});
|
|
111
152
|
program
|
|
112
153
|
.command('search')
|
|
@@ -116,16 +157,15 @@ program
|
|
|
116
157
|
.option('--reindex', 'force full re-indexing')
|
|
117
158
|
.action(async (query, opts) => {
|
|
118
159
|
const ctx = resolveContext(program.opts());
|
|
119
|
-
const {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
160
|
+
const { searchCommand, cliProgress } = await import('./search.js');
|
|
161
|
+
const progress = cliProgress(!!opts.reindex, ctx.styler);
|
|
162
|
+
const result = await searchCommand(ctx, query, { limit: parseInt(opts.limit), reindex: opts.reindex }, progress);
|
|
163
|
+
handleResult(result);
|
|
124
164
|
});
|
|
125
165
|
program
|
|
126
166
|
.command('gen')
|
|
127
|
-
.description('Generate a file to stdout (agents.md, claude.md)')
|
|
128
|
-
.argument('<target>', 'file to generate: agents.md
|
|
167
|
+
.description('Generate a file to stdout (agents.md, claude.md, cursor-rules.md)')
|
|
168
|
+
.argument('<target>', 'file to generate: agents.md, claude.md, cursor-rules.md')
|
|
129
169
|
.action(async (target) => {
|
|
130
170
|
const { genCmd } = await import('./gen.js');
|
|
131
171
|
await genCmd(target);
|
|
@@ -138,6 +178,15 @@ program
|
|
|
138
178
|
const { initCmd } = await import('./init.js');
|
|
139
179
|
await initCmd(dir);
|
|
140
180
|
});
|
|
181
|
+
program
|
|
182
|
+
.command('hook')
|
|
183
|
+
.description('Handle agent hook events (called by agent hooks, not directly)')
|
|
184
|
+
.argument('<agent>', 'agent name (claude)')
|
|
185
|
+
.argument('<event>', 'hook event (UserPromptSubmit, Stop)')
|
|
186
|
+
.action(async (agent, event) => {
|
|
187
|
+
const { hookCmd } = await import('./hook.js');
|
|
188
|
+
await hookCmd(agent, event);
|
|
189
|
+
});
|
|
141
190
|
program
|
|
142
191
|
.command('mcp')
|
|
143
192
|
.description('Start the MCP server (stdio transport)')
|