@getlore/cli 0.2.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.
Files changed (148) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +80 -0
  3. package/dist/cli/colors.d.ts +48 -0
  4. package/dist/cli/colors.js +48 -0
  5. package/dist/cli/commands/ask.d.ts +7 -0
  6. package/dist/cli/commands/ask.js +97 -0
  7. package/dist/cli/commands/auth.d.ts +10 -0
  8. package/dist/cli/commands/auth.js +484 -0
  9. package/dist/cli/commands/daemon.d.ts +22 -0
  10. package/dist/cli/commands/daemon.js +244 -0
  11. package/dist/cli/commands/docs.d.ts +7 -0
  12. package/dist/cli/commands/docs.js +188 -0
  13. package/dist/cli/commands/extensions.d.ts +7 -0
  14. package/dist/cli/commands/extensions.js +204 -0
  15. package/dist/cli/commands/misc.d.ts +7 -0
  16. package/dist/cli/commands/misc.js +172 -0
  17. package/dist/cli/commands/pending.d.ts +7 -0
  18. package/dist/cli/commands/pending.js +63 -0
  19. package/dist/cli/commands/projects.d.ts +7 -0
  20. package/dist/cli/commands/projects.js +136 -0
  21. package/dist/cli/commands/search.d.ts +7 -0
  22. package/dist/cli/commands/search.js +102 -0
  23. package/dist/cli/commands/skills.d.ts +24 -0
  24. package/dist/cli/commands/skills.js +447 -0
  25. package/dist/cli/commands/sources.d.ts +7 -0
  26. package/dist/cli/commands/sources.js +121 -0
  27. package/dist/cli/commands/sync.d.ts +31 -0
  28. package/dist/cli/commands/sync.js +768 -0
  29. package/dist/cli/helpers.d.ts +30 -0
  30. package/dist/cli/helpers.js +119 -0
  31. package/dist/core/auth.d.ts +62 -0
  32. package/dist/core/auth.js +330 -0
  33. package/dist/core/config.d.ts +41 -0
  34. package/dist/core/config.js +96 -0
  35. package/dist/core/data-repo.d.ts +31 -0
  36. package/dist/core/data-repo.js +146 -0
  37. package/dist/core/embedder.d.ts +22 -0
  38. package/dist/core/embedder.js +104 -0
  39. package/dist/core/git.d.ts +37 -0
  40. package/dist/core/git.js +140 -0
  41. package/dist/core/index.d.ts +4 -0
  42. package/dist/core/index.js +5 -0
  43. package/dist/core/insight-extractor.d.ts +26 -0
  44. package/dist/core/insight-extractor.js +114 -0
  45. package/dist/core/local-search.d.ts +43 -0
  46. package/dist/core/local-search.js +221 -0
  47. package/dist/core/themes.d.ts +15 -0
  48. package/dist/core/themes.js +77 -0
  49. package/dist/core/types.d.ts +177 -0
  50. package/dist/core/types.js +9 -0
  51. package/dist/core/user-settings.d.ts +15 -0
  52. package/dist/core/user-settings.js +42 -0
  53. package/dist/core/vector-store-lance.d.ts +98 -0
  54. package/dist/core/vector-store-lance.js +384 -0
  55. package/dist/core/vector-store-supabase.d.ts +89 -0
  56. package/dist/core/vector-store-supabase.js +295 -0
  57. package/dist/core/vector-store.d.ts +131 -0
  58. package/dist/core/vector-store.js +503 -0
  59. package/dist/daemon-runner.d.ts +8 -0
  60. package/dist/daemon-runner.js +246 -0
  61. package/dist/extensions/config.d.ts +22 -0
  62. package/dist/extensions/config.js +102 -0
  63. package/dist/extensions/proposals.d.ts +30 -0
  64. package/dist/extensions/proposals.js +178 -0
  65. package/dist/extensions/registry.d.ts +35 -0
  66. package/dist/extensions/registry.js +309 -0
  67. package/dist/extensions/sandbox.d.ts +16 -0
  68. package/dist/extensions/sandbox.js +17 -0
  69. package/dist/extensions/types.d.ts +114 -0
  70. package/dist/extensions/types.js +4 -0
  71. package/dist/extensions/worker.d.ts +1 -0
  72. package/dist/extensions/worker.js +49 -0
  73. package/dist/index.d.ts +17 -0
  74. package/dist/index.js +105 -0
  75. package/dist/mcp/handlers/archive-project.d.ts +51 -0
  76. package/dist/mcp/handlers/archive-project.js +112 -0
  77. package/dist/mcp/handlers/get-quotes.d.ts +27 -0
  78. package/dist/mcp/handlers/get-quotes.js +61 -0
  79. package/dist/mcp/handlers/get-source.d.ts +9 -0
  80. package/dist/mcp/handlers/get-source.js +40 -0
  81. package/dist/mcp/handlers/ingest.d.ts +25 -0
  82. package/dist/mcp/handlers/ingest.js +305 -0
  83. package/dist/mcp/handlers/list-projects.d.ts +4 -0
  84. package/dist/mcp/handlers/list-projects.js +16 -0
  85. package/dist/mcp/handlers/list-sources.d.ts +11 -0
  86. package/dist/mcp/handlers/list-sources.js +20 -0
  87. package/dist/mcp/handlers/research-agent.d.ts +21 -0
  88. package/dist/mcp/handlers/research-agent.js +369 -0
  89. package/dist/mcp/handlers/research.d.ts +22 -0
  90. package/dist/mcp/handlers/research.js +225 -0
  91. package/dist/mcp/handlers/retain.d.ts +18 -0
  92. package/dist/mcp/handlers/retain.js +92 -0
  93. package/dist/mcp/handlers/search.d.ts +52 -0
  94. package/dist/mcp/handlers/search.js +145 -0
  95. package/dist/mcp/handlers/sync.d.ts +47 -0
  96. package/dist/mcp/handlers/sync.js +211 -0
  97. package/dist/mcp/server.d.ts +10 -0
  98. package/dist/mcp/server.js +268 -0
  99. package/dist/mcp/tools.d.ts +16 -0
  100. package/dist/mcp/tools.js +297 -0
  101. package/dist/sync/config.d.ts +26 -0
  102. package/dist/sync/config.js +140 -0
  103. package/dist/sync/discover.d.ts +51 -0
  104. package/dist/sync/discover.js +190 -0
  105. package/dist/sync/index.d.ts +11 -0
  106. package/dist/sync/index.js +11 -0
  107. package/dist/sync/process.d.ts +50 -0
  108. package/dist/sync/process.js +285 -0
  109. package/dist/sync/processors.d.ts +24 -0
  110. package/dist/sync/processors.js +351 -0
  111. package/dist/tui/browse-handlers-ask.d.ts +30 -0
  112. package/dist/tui/browse-handlers-ask.js +372 -0
  113. package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
  114. package/dist/tui/browse-handlers-autocomplete.js +270 -0
  115. package/dist/tui/browse-handlers-extensions.d.ts +18 -0
  116. package/dist/tui/browse-handlers-extensions.js +107 -0
  117. package/dist/tui/browse-handlers-pending.d.ts +22 -0
  118. package/dist/tui/browse-handlers-pending.js +100 -0
  119. package/dist/tui/browse-handlers-research.d.ts +32 -0
  120. package/dist/tui/browse-handlers-research.js +363 -0
  121. package/dist/tui/browse-handlers-tools.d.ts +42 -0
  122. package/dist/tui/browse-handlers-tools.js +289 -0
  123. package/dist/tui/browse-handlers.d.ts +239 -0
  124. package/dist/tui/browse-handlers.js +1944 -0
  125. package/dist/tui/browse-render-extensions.d.ts +14 -0
  126. package/dist/tui/browse-render-extensions.js +114 -0
  127. package/dist/tui/browse-render-tools.d.ts +18 -0
  128. package/dist/tui/browse-render-tools.js +259 -0
  129. package/dist/tui/browse-render.d.ts +51 -0
  130. package/dist/tui/browse-render.js +599 -0
  131. package/dist/tui/browse-types.d.ts +142 -0
  132. package/dist/tui/browse-types.js +70 -0
  133. package/dist/tui/browse-ui.d.ts +10 -0
  134. package/dist/tui/browse-ui.js +432 -0
  135. package/dist/tui/browse.d.ts +17 -0
  136. package/dist/tui/browse.js +625 -0
  137. package/dist/tui/markdown.d.ts +22 -0
  138. package/dist/tui/markdown.js +223 -0
  139. package/package.json +71 -0
  140. package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
  141. package/plugins/claude-code/.mcp.json +6 -0
  142. package/plugins/claude-code/skills/lore/SKILL.md +63 -0
  143. package/plugins/codex/SKILL.md +36 -0
  144. package/plugins/codex/agents/openai.yaml +10 -0
  145. package/plugins/gemini/GEMINI.md +31 -0
  146. package/plugins/gemini/gemini-extension.json +11 -0
  147. package/skills/generic-agent.md +99 -0
  148. package/skills/openclaw.md +67 -0
