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.
Files changed (39) hide show
  1. package/dist/src/cli/check.d.ts +7 -5
  2. package/dist/src/cli/check.js +243 -74
  3. package/dist/src/cli/context.d.ts +3 -7
  4. package/dist/src/cli/context.js +15 -1
  5. package/dist/src/cli/expand.d.ts +7 -0
  6. package/dist/src/cli/expand.js +92 -0
  7. package/dist/src/cli/gen.js +11 -4
  8. package/dist/src/cli/hook.d.ts +1 -0
  9. package/dist/src/cli/hook.js +147 -0
  10. package/dist/src/cli/index.js +77 -28
  11. package/dist/src/cli/init.js +148 -120
  12. package/dist/src/cli/locate.d.ts +2 -2
  13. package/dist/src/cli/locate.js +9 -4
  14. package/dist/src/cli/refs.d.ts +20 -4
  15. package/dist/src/cli/refs.js +64 -42
  16. package/dist/src/cli/search.d.ts +25 -3
  17. package/dist/src/cli/search.js +87 -47
  18. package/dist/src/cli/section.d.ts +26 -0
  19. package/dist/src/cli/section.js +133 -0
  20. package/dist/src/code-refs.js +2 -1
  21. package/dist/src/config.js +3 -2
  22. package/dist/src/context.d.ts +21 -0
  23. package/dist/src/context.js +11 -0
  24. package/dist/src/format.d.ts +4 -3
  25. package/dist/src/format.js +16 -20
  26. package/dist/src/init-version.d.ts +10 -0
  27. package/dist/src/init-version.js +49 -0
  28. package/dist/src/lattice.d.ts +11 -5
  29. package/dist/src/lattice.js +87 -38
  30. package/dist/src/mcp/server.js +27 -279
  31. package/dist/src/search/index.js +5 -4
  32. package/dist/src/source-parser.d.ts +23 -0
  33. package/dist/src/source-parser.js +720 -0
  34. package/package.json +3 -1
  35. package/templates/AGENTS.md +38 -6
  36. package/templates/cursor-rules.md +11 -5
  37. package/templates/lat-prompt-hook.sh +2 -2
  38. package/dist/src/cli/prompt.d.ts +0 -2
  39. 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
+ }
@@ -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 locateCmd(ctx, query);
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 refsCmd(ctx, query, scope);
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 { checkAllCmd } = await import('./check.js');
64
- await checkAllCmd(ctx);
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 { checkMdCmd } = await import('./check.js');
72
- await checkMdCmd(ctx);
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 { checkCodeRefsCmd } = await import('./check.js');
80
- await checkCodeRefsCmd(ctx);
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 { checkIndexCmd } = await import('./check.js');
88
- await checkIndexCmd(ctx);
104
+ const { checkIndexCommand } = await import('./check.js');
105
+ handleResult(await checkIndexCommand(ctx));
89
106
  });
90
- program
91
- .command('prompt')
92
- .description('Expand [[refs]] in a prompt to lat.md section locations')
93
- .argument('[text]', 'prompt text')
94
- .option('--stdin', 'read prompt from stdin')
95
- .action(async (text, opts) => {
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 prompt text as an argument or use --stdin');
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 { promptCmd } = await import('./prompt.js');
109
- await promptCmd(ctx, text);
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 { searchCmd } = await import('./search.js');
120
- await searchCmd(ctx, query, {
121
- limit: parseInt(opts.limit),
122
- reindex: opts.reindex,
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 or claude.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)')