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.
@@ -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)')
@@ -1,19 +1,28 @@
1
- import { existsSync, cpSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, chmodSync, } from 'node:fs';
1
+ import { existsSync, cpSync, mkdirSync, writeFileSync, readFileSync, } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { createInterface } from 'node:readline/promises';
4
4
  import chalk from 'chalk';
5
5
  import { findTemplatesDir } from './templates.js';
6
6
  import { readAgentsTemplate, readCursorRulesTemplate } from './gen.js';
7
7
  import { getConfigPath, readConfig, writeConfig, } from '../config.js';
8
+ import { writeInitMeta, readFileHash, contentHash } from '../init-version.js';
8
9
  async function confirm(rl, message) {
9
- try {
10
- const answer = await rl.question(`${message} ${chalk.dim('[Y/n]')} `);
11
- return answer.trim().toLowerCase() !== 'n';
12
- }
13
- catch {
14
- // Ctrl+C or closed stdin — abort
15
- console.log('');
16
- process.exit(130);
10
+ while (true) {
11
+ let answer;
12
+ try {
13
+ answer = await rl.question(`${message} ${chalk.dim('[Y/n]')} `);
14
+ }
15
+ catch {
16
+ // Ctrl+C or closed stdin — abort
17
+ console.log('');
18
+ process.exit(130);
19
+ }
20
+ const val = answer.trim().toLowerCase();
21
+ if (val === '' || val === 'y' || val === 'yes')
22
+ return true;
23
+ if (val === 'n' || val === 'no')
24
+ return false;
25
+ console.log(chalk.yellow(' Please answer Y or n.'));
17
26
  }
18
27
  }
19
28
  async function prompt(rl, message) {
@@ -27,22 +36,21 @@ async function prompt(rl, message) {
27
36
  }
28
37
  }
29
38
  // ── Claude Code helpers ──────────────────────────────────────────────
30
- const HOOK_COMMAND = '.claude/hooks/lat-prompt-hook.sh';
31
- function hasLatHook(settingsPath) {
32
- if (!existsSync(settingsPath))
33
- return false;
34
- try {
35
- const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
36
- const entries = settings?.hooks?.UserPromptSubmit;
37
- if (!Array.isArray(entries))
38
- return false;
39
- return entries.some((entry) => entry.hooks?.some((h) => h.command === HOOK_COMMAND));
40
- }
41
- catch {
42
- return false;
43
- }
39
+ /** Derive the hook command prefix from the currently running binary. */
40
+ function latHookCommand(event) {
41
+ return `${resolve(process.argv[1])} hook claude ${event}`;
44
42
  }
45
- function addLatHook(settingsPath) {
43
+ /** True if any command in this entry looks like it was installed by lat. */
44
+ function isLatHookEntry(entry) {
45
+ const bin = resolve(process.argv[1]);
46
+ return (entry.hooks?.some((h) => typeof h.command === 'string' &&
47
+ (/\blat\b/.test(h.command) || h.command.startsWith(bin + ' '))) ?? false);
48
+ }
49
+ /**
50
+ * Remove all lat-owned hook entries from settings, then add fresh ones.
51
+ * Preserves any non-lat hooks the user may have configured.
52
+ */
53
+ function syncLatHooks(settingsPath) {
46
54
  let settings = {};
47
55
  if (existsSync(settingsPath)) {
48
56
  const raw = readFileSync(settingsPath, 'utf-8');
@@ -57,12 +65,27 @@ function addLatHook(settingsPath) {
57
65
  settings.hooks = {};
58
66
  }
59
67
  const hooks = settings.hooks;
60
- if (!Array.isArray(hooks.UserPromptSubmit)) {
61
- hooks.UserPromptSubmit = [];
68
+ // Strip lat-owned entries from ALL event types (cleans up stale events too)
69
+ for (const [event, entries] of Object.entries(hooks)) {
70
+ if (!Array.isArray(entries))
71
+ continue;
72
+ const filtered = entries.filter((entry) => !isLatHookEntry(entry));
73
+ if (filtered.length > 0) {
74
+ hooks[event] = filtered;
75
+ }
76
+ else {
77
+ delete hooks[event];
78
+ }
79
+ }
80
+ // Add fresh hooks for current events
81
+ for (const event of ['UserPromptSubmit', 'Stop']) {
82
+ if (!Array.isArray(hooks[event])) {
83
+ hooks[event] = [];
84
+ }
85
+ hooks[event].push({
86
+ hooks: [{ type: 'command', command: latHookCommand(event) }],
87
+ });
62
88
  }
63
- hooks.UserPromptSubmit.push({
64
- hooks: [{ type: 'command', command: HOOK_COMMAND }],
65
- });
66
89
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
67
90
  }
68
91
  // ── Gitignore helper ─────────────────────────────────────────────────
@@ -113,7 +136,8 @@ function hasMcpServer(configPath, key) {
113
136
  const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
114
137
  return !!cfg?.[key]?.lat;
115
138
  }
116
- catch {
139
+ catch (err) {
140
+ process.stderr.write(`Warning: failed to parse ${configPath}: ${err.message}\n`);
117
141
  return false;
118
142
  }
119
143
  }
@@ -134,49 +158,73 @@ function addMcpServer(configPath, key) {
134
158
  mkdirSync(join(configPath, '..'), { recursive: true });
135
159
  writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
136
160
  }
161
+ // ── Template file helpers ─────────────────────────────────────────────
162
+ /**
163
+ * Write a template-generated file, using stored hashes to decide whether
164
+ * to overwrite or prompt the user about local modifications.
165
+ *
166
+ * Returns the hash of the written content, or null if the file was skipped.
167
+ */
168
+ async function writeTemplateFile(root, latDir, relPath, template, genTarget, label, indent, ask) {
169
+ const absPath = join(root, relPath);
170
+ const templateHash = contentHash(template);
171
+ if (!existsSync(absPath)) {
172
+ mkdirSync(join(absPath, '..'), { recursive: true });
173
+ writeFileSync(absPath, template);
174
+ console.log(chalk.green(`${indent}Created ${label}`));
175
+ return templateHash;
176
+ }
177
+ // File exists — check if user has modified it
178
+ const currentContent = readFileSync(absPath, 'utf-8');
179
+ const currentHash = contentHash(currentContent);
180
+ const storedHash = readFileHash(latDir, relPath);
181
+ if (currentHash === templateHash) {
182
+ // Already matches the latest template
183
+ console.log(chalk.green(`${indent}${label}`) + ' already up to date');
184
+ return templateHash;
185
+ }
186
+ if (storedHash && currentHash === storedHash) {
187
+ // Unmodified by user — safe to overwrite with new template
188
+ writeFileSync(absPath, template);
189
+ console.log(chalk.green(`${indent}Updated ${label}`));
190
+ return templateHash;
191
+ }
192
+ // User has modified the file — ask whether to overwrite
193
+ console.log(chalk.yellow(`${indent}${label}`) +
194
+ ' exists and may contain your own content.');
195
+ if (await ask(`${indent}Overwrite with latest lat template?`)) {
196
+ writeFileSync(absPath, template);
197
+ console.log(chalk.green(`${indent}Updated ${label}`));
198
+ return templateHash;
199
+ }
200
+ console.log(chalk.dim(`${indent}Kept existing file.`) +
201
+ ' Run ' +
202
+ chalk.cyan(`lat gen ${genTarget}`) +
203
+ ' to see the latest template.');
204
+ return null;
205
+ }
137
206
  // ── Per-agent setup ──────────────────────────────────────────────────
138
- function setupAgentsMd(root, template) {
139
- const agentsPath = join(root, 'AGENTS.md');
140
- if (!existsSync(agentsPath)) {
141
- writeFileSync(agentsPath, template);
142
- console.log(chalk.green('Created AGENTS.md'));
143
- }
144
- else {
145
- console.log(chalk.green('AGENTS.md') + ' already exists');
146
- }
207
+ async function setupAgentsMd(root, latDir, template, hashes, ask) {
208
+ const hash = await writeTemplateFile(root, latDir, 'AGENTS.md', template, 'agents.md', 'AGENTS.md', '', ask);
209
+ if (hash)
210
+ hashes['AGENTS.md'] = hash;
147
211
  }
148
- async function setupClaudeCode(root, template) {
149
- const created = [];
212
+ async function setupClaudeCode(root, latDir, template, hashes, ask) {
150
213
  // CLAUDE.md — written directly (not a symlink)
151
- const claudePath = join(root, 'CLAUDE.md');
152
- if (!existsSync(claudePath)) {
153
- writeFileSync(claudePath, template);
154
- console.log(chalk.green(' Created CLAUDE.md'));
155
- created.push('CLAUDE.md');
156
- }
157
- else {
158
- console.log(chalk.green(' CLAUDE.md') + ' already exists');
159
- }
160
- // Prompt hook
214
+ const hash = await writeTemplateFile(root, latDir, 'CLAUDE.md', template, 'claude.md', 'CLAUDE.md', ' ', ask);
215
+ if (hash)
216
+ hashes['CLAUDE.md'] = hash;
217
+ // Hooks — UserPromptSubmit (lat.md reminders + [[ref]] expansion) and Stop (update reminder)
161
218
  console.log('');
162
- console.log(chalk.dim(" Claude Code doesn't reliably follow CLAUDE.md for per-prompt actions,"));
163
- console.log(chalk.dim(' so we install a hook that injects lat.md workflow reminders into every prompt.'));
219
+ console.log(chalk.dim(' Hooks inject lat.md workflow reminders into every prompt and remind'));
220
+ console.log(chalk.dim(' the agent to update lat.md/ before finishing.'));
164
221
  const claudeDir = join(root, '.claude');
165
- const hooksDir = join(claudeDir, 'hooks');
166
- const hookPath = join(hooksDir, 'lat-prompt-hook.sh');
167
222
  const settingsPath = join(claudeDir, 'settings.json');
168
- if (hasLatHook(settingsPath)) {
169
- console.log(chalk.green(' Prompt hook') + ' already configured');
170
- }
171
- else {
172
- mkdirSync(hooksDir, { recursive: true });
173
- const templateHook = join(findTemplatesDir(), 'lat-prompt-hook.sh');
174
- copyFileSync(templateHook, hookPath);
175
- chmodSync(hookPath, 0o755);
176
- addLatHook(settingsPath);
177
- console.log(chalk.green(' Prompt hook') + ' installed');
178
- created.push('.claude/hooks/lat-prompt-hook.sh');
179
- }
223
+ mkdirSync(claudeDir, { recursive: true });
224
+ syncLatHooks(settingsPath);
225
+ console.log(chalk.green(' Hooks') + ' synced (UserPromptSubmit + Stop)');
226
+ // Ensure .claude is gitignored (settings contain local absolute paths)
227
+ ensureGitignored(root, '.claude');
180
228
  // MCP server → .mcp.json at project root
181
229
  console.log('');
182
230
  console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
@@ -188,26 +236,15 @@ async function setupClaudeCode(root, template) {
188
236
  else {
189
237
  addMcpServer(mcpPath, 'mcpServers');
190
238
  console.log(chalk.green(' MCP server') + ' registered in .mcp.json');
191
- created.push('.mcp.json');
192
239
  }
193
240
  // Ensure .mcp.json is gitignored (it contains local absolute paths)
194
241
  ensureGitignored(root, '.mcp.json');
195
- return created;
196
242
  }
197
- async function setupCursor(root) {
198
- const created = [];
243
+ async function setupCursor(root, latDir, hashes, ask) {
199
244
  // .cursor/rules/lat.md
200
- const rulesDir = join(root, '.cursor', 'rules');
201
- const rulesPath = join(rulesDir, 'lat.md');
202
- if (!existsSync(rulesPath)) {
203
- mkdirSync(rulesDir, { recursive: true });
204
- writeFileSync(rulesPath, readCursorRulesTemplate());
205
- console.log(chalk.green(' Rules') + ' created at .cursor/rules/lat.md');
206
- created.push('.cursor/rules/lat.md');
207
- }
208
- else {
209
- console.log(chalk.green(' Rules') + ' already exist');
210
- }
245
+ const hash = await writeTemplateFile(root, latDir, '.cursor/rules/lat.md', readCursorRulesTemplate(), 'cursor-rules.md', 'Rules (.cursor/rules/lat.md)', ' ', ask);
246
+ if (hash)
247
+ hashes['.cursor/rules/lat.md'] = hash;
211
248
  // .cursor/mcp.json
212
249
  console.log('');
213
250
  console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
@@ -219,30 +256,18 @@ async function setupCursor(root) {
219
256
  else {
220
257
  addMcpServer(mcpPath, 'mcpServers');
221
258
  console.log(chalk.green(' MCP server') + ' registered in .cursor/mcp.json');
222
- created.push('.cursor/mcp.json');
223
259
  }
224
260
  // Ensure .cursor/mcp.json is gitignored (it contains local absolute paths)
225
261
  ensureGitignored(root, '.cursor/mcp.json');
226
262
  console.log('');
227
263
  console.log(chalk.yellow(' Note:') +
228
264
  ' Enable MCP in Cursor: Settings → Features → MCP → check "Enable MCP"');
229
- return created;
230
265
  }
231
- async function setupCopilot(root) {
232
- const created = [];
266
+ async function setupCopilot(root, latDir, hashes, ask) {
233
267
  // .github/copilot-instructions.md
234
- const githubDir = join(root, '.github');
235
- const instructionsPath = join(githubDir, 'copilot-instructions.md');
236
- if (!existsSync(instructionsPath)) {
237
- mkdirSync(githubDir, { recursive: true });
238
- writeFileSync(instructionsPath, readAgentsTemplate());
239
- console.log(chalk.green(' Instructions') +
240
- ' created at .github/copilot-instructions.md');
241
- created.push('.github/copilot-instructions.md');
242
- }
243
- else {
244
- console.log(chalk.green(' Instructions') + ' already exist');
245
- }
268
+ const hash = await writeTemplateFile(root, latDir, '.github/copilot-instructions.md', readAgentsTemplate(), 'agents.md', 'Instructions (.github/copilot-instructions.md)', ' ', ask);
269
+ if (hash)
270
+ hashes['.github/copilot-instructions.md'] = hash;
246
271
  // .vscode/mcp.json
247
272
  console.log('');
248
273
  console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
@@ -254,40 +279,40 @@ async function setupCopilot(root) {
254
279
  else {
255
280
  addMcpServer(mcpPath, 'servers');
256
281
  console.log(chalk.green(' MCP server') + ' registered in .vscode/mcp.json');
257
- created.push('.vscode/mcp.json');
258
282
  }
259
- return created;
260
283
  }
261
284
  // ── LLM key setup ───────────────────────────────────────────────────
262
285
  async function setupLlmKey(rl) {
263
- console.log('');
264
- console.log(chalk.bold('Semantic search'));
265
- console.log('');
266
- console.log(' lat.md includes semantic search (' +
267
- chalk.cyan('lat search') +
268
- ') that lets agents find');
269
- console.log(' relevant documentation by meaning, not just keywords. This requires an');
270
- console.log(' embedding API key (OpenAI or Vercel AI Gateway). Without it, agents can still');
271
- console.log(' use ' +
272
- chalk.cyan('lat locate') +
273
- ' for exact lookups, but will miss semantic matches.');
274
- console.log('');
275
286
  // Check env var first
276
287
  const envKey = process.env.LAT_LLM_KEY;
277
288
  if (envKey) {
278
- console.log(chalk.green(' LAT_LLM_KEY') +
279
- ' is set in your environment. Semantic search is ready.');
289
+ console.log('');
290
+ console.log(chalk.green('Semantic search') + ' LAT_LLM_KEY is set. Ready.');
280
291
  return;
281
292
  }
282
293
  // Check existing config
283
294
  const config = readConfig();
284
295
  const configPath = getConfigPath();
285
296
  if (config.llm_key) {
286
- console.log(chalk.green(' LLM key') +
287
- ' already configured in ' +
297
+ console.log('');
298
+ console.log(chalk.green('Semantic search') +
299
+ ' — LLM key configured in ' +
288
300
  chalk.dim(configPath));
289
301
  return;
290
302
  }
303
+ // No key found — explain what semantic search is and prompt
304
+ console.log('');
305
+ console.log(chalk.bold('Semantic search'));
306
+ console.log('');
307
+ console.log(' lat.md includes semantic search (' +
308
+ chalk.cyan('lat search') +
309
+ ') that lets agents find');
310
+ console.log(' relevant documentation by meaning, not just keywords. This requires an');
311
+ console.log(' embedding API key (OpenAI or Vercel AI Gateway). Without it, agents can still');
312
+ console.log(' use ' +
313
+ chalk.cyan('lat locate') +
314
+ ' for exact lookups, but will miss semantic matches.');
315
+ console.log('');
291
316
  // Interactive prompt
292
317
  if (!rl) {
293
318
  console.log(chalk.yellow(' No LLM key found.') +
@@ -382,26 +407,27 @@ export async function initCmd(targetDir) {
382
407
  }
383
408
  console.log('');
384
409
  const template = readAgentsTemplate();
410
+ const fileHashes = {};
385
411
  // Step 3: AGENTS.md (shared by non-Claude agents)
386
412
  const needsAgentsMd = useCursor || useCopilot || useCodex;
387
413
  if (needsAgentsMd) {
388
- setupAgentsMd(root, template);
414
+ await setupAgentsMd(root, latDir, template, fileHashes, ask);
389
415
  }
390
416
  // Step 4: Per-agent setup
391
417
  if (useClaudeCode) {
392
418
  console.log('');
393
419
  console.log(chalk.bold('Setting up Claude Code...'));
394
- await setupClaudeCode(root, template);
420
+ await setupClaudeCode(root, latDir, template, fileHashes, ask);
395
421
  }
396
422
  if (useCursor) {
397
423
  console.log('');
398
424
  console.log(chalk.bold('Setting up Cursor...'));
399
- await setupCursor(root);
425
+ await setupCursor(root, latDir, fileHashes, ask);
400
426
  }
401
427
  if (useCopilot) {
402
428
  console.log('');
403
429
  console.log(chalk.bold('Setting up VS Code Copilot...'));
404
- await setupCopilot(root);
430
+ await setupCopilot(root, latDir, fileHashes, ask);
405
431
  }
406
432
  if (useCodex) {
407
433
  console.log('');
@@ -410,6 +436,8 @@ export async function initCmd(targetDir) {
410
436
  }
411
437
  // Step 5: LLM key setup
412
438
  await setupLlmKey(rl);
439
+ // Record init version and file hashes so `lat check` can detect stale setups
440
+ writeInitMeta(latDir, fileHashes);
413
441
  console.log('');
414
442
  console.log(chalk.green('Done!') +
415
443
  ' Run ' +
@@ -1,2 +1,2 @@
1
- import type { CliContext } from './context.js';
2
- export declare function locateCmd(ctx: CliContext, query: string): Promise<void>;
1
+ import type { CmdContext, CmdResult } from '../context.js';
2
+ export declare function locateCommand(ctx: CmdContext, query: string): Promise<CmdResult>;
@@ -1,12 +1,17 @@
1
1
  import { loadAllSections, findSections } from '../lattice.js';
2
2
  import { formatResultList } from '../format.js';
3
- export async function locateCmd(ctx, query) {
3
+ export async function locateCommand(ctx, query) {
4
4
  const stripped = query.replace(/^\[\[|\]\]$/g, '');
5
5
  const sections = await loadAllSections(ctx.latDir);
6
6
  const matches = findSections(sections, stripped);
7
7
  if (matches.length === 0) {
8
- console.error(ctx.chalk.red(`No sections matching "${stripped}" (no exact, substring, or fuzzy matches)`));
9
- process.exit(1);
8
+ const s = ctx.styler;
9
+ return {
10
+ output: s.red(`No sections matching "${stripped}" (no exact, substring, or fuzzy matches)`),
11
+ isError: true,
12
+ };
10
13
  }
11
- console.log(formatResultList(`Sections matching "${stripped}":`, matches, ctx.projectRoot));
14
+ return {
15
+ output: formatResultList(ctx, `Sections matching "${stripped}":`, matches),
16
+ };
12
17
  }
@@ -1,4 +1,20 @@
1
- import type { CliContext } from './context.js';
2
- type Scope = 'md' | 'code' | 'md+code';
3
- export declare function refsCmd(ctx: CliContext, query: string, scope: Scope): Promise<void>;
4
- export {};
1
+ import { type Section, type SectionMatch } from '../lattice.js';
2
+ import type { CmdContext, CmdResult } from '../context.js';
3
+ export type Scope = 'md' | 'code' | 'md+code';
4
+ export type RefsFound = {
5
+ kind: 'found';
6
+ target: Section;
7
+ mdRefs: SectionMatch[];
8
+ codeRefs: string[];
9
+ };
10
+ export type RefsError = {
11
+ kind: 'no-match';
12
+ suggestions: SectionMatch[];
13
+ };
14
+ export type RefsResult = RefsFound | RefsError;
15
+ /**
16
+ * Find all sections and code locations that reference a given section.
17
+ * Accepts any valid section id (full-path, short-form, with or without brackets).
18
+ */
19
+ export declare function findRefs(ctx: CmdContext, query: string, scope: Scope): Promise<RefsResult>;
20
+ export declare function refsCommand(ctx: CmdContext, query: string, scope: Scope): Promise<CmdResult>;