@getlore/cli 0.3.0 → 0.5.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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Ingest Command
3
+ *
4
+ * Push content directly into Lore from the CLI.
5
+ * Accepts inline text, a file path, or piped stdin.
6
+ */
7
+ import type { Command } from 'commander';
8
+ export declare function registerIngestCommand(program: Command, defaultDataDir: string): void;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Ingest Command
3
+ *
4
+ * Push content directly into Lore from the CLI.
5
+ * Accepts inline text, a file path, or piped stdin.
6
+ */
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import path from 'path';
9
+ export function registerIngestCommand(program, defaultDataDir) {
10
+ program
11
+ .command('ingest')
12
+ .description('Ingest content into the knowledge base')
13
+ .argument('[content]', 'Content to ingest (or use --file / stdin)')
14
+ .option('-f, --file <path>', 'Read content from a file')
15
+ .option('-t, --title <title>', 'Document title')
16
+ .option('-p, --project <project>', 'Project name', 'default')
17
+ .option('--type <type>', 'Source type (e.g. meeting, notes, article)')
18
+ .option('--url <url>', 'Source URL for citation linking')
19
+ .option('--name <name>', 'Human-readable source name')
20
+ .option('--tags <tags>', 'Comma-separated tags')
21
+ .option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
22
+ .action(async (contentArg, options) => {
23
+ const { handleIngest } = await import('../../mcp/handlers/ingest.js');
24
+ const dataDir = options.dataDir;
25
+ const dbPath = path.join(dataDir, 'lore.lance');
26
+ // Resolve content from argument, file, or stdin
27
+ let content;
28
+ if (options.file) {
29
+ const filePath = options.file.replace(/^~/, process.env.HOME || '~');
30
+ if (!existsSync(filePath)) {
31
+ console.error(`File not found: ${filePath}`);
32
+ process.exit(1);
33
+ }
34
+ content = readFileSync(filePath, 'utf-8');
35
+ }
36
+ else if (contentArg) {
37
+ content = contentArg;
38
+ }
39
+ else if (!process.stdin.isTTY) {
40
+ // Reading from pipe/stdin
41
+ content = readFileSync(0, 'utf-8');
42
+ }
43
+ else {
44
+ console.error('No content provided. Use one of:');
45
+ console.error(' lore ingest "Your content here"');
46
+ console.error(' lore ingest --file ./notes.md');
47
+ console.error(' echo "content" | lore ingest');
48
+ process.exit(1);
49
+ }
50
+ content = content.trim();
51
+ if (!content) {
52
+ console.error('Content is empty.');
53
+ process.exit(1);
54
+ }
55
+ // Derive title from file name or content
56
+ let title = options.title;
57
+ if (!title && options.file) {
58
+ title = path.basename(options.file, path.extname(options.file));
59
+ }
60
+ if (!title) {
61
+ // Use first line or first 60 chars
62
+ const firstLine = content.split('\n')[0].replace(/^#+\s*/, '');
63
+ title = firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
64
+ }
65
+ const tags = options.tags ? options.tags.split(',').map((t) => t.trim()) : [];
66
+ console.log(`\nIngesting: ${title}`);
67
+ console.log(`Project: ${options.project}`);
68
+ if (options.type)
69
+ console.log(`Type: ${options.type}`);
70
+ console.log(`Content: ${content.length} chars`);
71
+ console.log('');
72
+ const result = await handleIngest(dbPath, dataDir, {
73
+ content,
74
+ title,
75
+ project: options.project,
76
+ source_type: options.type,
77
+ tags,
78
+ source_url: options.url,
79
+ source_name: options.name,
80
+ }, {
81
+ hookContext: { mode: 'cli' },
82
+ });
83
+ if (result.deduplicated) {
84
+ console.log('Already exists (identical content). Skipped.');
85
+ return;
86
+ }
87
+ if (result.success) {
88
+ console.log(`Ingested (ID: ${result.id})`);
89
+ if (result.indexed) {
90
+ console.log('Indexed and searchable.');
91
+ }
92
+ else {
93
+ console.log('Saved to disk. Run "lore sync" to index.');
94
+ }
95
+ if (result.synced) {
96
+ console.log('Pushed to git.');
97
+ }
98
+ }
99
+ else {
100
+ console.error('Ingestion failed.');
101
+ process.exit(1);
102
+ }
103
+ });
104
+ }
@@ -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,8 @@ 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';
33
+ import { registerIngestCommand } from './cli/commands/ingest.js';
32
34
  import { getExtensionRegistry, getLoreVersionString } from './extensions/registry.js';
33
35
  import { bridgeConfigToEnv } from './core/config.js';
34
36
  import { expandPath } from './sync/config.js';
@@ -76,6 +78,8 @@ registerMiscCommands(program, DEFAULT_DATA_DIR);
76
78
  registerAskCommand(program, DEFAULT_DATA_DIR);
77
79
  registerAuthCommands(program);
78
80
  registerSkillsCommand(program);
81
+ registerUpdateCommand(program, DEFAULT_DATA_DIR);
82
+ registerIngestCommand(program, DEFAULT_DATA_DIR);
79
83
  // Extension system — hidden from top-level help for now
80
84
  const extensionCmd = registerExtensionCommands(program);
81
85
  extensionCmd._hidden = true;
@@ -101,5 +105,11 @@ process.on('unhandledRejection', (reason) => {
101
105
  console.error(`\nError: ${message}`);
102
106
  process.exit(1);
103
107
  });
104
- // Parse and run
105
- program.parse();
108
+ // Parse and run, then show update notification after command output
109
+ await program.parseAsync();
110
+ // Passive update notification (non-blocking, silent on errors)
111
+ try {
112
+ const { checkForUpdates } = await import('./cli/update-notifier.js');
113
+ await checkForUpdates();
114
+ }
115
+ catch { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getlore/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",