lat.md 0.8.1 → 0.9.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/gen.d.ts +1 -0
- package/dist/src/cli/gen.js +7 -1
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +137 -41
- package/dist/src/cli/select-menu.js +1 -1
- package/dist/src/version.d.ts +7 -0
- package/dist/src/version.js +39 -0
- package/package.json +1 -1
- package/templates/logo.txt +7 -0
- package/templates/skill/SKILL.md +169 -0
package/dist/src/cli/gen.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function readAgentsTemplate(): string;
|
|
2
2
|
export declare function readCursorRulesTemplate(): string;
|
|
3
3
|
export declare function readPiExtensionTemplate(): string;
|
|
4
|
+
export declare function readSkillTemplate(): string;
|
|
4
5
|
export declare function genCmd(target: string): Promise<void>;
|
package/dist/src/cli/gen.js
CHANGED
|
@@ -10,6 +10,9 @@ export function readCursorRulesTemplate() {
|
|
|
10
10
|
export function readPiExtensionTemplate() {
|
|
11
11
|
return readFileSync(join(findTemplatesDir(), 'pi-extension.ts'), 'utf-8');
|
|
12
12
|
}
|
|
13
|
+
export function readSkillTemplate() {
|
|
14
|
+
return readFileSync(join(findTemplatesDir(), 'skill', 'SKILL.md'), 'utf-8');
|
|
15
|
+
}
|
|
13
16
|
export async function genCmd(target) {
|
|
14
17
|
const normalized = target.toLowerCase();
|
|
15
18
|
switch (normalized) {
|
|
@@ -23,8 +26,11 @@ export async function genCmd(target) {
|
|
|
23
26
|
case 'pi-extension.ts':
|
|
24
27
|
process.stdout.write(readPiExtensionTemplate());
|
|
25
28
|
break;
|
|
29
|
+
case 'skill.md':
|
|
30
|
+
process.stdout.write(readSkillTemplate());
|
|
31
|
+
break;
|
|
26
32
|
default:
|
|
27
|
-
console.error(`Unknown target: ${target}. Supported: agents.md, claude.md, cursor-rules.md, pi-extension.ts`);
|
|
33
|
+
console.error(`Unknown target: ${target}. Supported: agents.md, claude.md, cursor-rules.md, pi-extension.ts, skill.md`);
|
|
28
34
|
process.exit(1);
|
|
29
35
|
}
|
|
30
36
|
}
|
package/dist/src/cli/init.d.ts
CHANGED
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
|
-
import { readAgentsTemplate, readCursorRulesTemplate, readPiExtensionTemplate, } from './gen.js';
|
|
7
|
-
import { getConfigPath, readConfig, writeConfig, } from '../config.js';
|
|
7
|
+
import { readAgentsTemplate, readCursorRulesTemplate, readPiExtensionTemplate, readSkillTemplate, } from './gen.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
|
}
|
|
@@ -277,13 +314,22 @@ async function writeTemplateFile(root, latDir, relPath, template, genTarget, lab
|
|
|
277
314
|
' to see the latest template.');
|
|
278
315
|
return null;
|
|
279
316
|
}
|
|
317
|
+
// ── Shared skill setup ───────────────────────────────────────────────
|
|
318
|
+
async function writeAgentsSkill(root, latDir, hashes, ask) {
|
|
319
|
+
console.log('');
|
|
320
|
+
console.log(chalk.dim(' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
|
|
321
|
+
const skillTemplate = readSkillTemplate();
|
|
322
|
+
const skillHash = await writeTemplateFile(root, latDir, '.agents/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.agents/skills/lat-md/SKILL.md)', ' ', ask);
|
|
323
|
+
if (skillHash)
|
|
324
|
+
hashes['.agents/skills/lat-md/SKILL.md'] = skillHash;
|
|
325
|
+
}
|
|
280
326
|
// ── Per-agent setup ──────────────────────────────────────────────────
|
|
281
327
|
async function setupAgentsMd(root, latDir, template, hashes, ask) {
|
|
282
328
|
const hash = await writeTemplateFile(root, latDir, 'AGENTS.md', template, 'agents.md', 'AGENTS.md', '', ask);
|
|
283
329
|
if (hash)
|
|
284
330
|
hashes['AGENTS.md'] = hash;
|
|
285
331
|
}
|
|
286
|
-
async function setupClaudeCode(root, latDir, template, hashes, ask) {
|
|
332
|
+
async function setupClaudeCode(root, latDir, template, hashes, ask, style) {
|
|
287
333
|
// CLAUDE.md — written directly (not a symlink)
|
|
288
334
|
const hash = await writeTemplateFile(root, latDir, 'CLAUDE.md', template, 'claude.md', 'CLAUDE.md', ' ', ask);
|
|
289
335
|
if (hash)
|
|
@@ -295,8 +341,15 @@ async function setupClaudeCode(root, latDir, template, hashes, ask) {
|
|
|
295
341
|
const claudeDir = join(root, '.claude');
|
|
296
342
|
const settingsPath = join(claudeDir, 'settings.json');
|
|
297
343
|
mkdirSync(claudeDir, { recursive: true });
|
|
298
|
-
syncLatHooks(settingsPath);
|
|
344
|
+
syncLatHooks(settingsPath, style);
|
|
299
345
|
console.log(chalk.green(' Hooks') + ' synced (UserPromptSubmit + Stop)');
|
|
346
|
+
// .claude/skills/lat-md/SKILL.md — skill for authoring lat.md files
|
|
347
|
+
console.log('');
|
|
348
|
+
console.log(chalk.dim(' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
|
|
349
|
+
const skillTemplate = readSkillTemplate();
|
|
350
|
+
const skillHash = await writeTemplateFile(root, latDir, '.claude/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.claude/skills/lat-md/SKILL.md)', ' ', ask);
|
|
351
|
+
if (skillHash)
|
|
352
|
+
hashes['.claude/skills/lat-md/SKILL.md'] = skillHash;
|
|
300
353
|
// Ensure .claude is gitignored (settings contain local absolute paths)
|
|
301
354
|
ensureGitignored(root, '.claude');
|
|
302
355
|
// MCP server → .mcp.json at project root
|
|
@@ -308,13 +361,13 @@ async function setupClaudeCode(root, latDir, template, hashes, ask) {
|
|
|
308
361
|
console.log(chalk.green(' MCP server') + ' already configured');
|
|
309
362
|
}
|
|
310
363
|
else {
|
|
311
|
-
addMcpServer(mcpPath, 'mcpServers');
|
|
364
|
+
addMcpServer(mcpPath, 'mcpServers', style);
|
|
312
365
|
console.log(chalk.green(' MCP server') + ' registered in .mcp.json');
|
|
313
366
|
}
|
|
314
367
|
// Ensure .mcp.json is gitignored (it contains local absolute paths)
|
|
315
368
|
ensureGitignored(root, '.mcp.json');
|
|
316
369
|
}
|
|
317
|
-
async function setupCursor(root, latDir, hashes, ask) {
|
|
370
|
+
async function setupCursor(root, latDir, hashes, ask, style) {
|
|
318
371
|
// .cursor/rules/lat.md
|
|
319
372
|
const hash = await writeTemplateFile(root, latDir, '.cursor/rules/lat.md', readCursorRulesTemplate(), 'cursor-rules.md', 'Rules (.cursor/rules/lat.md)', ' ', ask);
|
|
320
373
|
if (hash)
|
|
@@ -328,16 +381,18 @@ async function setupCursor(root, latDir, hashes, ask) {
|
|
|
328
381
|
console.log(chalk.green(' MCP server') + ' already configured');
|
|
329
382
|
}
|
|
330
383
|
else {
|
|
331
|
-
addMcpServer(mcpPath, 'mcpServers');
|
|
384
|
+
addMcpServer(mcpPath, 'mcpServers', style);
|
|
332
385
|
console.log(chalk.green(' MCP server') + ' registered in .cursor/mcp.json');
|
|
333
386
|
}
|
|
334
387
|
// Ensure .cursor/mcp.json is gitignored (it contains local absolute paths)
|
|
335
388
|
ensureGitignored(root, '.cursor/mcp.json');
|
|
389
|
+
// .agents/skills/lat-md/SKILL.md — skill for authoring lat.md files
|
|
390
|
+
await writeAgentsSkill(root, latDir, hashes, ask);
|
|
336
391
|
console.log('');
|
|
337
392
|
console.log(chalk.yellow(' Note:') +
|
|
338
393
|
' Enable MCP in Cursor: Settings → Features → MCP → check "Enable MCP"');
|
|
339
394
|
}
|
|
340
|
-
async function setupCopilot(root, latDir, hashes, ask) {
|
|
395
|
+
async function setupCopilot(root, latDir, hashes, ask, style) {
|
|
341
396
|
// .github/copilot-instructions.md
|
|
342
397
|
const hash = await writeTemplateFile(root, latDir, '.github/copilot-instructions.md', readAgentsTemplate(), 'agents.md', 'Instructions (.github/copilot-instructions.md)', ' ', ask);
|
|
343
398
|
if (hash)
|
|
@@ -351,41 +406,40 @@ async function setupCopilot(root, latDir, hashes, ask) {
|
|
|
351
406
|
console.log(chalk.green(' MCP server') + ' already configured');
|
|
352
407
|
}
|
|
353
408
|
else {
|
|
354
|
-
addMcpServer(mcpPath, 'servers');
|
|
409
|
+
addMcpServer(mcpPath, 'servers', style);
|
|
355
410
|
console.log(chalk.green(' MCP server') + ' registered in .vscode/mcp.json');
|
|
356
411
|
}
|
|
412
|
+
// .agents/skills/lat-md/SKILL.md — skill for authoring lat.md files
|
|
413
|
+
await writeAgentsSkill(root, latDir, hashes, ask);
|
|
357
414
|
}
|
|
358
|
-
async function setupPi(root, latDir, hashes, ask) {
|
|
415
|
+
async function setupPi(root, latDir, hashes, ask, style) {
|
|
359
416
|
// AGENTS.md — Pi reads this natively
|
|
360
417
|
// (already created in the shared step if any non-Claude agent is selected)
|
|
361
418
|
// .pi/extensions/lat.ts — extension that registers tools + lifecycle hooks
|
|
362
419
|
console.log('');
|
|
363
420
|
console.log(chalk.dim(' The Pi extension registers lat tools and hooks into the agent lifecycle'));
|
|
364
421
|
console.log(chalk.dim(' to inject search context and validate lat.md/ before finishing.'));
|
|
365
|
-
const template = readPiExtensionTemplate().replace('__LAT_BIN__',
|
|
422
|
+
const template = readPiExtensionTemplate().replace('__LAT_BIN__', latBinString(style));
|
|
366
423
|
const hash = await writeTemplateFile(root, latDir, '.pi/extensions/lat.ts', template, 'pi-extension.ts', 'Extension (.pi/extensions/lat.ts)', ' ', ask);
|
|
367
424
|
if (hash)
|
|
368
425
|
hashes['.pi/extensions/lat.ts'] = hash;
|
|
426
|
+
// .pi/skills/lat-md/SKILL.md — skill for authoring lat.md files
|
|
427
|
+
console.log('');
|
|
428
|
+
console.log(chalk.dim(' The lat-md skill teaches the agent how to write and maintain lat.md/ files.'));
|
|
429
|
+
const skillTemplate = readSkillTemplate();
|
|
430
|
+
const skillHash = await writeTemplateFile(root, latDir, '.pi/skills/lat-md/SKILL.md', skillTemplate, 'skill.md', 'Skill (.pi/skills/lat-md/SKILL.md)', ' ', ask);
|
|
431
|
+
if (skillHash)
|
|
432
|
+
hashes['.pi/skills/lat-md/SKILL.md'] = skillHash;
|
|
369
433
|
// Ensure .pi is gitignored (extension contains local absolute paths)
|
|
370
434
|
ensureGitignored(root, '.pi');
|
|
371
435
|
}
|
|
372
436
|
// ── LLM key setup ───────────────────────────────────────────────────
|
|
373
437
|
async function setupLlmKey(rl) {
|
|
374
|
-
//
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
438
|
+
// Use the centralized key resolution (env var → file → helper → config)
|
|
439
|
+
const existingKey = getLlmKey();
|
|
440
|
+
if (existingKey) {
|
|
377
441
|
console.log('');
|
|
378
|
-
console.log(chalk.green('Semantic search') + ' —
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
// Check existing config
|
|
382
|
-
const config = readConfig();
|
|
383
|
-
const configPath = getConfigPath();
|
|
384
|
-
if (config.llm_key) {
|
|
385
|
-
console.log('');
|
|
386
|
-
console.log(chalk.green('Semantic search') +
|
|
387
|
-
' — LLM key configured in ' +
|
|
388
|
-
chalk.dim(configPath));
|
|
442
|
+
console.log(chalk.green('Semantic search') + ' — LLM key found. Ready.');
|
|
389
443
|
return;
|
|
390
444
|
}
|
|
391
445
|
// No key found — explain what semantic search is and prompt
|
|
@@ -445,12 +499,35 @@ async function setupLlmKey(rl) {
|
|
|
445
499
|
console.log(' Saving anyway — you can update it later.');
|
|
446
500
|
}
|
|
447
501
|
// Save to config
|
|
448
|
-
const updatedConfig = { ...
|
|
502
|
+
const updatedConfig = { ...readConfig(), llm_key: key };
|
|
449
503
|
writeConfig(updatedConfig);
|
|
450
|
-
console.log(chalk.green(' Key saved') + ' to ' + chalk.dim(
|
|
504
|
+
console.log(chalk.green(' Key saved') + ' to ' + chalk.dim(getConfigPath()));
|
|
451
505
|
}
|
|
452
506
|
// ── Main init flow ───────────────────────────────────────────────────
|
|
507
|
+
export function readLogo() {
|
|
508
|
+
return readFileSync(join(findTemplatesDir(), 'logo.txt'), 'utf-8');
|
|
509
|
+
}
|
|
453
510
|
export async function initCmd(targetDir) {
|
|
511
|
+
console.log(chalk.cyan(readLogo()));
|
|
512
|
+
// Upfront version check — let the user upgrade before proceeding
|
|
513
|
+
process.stdout.write(chalk.dim('Checking latest version...'));
|
|
514
|
+
const latest = await fetchLatestVersion();
|
|
515
|
+
const local = getLocalVersion();
|
|
516
|
+
if (latest && latest !== local) {
|
|
517
|
+
console.log(' ' +
|
|
518
|
+
chalk.yellow('update available:') +
|
|
519
|
+
' ' +
|
|
520
|
+
local +
|
|
521
|
+
' → ' +
|
|
522
|
+
chalk.green(latest) +
|
|
523
|
+
' — run ' +
|
|
524
|
+
chalk.cyan('npm install -g lat.md') +
|
|
525
|
+
' to update.');
|
|
526
|
+
console.log('');
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
console.log(' ' + chalk.green(`latest version is used (${local})`));
|
|
530
|
+
}
|
|
454
531
|
const root = resolve(targetDir ?? process.cwd());
|
|
455
532
|
const latDir = join(root, 'lat.md');
|
|
456
533
|
const interactive = process.stdin.isTTY ?? false;
|
|
@@ -508,7 +585,7 @@ export async function initCmd(targetDir) {
|
|
|
508
585
|
{
|
|
509
586
|
label: selectedAgents.length === 0
|
|
510
587
|
? "I don't use any of these"
|
|
511
|
-
: 'This is it:
|
|
588
|
+
: 'This is it: continue',
|
|
512
589
|
value: '__done__',
|
|
513
590
|
accent: true,
|
|
514
591
|
},
|
|
@@ -528,6 +605,24 @@ export async function initCmd(targetDir) {
|
|
|
528
605
|
const useCopilot = selectedAgents.includes('copilot');
|
|
529
606
|
const useCodex = selectedAgents.includes('codex');
|
|
530
607
|
const anySelected = selectedAgents.length > 0;
|
|
608
|
+
const needsLatCommand = useClaudeCode || usePi || useCursor || useCopilot;
|
|
609
|
+
// Step 2b: How should agents run lat?
|
|
610
|
+
let commandStyle = 'local';
|
|
611
|
+
if (anySelected && needsLatCommand && interactive) {
|
|
612
|
+
console.log('');
|
|
613
|
+
const localBin = resolveLatBin();
|
|
614
|
+
const styleOptions = [
|
|
615
|
+
{ label: 'lat', value: 'global' },
|
|
616
|
+
{ label: localBin, value: 'local' },
|
|
617
|
+
{ label: 'npx lat.md@latest', value: 'npx' },
|
|
618
|
+
];
|
|
619
|
+
const styleChoice = await selectMenu(styleOptions, 'How should agents run lat?', 0);
|
|
620
|
+
if (!styleChoice) {
|
|
621
|
+
console.log('Aborted.');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
commandStyle = styleChoice;
|
|
625
|
+
}
|
|
531
626
|
// Now that selectMenu is done, it's safe to create the readline interface.
|
|
532
627
|
// selectMenu has restored stdin to its original state (paused, non-raw).
|
|
533
628
|
if (interactive) {
|
|
@@ -555,27 +650,28 @@ export async function initCmd(targetDir) {
|
|
|
555
650
|
if (useClaudeCode) {
|
|
556
651
|
console.log('');
|
|
557
652
|
console.log(chalk.bold('Setting up Claude Code...'));
|
|
558
|
-
await setupClaudeCode(root, latDir, template, fileHashes, ask);
|
|
653
|
+
await setupClaudeCode(root, latDir, template, fileHashes, ask, commandStyle);
|
|
559
654
|
}
|
|
560
655
|
if (usePi) {
|
|
561
656
|
console.log('');
|
|
562
657
|
console.log(chalk.bold('Setting up Pi...'));
|
|
563
|
-
await setupPi(root, latDir, fileHashes, ask);
|
|
658
|
+
await setupPi(root, latDir, fileHashes, ask, commandStyle);
|
|
564
659
|
}
|
|
565
660
|
if (useCursor) {
|
|
566
661
|
console.log('');
|
|
567
662
|
console.log(chalk.bold('Setting up Cursor...'));
|
|
568
|
-
await setupCursor(root, latDir, fileHashes, ask);
|
|
663
|
+
await setupCursor(root, latDir, fileHashes, ask, commandStyle);
|
|
569
664
|
}
|
|
570
665
|
if (useCopilot) {
|
|
571
666
|
console.log('');
|
|
572
667
|
console.log(chalk.bold('Setting up VS Code Copilot...'));
|
|
573
|
-
await setupCopilot(root, latDir, fileHashes, ask);
|
|
668
|
+
await setupCopilot(root, latDir, fileHashes, ask, commandStyle);
|
|
574
669
|
}
|
|
575
670
|
if (useCodex) {
|
|
576
671
|
console.log('');
|
|
577
|
-
console.log(chalk.bold('Codex / OpenCode')
|
|
578
|
-
|
|
672
|
+
console.log(chalk.bold('Setting up Codex / OpenCode...'));
|
|
673
|
+
console.log(chalk.dim(' Uses AGENTS.md (already created).'));
|
|
674
|
+
await writeAgentsSkill(root, latDir, fileHashes, ask);
|
|
579
675
|
}
|
|
580
676
|
// Step 5: LLM key setup
|
|
581
677
|
await setupLlmKey(rl);
|
|
@@ -32,7 +32,7 @@ export async function selectMenu(options, prompt, defaultIndex) {
|
|
|
32
32
|
const pointer = selected ? '❯' : ' ';
|
|
33
33
|
if (selected) {
|
|
34
34
|
if (opt.accent) {
|
|
35
|
-
lines.push(` ${pointer} ${chalk.
|
|
35
|
+
lines.push(` ${pointer} ${chalk.bgGreen.black.bold(` ${opt.label} `)}`);
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
38
|
lines.push(` ${pointer} ${chalk.bgCyan.black.bold(` ${opt.label} `)}`);
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lat-md
|
|
3
|
+
description: >-
|
|
4
|
+
Writing and maintaining lat.md documentation files — structured markdown that
|
|
5
|
+
describes a project's architecture, design decisions, and test specs. Use when
|
|
6
|
+
creating, editing, or reviewing files in the lat.md/ directory.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# lat.md Authoring Guide
|
|
10
|
+
|
|
11
|
+
This skill covers the syntax, structure rules, and conventions for writing `lat.md/` files. Load it whenever you need to create or edit sections in the `lat.md/` directory.
|
|
12
|
+
|
|
13
|
+
## What belongs in lat.md
|
|
14
|
+
|
|
15
|
+
`lat.md/` files describe **what** the project does and **why** — domain concepts, key design decisions, business logic, and test specifications. They do NOT duplicate source code. Think of each section as an anchor that source code references back to.
|
|
16
|
+
|
|
17
|
+
Good candidates for sections:
|
|
18
|
+
- Architecture decisions and their rationale
|
|
19
|
+
- Domain concepts and business rules
|
|
20
|
+
- API contracts and protocols
|
|
21
|
+
- Test specifications (what is tested and why)
|
|
22
|
+
- Non-obvious constraints or invariants
|
|
23
|
+
|
|
24
|
+
Bad candidates:
|
|
25
|
+
- Step-by-step code walkthroughs (the code itself is the walkthrough)
|
|
26
|
+
- Auto-generated API docs (use tools for that)
|
|
27
|
+
- Temporary notes or TODOs
|
|
28
|
+
|
|
29
|
+
## Section structure
|
|
30
|
+
|
|
31
|
+
Every section **must** have a leading paragraph — at least one sentence immediately after the heading, before any child headings or other block content.
|
|
32
|
+
|
|
33
|
+
The first paragraph must be ≤250 characters (excluding `[[wiki link]]` content). This paragraph is the section's identity — it appears in search results, command output, and RAG context.
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
# Good Section
|
|
37
|
+
|
|
38
|
+
Brief overview of what this section documents and why it matters.
|
|
39
|
+
|
|
40
|
+
More detail can go in subsequent paragraphs, code blocks, or lists.
|
|
41
|
+
|
|
42
|
+
## Child heading
|
|
43
|
+
|
|
44
|
+
Details about this child topic.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```markdown
|
|
48
|
+
# Bad Section
|
|
49
|
+
|
|
50
|
+
## Child heading
|
|
51
|
+
|
|
52
|
+
This is invalid — "Bad Section" has no leading paragraph.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`lat check` enforces this rule.
|
|
56
|
+
|
|
57
|
+
## Section IDs
|
|
58
|
+
|
|
59
|
+
Sections are addressed by file path and heading chain:
|
|
60
|
+
|
|
61
|
+
- **Full form**: `lat.md/path/to/file#Heading#SubHeading`
|
|
62
|
+
- **Short form**: `file#Heading#SubHeading` (when the file stem is unique)
|
|
63
|
+
|
|
64
|
+
Examples: `lat.md/tests/search#RAG Replay Tests`, `cli#init`, `parser#Wiki Links`.
|
|
65
|
+
|
|
66
|
+
## Wiki links
|
|
67
|
+
|
|
68
|
+
Cross-reference other sections or source code with `[[target]]` or `[[target|alias]]`.
|
|
69
|
+
|
|
70
|
+
### Section links
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
See [[cli#init]] for setup details.
|
|
74
|
+
The parser validates [[parser#Wiki Links|wiki link syntax]].
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Source code links
|
|
78
|
+
|
|
79
|
+
Reference functions, classes, constants, and methods in source files:
|
|
80
|
+
|
|
81
|
+
```markdown
|
|
82
|
+
[[src/config.ts#getConfigDir]] — function
|
|
83
|
+
[[src/server.ts#App#listen]] — class method
|
|
84
|
+
[[lib/utils.py#parse_args]] — Python function
|
|
85
|
+
[[src/lib.rs#Greeter#greet]] — Rust impl method
|
|
86
|
+
[[src/app.go#Greeter#Greet]] — Go method
|
|
87
|
+
[[src/app.h#Greeter]] — C struct
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`lat check` validates that all targets exist.
|
|
91
|
+
|
|
92
|
+
## Code refs
|
|
93
|
+
|
|
94
|
+
Tie source code back to `lat.md/` sections with `@lat:` comments:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// @lat: [[cli#init]]
|
|
98
|
+
export function init() { ... }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# @lat: [[cli#init]]
|
|
103
|
+
def init():
|
|
104
|
+
...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Supported comment styles: `//` (JS/TS/Rust/Go/C) and `#` (Python).
|
|
108
|
+
|
|
109
|
+
Place one `@lat:` comment per section, at the relevant code — not at the top of the file.
|
|
110
|
+
|
|
111
|
+
## Test specs
|
|
112
|
+
|
|
113
|
+
Describe tests as sections in `lat.md/` files. Add frontmatter to require that every leaf section has a matching `@lat:` comment in test code:
|
|
114
|
+
|
|
115
|
+
```markdown
|
|
116
|
+
---
|
|
117
|
+
lat:
|
|
118
|
+
require-code-mention: true
|
|
119
|
+
---
|
|
120
|
+
# Tests
|
|
121
|
+
|
|
122
|
+
Authentication test specifications.
|
|
123
|
+
|
|
124
|
+
## User login
|
|
125
|
+
|
|
126
|
+
Verify credential validation and error handling.
|
|
127
|
+
|
|
128
|
+
### Rejects expired tokens
|
|
129
|
+
|
|
130
|
+
Tokens past their expiry timestamp are rejected with 401, even if otherwise valid.
|
|
131
|
+
|
|
132
|
+
### Handles missing password
|
|
133
|
+
|
|
134
|
+
Login request without a password field returns 400 with a descriptive error.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Each test references its spec:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# @lat: [[tests#User login#Rejects expired tokens]]
|
|
141
|
+
def test_rejects_expired_tokens():
|
|
142
|
+
...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Rules:
|
|
146
|
+
- Every leaf section under `require-code-mention: true` must be referenced by exactly one `@lat:` comment
|
|
147
|
+
- Every section MUST have a description — at least one sentence explaining what the test verifies and why
|
|
148
|
+
- `lat check` flags unreferenced specs and dangling code refs
|
|
149
|
+
|
|
150
|
+
## Frontmatter
|
|
151
|
+
|
|
152
|
+
Optional YAML frontmatter at the top of `lat.md/` files:
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
---
|
|
156
|
+
lat:
|
|
157
|
+
require-code-mention: true
|
|
158
|
+
---
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Currently the only supported field is `require-code-mention` for test spec enforcement.
|
|
162
|
+
|
|
163
|
+
## Validation
|
|
164
|
+
|
|
165
|
+
Always run `lat check` after editing `lat.md/` files. It validates:
|
|
166
|
+
- All wiki links point to existing sections or source code symbols
|
|
167
|
+
- All `@lat:` code refs point to existing sections
|
|
168
|
+
- Every section has a leading paragraph (≤250 chars)
|
|
169
|
+
- All `require-code-mention` leaf sections are referenced in code
|