@plebeiantech/lat.md 0.12.3-fork.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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/src/cli/check-frontmatter.d.ts +12 -0
  4. package/dist/src/cli/check-frontmatter.js +44 -0
  5. package/dist/src/cli/check-mode.d.ts +6 -0
  6. package/dist/src/cli/check-mode.js +303 -0
  7. package/dist/src/cli/check-status.d.ts +74 -0
  8. package/dist/src/cli/check-status.js +217 -0
  9. package/dist/src/cli/check.d.ts +45 -0
  10. package/dist/src/cli/check.js +777 -0
  11. package/dist/src/cli/checklist-menu.d.ts +12 -0
  12. package/dist/src/cli/checklist-menu.js +111 -0
  13. package/dist/src/cli/comment-guard.d.ts +16 -0
  14. package/dist/src/cli/comment-guard.js +111 -0
  15. package/dist/src/cli/comment-reminder.d.ts +62 -0
  16. package/dist/src/cli/comment-reminder.js +210 -0
  17. package/dist/src/cli/context.d.ts +11 -0
  18. package/dist/src/cli/context.js +58 -0
  19. package/dist/src/cli/expand.d.ts +8 -0
  20. package/dist/src/cli/expand.js +94 -0
  21. package/dist/src/cli/gen-index.d.ts +45 -0
  22. package/dist/src/cli/gen-index.js +235 -0
  23. package/dist/src/cli/gen.d.ts +6 -0
  24. package/dist/src/cli/gen.js +42 -0
  25. package/dist/src/cli/graph.d.ts +7 -0
  26. package/dist/src/cli/graph.js +47 -0
  27. package/dist/src/cli/hook.d.ts +1 -0
  28. package/dist/src/cli/hook.js +432 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +307 -0
  31. package/dist/src/cli/init.d.ts +9 -0
  32. package/dist/src/cli/init.js +1031 -0
  33. package/dist/src/cli/link-scheme.d.ts +54 -0
  34. package/dist/src/cli/link-scheme.js +101 -0
  35. package/dist/src/cli/locate.d.ts +2 -0
  36. package/dist/src/cli/locate.js +17 -0
  37. package/dist/src/cli/refs.d.ts +22 -0
  38. package/dist/src/cli/refs.js +229 -0
  39. package/dist/src/cli/reindex.d.ts +13 -0
  40. package/dist/src/cli/reindex.js +136 -0
  41. package/dist/src/cli/search.d.ts +36 -0
  42. package/dist/src/cli/search.js +217 -0
  43. package/dist/src/cli/section.d.ts +43 -0
  44. package/dist/src/cli/section.js +252 -0
  45. package/dist/src/cli/select-menu.d.ts +13 -0
  46. package/dist/src/cli/select-menu.js +100 -0
  47. package/dist/src/cli/templates.d.ts +1 -0
  48. package/dist/src/cli/templates.js +15 -0
  49. package/dist/src/cli/ui.d.ts +11 -0
  50. package/dist/src/cli/ui.js +34 -0
  51. package/dist/src/code-refs.d.ts +17 -0
  52. package/dist/src/code-refs.js +277 -0
  53. package/dist/src/config.d.ts +29 -0
  54. package/dist/src/config.js +83 -0
  55. package/dist/src/context.d.ts +23 -0
  56. package/dist/src/context.js +11 -0
  57. package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
  58. package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
  59. package/dist/src/extensions/wiki-link/index.d.ts +4 -0
  60. package/dist/src/extensions/wiki-link/index.js +3 -0
  61. package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
  62. package/dist/src/extensions/wiki-link/syntax.js +121 -0
  63. package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
  64. package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
  65. package/dist/src/extensions/wiki-link/types.d.ts +34 -0
  66. package/dist/src/extensions/wiki-link/types.js +1 -0
  67. package/dist/src/format.d.ts +8 -0
  68. package/dist/src/format.js +43 -0
  69. package/dist/src/graph-export.d.ts +60 -0
  70. package/dist/src/graph-export.js +309 -0
  71. package/dist/src/init-version.d.ts +10 -0
  72. package/dist/src/init-version.js +49 -0
  73. package/dist/src/knowledge/bd.d.ts +2 -0
  74. package/dist/src/knowledge/bd.js +107 -0
  75. package/dist/src/knowledge/claude-memory.d.ts +13 -0
  76. package/dist/src/knowledge/claude-memory.js +204 -0
  77. package/dist/src/knowledge/cq.d.ts +2 -0
  78. package/dist/src/knowledge/cq.js +74 -0
  79. package/dist/src/knowledge/index.d.ts +49 -0
  80. package/dist/src/knowledge/index.js +271 -0
  81. package/dist/src/knowledge/ranking.d.ts +17 -0
  82. package/dist/src/knowledge/ranking.js +62 -0
  83. package/dist/src/knowledge/session.d.ts +46 -0
  84. package/dist/src/knowledge/session.js +203 -0
  85. package/dist/src/knowledge/types.d.ts +47 -0
  86. package/dist/src/knowledge/types.js +10 -0
  87. package/dist/src/lattice.d.ts +108 -0
  88. package/dist/src/lattice.js +816 -0
  89. package/dist/src/mcp/server.d.ts +1 -0
  90. package/dist/src/mcp/server.js +58 -0
  91. package/dist/src/parser.d.ts +3 -0
  92. package/dist/src/parser.js +18 -0
  93. package/dist/src/search/db.d.ts +16 -0
  94. package/dist/src/search/db.js +54 -0
  95. package/dist/src/search/embedder.d.ts +25 -0
  96. package/dist/src/search/embedder.js +58 -0
  97. package/dist/src/search/index.d.ts +9 -0
  98. package/dist/src/search/index.js +66 -0
  99. package/dist/src/search/search.d.ts +9 -0
  100. package/dist/src/search/search.js +16 -0
  101. package/dist/src/source-parser.d.ts +27 -0
  102. package/dist/src/source-parser.js +916 -0
  103. package/dist/src/untrusted.d.ts +24 -0
  104. package/dist/src/untrusted.js +97 -0
  105. package/dist/src/version.d.ts +7 -0
  106. package/dist/src/version.js +39 -0
  107. package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
  108. package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
  109. package/dist/src/view/client/index.html +14 -0
  110. package/dist/src/view/diagnostics.d.ts +5 -0
  111. package/dist/src/view/diagnostics.js +171 -0
  112. package/dist/src/view/git-diff.d.ts +3 -0
  113. package/dist/src/view/git-diff.js +255 -0
  114. package/dist/src/view/git.d.ts +22 -0
  115. package/dist/src/view/git.js +193 -0
  116. package/dist/src/view/highlight.d.ts +2 -0
  117. package/dist/src/view/highlight.js +3767 -0
  118. package/dist/src/view/markdown.d.ts +22 -0
  119. package/dist/src/view/markdown.js +322 -0
  120. package/dist/src/view/protocol.d.ts +89 -0
  121. package/dist/src/view/protocol.js +1 -0
  122. package/dist/src/view/references.d.ts +62 -0
  123. package/dist/src/view/references.js +345 -0
  124. package/dist/src/view/repository.d.ts +12 -0
  125. package/dist/src/view/repository.js +167 -0
  126. package/dist/src/view/search.d.ts +14 -0
  127. package/dist/src/view/search.js +64 -0
  128. package/dist/src/view/server.d.ts +20 -0
  129. package/dist/src/view/server.js +233 -0
  130. package/dist/src/view/store.d.ts +57 -0
  131. package/dist/src/view/store.js +404 -0
  132. package/dist/src/walk.d.ts +18 -0
  133. package/dist/src/walk.js +27 -0
  134. package/package.json +85 -0
  135. package/templates/AGENTS.md +118 -0
  136. package/templates/README +1 -0
  137. package/templates/cursor-rules.md +77 -0
  138. package/templates/init/lat.md +1 -0
  139. package/templates/logo-dark.svg +182 -0
  140. package/templates/logo-light.svg +182 -0
  141. package/templates/logo.txt +7 -0
  142. package/templates/opencode-plugin.ts +180 -0
  143. package/templates/pi-extension.ts +364 -0
  144. package/templates/skill/SKILL.md +189 -0
@@ -0,0 +1,1031 @@
1
+ import { existsSync, cpSync, mkdirSync, writeFileSync, readFileSync, } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { styleText } from 'node:util';
6
+ import { createEmbedder } from '@lat.md/embed';
7
+ import { findTemplatesDir } from './templates.js';
8
+ import { readAgentsTemplate, readCursorRulesTemplate, readPiExtensionTemplate, readOpenCodePluginTemplate, readSkillTemplate, } from './gen.js';
9
+ import { getLlmKey, getRepoEmbedding, setRepoEmbedding } from '../config.js';
10
+ import { makeStyler } from './context.js';
11
+ import { closeDb, ensureMeta, getStoredModel, openDb } from '../search/db.js';
12
+ import { modelKey } from '../search/embedder.js';
13
+ import { reindexCommand } from './reindex.js';
14
+ import { INIT_VERSION, writeInitMeta, readInitVersion, readFileHash, contentHash, } from '../init-version.js';
15
+ import { getLocalVersion, fetchLatestVersion } from '../version.js';
16
+ import { selectMenu } from './select-menu.js';
17
+ import { checklistMenu } from './checklist-menu.js';
18
+ async function confirm(rl, message) {
19
+ while (true) {
20
+ let answer;
21
+ try {
22
+ answer = await rl.question(`${message} ${styleText('dim', '[Y/n]')} `);
23
+ }
24
+ catch {
25
+ // Ctrl+C or closed stdin — abort
26
+ console.log('');
27
+ process.exit(130);
28
+ }
29
+ const val = answer.trim().toLowerCase();
30
+ if (val === '' || val === 'y' || val === 'yes')
31
+ return true;
32
+ if (val === 'n' || val === 'no')
33
+ return false;
34
+ console.log(styleText('yellow', ' Please answer Y or n.'));
35
+ }
36
+ }
37
+ // ── Binary resolution ────────────────────────────────────────────────
38
+ /**
39
+ * Return the loader-related flags from `process.execArgv`, stripping
40
+ * `--eval`/`-e`/`--print`/`-p` and their value arguments (those only
41
+ * appear when the process was started with `node -e`/`-p`).
42
+ */
43
+ function loaderExecArgs() {
44
+ const raw = process.execArgv;
45
+ const args = [];
46
+ for (let i = 0; i < raw.length; i++) {
47
+ if (raw[i] === '--eval' ||
48
+ raw[i] === '-e' ||
49
+ raw[i] === '--print' ||
50
+ raw[i] === '-p') {
51
+ i++; // skip the value argument
52
+ }
53
+ else {
54
+ args.push(raw[i]);
55
+ }
56
+ }
57
+ return args;
58
+ }
59
+ /**
60
+ * Reconstruct the executable and arguments used to invoke this process.
61
+ *
62
+ * Node script entry points must keep their Node launcher. Build output from
63
+ * `tsc` is not executable by default, even when it retains a shebang.
64
+ *
65
+ * When running via a TypeScript loader like tsx, the script itself can't be
66
+ * executed directly — we need to replay the same node flags that loaded tsx.
67
+ * Wrapper scripts and standalone binaries are already executable and can be
68
+ * invoked directly.
69
+ */
70
+ function resolveLatInvocation() {
71
+ const script = resolve(process.argv[1]);
72
+ const isTypeScript = /\.[cm]?ts$/.test(script);
73
+ const isJavaScript = /\.[cm]?js$/.test(script);
74
+ if (!isTypeScript && !isJavaScript) {
75
+ return { command: script, args: [] };
76
+ }
77
+ return {
78
+ command: process.execPath,
79
+ args: [...(isTypeScript ? loaderExecArgs() : []), script],
80
+ };
81
+ }
82
+ /** Format the current invocation for command-string based integrations. */
83
+ function resolveLatBin() {
84
+ const { command, args } = resolveLatInvocation();
85
+ return [command, ...args]
86
+ .map((arg) => (arg.includes(' ') ? `"${arg}"` : arg))
87
+ .join(' ');
88
+ }
89
+ /** Return the lat binary string for the given command style. */
90
+ function latBinString(style) {
91
+ if (style === 'global')
92
+ return 'lat';
93
+ if (style === 'npx')
94
+ return 'npx lat.md@latest';
95
+ return resolveLatBin();
96
+ }
97
+ /** Return the MCP server command descriptor for the given command style. */
98
+ function styledMcpCommand(style) {
99
+ if (style === 'global')
100
+ return { command: 'lat', args: ['mcp'] };
101
+ if (style === 'npx')
102
+ return { command: 'npx', args: ['lat.md@latest', 'mcp'] };
103
+ return mcpCommand();
104
+ }
105
+ // ── Claude Code helpers ──────────────────────────────────────────────
106
+ /** Derive the hook command prefix for the given command style. */
107
+ function latHookCommand(style, agent, event) {
108
+ return `${latBinString(style)} hook ${agent} ${event}`;
109
+ }
110
+ /** True if any command in this entry looks like it was installed by lat. */
111
+ function isLatHookEntry(entry) {
112
+ const bin = resolve(process.argv[1]);
113
+ return (entry.hooks?.some((h) => typeof h.command === 'string' &&
114
+ (/\blat\b/.test(h.command) ||
115
+ h.command.includes('hook claude ') ||
116
+ h.command.includes('hook codex ') ||
117
+ h.command.startsWith(bin + ' '))) ?? false);
118
+ }
119
+ /**
120
+ * Remove all lat-owned hook entries from settings, then add fresh ones.
121
+ * Preserves any non-lat hooks the user may have configured.
122
+ */
123
+ export function syncLatHooks(settingsPath, style, agent = 'claude') {
124
+ let settings = {};
125
+ if (existsSync(settingsPath)) {
126
+ const raw = readFileSync(settingsPath, 'utf-8');
127
+ try {
128
+ settings = JSON.parse(raw);
129
+ }
130
+ catch (e) {
131
+ throw new Error(`Cannot parse ${settingsPath}: ${e.message}`);
132
+ }
133
+ }
134
+ if (!settings.hooks || typeof settings.hooks !== 'object') {
135
+ settings.hooks = {};
136
+ }
137
+ const hooks = settings.hooks;
138
+ // Strip lat-owned entries from ALL event types (cleans up stale events too)
139
+ for (const [event, entries] of Object.entries(hooks)) {
140
+ if (!Array.isArray(entries))
141
+ continue;
142
+ const filtered = entries.filter((entry) => !isLatHookEntry(entry));
143
+ if (filtered.length > 0) {
144
+ hooks[event] = filtered;
145
+ }
146
+ else {
147
+ delete hooks[event];
148
+ }
149
+ }
150
+ // Add fresh hooks for current events
151
+ for (const event of ['UserPromptSubmit', 'Stop']) {
152
+ if (!Array.isArray(hooks[event])) {
153
+ hooks[event] = [];
154
+ }
155
+ hooks[event].push({
156
+ hooks: [
157
+ { type: 'command', command: latHookCommand(style, agent, event) },
158
+ ],
159
+ });
160
+ }
161
+ // PostToolUse fires the `// @lat:` comment reminder — only after Edit/Write,
162
+ // never on every tool call.
163
+ if (!Array.isArray(hooks.PostToolUse)) {
164
+ hooks.PostToolUse = [];
165
+ }
166
+ hooks.PostToolUse.push({
167
+ matcher: 'Edit|Write|MultiEdit',
168
+ hooks: [
169
+ {
170
+ type: 'command',
171
+ command: latHookCommand(style, agent, 'PostToolUse'),
172
+ },
173
+ ],
174
+ });
175
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
176
+ }
177
+ function cursorHooksTemplate(style) {
178
+ return (JSON.stringify({
179
+ version: 1,
180
+ hooks: {
181
+ stop: [{ command: latHookCommand(style, 'cursor', 'stop') }],
182
+ // postToolUse, not afterFileEdit. Both fire after a write, but
183
+ // afterFileEdit is purely observational — Cursor documents its
184
+ // output as having no supported fields and ignores it entirely, so
185
+ // a reminder sent from there would never reach the agent.
186
+ // postToolUse is the only post-write event that returns
187
+ // `additional_context`.
188
+ postToolUse: [
189
+ { command: latHookCommand(style, 'cursor', 'postToolUse') },
190
+ ],
191
+ },
192
+ }, null, 2) + '\n');
193
+ }
194
+ // ── Gitignore helper ─────────────────────────────────────────────────
195
+ function ensureGitignored(root, entry) {
196
+ const gitignorePath = join(root, '.gitignore');
197
+ const gitDir = join(root, '.git');
198
+ // Check if already ignored
199
+ if (existsSync(gitignorePath)) {
200
+ const content = readFileSync(gitignorePath, 'utf-8');
201
+ const lines = content.split('\n').map((l) => l.trim());
202
+ if (lines.includes(entry)) {
203
+ console.log(styleText('green', ` ${entry}`) + ' already in .gitignore');
204
+ return;
205
+ }
206
+ }
207
+ // Skip if the entry is already tracked in git — adding it to .gitignore
208
+ // would have no effect and confuse the user.
209
+ if (existsSync(gitDir)) {
210
+ try {
211
+ const result = execSync(`git ls-files "${entry}"`, {
212
+ cwd: root,
213
+ encoding: 'utf-8',
214
+ stdio: ['pipe', 'pipe', 'pipe'],
215
+ });
216
+ if (result.trim().length > 0) {
217
+ console.log(styleText('yellow', ` ${entry}`) +
218
+ ' is already checked in to git — skipping .gitignore');
219
+ return;
220
+ }
221
+ }
222
+ catch {
223
+ console.log(styleText('yellow', ` Warning:`) +
224
+ ' git ls-files failed — skipping check');
225
+ }
226
+ }
227
+ if (existsSync(gitignorePath)) {
228
+ // Append to existing .gitignore
229
+ let content = readFileSync(gitignorePath, 'utf-8');
230
+ if (!content.endsWith('\n'))
231
+ content += '\n';
232
+ writeFileSync(gitignorePath, content + entry + '\n');
233
+ console.log(styleText('green', ` Added ${entry}`) + ' to .gitignore');
234
+ }
235
+ else if (existsSync(gitDir)) {
236
+ // Create .gitignore with the entry
237
+ writeFileSync(gitignorePath, entry + '\n');
238
+ console.log(styleText('green', ` Created .gitignore`) + ` with ${entry}`);
239
+ }
240
+ else {
241
+ console.log(styleText('yellow', ` Warning:`) +
242
+ ` could not add ${entry} to .gitignore (not a git repository)`);
243
+ }
244
+ }
245
+ // ── MCP command detection ────────────────────────────────────────────
246
+ /**
247
+ * Derive the MCP server command from the currently running binary.
248
+ * If `lat init` was invoked as `/path/to/lat`, we emit
249
+ * `{ command: "/path/to/lat", args: ["mcp"] }` so the MCP client
250
+ * starts the same binary. Node scripts retain their Node executable; TypeScript
251
+ * scripts also retain loader arguments such as tsx's `--import` flag.
252
+ */
253
+ function mcpCommand() {
254
+ const { command, args } = resolveLatInvocation();
255
+ return {
256
+ command,
257
+ args: [...args, 'mcp'],
258
+ };
259
+ }
260
+ function hasMcpServer(configPath, key) {
261
+ if (!existsSync(configPath))
262
+ return false;
263
+ try {
264
+ const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
265
+ return !!cfg?.[key]?.lat;
266
+ }
267
+ catch (err) {
268
+ process.stderr.write(`Warning: failed to parse ${configPath}: ${err.message}\n`);
269
+ return false;
270
+ }
271
+ }
272
+ function addMcpServer(configPath, key, style) {
273
+ let cfg = { [key]: {} };
274
+ if (existsSync(configPath)) {
275
+ const raw = readFileSync(configPath, 'utf-8');
276
+ try {
277
+ cfg = JSON.parse(raw);
278
+ if (!cfg[key])
279
+ cfg[key] = {};
280
+ }
281
+ catch (e) {
282
+ throw new Error(`Cannot parse ${configPath}: ${e.message}`);
283
+ }
284
+ }
285
+ cfg[key].lat = styledMcpCommand(style);
286
+ mkdirSync(join(configPath, '..'), { recursive: true });
287
+ writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
288
+ }
289
+ // ── Codex TOML MCP helpers ────────────────────────────────────────────
290
+ /**
291
+ * Check whether `.codex/config.toml` already contains an `[mcp_servers.lat]`
292
+ * table. We use a simple regex match — no TOML parser needed.
293
+ */
294
+ function hasCodexMcpServer(configPath) {
295
+ if (!existsSync(configPath))
296
+ return false;
297
+ try {
298
+ const content = readFileSync(configPath, 'utf-8');
299
+ return /^\[mcp_servers\.lat\]/m.test(content);
300
+ }
301
+ catch {
302
+ return false;
303
+ }
304
+ }
305
+ /**
306
+ * Append an `[mcp_servers.lat]` table to `.codex/config.toml`.
307
+ *
308
+ * If the file exists, the block is appended (preserving existing content).
309
+ * If the file doesn't exist, it is created with just the MCP block.
310
+ *
311
+ * The TOML format is intentionally simple — Codex expects:
312
+ *
313
+ * ```toml
314
+ * [mcp_servers.lat]
315
+ * command = "lat"
316
+ * args = ["mcp"]
317
+ * ```
318
+ */
319
+ function addCodexMcpServer(configPath, style) {
320
+ const cmd = styledMcpCommand(style);
321
+ // Format args as a TOML inline array of quoted strings
322
+ const argsToml = '[' + cmd.args.map((a) => `"${a}"`).join(', ') + ']';
323
+ const block = `[mcp_servers.lat]\ncommand = "${cmd.command}"\nargs = ${argsToml}\n`;
324
+ mkdirSync(join(configPath, '..'), { recursive: true });
325
+ if (existsSync(configPath)) {
326
+ let content = readFileSync(configPath, 'utf-8');
327
+ if (!content.endsWith('\n'))
328
+ content += '\n';
329
+ content += '\n' + block;
330
+ writeFileSync(configPath, content);
331
+ }
332
+ else {
333
+ writeFileSync(configPath, block);
334
+ }
335
+ }
336
+ // ── Template file helpers ─────────────────────────────────────────────
337
+ /**
338
+ * Write a template-generated file, using stored hashes to decide whether
339
+ * to overwrite or prompt the user about local modifications.
340
+ *
341
+ * Returns the hash of the written content, or null if the file was skipped.
342
+ */
343
+ async function writeTemplateFile(root, latDir, relPath, template, genTarget, label, indent, ask) {
344
+ const absPath = join(root, relPath);
345
+ const templateHash = contentHash(template);
346
+ if (!existsSync(absPath)) {
347
+ mkdirSync(join(absPath, '..'), { recursive: true });
348
+ writeFileSync(absPath, template);
349
+ console.log(styleText('green', `${indent}Created ${label}`));
350
+ return templateHash;
351
+ }
352
+ // File exists — check if user has modified it
353
+ const currentContent = readFileSync(absPath, 'utf-8');
354
+ const currentHash = contentHash(currentContent);
355
+ const storedHash = readFileHash(latDir, relPath);
356
+ if (currentHash === templateHash) {
357
+ // Already matches the latest template
358
+ console.log(styleText('green', `${indent}${label}`) + ' already up to date');
359
+ return templateHash;
360
+ }
361
+ if (storedHash && currentHash === storedHash) {
362
+ // Unmodified by user — safe to overwrite with new template
363
+ writeFileSync(absPath, template);
364
+ console.log(styleText('green', `${indent}Updated ${label}`));
365
+ return templateHash;
366
+ }
367
+ // User has modified the file — ask whether to overwrite
368
+ console.log(styleText('yellow', `${indent}${label}`) +
369
+ ' exists and may contain your own content.');
370
+ if (await ask(`${indent}Overwrite with latest lat template?`)) {
371
+ writeFileSync(absPath, template);
372
+ console.log(styleText('green', `${indent}Updated ${label}`));
373
+ return templateHash;
374
+ }
375
+ console.log(genTarget
376
+ ? styleText('dim', `${indent}Kept existing file.`) +
377
+ ' Run ' +
378
+ styleText('cyan', `lat gen ${genTarget}`) +
379
+ ' to see the latest template.'
380
+ : styleText('dim', `${indent}Kept existing file.`) +
381
+ ' Re-run ' +
382
+ styleText('cyan', 'lat init') +
383
+ ' to regenerate this file.');
384
+ return null;
385
+ }
386
+ // ── Marker-based append for shared files ─────────────────────────────
387
+ const MARKER_BEGIN = '%% lat:begin %%';
388
+ const MARKER_END = '%% lat:end %%';
389
+ /**
390
+ * Extract the content between lat markers in a file's text.
391
+ * Returns null if markers are not found.
392
+ */
393
+ function extractMarkerSection(content) {
394
+ const beginIdx = content.indexOf(MARKER_BEGIN);
395
+ const endIdx = content.indexOf(MARKER_END);
396
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx)
397
+ return null;
398
+ return content.slice(beginIdx + MARKER_BEGIN.length + 1, endIdx);
399
+ }
400
+ /**
401
+ * Wrap template content with lat markers.
402
+ */
403
+ function wrapWithMarkers(template) {
404
+ return `${MARKER_BEGIN}\n${template}${template.endsWith('\n') ? '' : '\n'}${MARKER_END}\n`;
405
+ }
406
+ /**
407
+ * Write a template into a marker-fenced section of a file, preserving
408
+ * any user content outside the markers.
409
+ *
410
+ * Returns the hash of the written template content, or null if skipped.
411
+ */
412
+ async function appendTemplateSection(root, latDir, relPath, template, label, indent, ask) {
413
+ const absPath = join(root, relPath);
414
+ const templateHash = contentHash(template);
415
+ const wrapped = wrapWithMarkers(template);
416
+ if (!existsSync(absPath)) {
417
+ mkdirSync(join(absPath, '..'), { recursive: true });
418
+ writeFileSync(absPath, wrapped);
419
+ console.log(styleText('green', `${indent}Created ${label}`));
420
+ return templateHash;
421
+ }
422
+ const currentContent = readFileSync(absPath, 'utf-8');
423
+ const existingSection = extractMarkerSection(currentContent);
424
+ if (existingSection !== null) {
425
+ // File has markers — compare the section content
426
+ const existingSectionHash = contentHash(existingSection);
427
+ if (existingSectionHash === templateHash) {
428
+ console.log(styleText('green', `${indent}${label}`) + ' already up to date');
429
+ return templateHash;
430
+ }
431
+ // Check if section matches stored hash (unmodified by user)
432
+ const storedHash = readFileHash(latDir, relPath);
433
+ if (storedHash && existingSectionHash === storedHash) {
434
+ // User hasn't edited the section — safe to replace
435
+ const beginIdx = currentContent.indexOf(MARKER_BEGIN);
436
+ const endIdx = currentContent.indexOf(MARKER_END) + MARKER_END.length;
437
+ // Include trailing newline if present
438
+ const endWithNl = currentContent[endIdx] === '\n' ? endIdx + 1 : endIdx;
439
+ const updated = currentContent.slice(0, beginIdx) +
440
+ wrapped +
441
+ currentContent.slice(endWithNl);
442
+ writeFileSync(absPath, updated);
443
+ console.log(styleText('green', `${indent}Updated ${label}`));
444
+ return templateHash;
445
+ }
446
+ // User edited the section — ask before replacing
447
+ console.log(styleText('yellow', `${indent}${label}`) +
448
+ ' lat section has been modified.');
449
+ if (await ask(`${indent}Replace lat section with latest template?`)) {
450
+ const beginIdx = currentContent.indexOf(MARKER_BEGIN);
451
+ const endIdx = currentContent.indexOf(MARKER_END) + MARKER_END.length;
452
+ const endWithNl = currentContent[endIdx] === '\n' ? endIdx + 1 : endIdx;
453
+ const updated = currentContent.slice(0, beginIdx) +
454
+ wrapped +
455
+ currentContent.slice(endWithNl);
456
+ writeFileSync(absPath, updated);
457
+ console.log(styleText('green', `${indent}Updated ${label}`));
458
+ return templateHash;
459
+ }
460
+ console.log(styleText('dim', `${indent}Kept existing section.`));
461
+ return null;
462
+ }
463
+ // No markers — file exists from old init or user-created
464
+ // Check if full file matches stored hash (old full-overwrite init, unedited)
465
+ const currentHash = contentHash(currentContent);
466
+ const storedHash = readFileHash(latDir, relPath);
467
+ if (storedHash && currentHash === storedHash) {
468
+ // Unmodified old-style file — migrate: wrap existing content with markers
469
+ writeFileSync(absPath, wrapWithMarkers(currentContent));
470
+ console.log(styleText('green', `${indent}Migrated ${label}`) + ' to marker format');
471
+ // Return hash of what's now in the section (the old content)
472
+ return currentHash;
473
+ }
474
+ // File has user content and no markers — append section
475
+ let content = currentContent;
476
+ if (!content.endsWith('\n'))
477
+ content += '\n';
478
+ content += '\n' + wrapped;
479
+ writeFileSync(absPath, content);
480
+ console.log(styleText('green', `${indent}Appended lat section to ${label}`));
481
+ return templateHash;
482
+ }
483
+ // ── Shared skill setup ───────────────────────────────────────────────
484
+ async function writeAgentsSkill(root, latDir, hashes, ask) {
485
+ console.log('');
486
+ console.log(styleText('dim', ' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
487
+ const skillTemplate = readSkillTemplate();
488
+ const skillHash = await writeTemplateFile(root, latDir, '.agents/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.agents/skills/lat-md/SKILL.md)', ' ', ask);
489
+ if (skillHash)
490
+ hashes['.agents/skills/lat-md/SKILL.md'] = skillHash;
491
+ }
492
+ // ── Per-agent setup ──────────────────────────────────────────────────
493
+ async function setupAgentsMd(root, latDir, template, hashes, ask) {
494
+ const hash = await appendTemplateSection(root, latDir, 'AGENTS.md', template, 'AGENTS.md', '', ask);
495
+ if (hash)
496
+ hashes['AGENTS.md'] = hash;
497
+ }
498
+ async function setupClaudeCode(root, latDir, template, hashes, ask, style) {
499
+ // CLAUDE.md — append-mode with markers (preserves user content)
500
+ const hash = await appendTemplateSection(root, latDir, 'CLAUDE.md', template, 'CLAUDE.md', ' ', ask);
501
+ if (hash)
502
+ hashes['CLAUDE.md'] = hash;
503
+ // Hooks — UserPromptSubmit (lat.md reminders + [[ref]] expansion) and Stop (update reminder)
504
+ console.log('');
505
+ console.log(styleText('dim', ' Hooks inject lat.md workflow reminders into every prompt and remind'));
506
+ console.log(styleText('dim', ' the agent to update lat.md/ before finishing.'));
507
+ const claudeDir = join(root, '.claude');
508
+ const settingsPath = join(claudeDir, 'settings.json');
509
+ mkdirSync(claudeDir, { recursive: true });
510
+ syncLatHooks(settingsPath, style);
511
+ console.log(styleText('green', ' Hooks') +
512
+ ' synced (UserPromptSubmit + Stop + PostToolUse)');
513
+ // .claude/skills/lat-md/SKILL.md — skill for authoring lat.md files
514
+ console.log('');
515
+ console.log(styleText('dim', ' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
516
+ const skillTemplate = readSkillTemplate();
517
+ const skillHash = await writeTemplateFile(root, latDir, '.claude/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.claude/skills/lat-md/SKILL.md)', ' ', ask);
518
+ if (skillHash)
519
+ hashes['.claude/skills/lat-md/SKILL.md'] = skillHash;
520
+ // Ensure .claude is gitignored (settings contain local absolute paths)
521
+ ensureGitignored(root, '.claude');
522
+ // MCP server → .mcp.json at project root
523
+ console.log('');
524
+ console.log(styleText('dim', ' Agents can call `lat` from the command line, but an MCP server gives lat'));
525
+ console.log(styleText('dim', ' more visibility and makes agents more likely to use it proactively.'));
526
+ const mcpPath = join(root, '.mcp.json');
527
+ if (hasMcpServer(mcpPath, 'mcpServers')) {
528
+ console.log(styleText('green', ' MCP server') + ' already configured');
529
+ }
530
+ else {
531
+ addMcpServer(mcpPath, 'mcpServers', style);
532
+ console.log(styleText('green', ' MCP server') + ' registered in .mcp.json');
533
+ }
534
+ // Ensure .mcp.json is gitignored (it contains local absolute paths)
535
+ ensureGitignored(root, '.mcp.json');
536
+ }
537
+ async function setupCursor(root, latDir, hashes, ask, style) {
538
+ // .cursor/rules/lat.md
539
+ const hash = await writeTemplateFile(root, latDir, '.cursor/rules/lat.md', readCursorRulesTemplate(), 'cursor-rules.md', 'Rules (.cursor/rules/lat.md)', ' ', ask);
540
+ if (hash)
541
+ hashes['.cursor/rules/lat.md'] = hash;
542
+ // .cursor/hooks.json
543
+ console.log('');
544
+ console.log(styleText('dim', ' Cursor hooks can enforce the lat.md/ stop check, while prompt guidance'));
545
+ console.log(styleText('dim', ' stays in rules + MCP because Cursor cannot reliably inject prompt-specific context.'));
546
+ const hooksHash = await writeTemplateFile(root, latDir, '.cursor/hooks.json', cursorHooksTemplate(style), null, 'Hooks (.cursor/hooks.json)', ' ', ask);
547
+ if (hooksHash)
548
+ hashes['.cursor/hooks.json'] = hooksHash;
549
+ // .cursor/mcp.json
550
+ console.log('');
551
+ console.log(styleText('dim', ' Agents can call `lat` from the command line, but an MCP server gives lat'));
552
+ console.log(styleText('dim', ' more visibility and makes agents more likely to use it proactively.'));
553
+ const mcpPath = join(root, '.cursor', 'mcp.json');
554
+ if (hasMcpServer(mcpPath, 'mcpServers')) {
555
+ console.log(styleText('green', ' MCP server') + ' already configured');
556
+ }
557
+ else {
558
+ addMcpServer(mcpPath, 'mcpServers', style);
559
+ console.log(styleText('green', ' MCP server') + ' registered in .cursor/mcp.json');
560
+ }
561
+ // Ensure .cursor is gitignored (hooks and MCP config may contain local paths)
562
+ ensureGitignored(root, '.cursor');
563
+ // .agents/skills/lat-md/SKILL.md — skill for authoring lat.md files
564
+ await writeAgentsSkill(root, latDir, hashes, ask);
565
+ console.log('');
566
+ console.log(styleText('yellow', ' Note:') +
567
+ ' Enable MCP in Cursor: Settings → Features → MCP → check "Enable MCP"');
568
+ }
569
+ async function setupCopilot(root, latDir, hashes, ask, style) {
570
+ // .github/copilot-instructions.md — append-mode with markers
571
+ const hash = await appendTemplateSection(root, latDir, '.github/copilot-instructions.md', readAgentsTemplate(), 'Instructions (.github/copilot-instructions.md)', ' ', ask);
572
+ if (hash)
573
+ hashes['.github/copilot-instructions.md'] = hash;
574
+ // .vscode/mcp.json
575
+ console.log('');
576
+ console.log(styleText('dim', ' Agents can call `lat` from the command line, but an MCP server gives lat'));
577
+ console.log(styleText('dim', ' more visibility and makes agents more likely to use it proactively.'));
578
+ const mcpPath = join(root, '.vscode', 'mcp.json');
579
+ if (hasMcpServer(mcpPath, 'servers')) {
580
+ console.log(styleText('green', ' MCP server') + ' already configured');
581
+ }
582
+ else {
583
+ addMcpServer(mcpPath, 'servers', style);
584
+ console.log(styleText('green', ' MCP server') + ' registered in .vscode/mcp.json');
585
+ }
586
+ // .agents/skills/lat-md/SKILL.md — skill for authoring lat.md files
587
+ await writeAgentsSkill(root, latDir, hashes, ask);
588
+ }
589
+ async function setupPi(root, latDir, hashes, ask, style) {
590
+ // AGENTS.md — Pi reads this natively
591
+ // (already created in the shared step if any non-Claude agent is selected)
592
+ // .pi/extensions/lat.ts — extension that registers tools + lifecycle hooks
593
+ console.log('');
594
+ console.log(styleText('dim', ' The Pi extension registers lat tools and hooks into the agent lifecycle'));
595
+ console.log(styleText('dim', ' to inject search context and validate lat.md/ before finishing.'));
596
+ const template = readPiExtensionTemplate().replace('__LAT_BIN__', latBinString(style));
597
+ const hash = await writeTemplateFile(root, latDir, '.pi/extensions/lat.ts', template, 'pi-extension.ts', 'Extension (.pi/extensions/lat.ts)', ' ', ask);
598
+ if (hash)
599
+ hashes['.pi/extensions/lat.ts'] = hash;
600
+ // .pi/skills/lat-md/SKILL.md — skill for authoring lat.md files
601
+ console.log('');
602
+ console.log(styleText('dim', ' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
603
+ const skillTemplate = readSkillTemplate();
604
+ const skillHash = await writeTemplateFile(root, latDir, '.pi/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.pi/skills/lat-md/SKILL.md)', ' ', ask);
605
+ if (skillHash)
606
+ hashes['.pi/skills/lat-md/SKILL.md'] = skillHash;
607
+ // Ensure .pi is gitignored (extension contains local absolute paths)
608
+ ensureGitignored(root, '.pi');
609
+ }
610
+ async function setupOpenCode(root, latDir, hashes, ask, style) {
611
+ // AGENTS.md — OpenCode reads this natively
612
+ // (already created in the shared step if any non-Claude agent is selected)
613
+ // .opencode/plugins/lat.ts — plugin that registers tools + lifecycle hooks
614
+ console.log('');
615
+ console.log(styleText('dim', ' The OpenCode plugin registers lat tools and hooks into the session'));
616
+ console.log(styleText('dim', ' lifecycle to validate lat.md/ when the agent finishes.'));
617
+ const template = readOpenCodePluginTemplate().replace('__LAT_BIN__', latBinString(style));
618
+ const hash = await writeTemplateFile(root, latDir, '.opencode/plugins/lat.ts', template, 'opencode-plugin.ts', 'Plugin (.opencode/plugins/lat.ts)', ' ', ask);
619
+ if (hash)
620
+ hashes['.opencode/plugins/lat.ts'] = hash;
621
+ // .agents/skills/lat-md/SKILL.md — skill for authoring lat.md files
622
+ await writeAgentsSkill(root, latDir, hashes, ask);
623
+ // Ensure .opencode is gitignored (plugin contains local absolute paths)
624
+ ensureGitignored(root, '.opencode');
625
+ }
626
+ async function setupCodex(root, latDir, hashes, ask, style) {
627
+ // AGENTS.md — Codex reads this natively
628
+ // (already created in the shared step if any non-Claude agent is selected)
629
+ // Hooks — UserPromptSubmit (lat.md reminders + [[ref]] expansion) and Stop (update reminder)
630
+ console.log('');
631
+ console.log(styleText('dim', ' Hooks inject lat.md workflow reminders into every prompt and remind'));
632
+ console.log(styleText('dim', ' the agent to update lat.md/ before finishing.'));
633
+ const codexDir = join(root, '.codex');
634
+ const hooksPath = join(codexDir, 'hooks.json');
635
+ mkdirSync(codexDir, { recursive: true });
636
+ syncLatHooks(hooksPath, style, 'codex');
637
+ console.log(styleText('green', ' Hooks') +
638
+ ' synced (UserPromptSubmit + Stop + PostToolUse)');
639
+ // .codex/config.toml — MCP server registration
640
+ console.log('');
641
+ console.log(styleText('dim', ' Agents can call `lat` from the command line, but an MCP server gives lat'));
642
+ console.log(styleText('dim', ' more visibility and makes agents more likely to use it proactively.'));
643
+ const mcpPath = join(root, '.codex', 'config.toml');
644
+ if (hasCodexMcpServer(mcpPath)) {
645
+ console.log(styleText('green', ' MCP server') + ' already configured');
646
+ }
647
+ else {
648
+ addCodexMcpServer(mcpPath, style);
649
+ console.log(styleText('green', ' MCP server') + ' registered in .codex/config.toml');
650
+ }
651
+ // Ensure .codex is gitignored (config contains local absolute paths)
652
+ ensureGitignored(root, '.codex');
653
+ // .agents/skills/lat-md/SKILL.md — skill for authoring lat.md files
654
+ await writeAgentsSkill(root, latDir, hashes, ask);
655
+ // .codex/skills/lat-md/SKILL.md — Codex-specific skills directory
656
+ console.log('');
657
+ console.log(styleText('dim', ' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
658
+ const skillTemplate = readSkillTemplate();
659
+ const skillHash = await writeTemplateFile(root, latDir, '.codex/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.codex/skills/lat-md/SKILL.md)', ' ', ask);
660
+ if (skillHash)
661
+ hashes['.codex/skills/lat-md/SKILL.md'] = skillHash;
662
+ console.log('');
663
+ console.log(styleText('yellow', ' Note:') +
664
+ ' Run /hooks in Codex to review and trust the project hooks.');
665
+ }
666
+ async function readStoredEmbeddingModel(latDir) {
667
+ if (!existsSync(join(latDir, '.cache', 'vectors.db')))
668
+ return null;
669
+ const db = openDb(latDir);
670
+ try {
671
+ await ensureMeta(db);
672
+ return await getStoredModel(db);
673
+ }
674
+ finally {
675
+ await closeDb(db);
676
+ }
677
+ }
678
+ async function offerReindex(root, latDir, backend, remoteModel, storedModel, interactive) {
679
+ if (!storedModel)
680
+ return;
681
+ const storedBackend = storedModel.startsWith('local:')
682
+ ? 'local'
683
+ : 'remote';
684
+ const modelMatches = backend === 'local'
685
+ ? storedBackend === 'local'
686
+ : remoteModel
687
+ ? storedModel === remoteModel
688
+ : storedBackend === 'remote';
689
+ if (modelMatches)
690
+ return;
691
+ const command = `lat reindex --${backend}`;
692
+ const configuredModel = backend === 'remote' && remoteModel
693
+ ? `'${remoteModel}'`
694
+ : `${backend} embeddings`;
695
+ console.log('');
696
+ console.log(styleText('yellow', 'Existing search index') +
697
+ ` — built with '${storedModel}', but this repo is now configured for ${configuredModel}.`);
698
+ if (!interactive) {
699
+ console.log(' Run ' + styleText('cyan', command) + ' to rebuild it.');
700
+ return;
701
+ }
702
+ const action = await selectMenu([
703
+ { label: 'Reindex now', value: 'now' },
704
+ { label: `Later (run ${command})`, value: 'later' },
705
+ ], `Rebuild the existing index with ${backend} embeddings?`, 0);
706
+ if (action !== 'now')
707
+ return;
708
+ const result = await reindexCommand({ latDir, projectRoot: root, styler: makeStyler(), mode: 'cli' }, backend === 'local' ? { local: true } : { remote: true });
709
+ if (result.isError)
710
+ console.error(result.output);
711
+ else if (result.output)
712
+ console.log(result.output);
713
+ }
714
+ async function setupEmbeddingsForInit(root, latDir, interactive, configureDefault) {
715
+ let key;
716
+ let remoteModel = null;
717
+ try {
718
+ key = getLlmKey();
719
+ if (key)
720
+ remoteModel = modelKey(await createEmbedder({ key }));
721
+ }
722
+ catch (err) {
723
+ key = undefined;
724
+ console.log('');
725
+ console.log(styleText('yellow', 'Embedding key unavailable:') +
726
+ ' ' +
727
+ err.message);
728
+ }
729
+ let storedModel = null;
730
+ try {
731
+ storedModel = await readStoredEmbeddingModel(latDir);
732
+ }
733
+ catch (err) {
734
+ console.log('');
735
+ console.log(styleText('yellow', 'Could not inspect the existing search index:') +
736
+ ' ' +
737
+ err.message);
738
+ }
739
+ const repoIsLocal = getRepoEmbedding(latDir) === 'local';
740
+ const storedBackend = storedModel
741
+ ? storedModel.startsWith('local:')
742
+ ? 'local'
743
+ : 'remote'
744
+ : null;
745
+ const existingBackend = repoIsLocal
746
+ ? 'local'
747
+ : storedBackend;
748
+ // Fresh/outdated non-interactive setups default local. Current non-interactive
749
+ // setups preserve their backend. Only a TTY presents and applies a key choice.
750
+ //
751
+ // The local-first default deliberately skips a repo with a *working* hosted
752
+ // setup (hosted index + a provider/model-compatible key): `configureDefault`
753
+ // re-fires
754
+ // on every INIT_VERSION bump, so pinning local here would keep undoing a
755
+ // deliberate choice — and without a TTY the user has no way to object. A
756
+ // hosted index with no key is unusable, so that one does fall back to local.
757
+ const workingHosted = existingBackend === 'remote' && remoteModel === storedModel;
758
+ let backend;
759
+ let configuredNow = false;
760
+ if (key && interactive) {
761
+ console.log('');
762
+ console.log(styleText('bold', 'Semantic search'));
763
+ console.log('');
764
+ console.log(' An embedding API key is configured. Choose whether this repo should use');
765
+ console.log(' the bundled offline model or hosted embeddings.');
766
+ console.log('');
767
+ const defaultIndex = existingBackend
768
+ ? existingBackend === 'local'
769
+ ? 0
770
+ : 1
771
+ : configureDefault
772
+ ? 0
773
+ : 1;
774
+ const selected = await selectMenu([
775
+ { label: 'Local — bundled, offline (recommended)', value: 'local' },
776
+ { label: 'Hosted — use LAT_LLM_KEY', value: 'remote' },
777
+ ], 'Embedding backend', defaultIndex);
778
+ if (!selected)
779
+ return;
780
+ backend = selected;
781
+ setRepoEmbedding(latDir, backend === 'local' ? 'local' : null);
782
+ configuredNow = true;
783
+ }
784
+ else if (configureDefault && !workingHosted) {
785
+ backend = 'local';
786
+ setRepoEmbedding(latDir, 'local');
787
+ configuredNow = true;
788
+ console.log('');
789
+ console.log(styleText('bold', 'Semantic search'));
790
+ console.log('');
791
+ console.log(' lat.md includes semantic search (' +
792
+ styleText('cyan', 'lat search') +
793
+ ') that lets agents find');
794
+ console.log(' relevant documentation by meaning, not just keywords. This repo is configured');
795
+ console.log(' to use the bundled local model — fully offline, with no API key required.');
796
+ }
797
+ else {
798
+ backend = existingBackend ?? (key ? 'remote' : 'local');
799
+ }
800
+ if (configuredNow && backend === 'local' && !key) {
801
+ console.log('');
802
+ console.log(' Power users can opt into hosted embeddings by setting ' +
803
+ styleText('cyan', 'LAT_LLM_KEY') +
804
+ ' and running');
805
+ console.log(' ' + styleText('cyan', 'lat reindex --remote') + '.');
806
+ }
807
+ await offerReindex(root, latDir, backend, remoteModel, storedModel, interactive);
808
+ }
809
+ // ── Post-onboarding guidance ─────────────────────────────────────────
810
+ const NEXT_STEP_PROMPT = 'Read through this codebase and set up lat.md/ to document its architecture, key design decisions, and domain concepts. Run `lat check` when done.';
811
+ function printNextSteps(selectedAgents) {
812
+ const hasClaudeCode = selectedAgents.includes('claude');
813
+ const ideAgents = selectedAgents.filter((a) => a !== 'claude');
814
+ const ideLabels = {
815
+ cursor: 'Cursor',
816
+ copilot: 'VS Code Copilot',
817
+ pi: 'Pi',
818
+ opencode: 'OpenCode',
819
+ codex: 'Codex',
820
+ };
821
+ if (!hasClaudeCode && ideAgents.length === 0)
822
+ return;
823
+ console.log('');
824
+ console.log(styleText('bold', 'Next step') +
825
+ ' — have your agent document this codebase:');
826
+ if (hasClaudeCode) {
827
+ console.log('');
828
+ console.log(' ' + styleText('bold', 'Claude Code:'));
829
+ console.log(' ' + styleText('cyan', `claude "${NEXT_STEP_PROMPT}"`));
830
+ }
831
+ if (ideAgents.length > 0) {
832
+ const names = ideAgents.map((a) => ideLabels[a] || a).join(' / ');
833
+ console.log('');
834
+ console.log(' ' + styleText('bold', `${names}`) + ' — paste into agent chat:');
835
+ console.log(' ' + styleText('cyan', NEXT_STEP_PROMPT));
836
+ }
837
+ }
838
+ // ── Main init flow ───────────────────────────────────────────────────
839
+ export function readLogo() {
840
+ return readFileSync(join(findTemplatesDir(), 'logo.txt'), 'utf-8');
841
+ }
842
+ export async function initCmd(targetDir) {
843
+ console.log(styleText('cyan', readLogo()));
844
+ // Upfront version check — let the user upgrade before proceeding
845
+ process.stdout.write(styleText('dim', 'Checking latest version...'));
846
+ const latest = await fetchLatestVersion();
847
+ const local = getLocalVersion();
848
+ if (latest && latest !== local) {
849
+ console.log(' ' +
850
+ styleText('yellow', 'update available:') +
851
+ ' ' +
852
+ local +
853
+ ' → ' +
854
+ styleText('green', latest) +
855
+ ' — run ' +
856
+ styleText('cyan', 'npm install -g lat.md') +
857
+ ' to update.');
858
+ console.log('');
859
+ }
860
+ else {
861
+ console.log(' ' + styleText('green', `latest version is used (${local})`));
862
+ }
863
+ const root = resolve(targetDir ?? process.cwd());
864
+ const latDir = join(root, 'lat.md');
865
+ const storedInitVersion = readInitVersion(latDir);
866
+ const interactive = process.stdin.isTTY ?? false;
867
+ // Readline is created AFTER the selectMenu loop below.
868
+ // selectMenu puts stdin into raw mode with its own 'data' listener;
869
+ // if readline is already attached it receives those raw keypresses,
870
+ // corrupting its internal state and causing rl.question() to hang/exit.
871
+ let rl = null;
872
+ const ask = async (message) => {
873
+ if (!rl)
874
+ return true;
875
+ return confirm(rl, message);
876
+ };
877
+ try {
878
+ // Step 1: lat.md/ directory
879
+ if (existsSync(latDir)) {
880
+ console.log(styleText('green', 'lat.md/') + ' already exists');
881
+ }
882
+ else {
883
+ // No rl yet — selectMenu hasn't run, so use a one-off confirm
884
+ if (interactive) {
885
+ const tmpRl = createInterface({
886
+ input: process.stdin,
887
+ output: process.stdout,
888
+ });
889
+ try {
890
+ if (!(await confirm(tmpRl, 'Create lat.md/ directory?'))) {
891
+ console.log('Aborted.');
892
+ return;
893
+ }
894
+ }
895
+ finally {
896
+ tmpRl.close();
897
+ }
898
+ }
899
+ const templateDir = join(findTemplatesDir(), 'init');
900
+ mkdirSync(latDir, { recursive: true });
901
+ cpSync(templateDir, latDir, { recursive: true });
902
+ console.log(styleText('green', 'Created lat.md/'));
903
+ }
904
+ // Step 2: Configure fresh/outdated setups, ask interactive users about an
905
+ // available key, and offer to rebuild an index whose backend differs. This
906
+ // happens before agent selection so "no agents" still completes it.
907
+ await setupEmbeddingsForInit(root, latDir, interactive, storedInitVersion === null || storedInitVersion < INIT_VERSION);
908
+ // Step 3: Which coding agents do you use? (interactive select menu)
909
+ console.log('');
910
+ const allAgents = [
911
+ { label: 'Claude Code', value: 'claude' },
912
+ { label: 'Pi', value: 'pi' },
913
+ { label: 'Cursor', value: 'cursor' },
914
+ { label: 'VS Code Copilot', value: 'copilot' },
915
+ { label: 'OpenCode', value: 'opencode' },
916
+ { label: 'Codex', value: 'codex' },
917
+ ];
918
+ const selectedAgents = await checklistMenu(allAgents, 'Which coding agents do you use?');
919
+ const useClaudeCode = selectedAgents.includes('claude');
920
+ const usePi = selectedAgents.includes('pi');
921
+ const useCursor = selectedAgents.includes('cursor');
922
+ const useCopilot = selectedAgents.includes('copilot');
923
+ const useOpenCode = selectedAgents.includes('opencode');
924
+ const useCodex = selectedAgents.includes('codex');
925
+ const anySelected = selectedAgents.length > 0;
926
+ const needsLatCommand = useClaudeCode ||
927
+ usePi ||
928
+ useCursor ||
929
+ useCopilot ||
930
+ useOpenCode ||
931
+ useCodex;
932
+ // Step 4: How should agents run lat?
933
+ let commandStyle = 'local';
934
+ if (anySelected && needsLatCommand && interactive) {
935
+ console.log('');
936
+ const localBin = resolveLatBin();
937
+ const styleOptions = [
938
+ { label: 'lat', value: 'global' },
939
+ { label: localBin, value: 'local' },
940
+ { label: 'npx lat.md@latest', value: 'npx' },
941
+ ];
942
+ const styleChoice = await selectMenu(styleOptions, 'How should agents run lat?', 0);
943
+ if (!styleChoice) {
944
+ console.log('Aborted.');
945
+ return;
946
+ }
947
+ commandStyle = styleChoice;
948
+ }
949
+ // Now that selectMenu is done, it's safe to create the readline interface.
950
+ // selectMenu has restored stdin to its original state (paused, non-raw).
951
+ if (interactive) {
952
+ rl = createInterface({
953
+ input: process.stdin,
954
+ output: process.stdout,
955
+ });
956
+ }
957
+ if (!anySelected) {
958
+ // Embedding setup is complete even when the user does not configure an
959
+ // agent. Stamp the version so future non-interactive runs do not reapply
960
+ // fresh/outdated defaults and overwrite the chosen backend.
961
+ writeInitMeta(latDir, {});
962
+ console.log('');
963
+ console.log(styleText('dim', 'No agents selected. You can re-run') +
964
+ ' lat init ' +
965
+ styleText('dim', 'later.'));
966
+ return;
967
+ }
968
+ console.log('');
969
+ const template = readAgentsTemplate();
970
+ const fileHashes = {};
971
+ // Step 5: AGENTS.md (shared by non-Claude agents)
972
+ const needsAgentsMd = usePi || useCursor || useCopilot || useOpenCode || useCodex;
973
+ if (needsAgentsMd) {
974
+ await setupAgentsMd(root, latDir, template, fileHashes, ask);
975
+ }
976
+ // Step 6: Per-agent setup
977
+ if (useClaudeCode) {
978
+ console.log('');
979
+ console.log(styleText('bold', 'Setting up Claude Code...'));
980
+ await setupClaudeCode(root, latDir, template, fileHashes, ask, commandStyle);
981
+ }
982
+ if (usePi) {
983
+ console.log('');
984
+ console.log(styleText('bold', 'Setting up Pi...'));
985
+ await setupPi(root, latDir, fileHashes, ask, commandStyle);
986
+ }
987
+ if (useCursor) {
988
+ console.log('');
989
+ console.log(styleText('bold', 'Setting up Cursor...'));
990
+ await setupCursor(root, latDir, fileHashes, ask, commandStyle);
991
+ }
992
+ if (useCopilot) {
993
+ console.log('');
994
+ console.log(styleText('bold', 'Setting up VS Code Copilot...'));
995
+ await setupCopilot(root, latDir, fileHashes, ask, commandStyle);
996
+ }
997
+ if (useOpenCode) {
998
+ console.log('');
999
+ console.log(styleText('bold', 'Setting up OpenCode...'));
1000
+ await setupOpenCode(root, latDir, fileHashes, ask, commandStyle);
1001
+ }
1002
+ if (useCodex) {
1003
+ console.log('');
1004
+ console.log(styleText('bold', 'Setting up Codex...'));
1005
+ await setupCodex(root, latDir, fileHashes, ask, commandStyle);
1006
+ }
1007
+ // Record init version and file hashes so `lat check` can detect stale setups
1008
+ writeInitMeta(latDir, fileHashes);
1009
+ console.log('');
1010
+ console.log(styleText('green', 'Done!') +
1011
+ ' Run ' +
1012
+ styleText('cyan', 'lat check') +
1013
+ ' to validate your setup.');
1014
+ // Suggest ripgrep if not available
1015
+ const { hasRipgrep } = await import('../code-refs.js');
1016
+ if (!(await hasRipgrep())) {
1017
+ console.log('');
1018
+ console.log(styleText('yellow', 'Tip:') +
1019
+ ' Install ' +
1020
+ styleText('cyan', 'ripgrep') +
1021
+ ' (rg) for faster code scanning.' +
1022
+ ' See ' +
1023
+ styleText('underline', 'https://github.com/BurntSushi/ripgrep#installation'));
1024
+ }
1025
+ // Post-onboarding: suggest having the agent document the codebase
1026
+ printNextSteps(selectedAgents);
1027
+ }
1028
+ finally {
1029
+ rl?.close();
1030
+ }
1031
+ }