lat.md 0.2.3 → 0.4.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.js +8 -0
- package/dist/src/cli/gen.d.ts +1 -0
- package/dist/src/cli/gen.js +3 -0
- package/dist/src/cli/index.js +7 -0
- package/dist/src/cli/init.js +376 -21
- package/dist/src/cli/locate.js +4 -5
- package/dist/src/cli/prompt.js +30 -36
- package/dist/src/cli/refs.js +24 -13
- package/dist/src/cli/search.js +11 -6
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +41 -0
- package/dist/src/format.d.ts +3 -5
- package/dist/src/format.js +9 -9
- package/dist/src/lattice.d.ts +5 -1
- package/dist/src/lattice.js +138 -18
- package/dist/src/mcp/server.d.ts +1 -0
- package/dist/src/mcp/server.js +301 -0
- package/dist/src/search/provider.js +2 -2
- package/package.json +5 -2
- package/templates/cursor-rules.md +71 -0
- package/templates/lat-prompt-hook.sh +16 -0
package/dist/src/cli/check.js
CHANGED
|
@@ -288,4 +288,12 @@ export async function checkAllCmd(ctx) {
|
|
|
288
288
|
if (totalErrors > 0)
|
|
289
289
|
process.exit(1);
|
|
290
290
|
console.log(ctx.chalk.green('All checks passed'));
|
|
291
|
+
const { getLlmKey } = await import('../config.js');
|
|
292
|
+
if (!getLlmKey()) {
|
|
293
|
+
console.log(ctx.chalk.yellow('Warning:') +
|
|
294
|
+
' No LLM key found — semantic search (lat search) will not work.' +
|
|
295
|
+
' Set LAT_LLM_KEY env var or run ' +
|
|
296
|
+
ctx.chalk.cyan('lat init') +
|
|
297
|
+
' to configure.');
|
|
298
|
+
}
|
|
291
299
|
}
|
package/dist/src/cli/gen.d.ts
CHANGED
package/dist/src/cli/gen.js
CHANGED
|
@@ -4,6 +4,9 @@ import { findTemplatesDir } from './templates.js';
|
|
|
4
4
|
export function readAgentsTemplate() {
|
|
5
5
|
return readFileSync(join(findTemplatesDir(), 'AGENTS.md'), 'utf-8');
|
|
6
6
|
}
|
|
7
|
+
export function readCursorRulesTemplate() {
|
|
8
|
+
return readFileSync(join(findTemplatesDir(), 'cursor-rules.md'), 'utf-8');
|
|
9
|
+
}
|
|
7
10
|
export async function genCmd(target) {
|
|
8
11
|
const normalized = target.toLowerCase();
|
|
9
12
|
if (normalized !== 'agents.md' && normalized !== 'claude.md') {
|
package/dist/src/cli/index.js
CHANGED
|
@@ -138,4 +138,11 @@ program
|
|
|
138
138
|
const { initCmd } = await import('./init.js');
|
|
139
139
|
await initCmd(dir);
|
|
140
140
|
});
|
|
141
|
+
program
|
|
142
|
+
.command('mcp')
|
|
143
|
+
.description('Start the MCP server (stdio transport)')
|
|
144
|
+
.action(async () => {
|
|
145
|
+
const { startMcpServer } = await import('../mcp/server.js');
|
|
146
|
+
await startMcpServer();
|
|
147
|
+
});
|
|
141
148
|
await program.parseAsync();
|
package/dist/src/cli/init.js
CHANGED
|
@@ -1,18 +1,342 @@
|
|
|
1
|
-
import { existsSync, cpSync, mkdirSync, writeFileSync,
|
|
1
|
+
import { existsSync, cpSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, chmodSync, } 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
|
-
import { readAgentsTemplate } from './gen.js';
|
|
6
|
+
import { readAgentsTemplate, readCursorRulesTemplate } from './gen.js';
|
|
7
|
+
import { getConfigPath, readConfig, writeConfig, } from '../config.js';
|
|
7
8
|
async function confirm(rl, message) {
|
|
8
9
|
try {
|
|
9
10
|
const answer = await rl.question(`${message} ${chalk.dim('[Y/n]')} `);
|
|
10
11
|
return answer.trim().toLowerCase() !== 'n';
|
|
11
12
|
}
|
|
12
13
|
catch {
|
|
13
|
-
|
|
14
|
+
// Ctrl+C or closed stdin — abort
|
|
15
|
+
console.log('');
|
|
16
|
+
process.exit(130);
|
|
14
17
|
}
|
|
15
18
|
}
|
|
19
|
+
async function prompt(rl, message) {
|
|
20
|
+
try {
|
|
21
|
+
const answer = await rl.question(message);
|
|
22
|
+
return answer.trim();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.log('');
|
|
26
|
+
process.exit(130);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// ── 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
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function addLatHook(settingsPath) {
|
|
46
|
+
let settings = {};
|
|
47
|
+
if (existsSync(settingsPath)) {
|
|
48
|
+
const raw = readFileSync(settingsPath, 'utf-8');
|
|
49
|
+
try {
|
|
50
|
+
settings = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
throw new Error(`Cannot parse ${settingsPath}: ${e.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
|
57
|
+
settings.hooks = {};
|
|
58
|
+
}
|
|
59
|
+
const hooks = settings.hooks;
|
|
60
|
+
if (!Array.isArray(hooks.UserPromptSubmit)) {
|
|
61
|
+
hooks.UserPromptSubmit = [];
|
|
62
|
+
}
|
|
63
|
+
hooks.UserPromptSubmit.push({
|
|
64
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
65
|
+
});
|
|
66
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
67
|
+
}
|
|
68
|
+
// ── Gitignore helper ─────────────────────────────────────────────────
|
|
69
|
+
function ensureGitignored(root, entry) {
|
|
70
|
+
const gitignorePath = join(root, '.gitignore');
|
|
71
|
+
const gitDir = join(root, '.git');
|
|
72
|
+
// Check if already ignored
|
|
73
|
+
if (existsSync(gitignorePath)) {
|
|
74
|
+
const content = readFileSync(gitignorePath, 'utf-8');
|
|
75
|
+
const lines = content.split('\n').map((l) => l.trim());
|
|
76
|
+
if (lines.includes(entry)) {
|
|
77
|
+
console.log(chalk.green(` ${entry}`) + ' already in .gitignore');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (existsSync(gitignorePath)) {
|
|
82
|
+
// Append to existing .gitignore
|
|
83
|
+
let content = readFileSync(gitignorePath, 'utf-8');
|
|
84
|
+
if (!content.endsWith('\n'))
|
|
85
|
+
content += '\n';
|
|
86
|
+
writeFileSync(gitignorePath, content + entry + '\n');
|
|
87
|
+
console.log(chalk.green(` Added ${entry}`) + ' to .gitignore');
|
|
88
|
+
}
|
|
89
|
+
else if (existsSync(gitDir)) {
|
|
90
|
+
// Create .gitignore with the entry
|
|
91
|
+
writeFileSync(gitignorePath, entry + '\n');
|
|
92
|
+
console.log(chalk.green(` Created .gitignore`) + ` with ${entry}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(chalk.yellow(` Warning:`) +
|
|
96
|
+
` could not add ${entry} to .gitignore (not a git repository)`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── MCP command detection ────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Derive the MCP server command from the currently running binary.
|
|
102
|
+
* If `lat init` was invoked as `/path/to/lat`, we emit
|
|
103
|
+
* `{ command: "/path/to/lat", args: ["mcp"] }` so the MCP client
|
|
104
|
+
* starts the same binary.
|
|
105
|
+
*/
|
|
106
|
+
function mcpCommand() {
|
|
107
|
+
return { command: resolve(process.argv[1]), args: ['mcp'] };
|
|
108
|
+
}
|
|
109
|
+
function hasMcpServer(configPath) {
|
|
110
|
+
if (!existsSync(configPath))
|
|
111
|
+
return false;
|
|
112
|
+
try {
|
|
113
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
114
|
+
return !!cfg?.mcpServers?.lat;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function addMcpServer(configPath) {
|
|
121
|
+
let cfg = { mcpServers: {} };
|
|
122
|
+
if (existsSync(configPath)) {
|
|
123
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
124
|
+
try {
|
|
125
|
+
cfg = JSON.parse(raw);
|
|
126
|
+
if (!cfg.mcpServers)
|
|
127
|
+
cfg.mcpServers = {};
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
throw new Error(`Cannot parse ${configPath}: ${e.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
cfg.mcpServers.lat = mcpCommand();
|
|
134
|
+
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
135
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
136
|
+
}
|
|
137
|
+
// ── 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
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function setupClaudeCode(root, template) {
|
|
149
|
+
const created = [];
|
|
150
|
+
// 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
|
|
161
|
+
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.'));
|
|
164
|
+
const claudeDir = join(root, '.claude');
|
|
165
|
+
const hooksDir = join(claudeDir, 'hooks');
|
|
166
|
+
const hookPath = join(hooksDir, 'lat-prompt-hook.sh');
|
|
167
|
+
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
|
+
}
|
|
180
|
+
// MCP server → .mcp.json at project root
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
|
|
183
|
+
console.log(chalk.dim(' more visibility and makes agents more likely to use it proactively.'));
|
|
184
|
+
const mcpPath = join(root, '.mcp.json');
|
|
185
|
+
if (hasMcpServer(mcpPath)) {
|
|
186
|
+
console.log(chalk.green(' MCP server') + ' already configured');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
addMcpServer(mcpPath);
|
|
190
|
+
console.log(chalk.green(' MCP server') + ' registered in .mcp.json');
|
|
191
|
+
created.push('.mcp.json');
|
|
192
|
+
}
|
|
193
|
+
// Ensure .mcp.json is gitignored (it contains local absolute paths)
|
|
194
|
+
ensureGitignored(root, '.mcp.json');
|
|
195
|
+
return created;
|
|
196
|
+
}
|
|
197
|
+
async function setupCursor(root) {
|
|
198
|
+
const created = [];
|
|
199
|
+
// .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
|
+
}
|
|
211
|
+
// .cursor/mcp.json
|
|
212
|
+
console.log('');
|
|
213
|
+
console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
|
|
214
|
+
console.log(chalk.dim(' more visibility and makes agents more likely to use it proactively.'));
|
|
215
|
+
const mcpPath = join(root, '.cursor', 'mcp.json');
|
|
216
|
+
if (hasMcpServer(mcpPath)) {
|
|
217
|
+
console.log(chalk.green(' MCP server') + ' already configured');
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
addMcpServer(mcpPath);
|
|
221
|
+
console.log(chalk.green(' MCP server') + ' registered in .cursor/mcp.json');
|
|
222
|
+
created.push('.cursor/mcp.json');
|
|
223
|
+
}
|
|
224
|
+
// Ensure .cursor/mcp.json is gitignored (it contains local absolute paths)
|
|
225
|
+
ensureGitignored(root, '.cursor/mcp.json');
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log(chalk.yellow(' Note:') +
|
|
228
|
+
' Enable MCP in Cursor: Settings → Features → MCP → check "Enable MCP"');
|
|
229
|
+
return created;
|
|
230
|
+
}
|
|
231
|
+
async function setupCopilot(root) {
|
|
232
|
+
const created = [];
|
|
233
|
+
// .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
|
+
}
|
|
246
|
+
// .vscode/mcp.json
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
|
|
249
|
+
console.log(chalk.dim(' more visibility and makes agents more likely to use it proactively.'));
|
|
250
|
+
const mcpPath = join(root, '.vscode', 'mcp.json');
|
|
251
|
+
if (hasMcpServer(mcpPath)) {
|
|
252
|
+
console.log(chalk.green(' MCP server') + ' already configured');
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
addMcpServer(mcpPath);
|
|
256
|
+
console.log(chalk.green(' MCP server') + ' registered in .vscode/mcp.json');
|
|
257
|
+
created.push('.vscode/mcp.json');
|
|
258
|
+
}
|
|
259
|
+
return created;
|
|
260
|
+
}
|
|
261
|
+
// ── LLM key setup ───────────────────────────────────────────────────
|
|
262
|
+
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
|
+
// Check env var first
|
|
276
|
+
const envKey = process.env.LAT_LLM_KEY;
|
|
277
|
+
if (envKey) {
|
|
278
|
+
console.log(chalk.green(' LAT_LLM_KEY') +
|
|
279
|
+
' is set in your environment. Semantic search is ready.');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// Check existing config
|
|
283
|
+
const config = readConfig();
|
|
284
|
+
const configPath = getConfigPath();
|
|
285
|
+
if (config.llm_key) {
|
|
286
|
+
console.log(chalk.green(' LLM key') +
|
|
287
|
+
' already configured in ' +
|
|
288
|
+
chalk.dim(configPath));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
// Interactive prompt
|
|
292
|
+
if (!rl) {
|
|
293
|
+
console.log(chalk.yellow(' No LLM key found.') +
|
|
294
|
+
' Set LAT_LLM_KEY env var or run ' +
|
|
295
|
+
chalk.cyan('lat init') +
|
|
296
|
+
' interactively.');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log(' You can provide a key now, or skip and set ' +
|
|
300
|
+
chalk.cyan('LAT_LLM_KEY') +
|
|
301
|
+
' env var later.');
|
|
302
|
+
console.log(' Supported: OpenAI (' +
|
|
303
|
+
chalk.dim('sk-...') +
|
|
304
|
+
') or Vercel AI Gateway (' +
|
|
305
|
+
chalk.dim('vck_...') +
|
|
306
|
+
')');
|
|
307
|
+
console.log('');
|
|
308
|
+
const key = await prompt(rl, ` Paste your key (or press Enter to skip): `);
|
|
309
|
+
if (!key) {
|
|
310
|
+
console.log(chalk.dim(' Skipped.') +
|
|
311
|
+
' You can set ' +
|
|
312
|
+
chalk.cyan('LAT_LLM_KEY') +
|
|
313
|
+
' later or re-run ' +
|
|
314
|
+
chalk.cyan('lat init') +
|
|
315
|
+
'.');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Validate prefix
|
|
319
|
+
if (key.startsWith('sk-ant-')) {
|
|
320
|
+
console.log(chalk.red(' That looks like an Anthropic key.') +
|
|
321
|
+
" Anthropic doesn't offer embeddings.");
|
|
322
|
+
console.log(' lat.md needs an OpenAI (' +
|
|
323
|
+
chalk.dim('sk-...') +
|
|
324
|
+
') or Vercel AI Gateway (' +
|
|
325
|
+
chalk.dim('vck_...') +
|
|
326
|
+
') key.');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!key.startsWith('sk-') && !key.startsWith('vck_')) {
|
|
330
|
+
console.log(chalk.yellow(' Unrecognized key prefix.') +
|
|
331
|
+
' Expected sk-... (OpenAI) or vck_... (Vercel AI Gateway).');
|
|
332
|
+
console.log(' Saving anyway — you can update it later.');
|
|
333
|
+
}
|
|
334
|
+
// Save to config
|
|
335
|
+
const updatedConfig = { ...config, llm_key: key };
|
|
336
|
+
writeConfig(updatedConfig);
|
|
337
|
+
console.log(chalk.green(' Key saved') + ' to ' + chalk.dim(configPath));
|
|
338
|
+
}
|
|
339
|
+
// ── Main init flow ───────────────────────────────────────────────────
|
|
16
340
|
export async function initCmd(targetDir) {
|
|
17
341
|
const root = resolve(targetDir ?? process.cwd());
|
|
18
342
|
const latDir = join(root, 'lat.md');
|
|
@@ -40,26 +364,57 @@ export async function initCmd(targetDir) {
|
|
|
40
364
|
cpSync(templateDir, latDir, { recursive: true });
|
|
41
365
|
console.log(chalk.green('Created lat.md/'));
|
|
42
366
|
}
|
|
43
|
-
// Step 2:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
367
|
+
// Step 2: Which coding agents do you use?
|
|
368
|
+
console.log('');
|
|
369
|
+
console.log(chalk.bold('Which coding agents do you use?'));
|
|
370
|
+
console.log('');
|
|
371
|
+
const useClaudeCode = await ask(' Claude Code?');
|
|
372
|
+
const useCursor = await ask(' Cursor?');
|
|
373
|
+
const useCopilot = await ask(' VS Code Copilot?');
|
|
374
|
+
const useCodex = await ask(' Codex / OpenCode?');
|
|
375
|
+
const anySelected = useClaudeCode || useCursor || useCopilot || useCodex;
|
|
376
|
+
if (!anySelected) {
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log(chalk.dim('No agents selected. You can re-run') +
|
|
379
|
+
' lat init ' +
|
|
380
|
+
chalk.dim('later.'));
|
|
381
|
+
return;
|
|
55
382
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
383
|
+
console.log('');
|
|
384
|
+
const template = readAgentsTemplate();
|
|
385
|
+
// Step 3: AGENTS.md (shared by non-Claude agents)
|
|
386
|
+
const needsAgentsMd = useCursor || useCopilot || useCodex;
|
|
387
|
+
if (needsAgentsMd) {
|
|
388
|
+
setupAgentsMd(root, template);
|
|
389
|
+
}
|
|
390
|
+
// Step 4: Per-agent setup
|
|
391
|
+
if (useClaudeCode) {
|
|
392
|
+
console.log('');
|
|
393
|
+
console.log(chalk.bold('Setting up Claude Code...'));
|
|
394
|
+
await setupClaudeCode(root, template);
|
|
395
|
+
}
|
|
396
|
+
if (useCursor) {
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log(chalk.bold('Setting up Cursor...'));
|
|
399
|
+
await setupCursor(root);
|
|
400
|
+
}
|
|
401
|
+
if (useCopilot) {
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(chalk.bold('Setting up VS Code Copilot...'));
|
|
404
|
+
await setupCopilot(root);
|
|
405
|
+
}
|
|
406
|
+
if (useCodex) {
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log(chalk.bold('Codex / OpenCode') +
|
|
409
|
+
' — uses AGENTS.md (already created). No additional setup needed.');
|
|
62
410
|
}
|
|
411
|
+
// Step 5: LLM key setup
|
|
412
|
+
await setupLlmKey(rl);
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log(chalk.green('Done!') +
|
|
415
|
+
' Run ' +
|
|
416
|
+
chalk.cyan('lat check') +
|
|
417
|
+
' to validate your setup.');
|
|
63
418
|
}
|
|
64
419
|
finally {
|
|
65
420
|
rl?.close();
|
package/dist/src/cli/locate.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { loadAllSections, findSections } from '../lattice.js';
|
|
2
2
|
import { formatResultList } from '../format.js';
|
|
3
3
|
export async function locateCmd(ctx, query) {
|
|
4
|
+
const stripped = query.replace(/^\[\[|\]\]$/g, '');
|
|
4
5
|
const sections = await loadAllSections(ctx.latDir);
|
|
5
|
-
const matches = findSections(sections,
|
|
6
|
+
const matches = findSections(sections, stripped);
|
|
6
7
|
if (matches.length === 0) {
|
|
7
|
-
console.error(ctx.chalk.red(`No sections matching "${
|
|
8
|
+
console.error(ctx.chalk.red(`No sections matching "${stripped}" (no exact, substring, or fuzzy matches)`));
|
|
8
9
|
process.exit(1);
|
|
9
10
|
}
|
|
10
|
-
console.log(formatResultList(`Sections matching "${
|
|
11
|
-
numbered: true,
|
|
12
|
-
}));
|
|
11
|
+
console.log(formatResultList(`Sections matching "${stripped}":`, matches, ctx.latDir));
|
|
13
12
|
}
|
package/dist/src/cli/prompt.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { relative } from 'node:path';
|
|
2
|
-
import { loadAllSections, findSections,
|
|
2
|
+
import { loadAllSections, findSections, } from '../lattice.js';
|
|
3
3
|
const WIKI_LINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
4
|
-
function
|
|
4
|
+
function formatLocation(section, latDir) {
|
|
5
5
|
const relPath = relative(process.cwd(), latDir + '/' + section.file + '.md');
|
|
6
|
-
|
|
7
|
-
let text = `[${section.id}](${loc})`;
|
|
8
|
-
if (section.body) {
|
|
9
|
-
text += `: ${section.body}`;
|
|
10
|
-
}
|
|
11
|
-
return text;
|
|
6
|
+
return `${relPath}:${section.startLine}-${section.endLine}`;
|
|
12
7
|
}
|
|
13
8
|
export async function promptCmd(ctx, text) {
|
|
14
9
|
const allSections = await loadAllSections(ctx.latDir);
|
|
15
|
-
const flat = flattenSections(allSections);
|
|
16
|
-
const sectionIds = new Set(flat.map((s) => s.id.toLowerCase()));
|
|
17
|
-
const fileIndex = buildFileIndex(allSections);
|
|
18
10
|
const refs = [...text.matchAll(WIKI_LINK_RE)];
|
|
19
11
|
if (refs.length === 0) {
|
|
20
12
|
process.stdout.write(text);
|
|
@@ -25,41 +17,43 @@ export async function promptCmd(ctx, text) {
|
|
|
25
17
|
const target = match[1];
|
|
26
18
|
if (resolved.has(target))
|
|
27
19
|
continue;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
const fuzzy = findSections(allSections, target);
|
|
37
|
-
if (fuzzy.length === 1) {
|
|
38
|
-
resolved.set(target, fuzzy[0]);
|
|
20
|
+
const matches = findSections(allSections, target);
|
|
21
|
+
if (matches.length >= 1) {
|
|
22
|
+
resolved.set(target, {
|
|
23
|
+
target,
|
|
24
|
+
best: matches[0],
|
|
25
|
+
alternatives: matches.slice(1),
|
|
26
|
+
});
|
|
39
27
|
continue;
|
|
40
28
|
}
|
|
41
|
-
if (fuzzy.length > 1) {
|
|
42
|
-
console.error(ctx.chalk.red(`Ambiguous reference [[${target}]].`));
|
|
43
|
-
console.error(ctx.chalk.dim('\nCould match:\n'));
|
|
44
|
-
for (const m of fuzzy) {
|
|
45
|
-
console.error(' ' + m.id);
|
|
46
|
-
}
|
|
47
|
-
console.error(ctx.chalk.dim('\nAsk the user which section they meant.'));
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
29
|
console.error(ctx.chalk.red(`No section found for [[${target}]] (no exact, substring, or fuzzy matches).`));
|
|
51
30
|
console.error(ctx.chalk.dim('Ask the user to correct the reference.'));
|
|
52
31
|
process.exit(1);
|
|
53
32
|
}
|
|
54
33
|
// Replace [[refs]] inline
|
|
55
34
|
let output = text.replace(WIKI_LINK_RE, (_match, target) => {
|
|
56
|
-
const
|
|
57
|
-
return `[[${section.id}]]`;
|
|
35
|
+
const ref = resolved.get(target);
|
|
36
|
+
return `[[${ref.best.section.id}]]`;
|
|
58
37
|
});
|
|
59
|
-
// Append context block
|
|
38
|
+
// Append context block as nested outliner
|
|
60
39
|
output += '\n\n<lat-context>\n';
|
|
61
|
-
for (const
|
|
62
|
-
|
|
40
|
+
for (const ref of resolved.values()) {
|
|
41
|
+
const isExact = ref.best.reason === 'exact match';
|
|
42
|
+
const all = isExact ? [ref.best] : [ref.best, ...ref.alternatives];
|
|
43
|
+
if (isExact) {
|
|
44
|
+
output += `* \`[[${ref.target}]]\` is referring to:\n`;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
output += `* \`[[${ref.target}]]\` might be referring to either of the following:\n`;
|
|
48
|
+
}
|
|
49
|
+
for (const m of all) {
|
|
50
|
+
const reason = isExact ? '' : ` (${m.reason})`;
|
|
51
|
+
output += ` * [[${m.section.id}]]${reason}\n`;
|
|
52
|
+
output += ` * ${formatLocation(m.section, ctx.latDir)}\n`;
|
|
53
|
+
if (m.section.body) {
|
|
54
|
+
output += ` * ${m.section.body}\n`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
63
57
|
}
|
|
64
58
|
output += '</lat-context>\n';
|
|
65
59
|
process.stdout.write(output);
|
package/dist/src/cli/refs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { listLatticeFiles, loadAllSections, findSections, extractRefs, flattenSections, buildFileIndex, resolveRef, } from '../lattice.js';
|
|
4
|
-
import {
|
|
4
|
+
import { formatResultList } from '../format.js';
|
|
5
5
|
import { scanCodeRefs } from '../code-refs.js';
|
|
6
6
|
export async function refsCmd(ctx, query, scope) {
|
|
7
7
|
const allSections = await loadAllSections(ctx.latDir);
|
|
@@ -22,13 +22,18 @@ export async function refsCmd(ctx, query, scope) {
|
|
|
22
22
|
if (matches.length > 0) {
|
|
23
23
|
console.error(ctx.chalk.dim('\nDid you mean:\n'));
|
|
24
24
|
for (const m of matches) {
|
|
25
|
-
console.error(
|
|
25
|
+
console.error(ctx.chalk.dim('*') +
|
|
26
|
+
' ' +
|
|
27
|
+
ctx.chalk.white(m.section.id) +
|
|
28
|
+
' ' +
|
|
29
|
+
ctx.chalk.dim(`(${m.reason})`));
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
process.exit(1);
|
|
29
33
|
}
|
|
30
34
|
const targetId = exactMatch.id.toLowerCase();
|
|
31
|
-
|
|
35
|
+
const mdMatches = [];
|
|
36
|
+
const codeLines = [];
|
|
32
37
|
if (scope === 'md' || scope === 'md+code') {
|
|
33
38
|
const files = await listLatticeFiles(ctx.latDir);
|
|
34
39
|
const matchingFromSections = new Set();
|
|
@@ -44,11 +49,8 @@ export async function refsCmd(ctx, query, scope) {
|
|
|
44
49
|
}
|
|
45
50
|
if (matchingFromSections.size > 0) {
|
|
46
51
|
const referrers = flat.filter((s) => matchingFromSections.has(s.id.toLowerCase()));
|
|
47
|
-
for (
|
|
48
|
-
|
|
49
|
-
console.log('');
|
|
50
|
-
console.log(formatSectionPreview(referrers[i], ctx.latDir));
|
|
51
|
-
hasOutput = true;
|
|
52
|
+
for (const s of referrers) {
|
|
53
|
+
mdMatches.push({ section: s, reason: 'wiki link' });
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -58,15 +60,24 @@ export async function refsCmd(ctx, query, scope) {
|
|
|
58
60
|
for (const ref of codeRefs) {
|
|
59
61
|
const { resolved: codeResolved } = resolveRef(ref.target, sectionIds, fileIndex);
|
|
60
62
|
if (codeResolved.toLowerCase() === targetId) {
|
|
61
|
-
|
|
62
|
-
console.log('');
|
|
63
|
-
console.log(` ${ref.file}:${ref.line}`);
|
|
64
|
-
hasOutput = true;
|
|
63
|
+
codeLines.push(`${ref.file}:${ref.line}`);
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
|
-
if (
|
|
67
|
+
if (mdMatches.length === 0 && codeLines.length === 0) {
|
|
69
68
|
console.error(ctx.chalk.red(`No references to "${exactMatch.id}" found`));
|
|
70
69
|
process.exit(1);
|
|
71
70
|
}
|
|
71
|
+
if (mdMatches.length > 0) {
|
|
72
|
+
console.log(formatResultList(`References to "${exactMatch.id}":`, mdMatches, ctx.latDir));
|
|
73
|
+
}
|
|
74
|
+
if (codeLines.length > 0) {
|
|
75
|
+
if (mdMatches.length > 0)
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(ctx.chalk.bold('Code references:'));
|
|
78
|
+
console.log('');
|
|
79
|
+
for (const line of codeLines) {
|
|
80
|
+
console.log(`${ctx.chalk.dim('*')} ${line}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
72
83
|
}
|