@getlore/cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,8 @@ export function registerAuthCommands(program) {
37
37
  .command('login')
38
38
  .description('Sign in with email (OTP)')
39
39
  .option('-e, --email <email>', 'Email address')
40
+ .option('--code <code>', 'OTP code (skip interactive prompt — use after --send-only)')
41
+ .option('--send-only', 'Send OTP and exit without waiting for code')
40
42
  .action(async (options) => {
41
43
  const { sendOTP, verifyOTP, sessionFromMagicLink, waitForMagicLinkCallback, isAuthenticated } = await import('../../core/auth.js');
42
44
  // Check if already logged in
@@ -52,25 +54,37 @@ export function registerAuthCommands(program) {
52
54
  console.error(c.error('Email is required'));
53
55
  process.exit(1);
54
56
  }
55
- // Start the localhost callback server before sending the OTP
56
- // so it's ready when the user clicks the magic link
57
- const callback = waitForMagicLinkCallback({
58
- onListening: () => {
59
- // Server is ready now send the OTP
60
- },
61
- });
57
+ // Non-interactive: --code provided verify directly (OTP must have been sent already)
58
+ if (options.code) {
59
+ const { session, error } = await verifyOTP(email, options.code);
60
+ if (error || !session) {
61
+ console.error(c.error(`Verification failed: ${error || 'Unknown error'}`));
62
+ process.exit(1);
63
+ }
64
+ console.log(c.success(`Logged in as ${session.user.email}`));
65
+ return;
66
+ }
67
+ // Send OTP
62
68
  console.log(c.dim(`Sending code to ${email}...`));
63
69
  const { error: sendError } = await sendOTP(email);
64
70
  if (sendError) {
65
- callback.abort();
66
71
  console.error(c.error(`Failed to send code: ${sendError}`));
67
72
  process.exit(1);
68
73
  }
69
- console.log(c.success('Check your email!'));
74
+ console.log(c.success(`OTP sent to ${email}`));
75
+ // --send-only: exit after sending
76
+ if (options.sendOnly) {
77
+ console.log(c.dim('Re-run with --code <code> to complete login.'));
78
+ return;
79
+ }
70
80
  console.log(c.dim('Click the magic link in the email — it will sign you in automatically.'));
71
81
  console.log(c.dim('Or paste a 6-digit code if your email shows one.'));
72
82
  console.log('');
73
83
  console.log(c.dim('Waiting for magic link click...'));
84
+ // Start the localhost callback server
85
+ const callback = waitForMagicLinkCallback({
86
+ onListening: () => { },
87
+ });
74
88
  // Race: callback server catches the magic link, or user pastes a code/URL
75
89
  const readline = await import('readline');
76
90
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -158,19 +172,27 @@ export function registerAuthCommands(program) {
158
172
  program
159
173
  .command('setup')
160
174
  .description('Guided setup wizard (config, login, init)')
161
- .action(async () => {
175
+ .option('--openai-key <key>', 'OpenAI API key (skip prompt)')
176
+ .option('--anthropic-key <key>', 'Anthropic API key (skip prompt)')
177
+ .option('-e, --email <email>', 'Email address (skip prompt)')
178
+ .option('--data-dir <dir>', 'Data directory path (skip prompt)')
179
+ .option('--code <code>', 'OTP code (for non-interactive login)')
180
+ .option('--skip-login', 'Skip the login step (use if already authenticated)')
181
+ .action(async (options) => {
162
182
  const { saveLoreConfig, getLoreConfigPath } = await import('../../core/config.js');
163
183
  const { sendOTP, verifyOTP, sessionFromMagicLink, isAuthenticated, loadAuthSession } = await import('../../core/auth.js');
164
184
  const { bridgeConfigToEnv } = await import('../../core/config.js');
185
+ // Non-interactive mode: skip prompts when all key flags are provided
186
+ const nonInteractive = !!(options.openaiKey && options.anthropicKey && options.email);
165
187
  console.log(`\n${c.title('Lore Setup Wizard')}`);
166
188
  console.log(`${c.dim('=')}`.repeat(40) + '\n');
167
189
  // ── Step 1: Configuration ───────────────────────────────────────
168
190
  console.log(c.bold('Step 1: Configuration\n'));
169
191
  const { expandPath } = await import('../../sync/config.js');
170
192
  const defaultDataDir = process.env.LORE_DATA_DIR || '~/.lore';
171
- const dataDir = await prompt('Data directory', defaultDataDir);
172
- const openaiApiKey = await prompt('OpenAI API Key (for embeddings)');
173
- const anthropicApiKey = await prompt('Anthropic API Key (for sync & research)');
193
+ const dataDir = options.dataDir || await prompt('Data directory', defaultDataDir);
194
+ const openaiApiKey = options.openaiKey || process.env.OPENAI_API_KEY || await prompt('OpenAI API Key (for embeddings)');
195
+ const anthropicApiKey = options.anthropicKey || process.env.ANTHROPIC_API_KEY || await prompt('Anthropic API Key (for sync & research)');
174
196
  await saveLoreConfig({
175
197
  ...(dataDir ? { data_dir: expandPath(dataDir) } : {}),
176
198
  ...(openaiApiKey ? { openai_api_key: openaiApiKey } : {}),
@@ -181,46 +203,75 @@ export function registerAuthCommands(program) {
181
203
  await bridgeConfigToEnv();
182
204
  // ── Step 2: Login ──────────────────────────────────────────────────
183
205
  console.log(c.bold('Step 2: Login\n'));
184
- if (await isAuthenticated()) {
206
+ if (options.skipLogin) {
207
+ console.log(c.dim('Skipped (--skip-login)\n'));
208
+ }
209
+ else if (await isAuthenticated()) {
185
210
  const session = await loadAuthSession();
186
211
  console.log(c.success(`Already logged in as ${session?.user.email}\n`));
187
212
  }
188
213
  else {
189
- const email = await prompt('Email');
214
+ const email = options.email || await prompt('Email');
190
215
  if (!email) {
191
216
  console.error(c.error('Email is required'));
192
217
  process.exit(1);
193
218
  }
194
- console.log(c.dim(`Sending code to ${email}...`));
195
- const { error: sendError } = await sendOTP(email);
196
- if (sendError) {
197
- console.error(c.error(`Failed to send code: ${sendError}`));
198
- process.exit(1);
199
- }
200
- console.log(c.success('Check your email!'));
201
- console.log(c.dim('You may receive a 6-digit code or a magic link URL.'));
202
- const response = await prompt('Paste the code or the full magic link URL');
203
- if (!response) {
204
- console.error(c.error('Code or magic link is required'));
205
- process.exit(1);
219
+ if (options.code) {
220
+ // Non-interactive: --code provided verify directly (OTP must have been sent already)
221
+ const result = await verifyOTP(email, options.code);
222
+ if (result.error || !result.session) {
223
+ console.error(c.error(`Verification failed: ${result.error || 'Unknown error'}`));
224
+ process.exit(1);
225
+ }
226
+ console.log(c.success(`Logged in as ${result.session.user.email}\n`));
206
227
  }
207
- let session;
208
- let verifyError;
209
- if (response.startsWith('http')) {
210
- const result = await sessionFromMagicLink(response);
211
- session = result.session;
212
- verifyError = result.error;
228
+ else if (nonInteractive) {
229
+ // Non-interactive without --code: send OTP and exit so agent can retrieve code
230
+ console.log(c.dim(`Sending code to ${email}...`));
231
+ const { error: sendError } = await sendOTP(email);
232
+ if (sendError) {
233
+ console.error(c.error(`Failed to send code: ${sendError}`));
234
+ process.exit(1);
235
+ }
236
+ console.log(c.success(`OTP sent to ${email}`));
237
+ console.log(c.dim('Re-run with --code <code> to complete setup.\n'));
238
+ console.log(c.dim('Example:'));
239
+ console.log(c.dim(` lore setup --openai-key ... --anthropic-key ... --email ${email} --code <code>\n`));
240
+ return; // Exit — config is saved, agent re-runs with --code
213
241
  }
214
242
  else {
215
- const result = await verifyOTP(email, response);
216
- session = result.session;
217
- verifyError = result.error;
218
- }
219
- if (verifyError || !session) {
220
- console.error(c.error(`Verification failed: ${verifyError || 'Unknown error'}`));
221
- process.exit(1);
243
+ // Interactive: send OTP and prompt
244
+ console.log(c.dim(`Sending code to ${email}...`));
245
+ const { error: sendError } = await sendOTP(email);
246
+ if (sendError) {
247
+ console.error(c.error(`Failed to send code: ${sendError}`));
248
+ process.exit(1);
249
+ }
250
+ console.log(c.success('Check your email!'));
251
+ console.log(c.dim('You may receive a 6-digit code or a magic link URL.'));
252
+ const response = await prompt('Paste the code or the full magic link URL');
253
+ if (!response) {
254
+ console.error(c.error('Code or magic link is required'));
255
+ process.exit(1);
256
+ }
257
+ let session;
258
+ let verifyError;
259
+ if (response.startsWith('http')) {
260
+ const result = await sessionFromMagicLink(response);
261
+ session = result.session;
262
+ verifyError = result.error;
263
+ }
264
+ else {
265
+ const result = await verifyOTP(email, response);
266
+ session = result.session;
267
+ verifyError = result.error;
268
+ }
269
+ if (verifyError || !session) {
270
+ console.error(c.error(`Verification failed: ${verifyError || 'Unknown error'}`));
271
+ process.exit(1);
272
+ }
273
+ console.log(c.success(`Logged in as ${session.user.email}\n`));
222
274
  }
223
- console.log(c.success(`Logged in as ${session.user.email}\n`));
224
275
  }
225
276
  // ── Step 3: Data Repository ────────────────────────────────────────
226
277
  console.log(c.bold('Step 3: Data Repository\n'));
@@ -262,7 +313,7 @@ export function registerAuthCommands(program) {
262
313
  if (savedUrl) {
263
314
  // Machine B: clone existing repo
264
315
  console.log(c.success(`Found your data repo URL: ${savedUrl}`));
265
- const cloneIt = await prompt('Clone it to ' + resolvedDataDir + '? (y/n)', 'y');
316
+ const cloneIt = nonInteractive ? 'y' : await prompt('Clone it to ' + resolvedDataDir + '? (y/n)', 'y');
266
317
  if (cloneIt.toLowerCase() === 'y') {
267
318
  try {
268
319
  const { execSync } = await import('child_process');
@@ -284,9 +335,9 @@ export function registerAuthCommands(program) {
284
335
  console.log(c.success('Created data repository.\n'));
285
336
  // Try to set up GitHub remote
286
337
  if (await isGhAvailable()) {
287
- const createRepo = await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
338
+ const createRepo = nonInteractive ? 'y' : await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
288
339
  if (createRepo.toLowerCase() === 'y') {
289
- const repoName = await prompt('Repository name', 'lore-data');
340
+ const repoName = nonInteractive ? 'lore-data' : await prompt('Repository name', 'lore-data');
290
341
  const url = await createGithubRepo(resolvedDataDir, repoName);
291
342
  if (url) {
292
343
  console.log(c.success(`Created and pushed to ${url}\n`));
@@ -303,7 +354,7 @@ export function registerAuthCommands(program) {
303
354
  }
304
355
  }
305
356
  }
306
- else {
357
+ else if (!nonInteractive) {
307
358
  const remoteUrl = await prompt('Git remote URL for cross-machine sync (or press Enter to skip)');
308
359
  if (remoteUrl) {
309
360
  try {
@@ -344,7 +395,7 @@ export function registerAuthCommands(program) {
344
395
  if (savedUrl) {
345
396
  // Found existing repo — add as remote and pull
346
397
  console.log(c.success(`Found your data repo: ${savedUrl}`));
347
- const useIt = await prompt('Add as remote and pull? (y/n)', 'y');
398
+ const useIt = nonInteractive ? 'y' : await prompt('Add as remote and pull? (y/n)', 'y');
348
399
  if (useIt.toLowerCase() === 'y') {
349
400
  try {
350
401
  const { execSync } = await import('child_process');
@@ -359,9 +410,9 @@ export function registerAuthCommands(program) {
359
410
  }
360
411
  }
361
412
  else if (await isGhAvailable()) {
362
- const createRepo = await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
413
+ const createRepo = nonInteractive ? 'y' : await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
363
414
  if (createRepo.toLowerCase() === 'y') {
364
- const repoName = await prompt('Repository name', 'lore-data');
415
+ const repoName = nonInteractive ? 'lore-data' : await prompt('Repository name', 'lore-data');
365
416
  const url = await createGithubRepo(resolvedDataDir, repoName);
366
417
  if (url) {
367
418
  console.log(c.success(`Created and pushed to ${url}\n`));
@@ -377,7 +428,7 @@ export function registerAuthCommands(program) {
377
428
  }
378
429
  }
379
430
  }
380
- else {
431
+ else if (!nonInteractive) {
381
432
  const remoteUrl = await prompt('Git remote URL for cross-machine sync (or press Enter to skip)');
382
433
  if (remoteUrl) {
383
434
  try {
@@ -430,7 +481,7 @@ export function registerAuthCommands(program) {
430
481
  }
431
482
  // ── Step 5: Background Daemon ──────────────────────────────────────
432
483
  console.log(c.bold('Step 5: Background Daemon\n'));
433
- const startDaemon = await prompt('Start background sync daemon? (y/n)', 'y');
484
+ const startDaemon = nonInteractive ? 'y' : await prompt('Start background sync daemon? (y/n)', 'y');
434
485
  if (startDaemon.toLowerCase() === 'y') {
435
486
  try {
436
487
  const { startDaemonProcess } = await import('./sync.js');
@@ -455,22 +506,28 @@ export function registerAuthCommands(program) {
455
506
  }
456
507
  // ── Step 6: Agent Skills ──────────────────────────────────────────
457
508
  console.log(c.bold('Step 6: Agent Skills\n'));
458
- console.log(c.dim('Lore works best when your AI agents know how to use it.'));
459
- console.log(c.dim('Install instruction files so agents automatically search and ingest into Lore.\n'));
460
- try {
461
- const { interactiveSkillInstall } = await import('./skills.js');
462
- const installed = await interactiveSkillInstall();
463
- if (installed.length > 0) {
464
- console.log(c.success(`\nInstalled skills: ${installed.join(', ')}\n`));
509
+ if (nonInteractive) {
510
+ console.log(c.dim('Skipped in non-interactive mode.'));
511
+ console.log(c.dim('Install later with: lore skills install <name>\n'));
512
+ }
513
+ else {
514
+ console.log(c.dim('Lore works best when your AI agents know how to use it.'));
515
+ console.log(c.dim('Install instruction files so agents automatically search and ingest into Lore.\n'));
516
+ try {
517
+ const { interactiveSkillInstall } = await import('./skills.js');
518
+ const installed = await interactiveSkillInstall();
519
+ if (installed.length > 0) {
520
+ console.log(c.success(`\nInstalled skills: ${installed.join(', ')}\n`));
521
+ }
522
+ else {
523
+ console.log(c.dim('\nSkipped. You can install later with: lore skills install <name>\n'));
524
+ }
465
525
  }
466
- else {
467
- console.log(c.dim('\nSkipped. You can install later with: lore skills install <name>\n'));
526
+ catch (err) {
527
+ console.log(c.warning(`Could not install skills: ${err instanceof Error ? err.message : err}`));
528
+ console.log(c.dim('You can install later with: lore skills install <name>\n'));
468
529
  }
469
530
  }
470
- catch (err) {
471
- console.log(c.warning(`Could not install skills: ${err instanceof Error ? err.message : err}`));
472
- console.log(c.dim('You can install later with: lore skills install <name>\n'));
473
- }
474
531
  // ── Done ───────────────────────────────────────────────────────────
475
532
  console.log(c.title('Setup complete!\n'));
476
533
  console.log('Try these commands:');
@@ -19,6 +19,14 @@ export interface DaemonStatus {
19
19
  };
20
20
  }
21
21
  export declare function registerSyncCommand(program: Command, defaultDataDir: string): void;
22
+ /**
23
+ * Restart the background sync daemon.
24
+ * Stops the existing daemon (if running), then starts a fresh one.
25
+ * Returns { pid } on success, null on failure.
26
+ */
27
+ export declare function restartDaemon(dataDir: string): Promise<{
28
+ pid: number;
29
+ } | null>;
22
30
  /**
23
31
  * Start the background sync daemon process.
24
32
  * Returns { pid } on success, null on failure.
@@ -28,4 +36,8 @@ export declare function startDaemonProcess(dataDir: string): Promise<{
28
36
  pid: number;
29
37
  alreadyRunning: boolean;
30
38
  } | null>;
39
+ /**
40
+ * Check if the sync daemon is currently running.
41
+ */
42
+ export declare function isDaemonRunning(): boolean;
31
43
  export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };
@@ -270,25 +270,7 @@ export function registerSyncCommand(program, defaultDataDir) {
270
270
  .description('Restart background sync daemon')
271
271
  .option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
272
272
  .action(async (options) => {
273
- // Uninstall launchd agent so it doesn't auto-restart during our restart
274
- if (isLaunchdInstalled()) {
275
- uninstallLaunchdAgent();
276
- }
277
- const pid = getPid();
278
- if (pid) {
279
- try {
280
- process.kill(pid, 'SIGTERM');
281
- console.log(`Stopped existing daemon (PID: ${pid})`);
282
- if (existsSync(PID_FILE))
283
- unlinkSync(PID_FILE);
284
- await new Promise(resolve => setTimeout(resolve, 500));
285
- }
286
- catch {
287
- // Process might already be dead
288
- }
289
- }
290
- // startDaemonProcess will reinstall launchd with fresh config on macOS
291
- const result = await startDaemonProcess(options.dataDir);
273
+ const result = await restartDaemon(options.dataDir);
292
274
  if (!result) {
293
275
  console.error('Failed to restart daemon - check logs with: lore sync logs');
294
276
  return;
@@ -723,6 +705,34 @@ export function registerSyncCommand(program, defaultDataDir) {
723
705
  // ============================================================================
724
706
  // Exported helpers
725
707
  // ============================================================================
708
+ /**
709
+ * Restart the background sync daemon.
710
+ * Stops the existing daemon (if running), then starts a fresh one.
711
+ * Returns { pid } on success, null on failure.
712
+ */
713
+ export async function restartDaemon(dataDir) {
714
+ // Uninstall launchd agent so it doesn't auto-restart during our restart
715
+ if (isLaunchdInstalled()) {
716
+ uninstallLaunchdAgent();
717
+ }
718
+ const pid = getPid();
719
+ if (pid) {
720
+ try {
721
+ process.kill(pid, 'SIGTERM');
722
+ if (existsSync(PID_FILE))
723
+ unlinkSync(PID_FILE);
724
+ await new Promise(resolve => setTimeout(resolve, 500));
725
+ }
726
+ catch {
727
+ // Process might already be dead
728
+ }
729
+ }
730
+ // startDaemonProcess will reinstall launchd with fresh config on macOS
731
+ const result = await startDaemonProcess(dataDir);
732
+ if (!result)
733
+ return null;
734
+ return { pid: result.pid };
735
+ }
726
736
  /**
727
737
  * Start the background sync daemon process.
728
738
  * Returns { pid } on success, null on failure.
@@ -764,5 +774,11 @@ export async function startDaemonProcess(dataDir) {
764
774
  return null;
765
775
  }
766
776
  }
777
+ /**
778
+ * Check if the sync daemon is currently running.
779
+ */
780
+ export function isDaemonRunning() {
781
+ return getPid() !== null;
782
+ }
767
783
  // Export for daemon-runner and browse
768
784
  export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Check for and install updates to @getlore/cli.
5
+ * Restarts the background daemon after upgrading so it picks up new code.
6
+ */
7
+ import type { Command } from 'commander';
8
+ export declare function registerUpdateCommand(program: Command, defaultDataDir: string): void;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Update Command
3
+ *
4
+ * Check for and install updates to @getlore/cli.
5
+ * Restarts the background daemon after upgrading so it picks up new code.
6
+ */
7
+ import { spawnSync } from 'child_process';
8
+ import { c } from '../colors.js';
9
+ import { getLoreVersionString } from '../../extensions/registry.js';
10
+ import { restartDaemon, isDaemonRunning } from './sync.js';
11
+ const NPM_PACKAGE = '@getlore/cli';
12
+ function getLatestVersion() {
13
+ const result = spawnSync('npm', ['view', NPM_PACKAGE, 'version'], {
14
+ encoding: 'utf-8',
15
+ timeout: 10_000,
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ });
18
+ if (result.status !== 0 || !result.stdout)
19
+ return null;
20
+ return result.stdout.trim();
21
+ }
22
+ export function registerUpdateCommand(program, defaultDataDir) {
23
+ program
24
+ .command('update')
25
+ .description('Check for and install updates')
26
+ .option('--check', 'Check for updates without installing')
27
+ .action(async (options) => {
28
+ const currentVersion = (await getLoreVersionString()) || 'unknown';
29
+ console.log(`\nCurrent version: ${c.bold(currentVersion)}`);
30
+ console.log('Checking npm for latest version...');
31
+ const latestVersion = getLatestVersion();
32
+ if (!latestVersion) {
33
+ console.error('Could not check npm registry. Are you online?');
34
+ process.exit(1);
35
+ }
36
+ console.log(`Latest version: ${c.bold(latestVersion)}`);
37
+ if (currentVersion === latestVersion) {
38
+ console.log(c.success('\nAlready up to date!'));
39
+ return;
40
+ }
41
+ console.log(`\nUpdate available: ${c.dim(currentVersion)} → ${c.success(latestVersion)}`);
42
+ if (options.check) {
43
+ console.log(`\nRun ${c.bold('lore update')} to install.`);
44
+ return;
45
+ }
46
+ // Install
47
+ console.log(`\nInstalling ${NPM_PACKAGE}@${latestVersion}...`);
48
+ const installResult = spawnSync('npm', ['install', '-g', `${NPM_PACKAGE}@latest`], {
49
+ stdio: 'inherit',
50
+ timeout: 120_000,
51
+ });
52
+ if (installResult.status !== 0) {
53
+ console.error('\nInstallation failed. You may need to run with sudo:');
54
+ console.error(` sudo npm install -g ${NPM_PACKAGE}@latest`);
55
+ process.exit(1);
56
+ }
57
+ console.log(c.success(`\nUpdated to ${latestVersion}!`));
58
+ // Restart daemon if running
59
+ if (isDaemonRunning()) {
60
+ console.log('\nRestarting background daemon...');
61
+ const result = await restartDaemon(defaultDataDir);
62
+ if (result) {
63
+ console.log(c.success(`Daemon restarted (PID: ${result.pid})`));
64
+ }
65
+ else {
66
+ console.log(c.warning('Could not restart daemon. Run: lore sync restart'));
67
+ }
68
+ }
69
+ console.log('');
70
+ });
71
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Passive Update Notifier
3
+ *
4
+ * Checks npm for a newer version of @getlore/cli once every 24 hours.
5
+ * Prints a subtle notification after command output if an update is available.
6
+ * Never blocks or throws — all errors are silently swallowed.
7
+ */
8
+ /**
9
+ * Check for updates and print a notification if one is available.
10
+ * Safe to call fire-and-forget — never throws, never blocks meaningfully.
11
+ */
12
+ export declare function checkForUpdates(): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Passive Update Notifier
3
+ *
4
+ * Checks npm for a newer version of @getlore/cli once every 24 hours.
5
+ * Prints a subtle notification after command output if an update is available.
6
+ * Never blocks or throws — all errors are silently swallowed.
7
+ */
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { spawnSync } from 'child_process';
10
+ import path from 'path';
11
+ import os from 'os';
12
+ import { colors } from './colors.js';
13
+ import { getLoreVersionString } from '../extensions/registry.js';
14
+ const NPM_PACKAGE = '@getlore/cli';
15
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
16
+ const CACHE_FILE = path.join(CONFIG_DIR, 'update-check.json');
17
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
18
+ function readCache() {
19
+ try {
20
+ if (!existsSync(CACHE_FILE))
21
+ return null;
22
+ return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function writeCache(cache) {
29
+ try {
30
+ if (!existsSync(CONFIG_DIR))
31
+ mkdirSync(CONFIG_DIR, { recursive: true });
32
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
33
+ }
34
+ catch {
35
+ // Silently ignore
36
+ }
37
+ }
38
+ /**
39
+ * Check for updates and print a notification if one is available.
40
+ * Safe to call fire-and-forget — never throws, never blocks meaningfully.
41
+ */
42
+ export async function checkForUpdates() {
43
+ // Don't notify in non-interactive contexts
44
+ if (!process.stdout.isTTY)
45
+ return;
46
+ const cache = readCache();
47
+ const now = Date.now();
48
+ let latestVersion;
49
+ if (cache && (now - cache.last_check) < CHECK_INTERVAL_MS) {
50
+ // Use cached value
51
+ latestVersion = cache.latest_version;
52
+ }
53
+ else {
54
+ // Fetch from npm with short timeout
55
+ const result = spawnSync('npm', ['view', NPM_PACKAGE, 'version'], {
56
+ encoding: 'utf-8',
57
+ timeout: 5_000,
58
+ stdio: ['ignore', 'pipe', 'pipe'],
59
+ });
60
+ if (result.status !== 0 || !result.stdout)
61
+ return;
62
+ latestVersion = result.stdout.trim();
63
+ writeCache({
64
+ last_check: now,
65
+ latest_version: latestVersion,
66
+ last_notified_version: cache?.last_notified_version,
67
+ });
68
+ }
69
+ const currentVersion = await getLoreVersionString();
70
+ if (!currentVersion || currentVersion === latestVersion)
71
+ return;
72
+ // Don't re-notify for the same version
73
+ if (cache?.last_notified_version === latestVersion)
74
+ return;
75
+ // Print notification
76
+ const border = `${colors.dim}╭────────────────────────────────────────╮${colors.reset}`;
77
+ const bottom = `${colors.dim}╰────────────────────────────────────────╯${colors.reset}`;
78
+ const pad = (s, width) => {
79
+ // Strip ANSI for length calculation
80
+ const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
81
+ const padding = Math.max(0, width - stripped.length);
82
+ return s + ' '.repeat(padding);
83
+ };
84
+ console.log('');
85
+ console.log(border);
86
+ console.log(`${colors.dim}│${colors.reset} ${pad(`Update available: ${colors.dim}${currentVersion}${colors.reset} → ${colors.green}${latestVersion}${colors.reset}`, 39)}${colors.dim}│${colors.reset}`);
87
+ console.log(`${colors.dim}│${colors.reset} ${pad(`Run ${colors.bold}lore update${colors.reset} to upgrade`, 39)}${colors.dim}│${colors.reset}`);
88
+ console.log(bottom);
89
+ // Mark as notified
90
+ writeCache({
91
+ last_check: cache?.last_check || now,
92
+ latest_version: latestVersion,
93
+ last_notified_version: latestVersion,
94
+ });
95
+ }
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ import { registerPendingCommand } from './cli/commands/pending.js';
29
29
  import { registerAskCommand } from './cli/commands/ask.js';
30
30
  import { registerAuthCommands } from './cli/commands/auth.js';
31
31
  import { registerSkillsCommand } from './cli/commands/skills.js';
32
+ import { registerUpdateCommand } from './cli/commands/update.js';
32
33
  import { getExtensionRegistry, getLoreVersionString } from './extensions/registry.js';
33
34
  import { bridgeConfigToEnv } from './core/config.js';
34
35
  import { expandPath } from './sync/config.js';
@@ -76,6 +77,7 @@ registerMiscCommands(program, DEFAULT_DATA_DIR);
76
77
  registerAskCommand(program, DEFAULT_DATA_DIR);
77
78
  registerAuthCommands(program);
78
79
  registerSkillsCommand(program);
80
+ registerUpdateCommand(program, DEFAULT_DATA_DIR);
79
81
  // Extension system — hidden from top-level help for now
80
82
  const extensionCmd = registerExtensionCommands(program);
81
83
  extensionCmd._hidden = true;
@@ -101,5 +103,11 @@ process.on('unhandledRejection', (reason) => {
101
103
  console.error(`\nError: ${message}`);
102
104
  process.exit(1);
103
105
  });
104
- // Parse and run
105
- program.parse();
106
+ // Parse and run, then show update notification after command output
107
+ await program.parseAsync();
108
+ // Passive update notification (non-blocking, silent on errors)
109
+ try {
110
+ const { checkForUpdates } = await import('./cli/update-notifier.js');
111
+ await checkForUpdates();
112
+ }
113
+ catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getlore/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Research knowledge repository with semantic search, citations, and project lineage tracking",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",