lat.md 0.4.0 → 0.4.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/README.md CHANGED
@@ -29,7 +29,7 @@ npm install -g lat.md
29
29
 
30
30
  Or use directly with `npx lat.md@latest <command>`.
31
31
 
32
- For semantic search (`lat search`), set the `LAT_LLM_KEY` environment variable with an OpenAI (`sk-...`) or Vercel AI Gateway (`vck_...`) API key.
32
+ After installing, run `lat init` in the repo you want to use lat in.
33
33
 
34
34
  ## How it works
35
35
 
@@ -58,6 +58,15 @@ npx lat.md search "how do we auth?" # semantic search via embeddings
58
58
  npx lat.md prompt "fix [[OAuth Flow]]" # expand [[refs]] in a prompt for agents
59
59
  ```
60
60
 
61
+ ## Configuration
62
+
63
+ Semantic search (`lat search`) requires an OpenAI (`sk-...`) or Vercel AI Gateway (`vck_...`) API key. The key is resolved in order:
64
+
65
+ 1. `LAT_LLM_KEY` env var — direct value
66
+ 2. `LAT_LLM_KEY_FILE` env var — path to a file containing the key
67
+ 3. `LAT_LLM_KEY_HELPER` env var — shell command that prints the key (10s timeout)
68
+ 4. Config file — saved by `lat init`. Run `lat config` to see its location.
69
+
61
70
  ## Development
62
71
 
63
72
  Requires Node.js 22+ and pnpm.
@@ -289,10 +289,17 @@ export async function checkAllCmd(ctx) {
289
289
  process.exit(1);
290
290
  console.log(ctx.chalk.green('All checks passed'));
291
291
  const { getLlmKey } = await import('../config.js');
292
- if (!getLlmKey()) {
292
+ let hasKey = false;
293
+ try {
294
+ hasKey = !!getLlmKey();
295
+ }
296
+ catch {
297
+ // key resolution failed (e.g. empty file) — treat as missing
298
+ }
299
+ if (!hasKey) {
293
300
  console.log(ctx.chalk.yellow('Warning:') +
294
301
  ' No LLM key found — semantic search (lat search) will not work.' +
295
- ' Set LAT_LLM_KEY env var or run ' +
302
+ ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
296
303
  ctx.chalk.cyan('lat init') +
297
304
  ' to configure.');
298
305
  }
@@ -3,7 +3,7 @@
3
3
  if (!process.argv.includes('--verbose')) {
4
4
  process.noDeprecation = true;
5
5
  }
6
- import { readFileSync } from 'node:fs';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
7
  import { dirname, join } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { Command } from 'commander';
@@ -145,4 +145,13 @@ program
145
145
  const { startMcpServer } = await import('../mcp/server.js');
146
146
  await startMcpServer();
147
147
  });
148
+ program
149
+ .command('config')
150
+ .description('Show configuration file path')
151
+ .action(async () => {
152
+ const { getConfigPath } = await import('../config.js');
153
+ const configPath = getConfigPath();
154
+ const exists = existsSync(configPath);
155
+ console.log(`Config file: ${configPath}${exists ? '' : ' (not found)'}`);
156
+ });
148
157
  await program.parseAsync();
@@ -106,31 +106,31 @@ function ensureGitignored(root, entry) {
106
106
  function mcpCommand() {
107
107
  return { command: resolve(process.argv[1]), args: ['mcp'] };
108
108
  }
109
- function hasMcpServer(configPath) {
109
+ function hasMcpServer(configPath, key) {
110
110
  if (!existsSync(configPath))
111
111
  return false;
112
112
  try {
113
113
  const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
114
- return !!cfg?.mcpServers?.lat;
114
+ return !!cfg?.[key]?.lat;
115
115
  }
116
116
  catch {
117
117
  return false;
118
118
  }
119
119
  }
120
- function addMcpServer(configPath) {
121
- let cfg = { mcpServers: {} };
120
+ function addMcpServer(configPath, key) {
121
+ let cfg = { [key]: {} };
122
122
  if (existsSync(configPath)) {
123
123
  const raw = readFileSync(configPath, 'utf-8');
124
124
  try {
125
125
  cfg = JSON.parse(raw);
126
- if (!cfg.mcpServers)
127
- cfg.mcpServers = {};
126
+ if (!cfg[key])
127
+ cfg[key] = {};
128
128
  }
129
129
  catch (e) {
130
130
  throw new Error(`Cannot parse ${configPath}: ${e.message}`);
131
131
  }
132
132
  }
133
- cfg.mcpServers.lat = mcpCommand();
133
+ cfg[key].lat = mcpCommand();
134
134
  mkdirSync(join(configPath, '..'), { recursive: true });
135
135
  writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
136
136
  }
@@ -182,11 +182,11 @@ async function setupClaudeCode(root, template) {
182
182
  console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
183
183
  console.log(chalk.dim(' more visibility and makes agents more likely to use it proactively.'));
184
184
  const mcpPath = join(root, '.mcp.json');
185
- if (hasMcpServer(mcpPath)) {
185
+ if (hasMcpServer(mcpPath, 'mcpServers')) {
186
186
  console.log(chalk.green(' MCP server') + ' already configured');
187
187
  }
188
188
  else {
189
- addMcpServer(mcpPath);
189
+ addMcpServer(mcpPath, 'mcpServers');
190
190
  console.log(chalk.green(' MCP server') + ' registered in .mcp.json');
191
191
  created.push('.mcp.json');
192
192
  }
@@ -213,11 +213,11 @@ async function setupCursor(root) {
213
213
  console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
214
214
  console.log(chalk.dim(' more visibility and makes agents more likely to use it proactively.'));
215
215
  const mcpPath = join(root, '.cursor', 'mcp.json');
216
- if (hasMcpServer(mcpPath)) {
216
+ if (hasMcpServer(mcpPath, 'mcpServers')) {
217
217
  console.log(chalk.green(' MCP server') + ' already configured');
218
218
  }
219
219
  else {
220
- addMcpServer(mcpPath);
220
+ addMcpServer(mcpPath, 'mcpServers');
221
221
  console.log(chalk.green(' MCP server') + ' registered in .cursor/mcp.json');
222
222
  created.push('.cursor/mcp.json');
223
223
  }
@@ -248,11 +248,11 @@ async function setupCopilot(root) {
248
248
  console.log(chalk.dim(' Agents can call `lat` from the command line, but an MCP server gives lat'));
249
249
  console.log(chalk.dim(' more visibility and makes agents more likely to use it proactively.'));
250
250
  const mcpPath = join(root, '.vscode', 'mcp.json');
251
- if (hasMcpServer(mcpPath)) {
251
+ if (hasMcpServer(mcpPath, 'servers')) {
252
252
  console.log(chalk.green(' MCP server') + ' already configured');
253
253
  }
254
254
  else {
255
- addMcpServer(mcpPath);
255
+ addMcpServer(mcpPath, 'servers');
256
256
  console.log(chalk.green(' MCP server') + ' registered in .vscode/mcp.json');
257
257
  created.push('.vscode/mcp.json');
258
258
  }
@@ -7,12 +7,19 @@ import { loadAllSections, flattenSections } from '../lattice.js';
7
7
  import { formatResultList } from '../format.js';
8
8
  import { getLlmKey, getConfigPath } from '../config.js';
9
9
  export async function searchCmd(ctx, query, opts) {
10
- const key = getLlmKey();
10
+ let key;
11
+ try {
12
+ key = getLlmKey();
13
+ }
14
+ catch (err) {
15
+ console.error(chalk.red(err.message));
16
+ process.exit(1);
17
+ }
11
18
  if (!key) {
12
- console.error(chalk.red('LAT_LLM_KEY is not set.') +
13
- ' Set the LAT_LLM_KEY env var, or run ' +
19
+ console.error(chalk.red('No API key configured.') +
20
+ ' Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run ' +
14
21
  chalk.cyan('lat init') +
15
- ' to save a key in ' +
22
+ ' to save one in ' +
16
23
  chalk.dim(getConfigPath()) +
17
24
  '.');
18
25
  process.exit(1);
@@ -8,8 +8,10 @@ export declare function writeConfig(config: LatConfig): void;
8
8
  /**
9
9
  * Returns the LLM key from (in priority order):
10
10
  * 1. LAT_LLM_KEY environment variable
11
- * 2. llm_key field in ~/.config/lat/config.json
11
+ * 2. LAT_LLM_KEY_FILE path to a file containing the key
12
+ * 3. LAT_LLM_KEY_HELPER — shell command that prints the key
13
+ * 4. llm_key field in ~/.config/lat/config.json
12
14
  *
13
- * Returns undefined if neither is set.
15
+ * Returns undefined if none is set.
14
16
  */
15
17
  export declare function getLlmKey(): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import xdg from '@folder/xdg';
@@ -28,14 +29,37 @@ export function writeConfig(config) {
28
29
  /**
29
30
  * Returns the LLM key from (in priority order):
30
31
  * 1. LAT_LLM_KEY environment variable
31
- * 2. llm_key field in ~/.config/lat/config.json
32
+ * 2. LAT_LLM_KEY_FILE path to a file containing the key
33
+ * 3. LAT_LLM_KEY_HELPER — shell command that prints the key
34
+ * 4. llm_key field in ~/.config/lat/config.json
32
35
  *
33
- * Returns undefined if neither is set.
36
+ * Returns undefined if none is set.
34
37
  */
35
38
  export function getLlmKey() {
36
39
  const envKey = process.env.LAT_LLM_KEY;
37
40
  if (envKey)
38
41
  return envKey;
42
+ const file = process.env.LAT_LLM_KEY_FILE;
43
+ if (file) {
44
+ const content = readFileSync(file, 'utf-8').trim();
45
+ if (!content) {
46
+ throw new Error(`LAT_LLM_KEY_FILE (${file}) is empty.`);
47
+ }
48
+ return content;
49
+ }
50
+ const helper = process.env.LAT_LLM_KEY_HELPER;
51
+ if (helper) {
52
+ const result = execSync(helper, {
53
+ encoding: 'utf-8',
54
+ timeout: 10_000,
55
+ }).trim();
56
+ if (!result) {
57
+ throw new Error('LAT_LLM_KEY_HELPER command returned an empty string.');
58
+ }
59
+ return result;
60
+ }
39
61
  const config = readConfig();
40
- return config.llm_key || undefined;
62
+ if (config.llm_key)
63
+ return config.llm_key;
64
+ return undefined;
41
65
  }
@@ -70,13 +70,22 @@ export async function startMcpServer() {
70
70
  .describe('Max results (default 5)'),
71
71
  }, async ({ query, limit }) => {
72
72
  const { getLlmKey } = await import('../config.js');
73
- const key = getLlmKey();
73
+ let key;
74
+ try {
75
+ key = getLlmKey();
76
+ }
77
+ catch (err) {
78
+ return {
79
+ content: [{ type: 'text', text: err.message }],
80
+ isError: true,
81
+ };
82
+ }
74
83
  if (!key) {
75
84
  return {
76
85
  content: [
77
86
  {
78
87
  type: 'text',
79
- text: 'No LLM key found. Set LAT_LLM_KEY env var or run `lat init` to save a key in ~/.config/lat/config.json.',
88
+ text: 'No LLM key found. Provide a key via LAT_LLM_KEY, LAT_LLM_KEY_FILE, LAT_LLM_KEY_HELPER, or run `lat init`.',
80
89
  },
81
90
  ],
82
91
  isError: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lat.md",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A knowledge graph for your codebase, written in markdown",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.30.2",
@@ -29,7 +29,7 @@ lat check # validate all links and code refs
29
29
 
30
30
  Run `lat --help` when in doubt about available commands or options.
31
31
 
32
- If `lat search` fails because `LAT_LLM_KEY` is not set, explain to the user that semantic search requires an API key (`export LAT_LLM_KEY=sk-...` for OpenAI or `export LAT_LLM_KEY=vck_...` for Vercel). If the user doesn't want to set it up, use `lat locate` for direct lookups instead.
32
+ If `lat search` fails because no API key is configured, explain to the user that semantic search requires a key provided via `LAT_LLM_KEY` (direct value), `LAT_LLM_KEY_FILE` (path to key file), or `LAT_LLM_KEY_HELPER` (command that prints the key). Supported key prefixes: `sk-...` (OpenAI) or `vck_...` (Vercel). If the user doesn't want to set it up, use `lat locate` for direct lookups instead.
33
33
 
34
34
  # Syntax primer
35
35