@@ -0,0 +1,768 @@
1
+ /**
2
+ * Sync Command
3
+ *
4
+ * All sync-related functionality: one-time sync, daemon, watch, sources.
5
+ */
6
+ import { spawn, spawnSync } from 'child_process';
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
8
+ import { mkdir } from 'fs/promises';
9
+ import path from 'path';
10
+ import os from 'os';
11
+ import { fileURLToPath } from 'url';
12
+ import { colors, c } from '../colors.js';
13
+ // Get the directory of this module (for finding daemon-runner.js)
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ // Config directory for daemon files
17
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
18
+ const PID_FILE = path.join(CONFIG_DIR, 'daemon.pid');
19
+ const STATUS_FILE = path.join(CONFIG_DIR, 'daemon.status.json');
20
+ const LOG_FILE = path.join(CONFIG_DIR, 'daemon.log');
21
+ // launchd (macOS) constants
22
+ const LAUNCHD_LABEL = 'com.lore.daemon';
23
+ const LAUNCHD_PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
24
+ function isMacOS() {
25
+ return process.platform === 'darwin';
26
+ }
27
+ function generatePlist(dataDir) {
28
+ const nodePath = process.execPath;
29
+ const scriptPath = path.join(__dirname, '..', '..', 'daemon-runner.js');
30
+ return `<?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0">
33
+ <dict>
34
+ <key>Label</key>
35
+ <string>${LAUNCHD_LABEL}</string>
36
+ <key>ProgramArguments</key>
37
+ <array>
38
+ <string>${nodePath}</string>
39
+ <string>${scriptPath}</string>
40
+ <string>${dataDir}</string>
41
+ </array>
42
+ <key>KeepAlive</key>
43
+ <true/>
44
+ <key>RunAtLoad</key>
45
+ <true/>
46
+ <key>StandardOutPath</key>
47
+ <string>${LOG_FILE}</string>
48
+ <key>StandardErrorPath</key>
49
+ <string>${LOG_FILE}</string>
50
+ </dict>
51
+ </plist>`;
52
+ }
53
+ function isLaunchdInstalled() {
54
+ return isMacOS() && existsSync(LAUNCHD_PLIST_PATH);
55
+ }
56
+ function installLaunchdAgent(dataDir) {
57
+ const plistDir = path.dirname(LAUNCHD_PLIST_PATH);
58
+ if (!existsSync(plistDir)) {
59
+ // LaunchAgents dir should exist, but just in case
60
+ spawnSync('mkdir', ['-p', plistDir]);
61
+ }
62
+ writeFileSync(LAUNCHD_PLIST_PATH, generatePlist(dataDir));
63
+ // Unload first in case an old version is loaded
64
+ spawnSync('launchctl', ['unload', LAUNCHD_PLIST_PATH], { stdio: 'ignore' });
65
+ const result = spawnSync('launchctl', ['load', LAUNCHD_PLIST_PATH], { stdio: 'pipe' });
66
+ if (result.status !== 0) {
67
+ const stderr = result.stderr?.toString().trim();
68
+ if (stderr)
69
+ console.error(`launchctl load error: ${stderr}`);
70
+ return null;
71
+ }
72
+ // launchctl load starts the process (RunAtLoad=true). Wait for PID file.
73
+ // The daemon-runner writes PID_FILE on startup.
74
+ for (let i = 0; i < 20; i++) {
75
+ spawnSync('sleep', ['0.25']);
76
+ if (existsSync(PID_FILE)) {
77
+ try {
78
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
79
+ process.kill(pid, 0); // verify alive
80
+ return { pid };
81
+ }
82
+ catch {
83
+ // PID file exists but process not ready yet, keep waiting
84
+ }
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ function uninstallLaunchdAgent() {
90
+ if (!existsSync(LAUNCHD_PLIST_PATH))
91
+ return;
92
+ spawnSync('launchctl', ['unload', LAUNCHD_PLIST_PATH], { stdio: 'ignore' });
93
+ try {
94
+ unlinkSync(LAUNCHD_PLIST_PATH);
95
+ }
96
+ catch { }
97
+ }
98
+ async function ensureConfigDir() {
99
+ await mkdir(CONFIG_DIR, { recursive: true });
100
+ }
101
+ function getPid() {
102
+ if (!existsSync(PID_FILE))
103
+ return null;
104
+ try {
105
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
106
+ try {
107
+ process.kill(pid, 0);
108
+ return pid;
109
+ }
110
+ catch {
111
+ unlinkSync(PID_FILE);
112
+ return null;
113
+ }
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ }
119
+ function getStatus() {
120
+ if (!existsSync(STATUS_FILE))
121
+ return null;
122
+ try {
123
+ return JSON.parse(readFileSync(STATUS_FILE, 'utf-8'));
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ function formatUptime(ms) {
130
+ const seconds = Math.floor(ms / 1000);
131
+ const minutes = Math.floor(seconds / 60);
132
+ const hours = Math.floor(minutes / 60);
133
+ const days = Math.floor(hours / 24);
134
+ if (days > 0)
135
+ return `${days}d ${hours % 24}h`;
136
+ if (hours > 0)
137
+ return `${hours}h ${minutes % 60}m`;
138
+ if (minutes > 0)
139
+ return `${minutes}m ${seconds % 60}s`;
140
+ return `${seconds}s`;
141
+ }
142
+ function formatAgo(ms) {
143
+ const seconds = Math.floor(ms / 1000);
144
+ if (seconds < 60)
145
+ return 'just now';
146
+ const minutes = Math.floor(seconds / 60);
147
+ if (minutes < 60)
148
+ return `${minutes}m ago`;
149
+ const hours = Math.floor(minutes / 60);
150
+ if (hours < 24)
151
+ return `${hours}h ago`;
152
+ const days = Math.floor(hours / 24);
153
+ return `${days}d ago`;
154
+ }
155
+ export function registerSyncCommand(program, defaultDataDir) {
156
+ const syncCmd = program
157
+ .command('sync')
158
+ .description('Sync and manage the knowledge repository')
159
+ .option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
160
+ .option('--dry-run', 'Show what would be synced without processing')
161
+ .option('--legacy', 'Use legacy disk-based sync only')
162
+ .option('--no-git', 'Skip git operations')
163
+ .action(async (options) => {
164
+ // Default action: one-time sync
165
+ const { handleSync } = await import('../../mcp/handlers/sync.js');
166
+ const dataDir = options.dataDir;
167
+ const dbPath = path.join(dataDir, 'lore.lance');
168
+ console.log(`\nLore Sync`);
169
+ console.log(`=========`);
170
+ console.log(`Data dir: ${dataDir}`);
171
+ if (options.dryRun)
172
+ console.log(`Mode: DRY RUN`);
173
+ console.log('');
174
+ const result = await handleSync(dbPath, dataDir, {
175
+ git_pull: options.git !== false,
176
+ git_push: options.git !== false,
177
+ dry_run: options.dryRun,
178
+ use_legacy: options.legacy,
179
+ }, { hookContext: { mode: 'cli' } });
180
+ if (result.git_pulled) {
181
+ console.log('✓ Pulled latest changes from git');
182
+ }
183
+ if (result.git_error) {
184
+ console.log(`⚠ Git: ${result.git_error}`);
185
+ }
186
+ if (result.discovery) {
187
+ console.log(`\nDiscovery:`);
188
+ console.log(` Sources scanned: ${result.discovery.sources_scanned}`);
189
+ console.log(` Files found: ${result.discovery.total_files}`);
190
+ console.log(` New files: ${result.discovery.new_files}`);
191
+ if (result.discovery.edited_files && result.discovery.edited_files > 0) {
192
+ console.log(` Edited files: ${result.discovery.edited_files}`);
193
+ }
194
+ console.log(` Already indexed: ${result.discovery.existing_files}`);
195
+ if (result.discovery.errors > 0) {
196
+ console.log(` Errors: ${result.discovery.errors}`);
197
+ }
198
+ }
199
+ if (result.processing) {
200
+ console.log(`\nProcessed ${result.processing.processed} new files:`);
201
+ for (const title of result.processing.titles.slice(0, 10)) {
202
+ console.log(` • ${title}`);
203
+ }
204
+ if (result.processing.titles.length > 10) {
205
+ console.log(` ... and ${result.processing.titles.length - 10} more`);
206
+ }
207
+ if (result.processing.errors > 0) {
208
+ console.log(` Errors: ${result.processing.errors}`);
209
+ }
210
+ }
211
+ if (result.sources_found > 0 || result.sources_indexed > 0) {
212
+ console.log(`\nLegacy Sync:`);
213
+ console.log(` Sources on disk: ${result.sources_found}`);
214
+ console.log(` Newly indexed: ${result.sources_indexed}`);
215
+ console.log(` Already indexed: ${result.already_indexed}`);
216
+ }
217
+ if (result.git_pushed) {
218
+ console.log('\n✓ Pushed changes to git');
219
+ }
220
+ console.log('\nSync complete!');
221
+ });
222
+ // Start daemon
223
+ syncCmd
224
+ .command('start')
225
+ .description('Start background sync daemon')
226
+ .option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
227
+ .action(async (options) => {
228
+ const result = await startDaemonProcess(options.dataDir);
229
+ if (!result) {
230
+ console.error('Failed to start daemon - check logs with: lore sync logs');
231
+ return;
232
+ }
233
+ if (result.alreadyRunning) {
234
+ console.log(`Daemon already running (PID: ${result.pid})`);
235
+ console.log(`Use "lore sync status" to check status`);
236
+ console.log(`Use "lore sync stop" to stop it`);
237
+ return;
238
+ }
239
+ console.log(`Daemon started (PID: ${result.pid})`);
240
+ console.log(`Log file: ${LOG_FILE}`);
241
+ console.log(`Use "lore sync logs" to view activity`);
242
+ });
243
+ // Stop daemon
244
+ syncCmd
245
+ .command('stop')
246
+ .description('Stop background sync daemon')
247
+ .action(async () => {
248
+ // Uninstall launchd agent so the daemon doesn't restart on login
249
+ if (isLaunchdInstalled()) {
250
+ uninstallLaunchdAgent();
251
+ }
252
+ const pid = getPid();
253
+ if (!pid) {
254
+ console.log('Daemon is not running');
255
+ return;
256
+ }
257
+ try {
258
+ process.kill(pid, 'SIGTERM');
259
+ console.log(`Daemon stopped (PID: ${pid})`);
260
+ if (existsSync(PID_FILE))
261
+ unlinkSync(PID_FILE);
262
+ }
263
+ catch (error) {
264
+ console.error(`Failed to stop daemon: ${error}`);
265
+ }
266
+ });
267
+ // Restart daemon
268
+ syncCmd
269
+ .command('restart')
270
+ .description('Restart background sync daemon')
271
+ .option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
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);
292
+ if (!result) {
293
+ console.error('Failed to restart daemon - check logs with: lore sync logs');
294
+ return;
295
+ }
296
+ console.log(`Daemon restarted (PID: ${result.pid})`);
297
+ });
298
+ // Daemon status
299
+ syncCmd
300
+ .command('status')
301
+ .description('Check sync daemon status')
302
+ .action(async () => {
303
+ const pid = getPid();
304
+ const status = getStatus();
305
+ console.log('');
306
+ console.log('Lore Sync Status');
307
+ console.log('================');
308
+ if (!pid) {
309
+ console.log('Daemon: NOT RUNNING');
310
+ console.log(`Auto-start: ${isLaunchdInstalled() ? 'enabled (launchd)' : 'not configured'}`);
311
+ console.log('');
312
+ console.log('Start with: lore sync start');
313
+ return;
314
+ }
315
+ console.log(`Daemon: RUNNING (PID: ${pid})`);
316
+ console.log(`Auto-start: ${isLaunchdInstalled() ? 'enabled (launchd)' : 'not configured'}`);
317
+ if (status) {
318
+ const started = new Date(status.started_at);
319
+ const uptime = formatUptime(Date.now() - started.getTime());
320
+ console.log(`Uptime: ${uptime}`);
321
+ if (status.last_sync) {
322
+ const lastSync = new Date(status.last_sync);
323
+ const ago = formatAgo(Date.now() - lastSync.getTime());
324
+ console.log(`Last sync: ${ago}`);
325
+ if (status.last_sync_result) {
326
+ const r = status.last_sync_result;
327
+ console.log(` Files scanned: ${r.files_scanned}`);
328
+ console.log(` Files processed: ${r.files_processed}`);
329
+ if (r.errors > 0) {
330
+ console.log(` Errors: ${r.errors}`);
331
+ }
332
+ }
333
+ }
334
+ else {
335
+ console.log('Last sync: (not yet synced)');
336
+ }
337
+ }
338
+ console.log('');
339
+ console.log(`Log file: ${LOG_FILE}`);
340
+ console.log('View logs: lore sync logs');
341
+ });
342
+ // Daemon logs
343
+ syncCmd
344
+ .command('logs')
345
+ .description('View sync daemon logs')
346
+ .option('-f, --follow', 'Follow log output (like tail -f)')
347
+ .option('-n, --lines <n>', 'Number of lines to show', '50')
348
+ .action(async (options) => {
349
+ if (!existsSync(LOG_FILE)) {
350
+ console.log('No log file found. Daemon may not have run yet.');
351
+ console.log(`Expected: ${LOG_FILE}`);
352
+ return;
353
+ }
354
+ if (options.follow) {
355
+ const tail = spawn('tail', ['-f', LOG_FILE], {
356
+ stdio: 'inherit',
357
+ });
358
+ process.on('SIGINT', () => {
359
+ tail.kill();
360
+ process.exit(0);
361
+ });
362
+ await new Promise(() => { });
363
+ }
364
+ else {
365
+ const content = readFileSync(LOG_FILE, 'utf-8');
366
+ const lines = content.trim().split('\n');
367
+ const n = parseInt(options.lines, 10);
368
+ const lastLines = lines.slice(-n);
369
+ console.log(`Last ${Math.min(n, lastLines.length)} log entries:\n`);
370
+ console.log(lastLines.join('\n'));
371
+ }
372
+ });
373
+ // Watch (foreground)
374
+ syncCmd
375
+ .command('watch')
376
+ .description('Watch directories and sync in foreground (shows live output)')
377
+ .option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
378
+ .option('--interval <ms>', 'Debounce interval in ms', '2000')
379
+ .option('--no-initial', 'Skip initial sync on startup')
380
+ .action(async (options) => {
381
+ const chokidar = await import('chokidar');
382
+ const { loadSyncConfig, getEnabledSources, expandPath } = await import('../../sync/config.js');
383
+ const { handleSync } = await import('../../mcp/handlers/sync.js');
384
+ const { matchesGlob } = await import('../../sync/discover.js');
385
+ const dataDir = options.dataDir;
386
+ const dbPath = path.join(dataDir, 'lore.lance');
387
+ const debounceMs = parseInt(options.interval, 10);
388
+ // Header
389
+ console.log('');
390
+ console.log(c.title(' ╔══════════════════════════════════════╗'));
391
+ console.log(c.title(' ║ 🔍 LORE WATCH ║'));
392
+ console.log(c.title(' ╚══════════════════════════════════════╝'));
393
+ console.log('');
394
+ console.log(` ${c.dim('Data:')} ${dataDir}`);
395
+ console.log(` ${c.dim('Debounce:')} ${debounceMs}ms`);
396
+ console.log('');
397
+ const config = await loadSyncConfig();
398
+ const sources = getEnabledSources(config);
399
+ const watchPaths = [];
400
+ if (sources.length === 0) {
401
+ console.log(c.warning(' ⚠ No local sync sources configured'));
402
+ console.log(c.dim(' Will still pull from remote and process new files'));
403
+ console.log(c.dim(' Run "lore sync sources add" to watch local directories'));
404
+ console.log('');
405
+ }
406
+ else {
407
+ console.log(c.info(' 📁 Watching:'));
408
+ for (const source of sources) {
409
+ const expanded = expandPath(source.path);
410
+ console.log(` ${c.file(source.name)}`);
411
+ console.log(` ${c.path(expanded)}`);
412
+ console.log(` ${c.dim(`glob: ${source.glob} → project: ${source.project}`)}`);
413
+ console.log('');
414
+ watchPaths.push(expanded);
415
+ }
416
+ }
417
+ // Run initial sync
418
+ if (options.initial !== false) {
419
+ console.log(c.info(' ⚡ Initial sync...'));
420
+ try {
421
+ const result = await handleSync(dbPath, dataDir, {
422
+ git_pull: true,
423
+ git_push: true,
424
+ }, { hookContext: { mode: 'cli' } });
425
+ const totalFiles = result.discovery?.total_files || 0;
426
+ const newFiles = result.discovery?.new_files || 0;
427
+ const processed = result.processing?.processed || 0;
428
+ if (processed > 0) {
429
+ console.log(` ${c.success('✓')} Processed ${c.file(String(processed))} new file(s)`);
430
+ for (const title of result.processing?.titles || []) {
431
+ console.log(` ${c.dim('•')} ${title}`);
432
+ }
433
+ }
434
+ else {
435
+ console.log(` ${c.success('✓')} ${totalFiles} files indexed, ${newFiles} new`);
436
+ }
437
+ }
438
+ catch (error) {
439
+ console.log(` ${c.error('✗')} Initial sync failed: ${error}`);
440
+ }
441
+ console.log('');
442
+ }
443
+ // Divider
444
+ console.log(c.dim(' ─────────────────────────────────────────'));
445
+ console.log(` ${c.success('●')} Watching for changes... ${c.dim('(Ctrl+C to stop)')}`);
446
+ console.log(c.dim(' ─────────────────────────────────────────'));
447
+ console.log('');
448
+ let pendingChanges = new Map();
449
+ let syncTimeout = null;
450
+ let isSyncing = false;
451
+ function getTimestamp() {
452
+ return new Date().toLocaleTimeString('en-US', {
453
+ hour12: false,
454
+ hour: '2-digit',
455
+ minute: '2-digit',
456
+ second: '2-digit'
457
+ });
458
+ }
459
+ function fileMatchesAnySource(filePath) {
460
+ for (const source of sources) {
461
+ const expanded = expandPath(source.path);
462
+ if (filePath.startsWith(expanded)) {
463
+ const relativePath = path.relative(expanded, filePath);
464
+ if (matchesGlob(relativePath, source.glob)) {
465
+ return true;
466
+ }
467
+ }
468
+ }
469
+ return false;
470
+ }
471
+ async function runSync() {
472
+ if (isSyncing)
473
+ return;
474
+ isSyncing = true;
475
+ const changes = Array.from(pendingChanges.values());
476
+ pendingChanges.clear();
477
+ const ts = getTimestamp();
478
+ console.log(` ${c.time(ts)} ${c.badge('SYNC', colors.bgBlue)} Processing ${changes.length} file(s)...`);
479
+ for (const change of changes) {
480
+ const icon = change.type === 'add' ? '+' : '~';
481
+ const relativePath = change.path.replace(process.env.HOME || '', '~');
482
+ console.log(` ${c.dim(icon)} ${c.file(path.basename(change.path))}`);
483
+ console.log(` ${c.path(relativePath)}`);
484
+ }
485
+ try {
486
+ const result = await handleSync(dbPath, dataDir, {
487
+ git_pull: false,
488
+ git_push: true,
489
+ }, { hookContext: { mode: 'cli' } });
490
+ const processed = result.processing?.processed || 0;
491
+ const errors = result.processing?.errors || 0;
492
+ if (processed > 0) {
493
+ console.log(` ${c.time(ts)} ${c.badge('DONE', colors.bgGreen)} Indexed ${processed} file(s):`);
494
+ for (const title of result.processing?.titles || []) {
495
+ console.log(` ${c.success('✓')} ${title}`);
496
+ }
497
+ }
498
+ else if (result.discovery && result.discovery.new_files === 0) {
499
+ console.log(` ${c.time(ts)} ${c.badge('SKIP', colors.bgYellow)} Already indexed`);
500
+ }
501
+ if (errors > 0) {
502
+ console.log(` ${c.time(ts)} ${c.error(`${errors} ERROR(S)`)}`);
503
+ }
504
+ }
505
+ catch (error) {
506
+ console.log(` ${c.time(ts)} ${c.error('SYNC FAILED')} ${error}`);
507
+ }
508
+ isSyncing = false;
509
+ console.log('');
510
+ }
511
+ function scheduleSync(filePath, type) {
512
+ pendingChanges.set(filePath, { type, path: filePath });
513
+ if (syncTimeout) {
514
+ clearTimeout(syncTimeout);
515
+ }
516
+ syncTimeout = setTimeout(runSync, debounceMs);
517
+ }
518
+ // Set up file watcher
519
+ let watcher = null;
520
+ if (watchPaths.length > 0) {
521
+ watcher = chokidar.watch(watchPaths, {
522
+ ignored: [
523
+ /(^|[\\/])\../,
524
+ /node_modules/,
525
+ /__pycache__/,
526
+ /\.lance$/,
527
+ /vectors\.lance/,
528
+ /\.db$/,
529
+ /\.sqlite$/,
530
+ ],
531
+ persistent: true,
532
+ ignoreInitial: true,
533
+ awaitWriteFinish: {
534
+ stabilityThreshold: 500,
535
+ pollInterval: 100,
536
+ },
537
+ });
538
+ watcher
539
+ .on('add', (filePath) => {
540
+ if (!fileMatchesAnySource(filePath))
541
+ return;
542
+ const ts = getTimestamp();
543
+ console.log(` ${c.time(ts)} ${c.success('+')} ${c.file(path.basename(filePath))} ${c.dim('added')}`);
544
+ scheduleSync(filePath, 'add');
545
+ })
546
+ .on('change', (filePath) => {
547
+ if (!fileMatchesAnySource(filePath))
548
+ return;
549
+ const ts = getTimestamp();
550
+ console.log(` ${c.time(ts)} ${c.warning('~')} ${c.file(path.basename(filePath))} ${c.dim('modified')}`);
551
+ scheduleSync(filePath, 'change');
552
+ })
553
+ .on('error', (error) => {
554
+ console.log(` ${c.error('WATCHER ERROR')} ${error}`);
555
+ });
556
+ }
557
+ // Periodic sync
558
+ const PULL_INTERVAL_MS = 5 * 60 * 1000;
559
+ async function periodicSync() {
560
+ if (isSyncing)
561
+ return;
562
+ const ts = getTimestamp();
563
+ console.log(` ${c.time(ts)} ${c.badge('PULL', colors.bgBlue)} Checking for remote changes...`);
564
+ try {
565
+ const result = await handleSync(dbPath, dataDir, {
566
+ git_pull: true,
567
+ git_push: false,
568
+ }, { hookContext: { mode: 'cli' } });
569
+ if (result.git_pulled) {
570
+ console.log(` ${c.time(ts)} ${c.success('✓')} Pulled latest changes`);
571
+ }
572
+ const newFiles = result.discovery?.new_files || 0;
573
+ if (newFiles > 0) {
574
+ console.log(` ${c.time(ts)} ${c.info('→')} Found ${newFiles} new file(s) from remote`);
575
+ const processResult = await handleSync(dbPath, dataDir, {
576
+ git_pull: false,
577
+ git_push: true,
578
+ }, { hookContext: { mode: 'cli' } });
579
+ if (processResult.processing && processResult.processing.processed > 0) {
580
+ console.log(` ${c.time(ts)} ${c.badge('DONE', colors.bgGreen)} Indexed ${processResult.processing.processed} file(s):`);
581
+ for (const title of processResult.processing.titles) {
582
+ console.log(` ${c.success('✓')} ${title}`);
583
+ }
584
+ }
585
+ }
586
+ else {
587
+ console.log(` ${c.time(ts)} ${c.dim('✓ Up to date')}`);
588
+ }
589
+ }
590
+ catch (error) {
591
+ console.log(` ${c.time(ts)} ${c.warning('⚠')} Pull failed: ${error}`);
592
+ }
593
+ console.log('');
594
+ }
595
+ console.log(` ${c.dim(`Remote sync every ${PULL_INTERVAL_MS / 60000} minutes`)}`);
596
+ console.log('');
597
+ await periodicSync();
598
+ const pullInterval = setInterval(periodicSync, PULL_INTERVAL_MS);
599
+ // Handle shutdown
600
+ process.on('SIGINT', async () => {
601
+ console.log('\n\nShutting down...');
602
+ clearInterval(pullInterval);
603
+ if (watcher) {
604
+ await watcher.close();
605
+ }
606
+ console.log('Goodbye!');
607
+ process.exit(0);
608
+ });
609
+ });
610
+ // Source management (flat, not nested under "sources")
611
+ syncCmd
612
+ .command('list')
613
+ .description('List configured sync sources')
614
+ .action(async () => {
615
+ const { loadSyncConfig, getConfigPath } = await import('../../sync/config.js');
616
+ console.log(`\nSync Sources`);
617
+ console.log(`============`);
618
+ console.log(`Config: ${getConfigPath()}\n`);
619
+ const config = await loadSyncConfig();
620
+ if (config.sources.length === 0) {
621
+ console.log('No sources configured. Run "lore sync add" to add one.');
622
+ return;
623
+ }
624
+ for (const source of config.sources) {
625
+ const status = source.enabled ? '✓' : '○';
626
+ console.log(`${status} ${source.name}`);
627
+ console.log(` Path: ${source.path}`);
628
+ console.log(` Glob: ${source.glob}`);
629
+ console.log(` Project: ${source.project}`);
630
+ console.log('');
631
+ }
632
+ });
633
+ syncCmd
634
+ .command('add')
635
+ .description('Add a new sync source directory')
636
+ .option('-n, --name <name>', 'Source name')
637
+ .option('-p, --path <path>', 'Directory path')
638
+ .option('-g, --glob <glob>', 'File glob pattern', '**/*.md')
639
+ .option('--project <project>', 'Default project')
640
+ .action(async (options) => {
641
+ const { addSyncSource } = await import('../../sync/config.js');
642
+ const readline = await import('readline');
643
+ const rl = readline.createInterface({
644
+ input: process.stdin,
645
+ output: process.stdout,
646
+ });
647
+ const ask = (question, defaultValue) => new Promise((resolve) => {
648
+ const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
649
+ rl.question(prompt, (answer) => {
650
+ resolve(answer.trim() || defaultValue || '');
651
+ });
652
+ });
653
+ console.log(`\nAdd Sync Source`);
654
+ console.log(`===============\n`);
655
+ const name = options.name || await ask('Name (e.g., "Granola Meetings")');
656
+ const sourcePath = options.path || await ask('Path (e.g., ~/granola-extractor/output)');
657
+ const glob = options.glob || await ask('Glob pattern', '**/*.md');
658
+ const project = options.project || await ask('Default project');
659
+ rl.close();
660
+ if (!name || !sourcePath || !project) {
661
+ console.log('\nAll fields are required.');
662
+ process.exit(1);
663
+ }
664
+ try {
665
+ await addSyncSource({
666
+ name,
667
+ path: sourcePath,
668
+ glob,
669
+ project,
670
+ enabled: true,
671
+ });
672
+ console.log(`\n✓ Added source "${name}"`);
673
+ console.log(`\nRun "lore sync" to process files from this source.`);
674
+ }
675
+ catch (error) {
676
+ console.error(`\nError: ${error}`);
677
+ process.exit(1);
678
+ }
679
+ });
680
+ syncCmd
681
+ .command('enable <name>')
682
+ .description('Enable a sync source')
683
+ .action(async (name) => {
684
+ const { updateSyncSource } = await import('../../sync/config.js');
685
+ try {
686
+ await updateSyncSource(name, { enabled: true });
687
+ console.log(`✓ Enabled "${name}"`);
688
+ }
689
+ catch (error) {
690
+ console.error(`Error: ${error}`);
691
+ process.exit(1);
692
+ }
693
+ });
694
+ syncCmd
695
+ .command('disable <name>')
696
+ .description('Disable a sync source')
697
+ .action(async (name) => {
698
+ const { updateSyncSource } = await import('../../sync/config.js');
699
+ try {
700
+ await updateSyncSource(name, { enabled: false });
701
+ console.log(`✓ Disabled "${name}"`);
702
+ }
703
+ catch (error) {
704
+ console.error(`Error: ${error}`);
705
+ process.exit(1);
706
+ }
707
+ });
708
+ syncCmd
709
+ .command('remove <name>')
710
+ .description('Remove a sync source')
711
+ .action(async (name) => {
712
+ const { removeSyncSource } = await import('../../sync/config.js');
713
+ try {
714
+ await removeSyncSource(name);
715
+ console.log(`✓ Removed "${name}"`);
716
+ }
717
+ catch (error) {
718
+ console.error(`Error: ${error}`);
719
+ process.exit(1);
720
+ }
721
+ });
722
+ }
723
+ // ============================================================================
724
+ // Exported helpers
725
+ // ============================================================================
726
+ /**
727
+ * Start the background sync daemon process.
728
+ * Returns { pid } on success, null on failure.
729
+ * If already running, returns the existing PID.
730
+ */
731
+ export async function startDaemonProcess(dataDir) {
732
+ await ensureConfigDir();
733
+ const existingPid = getPid();
734
+ if (existingPid) {
735
+ return { pid: existingPid, alreadyRunning: true };
736
+ }
737
+ // macOS: use launchd for persistence across reboots
738
+ if (isMacOS()) {
739
+ const result = installLaunchdAgent(dataDir);
740
+ if (result) {
741
+ return { pid: result.pid, alreadyRunning: false };
742
+ }
743
+ return null;
744
+ }
745
+ // Non-macOS: use nohup fallback
746
+ const scriptPath = path.join(__dirname, '..', '..', 'daemon-runner.js');
747
+ const nodePath = process.execPath;
748
+ const tmpScript = path.join(os.tmpdir(), `lore-daemon-start-${Date.now()}.sh`);
749
+ const scriptContent = `#!/bin/bash\nnohup "${nodePath}" "${scriptPath}" "${dataDir}" > /dev/null 2>&1 &\n`;
750
+ writeFileSync(tmpScript, scriptContent, { mode: 0o755 });
751
+ spawnSync('/bin/bash', [tmpScript], { stdio: 'ignore' });
752
+ try {
753
+ unlinkSync(tmpScript);
754
+ }
755
+ catch { }
756
+ // Wait for daemon to start and write PID file
757
+ await new Promise(resolve => setTimeout(resolve, 1000));
758
+ try {
759
+ const daemonPid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
760
+ process.kill(daemonPid, 0); // Verify running
761
+ return { pid: daemonPid, alreadyRunning: false };
762
+ }
763
+ catch {
764
+ return null;
765
+ }
766
+ }
767
+ // Export for daemon-runner and browse
768
+ export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };