lat.md 0.8.0 → 0.8.2
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/init.js +129 -43
- package/dist/src/version.d.ts +7 -0
- package/dist/src/version.js +39 -0
- package/package.json +1 -1
package/dist/src/cli/init.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { existsSync, cpSync, mkdirSync, writeFileSync, readFileSync, } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
3
4
|
import { createInterface } from 'node:readline/promises';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { findTemplatesDir } from './templates.js';
|
|
6
7
|
import { readAgentsTemplate, readCursorRulesTemplate, readPiExtensionTemplate, } from './gen.js';
|
|
7
|
-
import { getConfigPath, readConfig, writeConfig, } from '../config.js';
|
|
8
|
+
import { getLlmKey, getConfigPath, readConfig, writeConfig, } from '../config.js';
|
|
8
9
|
import { writeInitMeta, readFileHash, contentHash } from '../init-version.js';
|
|
10
|
+
import { getLocalVersion, fetchLatestVersion } from '../version.js';
|
|
9
11
|
import { selectMenu } from './select-menu.js';
|
|
10
12
|
async function confirm(rl, message) {
|
|
11
13
|
while (true) {
|
|
@@ -86,10 +88,26 @@ function resolveLatBin() {
|
|
|
86
88
|
// .ts file but no special loader flags — best-effort, just return the path
|
|
87
89
|
return script;
|
|
88
90
|
}
|
|
91
|
+
/** Return the lat binary string for the given command style. */
|
|
92
|
+
function latBinString(style) {
|
|
93
|
+
if (style === 'global')
|
|
94
|
+
return 'lat';
|
|
95
|
+
if (style === 'npx')
|
|
96
|
+
return 'npx lat.md@latest';
|
|
97
|
+
return resolveLatBin();
|
|
98
|
+
}
|
|
99
|
+
/** Return the MCP server command descriptor for the given command style. */
|
|
100
|
+
function styledMcpCommand(style) {
|
|
101
|
+
if (style === 'global')
|
|
102
|
+
return { command: 'lat', args: ['mcp'] };
|
|
103
|
+
if (style === 'npx')
|
|
104
|
+
return { command: 'npx', args: ['lat.md@latest', 'mcp'] };
|
|
105
|
+
return mcpCommand();
|
|
106
|
+
}
|
|
89
107
|
// ── Claude Code helpers ──────────────────────────────────────────────
|
|
90
|
-
/** Derive the hook command prefix
|
|
91
|
-
function latHookCommand(event) {
|
|
92
|
-
return `${
|
|
108
|
+
/** Derive the hook command prefix for the given command style. */
|
|
109
|
+
function latHookCommand(style, event) {
|
|
110
|
+
return `${latBinString(style)} hook claude ${event}`;
|
|
93
111
|
}
|
|
94
112
|
/** True if any command in this entry looks like it was installed by lat. */
|
|
95
113
|
function isLatHookEntry(entry) {
|
|
@@ -103,7 +121,7 @@ function isLatHookEntry(entry) {
|
|
|
103
121
|
* Remove all lat-owned hook entries from settings, then add fresh ones.
|
|
104
122
|
* Preserves any non-lat hooks the user may have configured.
|
|
105
123
|
*/
|
|
106
|
-
function syncLatHooks(settingsPath) {
|
|
124
|
+
function syncLatHooks(settingsPath, style) {
|
|
107
125
|
let settings = {};
|
|
108
126
|
if (existsSync(settingsPath)) {
|
|
109
127
|
const raw = readFileSync(settingsPath, 'utf-8');
|
|
@@ -136,7 +154,7 @@ function syncLatHooks(settingsPath) {
|
|
|
136
154
|
hooks[event] = [];
|
|
137
155
|
}
|
|
138
156
|
hooks[event].push({
|
|
139
|
-
hooks: [{ type: 'command', command: latHookCommand(event) }],
|
|
157
|
+
hooks: [{ type: 'command', command: latHookCommand(style, event) }],
|
|
140
158
|
});
|
|
141
159
|
}
|
|
142
160
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
@@ -154,6 +172,25 @@ function ensureGitignored(root, entry) {
|
|
|
154
172
|
return;
|
|
155
173
|
}
|
|
156
174
|
}
|
|
175
|
+
// Skip if the entry is already tracked in git — adding it to .gitignore
|
|
176
|
+
// would have no effect and confuse the user.
|
|
177
|
+
if (existsSync(gitDir)) {
|
|
178
|
+
try {
|
|
179
|
+
const result = execSync(`git ls-files "${entry}"`, {
|
|
180
|
+
cwd: root,
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
183
|
+
});
|
|
184
|
+
if (result.trim().length > 0) {
|
|
185
|
+
console.log(chalk.yellow(` ${entry}`) +
|
|
186
|
+
' is already checked in to git — skipping .gitignore');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
console.log(chalk.yellow(` Warning:`) + ' git ls-files failed — skipping check');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
157
194
|
if (existsSync(gitignorePath)) {
|
|
158
195
|
// Append to existing .gitignore
|
|
159
196
|
let content = readFileSync(gitignorePath, 'utf-8');
|
|
@@ -215,7 +252,7 @@ function hasMcpServer(configPath, key) {
|
|
|
215
252
|
return false;
|
|
216
253
|
}
|
|
217
254
|
}
|
|
218
|
-
function addMcpServer(configPath, key) {
|
|
255
|
+
function addMcpServer(configPath, key, style) {
|
|
219
256
|
let cfg = { [key]: {} };
|
|
220
257
|
if (existsSync(configPath)) {
|
|
221
258
|
const raw = readFileSync(configPath, 'utf-8');
|
|
@@ -228,7 +265,7 @@ function addMcpServer(configPath, key) {
|
|
|
228
265
|
throw new Error(`Cannot parse ${configPath}: ${e.message}`);
|
|
229
266
|
}
|
|
230
267
|
}
|
|
231
|
-
cfg[key].lat =
|
|
268
|
+
cfg[key].lat = styledMcpCommand(style);
|
|
232
269
|
mkdirSync(join(configPath, '..'), { recursive: true });
|
|
233
270
|
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
234
271
|
}
|
|
@@ -283,7 +320,7 @@ async function setupAgentsMd(root, latDir, template, hashes, ask) {
|
|
|
283
320
|
if (hash)
|
|
284
321
|
hashes['AGENTS.md'] = hash;
|
|
285
322
|
}
|
|
286
|
-
async function setupClaudeCode(root, latDir, template, hashes, ask) {
|
|
323
|
+
async function setupClaudeCode(root, latDir, template, hashes, ask, style) {
|
|
287
324
|
// CLAUDE.md — written directly (not a symlink)
|
|
288
325
|
const hash = await writeTemplateFile(root, latDir, 'CLAUDE.md', template, 'claude.md', 'CLAUDE.md', ' ', ask);
|
|
289
326
|
if (hash)
|
|
@@ -295,7 +332,7 @@ async function setupClaudeCode(root, latDir, template, hashes, ask) {
|
|
|
295
332
|
const claudeDir = join(root, '.claude');
|
|
296
333
|
const settingsPath = join(claudeDir, 'settings.json');
|
|
297
334
|
mkdirSync(claudeDir, { recursive: true });
|
|
298
|
-
syncLatHooks(settingsPath);
|
|
335
|
+
syncLatHooks(settingsPath, style);
|
|
299
336
|
console.log(chalk.green(' Hooks') + ' synced (UserPromptSubmit + Stop)');
|
|
300
337
|
// Ensure .claude is gitignored (settings contain local absolute paths)
|
|
301
338
|
ensureGitignored(root, '.claude');
|
|
@@ -308,13 +345,13 @@ async function setupClaudeCode(root, latDir, template, hashes, ask) {
|
|
|
308
345
|
console.log(chalk.green(' MCP server') + ' already configured');
|
|
309
346
|
}
|
|
310
347
|
else {
|
|
311
|
-
addMcpServer(mcpPath, 'mcpServers');
|
|
348
|
+
addMcpServer(mcpPath, 'mcpServers', style);
|
|
312
349
|
console.log(chalk.green(' MCP server') + ' registered in .mcp.json');
|
|
313
350
|
}
|
|
314
351
|
// Ensure .mcp.json is gitignored (it contains local absolute paths)
|
|
315
352
|
ensureGitignored(root, '.mcp.json');
|
|
316
353
|
}
|
|
317
|
-
async function setupCursor(root, latDir, hashes, ask) {
|
|
354
|
+
async function setupCursor(root, latDir, hashes, ask, style) {
|
|
318
355
|
// .cursor/rules/lat.md
|
|
319
356
|
const hash = await writeTemplateFile(root, latDir, '.cursor/rules/lat.md', readCursorRulesTemplate(), 'cursor-rules.md', 'Rules (.cursor/rules/lat.md)', ' ', ask);
|
|
320
357
|
if (hash)
|
|
@@ -328,7 +365,7 @@ async function setupCursor(root, latDir, hashes, ask) {
|
|
|
328
365
|
console.log(chalk.green(' MCP server') + ' already configured');
|
|
329
366
|
}
|
|
330
367
|
else {
|
|
331
|
-
addMcpServer(mcpPath, 'mcpServers');
|
|
368
|
+
addMcpServer(mcpPath, 'mcpServers', style);
|
|
332
369
|
console.log(chalk.green(' MCP server') + ' registered in .cursor/mcp.json');
|
|
333
370
|
}
|
|
334
371
|
// Ensure .cursor/mcp.json is gitignored (it contains local absolute paths)
|
|
@@ -337,7 +374,7 @@ async function setupCursor(root, latDir, hashes, ask) {
|
|
|
337
374
|
console.log(chalk.yellow(' Note:') +
|
|
338
375
|
' Enable MCP in Cursor: Settings → Features → MCP → check "Enable MCP"');
|
|
339
376
|
}
|
|
340
|
-
async function setupCopilot(root, latDir, hashes, ask) {
|
|
377
|
+
async function setupCopilot(root, latDir, hashes, ask, style) {
|
|
341
378
|
// .github/copilot-instructions.md
|
|
342
379
|
const hash = await writeTemplateFile(root, latDir, '.github/copilot-instructions.md', readAgentsTemplate(), 'agents.md', 'Instructions (.github/copilot-instructions.md)', ' ', ask);
|
|
343
380
|
if (hash)
|
|
@@ -351,18 +388,18 @@ async function setupCopilot(root, latDir, hashes, ask) {
|
|
|
351
388
|
console.log(chalk.green(' MCP server') + ' already configured');
|
|
352
389
|
}
|
|
353
390
|
else {
|
|
354
|
-
addMcpServer(mcpPath, 'servers');
|
|
391
|
+
addMcpServer(mcpPath, 'servers', style);
|
|
355
392
|
console.log(chalk.green(' MCP server') + ' registered in .vscode/mcp.json');
|
|
356
393
|
}
|
|
357
394
|
}
|
|
358
|
-
async function setupPi(root, latDir, hashes, ask) {
|
|
395
|
+
async function setupPi(root, latDir, hashes, ask, style) {
|
|
359
396
|
// AGENTS.md — Pi reads this natively
|
|
360
397
|
// (already created in the shared step if any non-Claude agent is selected)
|
|
361
398
|
// .pi/extensions/lat.ts — extension that registers tools + lifecycle hooks
|
|
362
399
|
console.log('');
|
|
363
400
|
console.log(chalk.dim(' The Pi extension registers lat tools and hooks into the agent lifecycle'));
|
|
364
401
|
console.log(chalk.dim(' to inject search context and validate lat.md/ before finishing.'));
|
|
365
|
-
const template = readPiExtensionTemplate().replace('__LAT_BIN__',
|
|
402
|
+
const template = readPiExtensionTemplate().replace('__LAT_BIN__', latBinString(style));
|
|
366
403
|
const hash = await writeTemplateFile(root, latDir, '.pi/extensions/lat.ts', template, 'pi-extension.ts', 'Extension (.pi/extensions/lat.ts)', ' ', ask);
|
|
367
404
|
if (hash)
|
|
368
405
|
hashes['.pi/extensions/lat.ts'] = hash;
|
|
@@ -371,21 +408,11 @@ async function setupPi(root, latDir, hashes, ask) {
|
|
|
371
408
|
}
|
|
372
409
|
// ── LLM key setup ───────────────────────────────────────────────────
|
|
373
410
|
async function setupLlmKey(rl) {
|
|
374
|
-
//
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
console.log('');
|
|
378
|
-
console.log(chalk.green('Semantic search') + ' — LAT_LLM_KEY is set. Ready.');
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
// Check existing config
|
|
382
|
-
const config = readConfig();
|
|
383
|
-
const configPath = getConfigPath();
|
|
384
|
-
if (config.llm_key) {
|
|
411
|
+
// Use the centralized key resolution (env var → file → helper → config)
|
|
412
|
+
const existingKey = getLlmKey();
|
|
413
|
+
if (existingKey) {
|
|
385
414
|
console.log('');
|
|
386
|
-
console.log(chalk.green('Semantic search') +
|
|
387
|
-
' — LLM key configured in ' +
|
|
388
|
-
chalk.dim(configPath));
|
|
415
|
+
console.log(chalk.green('Semantic search') + ' — LLM key found. Ready.');
|
|
389
416
|
return;
|
|
390
417
|
}
|
|
391
418
|
// No key found — explain what semantic search is and prompt
|
|
@@ -445,18 +472,39 @@ async function setupLlmKey(rl) {
|
|
|
445
472
|
console.log(' Saving anyway — you can update it later.');
|
|
446
473
|
}
|
|
447
474
|
// Save to config
|
|
448
|
-
const updatedConfig = { ...
|
|
475
|
+
const updatedConfig = { ...readConfig(), llm_key: key };
|
|
449
476
|
writeConfig(updatedConfig);
|
|
450
|
-
console.log(chalk.green(' Key saved') + ' to ' + chalk.dim(
|
|
477
|
+
console.log(chalk.green(' Key saved') + ' to ' + chalk.dim(getConfigPath()));
|
|
451
478
|
}
|
|
452
479
|
// ── Main init flow ───────────────────────────────────────────────────
|
|
453
480
|
export async function initCmd(targetDir) {
|
|
481
|
+
// Upfront version check — let the user upgrade before proceeding
|
|
482
|
+
process.stdout.write(chalk.dim('Checking latest version...'));
|
|
483
|
+
const latest = await fetchLatestVersion();
|
|
484
|
+
const local = getLocalVersion();
|
|
485
|
+
if (latest && latest !== local) {
|
|
486
|
+
console.log(' ' +
|
|
487
|
+
chalk.yellow('update available:') +
|
|
488
|
+
' ' +
|
|
489
|
+
local +
|
|
490
|
+
' → ' +
|
|
491
|
+
chalk.green(latest) +
|
|
492
|
+
' — run ' +
|
|
493
|
+
chalk.cyan('npm install -g lat.md') +
|
|
494
|
+
' to update.');
|
|
495
|
+
console.log('');
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
console.log(' ' + chalk.green(`latest version is used (${local})`));
|
|
499
|
+
}
|
|
454
500
|
const root = resolve(targetDir ?? process.cwd());
|
|
455
501
|
const latDir = join(root, 'lat.md');
|
|
456
502
|
const interactive = process.stdin.isTTY ?? false;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
503
|
+
// Readline is created AFTER the selectMenu loop below.
|
|
504
|
+
// selectMenu puts stdin into raw mode with its own 'data' listener;
|
|
505
|
+
// if readline is already attached it receives those raw keypresses,
|
|
506
|
+
// corrupting its internal state and causing rl.question() to hang/exit.
|
|
507
|
+
let rl = null;
|
|
460
508
|
const ask = async (message) => {
|
|
461
509
|
if (!rl)
|
|
462
510
|
return true;
|
|
@@ -468,9 +516,21 @@ export async function initCmd(targetDir) {
|
|
|
468
516
|
console.log(chalk.green('lat.md/') + ' already exists');
|
|
469
517
|
}
|
|
470
518
|
else {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
519
|
+
// No rl yet — selectMenu hasn't run, so use a one-off confirm
|
|
520
|
+
if (interactive) {
|
|
521
|
+
const tmpRl = createInterface({
|
|
522
|
+
input: process.stdin,
|
|
523
|
+
output: process.stdout,
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
if (!(await confirm(tmpRl, 'Create lat.md/ directory?'))) {
|
|
527
|
+
console.log('Aborted.');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
tmpRl.close();
|
|
533
|
+
}
|
|
474
534
|
}
|
|
475
535
|
const templateDir = join(findTemplatesDir(), 'init');
|
|
476
536
|
mkdirSync(latDir, { recursive: true });
|
|
@@ -514,6 +574,32 @@ export async function initCmd(targetDir) {
|
|
|
514
574
|
const useCopilot = selectedAgents.includes('copilot');
|
|
515
575
|
const useCodex = selectedAgents.includes('codex');
|
|
516
576
|
const anySelected = selectedAgents.length > 0;
|
|
577
|
+
const needsLatCommand = useClaudeCode || usePi || useCursor || useCopilot;
|
|
578
|
+
// Step 2b: How should agents run lat?
|
|
579
|
+
let commandStyle = 'local';
|
|
580
|
+
if (anySelected && needsLatCommand && interactive) {
|
|
581
|
+
console.log('');
|
|
582
|
+
const localBin = resolveLatBin();
|
|
583
|
+
const styleOptions = [
|
|
584
|
+
{ label: 'lat', value: 'global' },
|
|
585
|
+
{ label: localBin, value: 'local' },
|
|
586
|
+
{ label: 'npx lat.md@latest', value: 'npx' },
|
|
587
|
+
];
|
|
588
|
+
const styleChoice = await selectMenu(styleOptions, 'How should agents run lat?', 0);
|
|
589
|
+
if (!styleChoice) {
|
|
590
|
+
console.log('Aborted.');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
commandStyle = styleChoice;
|
|
594
|
+
}
|
|
595
|
+
// Now that selectMenu is done, it's safe to create the readline interface.
|
|
596
|
+
// selectMenu has restored stdin to its original state (paused, non-raw).
|
|
597
|
+
if (interactive) {
|
|
598
|
+
rl = createInterface({
|
|
599
|
+
input: process.stdin,
|
|
600
|
+
output: process.stdout,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
517
603
|
if (!anySelected) {
|
|
518
604
|
console.log('');
|
|
519
605
|
console.log(chalk.dim('No agents selected. You can re-run') +
|
|
@@ -533,22 +619,22 @@ export async function initCmd(targetDir) {
|
|
|
533
619
|
if (useClaudeCode) {
|
|
534
620
|
console.log('');
|
|
535
621
|
console.log(chalk.bold('Setting up Claude Code...'));
|
|
536
|
-
await setupClaudeCode(root, latDir, template, fileHashes, ask);
|
|
622
|
+
await setupClaudeCode(root, latDir, template, fileHashes, ask, commandStyle);
|
|
537
623
|
}
|
|
538
624
|
if (usePi) {
|
|
539
625
|
console.log('');
|
|
540
626
|
console.log(chalk.bold('Setting up Pi...'));
|
|
541
|
-
await setupPi(root, latDir, fileHashes, ask);
|
|
627
|
+
await setupPi(root, latDir, fileHashes, ask, commandStyle);
|
|
542
628
|
}
|
|
543
629
|
if (useCursor) {
|
|
544
630
|
console.log('');
|
|
545
631
|
console.log(chalk.bold('Setting up Cursor...'));
|
|
546
|
-
await setupCursor(root, latDir, fileHashes, ask);
|
|
632
|
+
await setupCursor(root, latDir, fileHashes, ask, commandStyle);
|
|
547
633
|
}
|
|
548
634
|
if (useCopilot) {
|
|
549
635
|
console.log('');
|
|
550
636
|
console.log(chalk.bold('Setting up VS Code Copilot...'));
|
|
551
|
-
await setupCopilot(root, latDir, fileHashes, ask);
|
|
637
|
+
await setupCopilot(root, latDir, fileHashes, ask, commandStyle);
|
|
552
638
|
}
|
|
553
639
|
if (useCodex) {
|
|
554
640
|
console.log('');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Walk up from this file to find the nearest package.json version. */
|
|
2
|
+
export declare function getLocalVersion(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Fetch the latest published version of `lat.md` from the npm registry.
|
|
5
|
+
* Returns null if the fetch fails or times out (3s).
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchLatestVersion(): Promise<string | null>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/** Walk up from this file to find the nearest package.json version. */
|
|
5
|
+
export function getLocalVersion() {
|
|
6
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
while (true) {
|
|
8
|
+
const candidate = join(dir, 'package.json');
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(candidate, 'utf-8')).version;
|
|
11
|
+
}
|
|
12
|
+
catch { }
|
|
13
|
+
const parent = dirname(dir);
|
|
14
|
+
if (parent === dir)
|
|
15
|
+
return '0.0.0';
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Fetch the latest published version of `lat.md` from the npm registry.
|
|
21
|
+
* Returns null if the fetch fails or times out (3s).
|
|
22
|
+
*/
|
|
23
|
+
export async function fetchLatestVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
27
|
+
const res = await fetch('https://registry.npmjs.org/lat.md/latest', {
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
});
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
return null;
|
|
33
|
+
const data = (await res.json());
|
|
34
|
+
return data.version ?? null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|