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.
- package/README.md +3 -2
- package/dist/src/cli/check.d.ts +7 -5
- package/dist/src/cli/check.js +186 -65
- package/dist/src/cli/context.d.ts +3 -8
- package/dist/src/cli/context.js +13 -1
- package/dist/src/cli/expand.d.ts +7 -0
- package/dist/src/cli/{prompt.js → expand.js} +44 -13
- 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 +63 -39
- package/dist/src/cli/search.d.ts +25 -3
- package/dist/src/cli/search.js +82 -48
- package/dist/src/cli/section.d.ts +26 -0
- package/dist/src/cli/section.js +138 -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 +5 -3
- package/dist/src/format.js +24 -19
- package/dist/src/init-version.d.ts +10 -0
- package/dist/src/init-version.js +49 -0
- package/dist/src/lattice.d.ts +1 -2
- package/dist/src/lattice.js +5 -8
- package/dist/src/mcp/server.js +26 -279
- package/dist/src/parser.js +2 -0
- package/dist/src/source-parser.js +389 -2
- package/package.json +2 -1
- package/templates/AGENTS.md +36 -5
- package/templates/cursor-rules.md +9 -4
- package/templates/lat-prompt-hook.sh +2 -2
- package/dist/src/cli/prompt.d.ts +0 -2
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)')
|
package/dist/src/cli/init.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import { existsSync, cpSync, mkdirSync, writeFileSync, readFileSync,
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
31
|
-
function
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
140
|
-
if (
|
|
141
|
-
|
|
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
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
163
|
-
console.log(chalk.dim('
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
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(
|
|
279
|
-
|
|
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(
|
|
287
|
-
|
|
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 ' +
|
package/dist/src/cli/locate.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function
|
|
1
|
+
import type { CmdContext, CmdResult } from '../context.js';
|
|
2
|
+
export declare function locateCommand(ctx: CmdContext, query: string): Promise<CmdResult>;
|
package/dist/src/cli/locate.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { loadAllSections, findSections } from '../lattice.js';
|
|
2
2
|
import { formatResultList } from '../format.js';
|
|
3
|
-
export async function
|
|
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
+
return {
|
|
15
|
+
output: formatResultList(ctx, `Sections matching "${stripped}":`, matches),
|
|
16
|
+
};
|
|
12
17
|
}
|
package/dist/src/cli/refs.d.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
import type
|
|
2
|
-
type
|
|
3
|
-
export
|
|
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>;
|