@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.
- package/LICENSE +13 -0
- package/README.md +80 -0
- package/dist/cli/colors.d.ts +48 -0
- package/dist/cli/colors.js +48 -0
- package/dist/cli/commands/ask.d.ts +7 -0
- package/dist/cli/commands/ask.js +97 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.js +484 -0
- package/dist/cli/commands/daemon.d.ts +22 -0
- package/dist/cli/commands/daemon.js +244 -0
- package/dist/cli/commands/docs.d.ts +7 -0
- package/dist/cli/commands/docs.js +188 -0
- package/dist/cli/commands/extensions.d.ts +7 -0
- package/dist/cli/commands/extensions.js +204 -0
- package/dist/cli/commands/misc.d.ts +7 -0
- package/dist/cli/commands/misc.js +172 -0
- package/dist/cli/commands/pending.d.ts +7 -0
- package/dist/cli/commands/pending.js +63 -0
- package/dist/cli/commands/projects.d.ts +7 -0
- package/dist/cli/commands/projects.js +136 -0
- package/dist/cli/commands/search.d.ts +7 -0
- package/dist/cli/commands/search.js +102 -0
- package/dist/cli/commands/skills.d.ts +24 -0
- package/dist/cli/commands/skills.js +447 -0
- package/dist/cli/commands/sources.d.ts +7 -0
- package/dist/cli/commands/sources.js +121 -0
- package/dist/cli/commands/sync.d.ts +31 -0
- package/dist/cli/commands/sync.js +768 -0
- package/dist/cli/helpers.d.ts +30 -0
- package/dist/cli/helpers.js +119 -0
- package/dist/core/auth.d.ts +62 -0
- package/dist/core/auth.js +330 -0
- package/dist/core/config.d.ts +41 -0
- package/dist/core/config.js +96 -0
- package/dist/core/data-repo.d.ts +31 -0
- package/dist/core/data-repo.js +146 -0
- package/dist/core/embedder.d.ts +22 -0
- package/dist/core/embedder.js +104 -0
- package/dist/core/git.d.ts +37 -0
- package/dist/core/git.js +140 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +5 -0
- package/dist/core/insight-extractor.d.ts +26 -0
- package/dist/core/insight-extractor.js +114 -0
- package/dist/core/local-search.d.ts +43 -0
- package/dist/core/local-search.js +221 -0
- package/dist/core/themes.d.ts +15 -0
- package/dist/core/themes.js +77 -0
- package/dist/core/types.d.ts +177 -0
- package/dist/core/types.js +9 -0
- package/dist/core/user-settings.d.ts +15 -0
- package/dist/core/user-settings.js +42 -0
- package/dist/core/vector-store-lance.d.ts +98 -0
- package/dist/core/vector-store-lance.js +384 -0
- package/dist/core/vector-store-supabase.d.ts +89 -0
- package/dist/core/vector-store-supabase.js +295 -0
- package/dist/core/vector-store.d.ts +131 -0
- package/dist/core/vector-store.js +503 -0
- package/dist/daemon-runner.d.ts +8 -0
- package/dist/daemon-runner.js +246 -0
- package/dist/extensions/config.d.ts +22 -0
- package/dist/extensions/config.js +102 -0
- package/dist/extensions/proposals.d.ts +30 -0
- package/dist/extensions/proposals.js +178 -0
- package/dist/extensions/registry.d.ts +35 -0
- package/dist/extensions/registry.js +309 -0
- package/dist/extensions/sandbox.d.ts +16 -0
- package/dist/extensions/sandbox.js +17 -0
- package/dist/extensions/types.d.ts +114 -0
- package/dist/extensions/types.js +4 -0
- package/dist/extensions/worker.d.ts +1 -0
- package/dist/extensions/worker.js +49 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +105 -0
- package/dist/mcp/handlers/archive-project.d.ts +51 -0
- package/dist/mcp/handlers/archive-project.js +112 -0
- package/dist/mcp/handlers/get-quotes.d.ts +27 -0
- package/dist/mcp/handlers/get-quotes.js +61 -0
- package/dist/mcp/handlers/get-source.d.ts +9 -0
- package/dist/mcp/handlers/get-source.js +40 -0
- package/dist/mcp/handlers/ingest.d.ts +25 -0
- package/dist/mcp/handlers/ingest.js +305 -0
- package/dist/mcp/handlers/list-projects.d.ts +4 -0
- package/dist/mcp/handlers/list-projects.js +16 -0
- package/dist/mcp/handlers/list-sources.d.ts +11 -0
- package/dist/mcp/handlers/list-sources.js +20 -0
- package/dist/mcp/handlers/research-agent.d.ts +21 -0
- package/dist/mcp/handlers/research-agent.js +369 -0
- package/dist/mcp/handlers/research.d.ts +22 -0
- package/dist/mcp/handlers/research.js +225 -0
- package/dist/mcp/handlers/retain.d.ts +18 -0
- package/dist/mcp/handlers/retain.js +92 -0
- package/dist/mcp/handlers/search.d.ts +52 -0
- package/dist/mcp/handlers/search.js +145 -0
- package/dist/mcp/handlers/sync.d.ts +47 -0
- package/dist/mcp/handlers/sync.js +211 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +268 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +297 -0
- package/dist/sync/config.d.ts +26 -0
- package/dist/sync/config.js +140 -0
- package/dist/sync/discover.d.ts +51 -0
- package/dist/sync/discover.js +190 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.js +11 -0
- package/dist/sync/process.d.ts +50 -0
- package/dist/sync/process.js +285 -0
- package/dist/sync/processors.d.ts +24 -0
- package/dist/sync/processors.js +351 -0
- package/dist/tui/browse-handlers-ask.d.ts +30 -0
- package/dist/tui/browse-handlers-ask.js +372 -0
- package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
- package/dist/tui/browse-handlers-autocomplete.js +270 -0
- package/dist/tui/browse-handlers-extensions.d.ts +18 -0
- package/dist/tui/browse-handlers-extensions.js +107 -0
- package/dist/tui/browse-handlers-pending.d.ts +22 -0
- package/dist/tui/browse-handlers-pending.js +100 -0
- package/dist/tui/browse-handlers-research.d.ts +32 -0
- package/dist/tui/browse-handlers-research.js +363 -0
- package/dist/tui/browse-handlers-tools.d.ts +42 -0
- package/dist/tui/browse-handlers-tools.js +289 -0
- package/dist/tui/browse-handlers.d.ts +239 -0
- package/dist/tui/browse-handlers.js +1944 -0
- package/dist/tui/browse-render-extensions.d.ts +14 -0
- package/dist/tui/browse-render-extensions.js +114 -0
- package/dist/tui/browse-render-tools.d.ts +18 -0
- package/dist/tui/browse-render-tools.js +259 -0
- package/dist/tui/browse-render.d.ts +51 -0
- package/dist/tui/browse-render.js +599 -0
- package/dist/tui/browse-types.d.ts +142 -0
- package/dist/tui/browse-types.js +70 -0
- package/dist/tui/browse-ui.d.ts +10 -0
- package/dist/tui/browse-ui.js +432 -0
- package/dist/tui/browse.d.ts +17 -0
- package/dist/tui/browse.js +625 -0
- package/dist/tui/markdown.d.ts +22 -0
- package/dist/tui/markdown.js +223 -0
- package/package.json +71 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
- package/plugins/claude-code/.mcp.json +6 -0
- package/plugins/claude-code/skills/lore/SKILL.md +63 -0
- package/plugins/codex/SKILL.md +36 -0
- package/plugins/codex/agents/openai.yaml +10 -0
- package/plugins/gemini/GEMINI.md +31 -0
- package/plugins/gemini/gemini-extension.json +11 -0
- package/skills/generic-agent.md +99 -0
- package/skills/openclaw.md +67 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Command
|
|
3
|
+
*
|
|
4
|
+
* Run lore watch as a background daemon with logging and status tracking.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } 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
|
+
// Config directory for daemon files
|
|
12
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
|
|
13
|
+
const PID_FILE = path.join(CONFIG_DIR, 'daemon.pid');
|
|
14
|
+
const STATUS_FILE = path.join(CONFIG_DIR, 'daemon.status.json');
|
|
15
|
+
const LOG_FILE = path.join(CONFIG_DIR, 'daemon.log');
|
|
16
|
+
async function ensureConfigDir() {
|
|
17
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
function getPid() {
|
|
20
|
+
if (!existsSync(PID_FILE))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
24
|
+
// Check if process is actually running
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
27
|
+
return pid;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Process not running, clean up stale PID file
|
|
31
|
+
unlinkSync(PID_FILE);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getStatus() {
|
|
40
|
+
if (!existsSync(STATUS_FILE))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(STATUS_FILE, 'utf-8'));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function registerDaemonCommand(program, defaultDataDir) {
|
|
50
|
+
const daemonCmd = program
|
|
51
|
+
.command('daemon')
|
|
52
|
+
.description('Manage the lore background sync daemon');
|
|
53
|
+
daemonCmd
|
|
54
|
+
.command('start')
|
|
55
|
+
.description('Start the daemon in background')
|
|
56
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
await ensureConfigDir();
|
|
59
|
+
const existingPid = getPid();
|
|
60
|
+
if (existingPid) {
|
|
61
|
+
console.log(`Daemon already running (PID: ${existingPid})`);
|
|
62
|
+
console.log(`Use "lore daemon status" to check status`);
|
|
63
|
+
console.log(`Use "lore daemon stop" to stop it`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Find the daemon runner script path
|
|
67
|
+
const scriptPath = path.join(path.dirname(process.argv[1]), 'daemon-runner.js');
|
|
68
|
+
// Spawn the daemon process
|
|
69
|
+
const child = spawn('node', [scriptPath, options.dataDir], {
|
|
70
|
+
detached: true,
|
|
71
|
+
stdio: 'ignore',
|
|
72
|
+
env: { ...process.env, LORE_DAEMON_MODE: 'true' },
|
|
73
|
+
});
|
|
74
|
+
child.unref();
|
|
75
|
+
// Write PID file
|
|
76
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
77
|
+
// Write initial status
|
|
78
|
+
const status = {
|
|
79
|
+
pid: child.pid,
|
|
80
|
+
started_at: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2));
|
|
83
|
+
console.log(`Daemon started (PID: ${child.pid})`);
|
|
84
|
+
console.log(`Log file: ${LOG_FILE}`);
|
|
85
|
+
console.log(`Use "lore daemon logs" to view activity`);
|
|
86
|
+
});
|
|
87
|
+
daemonCmd
|
|
88
|
+
.command('stop')
|
|
89
|
+
.description('Stop the daemon')
|
|
90
|
+
.action(async () => {
|
|
91
|
+
const pid = getPid();
|
|
92
|
+
if (!pid) {
|
|
93
|
+
console.log('Daemon is not running');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, 'SIGTERM');
|
|
98
|
+
console.log(`Daemon stopped (PID: ${pid})`);
|
|
99
|
+
// Clean up files
|
|
100
|
+
if (existsSync(PID_FILE))
|
|
101
|
+
unlinkSync(PID_FILE);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`Failed to stop daemon: ${error}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
daemonCmd
|
|
108
|
+
.command('restart')
|
|
109
|
+
.description('Restart the daemon')
|
|
110
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
111
|
+
.action(async (options) => {
|
|
112
|
+
const pid = getPid();
|
|
113
|
+
if (pid) {
|
|
114
|
+
try {
|
|
115
|
+
process.kill(pid, 'SIGTERM');
|
|
116
|
+
console.log(`Stopped existing daemon (PID: ${pid})`);
|
|
117
|
+
if (existsSync(PID_FILE))
|
|
118
|
+
unlinkSync(PID_FILE);
|
|
119
|
+
// Wait a moment for process to exit
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Process might already be dead
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Start new daemon
|
|
127
|
+
await ensureConfigDir();
|
|
128
|
+
const scriptPath = path.join(path.dirname(process.argv[1]), 'daemon-runner.js');
|
|
129
|
+
const child = spawn('node', [scriptPath, options.dataDir], {
|
|
130
|
+
detached: true,
|
|
131
|
+
stdio: 'ignore',
|
|
132
|
+
env: { ...process.env, LORE_DAEMON_MODE: 'true' },
|
|
133
|
+
});
|
|
134
|
+
child.unref();
|
|
135
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
136
|
+
const status = {
|
|
137
|
+
pid: child.pid,
|
|
138
|
+
started_at: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2));
|
|
141
|
+
console.log(`Daemon restarted (PID: ${child.pid})`);
|
|
142
|
+
});
|
|
143
|
+
daemonCmd
|
|
144
|
+
.command('status')
|
|
145
|
+
.description('Check daemon status')
|
|
146
|
+
.action(async () => {
|
|
147
|
+
const pid = getPid();
|
|
148
|
+
const status = getStatus();
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log('Lore Daemon Status');
|
|
151
|
+
console.log('==================');
|
|
152
|
+
if (!pid) {
|
|
153
|
+
console.log('Status: NOT RUNNING');
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('Start with: lore daemon start');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`Status: RUNNING (PID: ${pid})`);
|
|
159
|
+
if (status) {
|
|
160
|
+
const started = new Date(status.started_at);
|
|
161
|
+
const uptime = formatUptime(Date.now() - started.getTime());
|
|
162
|
+
console.log(`Uptime: ${uptime}`);
|
|
163
|
+
if (status.last_sync) {
|
|
164
|
+
const lastSync = new Date(status.last_sync);
|
|
165
|
+
const ago = formatAgo(Date.now() - lastSync.getTime());
|
|
166
|
+
console.log(`Last sync: ${ago}`);
|
|
167
|
+
if (status.last_sync_result) {
|
|
168
|
+
const r = status.last_sync_result;
|
|
169
|
+
console.log(` Files scanned: ${r.files_scanned}`);
|
|
170
|
+
console.log(` Files processed: ${r.files_processed}`);
|
|
171
|
+
if (r.errors > 0) {
|
|
172
|
+
console.log(` Errors: ${r.errors}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log('Last sync: (not yet synced)');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(`Log file: ${LOG_FILE}`);
|
|
182
|
+
console.log('View logs: lore daemon logs');
|
|
183
|
+
});
|
|
184
|
+
daemonCmd
|
|
185
|
+
.command('logs')
|
|
186
|
+
.description('View daemon logs')
|
|
187
|
+
.option('-f, --follow', 'Follow log output (like tail -f)')
|
|
188
|
+
.option('-n, --lines <n>', 'Number of lines to show', '50')
|
|
189
|
+
.action(async (options) => {
|
|
190
|
+
if (!existsSync(LOG_FILE)) {
|
|
191
|
+
console.log('No log file found. Daemon may not have run yet.');
|
|
192
|
+
console.log(`Expected: ${LOG_FILE}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (options.follow) {
|
|
196
|
+
// Use tail -f for following
|
|
197
|
+
const tail = spawn('tail', ['-f', LOG_FILE], {
|
|
198
|
+
stdio: 'inherit',
|
|
199
|
+
});
|
|
200
|
+
process.on('SIGINT', () => {
|
|
201
|
+
tail.kill();
|
|
202
|
+
process.exit(0);
|
|
203
|
+
});
|
|
204
|
+
await new Promise(() => { }); // Wait forever
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// Show last N lines
|
|
208
|
+
const content = readFileSync(LOG_FILE, 'utf-8');
|
|
209
|
+
const lines = content.trim().split('\n');
|
|
210
|
+
const n = parseInt(options.lines, 10);
|
|
211
|
+
const lastLines = lines.slice(-n);
|
|
212
|
+
console.log(`Last ${Math.min(n, lastLines.length)} log entries:\n`);
|
|
213
|
+
console.log(lastLines.join('\n'));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function formatUptime(ms) {
|
|
218
|
+
const seconds = Math.floor(ms / 1000);
|
|
219
|
+
const minutes = Math.floor(seconds / 60);
|
|
220
|
+
const hours = Math.floor(minutes / 60);
|
|
221
|
+
const days = Math.floor(hours / 24);
|
|
222
|
+
if (days > 0)
|
|
223
|
+
return `${days}d ${hours % 24}h`;
|
|
224
|
+
if (hours > 0)
|
|
225
|
+
return `${hours}h ${minutes % 60}m`;
|
|
226
|
+
if (minutes > 0)
|
|
227
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
228
|
+
return `${seconds}s`;
|
|
229
|
+
}
|
|
230
|
+
function formatAgo(ms) {
|
|
231
|
+
const seconds = Math.floor(ms / 1000);
|
|
232
|
+
if (seconds < 60)
|
|
233
|
+
return 'just now';
|
|
234
|
+
const minutes = Math.floor(seconds / 60);
|
|
235
|
+
if (minutes < 60)
|
|
236
|
+
return `${minutes}m ago`;
|
|
237
|
+
const hours = Math.floor(minutes / 60);
|
|
238
|
+
if (hours < 24)
|
|
239
|
+
return `${hours}h ago`;
|
|
240
|
+
const days = Math.floor(hours / 24);
|
|
241
|
+
return `${days}d ago`;
|
|
242
|
+
}
|
|
243
|
+
// Export paths for use by daemon runner
|
|
244
|
+
export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documents CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for documents: list, get, create, delete
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { getSourceById } from '../../core/vector-store.js';
|
|
8
|
+
export function registerDocsCommand(program, defaultDataDir) {
|
|
9
|
+
const docsCmd = program
|
|
10
|
+
.command('docs')
|
|
11
|
+
.description('Document operations (CRUD)');
|
|
12
|
+
// List documents
|
|
13
|
+
docsCmd
|
|
14
|
+
.command('list')
|
|
15
|
+
.description('List all documents')
|
|
16
|
+
.option('-p, --project <project>', 'Filter by project')
|
|
17
|
+
.option('-t, --type <type>', 'Filter by source type')
|
|
18
|
+
.option('-l, --limit <limit>', 'Max results', '20')
|
|
19
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
const { handleListSources } = await import('../../mcp/handlers/list-sources.js');
|
|
22
|
+
const dataDir = options.dataDir;
|
|
23
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
24
|
+
const result = await handleListSources(dbPath, {
|
|
25
|
+
project: options.project,
|
|
26
|
+
source_type: options.type,
|
|
27
|
+
limit: parseInt(options.limit),
|
|
28
|
+
});
|
|
29
|
+
console.log(`\nDocuments (${result.total}):`);
|
|
30
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
31
|
+
if (result.sources.length === 0) {
|
|
32
|
+
console.log('No documents found.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const source of result.sources) {
|
|
36
|
+
const date = new Date(source.created_at).toLocaleDateString();
|
|
37
|
+
console.log(`\n📄 ${source.title}`);
|
|
38
|
+
console.log(` ID: ${source.id}`);
|
|
39
|
+
console.log(` Type: ${source.source_type} | ${source.content_type}`);
|
|
40
|
+
console.log(` Projects: ${source.projects.join(', ') || '(none)'}`);
|
|
41
|
+
console.log(` Date: ${date}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('');
|
|
44
|
+
});
|
|
45
|
+
// Get document
|
|
46
|
+
docsCmd
|
|
47
|
+
.command('get')
|
|
48
|
+
.description('Get full details of a document')
|
|
49
|
+
.argument('<id>', 'Document ID')
|
|
50
|
+
.option('-c, --content', 'Include full content')
|
|
51
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
52
|
+
.action(async (docId, options) => {
|
|
53
|
+
const { handleGetSource } = await import('../../mcp/handlers/get-source.js');
|
|
54
|
+
const dataDir = options.dataDir;
|
|
55
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
56
|
+
const result = await handleGetSource(dbPath, dataDir, {
|
|
57
|
+
source_id: docId,
|
|
58
|
+
include_content: options.content,
|
|
59
|
+
});
|
|
60
|
+
if (result.error) {
|
|
61
|
+
console.error(`Error: ${result.error}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
console.log(`\n📄 ${result.title}`);
|
|
65
|
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
|
66
|
+
console.log(`ID: ${result.id}`);
|
|
67
|
+
console.log(`Type: ${result.source_type} | ${result.content_type}`);
|
|
68
|
+
console.log(`Projects: ${result.projects?.join(', ') || '(none)'}`);
|
|
69
|
+
console.log(`Tags: ${result.tags?.join(', ') || '(none)'}`);
|
|
70
|
+
console.log(`Created: ${result.created_at}`);
|
|
71
|
+
console.log(`\nSummary:\n${result.summary}`);
|
|
72
|
+
const themes = result.themes;
|
|
73
|
+
if (themes && themes.length > 0) {
|
|
74
|
+
console.log(`\nThemes:`);
|
|
75
|
+
for (const theme of themes) {
|
|
76
|
+
console.log(` • ${theme.name}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const quotes = result.quotes;
|
|
80
|
+
if (quotes && quotes.length > 0) {
|
|
81
|
+
console.log(`\nQuotes (${quotes.length}):`);
|
|
82
|
+
for (const quote of quotes.slice(0, 5)) {
|
|
83
|
+
const speaker = quote.speaker === 'user' ? '[You]' : '[Participant]';
|
|
84
|
+
console.log(` ${speaker} "${quote.text.substring(0, 100)}${quote.text.length > 100 ? '...' : ''}"`);
|
|
85
|
+
}
|
|
86
|
+
if (quotes.length > 5) {
|
|
87
|
+
console.log(` ... and ${quotes.length - 5} more`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (result.full_content) {
|
|
91
|
+
console.log(`\n${'─'.repeat(60)}`);
|
|
92
|
+
console.log(`Full Content:\n`);
|
|
93
|
+
console.log(result.full_content);
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
});
|
|
97
|
+
// Create (save a note/insight)
|
|
98
|
+
docsCmd
|
|
99
|
+
.command('create')
|
|
100
|
+
.description('Create a note or insight')
|
|
101
|
+
.argument('<content>', 'Content to save')
|
|
102
|
+
.requiredOption('-p, --project <project>', 'Project this belongs to')
|
|
103
|
+
.option('-t, --type <type>', 'Type: insight, decision, requirement, note', 'note')
|
|
104
|
+
.option('--context <context>', 'Source context (e.g., "from interview with Sarah")')
|
|
105
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
106
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
107
|
+
.option('--no-push', 'Skip git push')
|
|
108
|
+
.action(async (content, options) => {
|
|
109
|
+
const { handleRetain } = await import('../../mcp/handlers/retain.js');
|
|
110
|
+
const dataDir = options.dataDir;
|
|
111
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
112
|
+
const validTypes = ['insight', 'decision', 'requirement', 'note'];
|
|
113
|
+
if (!validTypes.includes(options.type)) {
|
|
114
|
+
console.error(`Invalid type: ${options.type}. Must be one of: ${validTypes.join(', ')}`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const result = await handleRetain(dbPath, dataDir, {
|
|
118
|
+
content,
|
|
119
|
+
project: options.project,
|
|
120
|
+
type: options.type,
|
|
121
|
+
source_context: options.context,
|
|
122
|
+
tags: options.tags?.split(',').map((t) => t.trim()),
|
|
123
|
+
}, { autoPush: options.push !== false });
|
|
124
|
+
if (result.success) {
|
|
125
|
+
console.log(`\n✓ ${result.message}`);
|
|
126
|
+
console.log(` ID: ${result.id}`);
|
|
127
|
+
console.log(` Indexed: ${result.indexed ? 'yes' : 'no'}`);
|
|
128
|
+
console.log(` Synced: ${result.synced ? 'yes' : 'no'}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.error(`\nFailed to create: ${result.message}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// Delete document
|
|
136
|
+
docsCmd
|
|
137
|
+
.command('delete')
|
|
138
|
+
.description('Delete a document')
|
|
139
|
+
.argument('<id>', 'Document ID')
|
|
140
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
141
|
+
.option('--force', 'Skip confirmation')
|
|
142
|
+
.action(async (docId, options) => {
|
|
143
|
+
const dataDir = options.dataDir;
|
|
144
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
145
|
+
// Get document info first
|
|
146
|
+
const source = await getSourceById(dbPath, docId);
|
|
147
|
+
if (!source) {
|
|
148
|
+
console.error(`Document not found: ${docId}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
if (!options.force) {
|
|
152
|
+
const readline = await import('readline');
|
|
153
|
+
const rl = readline.createInterface({
|
|
154
|
+
input: process.stdin,
|
|
155
|
+
output: process.stdout,
|
|
156
|
+
});
|
|
157
|
+
const answer = await new Promise((resolve) => {
|
|
158
|
+
rl.question(`Delete "${source.title}"? (y/N) `, resolve);
|
|
159
|
+
});
|
|
160
|
+
rl.close();
|
|
161
|
+
if (answer.toLowerCase() !== 'y') {
|
|
162
|
+
console.log('Cancelled.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Delete from vector store
|
|
167
|
+
const { deleteSource } = await import('../../core/vector-store.js');
|
|
168
|
+
const { sourcePath: originalPath } = await deleteSource(dbPath, docId);
|
|
169
|
+
// Delete from disk (lore-data copy)
|
|
170
|
+
const { rm } = await import('fs/promises');
|
|
171
|
+
const loreSourcePath = path.join(dataDir, 'sources', docId);
|
|
172
|
+
try {
|
|
173
|
+
await rm(loreSourcePath, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// File may not exist on disk
|
|
177
|
+
}
|
|
178
|
+
// Delete original source file from sync directory (and commit to its repo)
|
|
179
|
+
if (originalPath) {
|
|
180
|
+
const { deleteFileAndCommit } = await import('../../core/git.js');
|
|
181
|
+
await deleteFileAndCommit(originalPath, `Delete: ${source.title.slice(0, 50)}`);
|
|
182
|
+
}
|
|
183
|
+
// Git commit the lore-data changes
|
|
184
|
+
const { gitCommitAndPush } = await import('../../core/git.js');
|
|
185
|
+
await gitCommitAndPush(dataDir, `Delete source: ${source.title.slice(0, 50)}`);
|
|
186
|
+
console.log(`\n✓ Deleted: ${source.title}`);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension Management Commands
|
|
3
|
+
*
|
|
4
|
+
* lore extension install|list|remove
|
|
5
|
+
*/
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { readFile, stat } from 'fs/promises';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { exec, spawn } from 'child_process';
|
|
11
|
+
import { addExtensionToConfig, removeExtensionFromConfig, loadExtensionConfig, ensureExtensionsDir, getExtensionsDir, } from '../../extensions/config.js';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
async function readInstalledVersion(extensionsDir, packageName) {
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = path.join(extensionsDir, 'node_modules', packageName, 'package.json');
|
|
16
|
+
const content = await readFile(pkgPath, 'utf-8');
|
|
17
|
+
const parsed = JSON.parse(content);
|
|
18
|
+
return parsed.version;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function resolveExtensionPath(inputPath) {
|
|
25
|
+
if (inputPath.startsWith('~')) {
|
|
26
|
+
return path.resolve(path.join(os.homedir(), inputPath.slice(1)));
|
|
27
|
+
}
|
|
28
|
+
return path.resolve(inputPath);
|
|
29
|
+
}
|
|
30
|
+
export function registerExtensionCommands(program, defaultDataDir) {
|
|
31
|
+
const extension = program
|
|
32
|
+
.command('extension')
|
|
33
|
+
.description('Manage Lore extensions');
|
|
34
|
+
extension
|
|
35
|
+
.command('list')
|
|
36
|
+
.description('List installed extensions')
|
|
37
|
+
.action(async () => {
|
|
38
|
+
const config = await loadExtensionConfig();
|
|
39
|
+
if (config.extensions.length === 0) {
|
|
40
|
+
console.log('No extensions installed.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log('Installed extensions:');
|
|
44
|
+
for (const ext of config.extensions) {
|
|
45
|
+
const status = ext.enabled === false ? 'disabled' : 'enabled';
|
|
46
|
+
const version = ext.version ? `@${ext.version}` : '';
|
|
47
|
+
console.log(` - ${ext.name}${version} (${status})`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
extension
|
|
51
|
+
.command('install')
|
|
52
|
+
.description('Install an extension from npm')
|
|
53
|
+
.argument('<package>', 'npm package name')
|
|
54
|
+
.action(async (packageName) => {
|
|
55
|
+
const extensionsDir = getExtensionsDir();
|
|
56
|
+
await ensureExtensionsDir();
|
|
57
|
+
console.log(`Installing ${packageName}...`);
|
|
58
|
+
await execAsync(`npm install ${packageName}`, { cwd: extensionsDir });
|
|
59
|
+
const version = await readInstalledVersion(extensionsDir, packageName);
|
|
60
|
+
await addExtensionToConfig(packageName, version);
|
|
61
|
+
console.log(`✓ Installed ${packageName}${version ? `@${version}` : ''}`);
|
|
62
|
+
});
|
|
63
|
+
extension
|
|
64
|
+
.command('dev')
|
|
65
|
+
.description('Link a local extension for development')
|
|
66
|
+
.argument('<path>', 'local path to extension')
|
|
67
|
+
.option('--serve', 'start lore serve --watch after install')
|
|
68
|
+
.action(async (inputPath, options) => {
|
|
69
|
+
const extensionsDir = getExtensionsDir();
|
|
70
|
+
await ensureExtensionsDir();
|
|
71
|
+
const resolvedPath = resolveExtensionPath(inputPath);
|
|
72
|
+
try {
|
|
73
|
+
await stat(resolvedPath);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
console.error(`Path does not exist: ${inputPath}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
let packageName;
|
|
80
|
+
let packageVersion;
|
|
81
|
+
try {
|
|
82
|
+
const pkgPath = path.join(resolvedPath, 'package.json');
|
|
83
|
+
const content = await readFile(pkgPath, 'utf-8');
|
|
84
|
+
const parsed = JSON.parse(content);
|
|
85
|
+
packageName = parsed.name;
|
|
86
|
+
packageVersion = parsed.version;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
console.error(`No package.json found in: ${inputPath}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (!packageName) {
|
|
93
|
+
console.error(`package.json is missing a name in: ${inputPath}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
console.log(`Installing from ${inputPath}...`);
|
|
97
|
+
await execAsync(`npm install ${resolvedPath}`, { cwd: extensionsDir });
|
|
98
|
+
await addExtensionToConfig(packageName, packageVersion);
|
|
99
|
+
console.log(`✓ Linked ${packageName}${packageVersion ? `@${packageVersion}` : ''}`);
|
|
100
|
+
if (options.serve) {
|
|
101
|
+
console.log('Starting lore serve --watch...');
|
|
102
|
+
const child = spawn('lore serve --watch', { cwd: process.cwd(), stdio: 'inherit', shell: true });
|
|
103
|
+
await new Promise((resolve, reject) => {
|
|
104
|
+
child.on('exit', code => {
|
|
105
|
+
if (code === 0) {
|
|
106
|
+
resolve();
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
reject(new Error(`lore serve --watch exited with code ${code ?? 'unknown'}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
child.on('error', reject);
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log('\nTo test: lore serve --watch');
|
|
117
|
+
});
|
|
118
|
+
extension
|
|
119
|
+
.command('remove')
|
|
120
|
+
.description('Remove an installed extension')
|
|
121
|
+
.argument('<package>', 'npm package name')
|
|
122
|
+
.action(async (packageName) => {
|
|
123
|
+
const extensionsDir = getExtensionsDir();
|
|
124
|
+
await ensureExtensionsDir();
|
|
125
|
+
console.log(`Removing ${packageName}...`);
|
|
126
|
+
await execAsync(`npm remove ${packageName}`, { cwd: extensionsDir });
|
|
127
|
+
await removeExtensionFromConfig(packageName);
|
|
128
|
+
console.log(`✓ Removed ${packageName}`);
|
|
129
|
+
});
|
|
130
|
+
extension
|
|
131
|
+
.command('update')
|
|
132
|
+
.description('Update an extension to the latest version')
|
|
133
|
+
.argument('[package]', 'npm package name (or update all if omitted)')
|
|
134
|
+
.action(async (packageName) => {
|
|
135
|
+
const extensionsDir = getExtensionsDir();
|
|
136
|
+
await ensureExtensionsDir();
|
|
137
|
+
const config = await loadExtensionConfig();
|
|
138
|
+
if (packageName) {
|
|
139
|
+
// Update single extension
|
|
140
|
+
const ext = config.extensions.find(e => e.name === packageName);
|
|
141
|
+
if (!ext) {
|
|
142
|
+
console.error(`Extension ${packageName} is not installed.`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
console.log(`Updating ${packageName}...`);
|
|
146
|
+
await execAsync(`npm update ${packageName}`, { cwd: extensionsDir });
|
|
147
|
+
const newVersion = await readInstalledVersion(extensionsDir, packageName);
|
|
148
|
+
await addExtensionToConfig(packageName, newVersion);
|
|
149
|
+
console.log(`✓ Updated ${packageName}${newVersion ? ` to ${newVersion}` : ''}`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Update all extensions
|
|
153
|
+
if (config.extensions.length === 0) {
|
|
154
|
+
console.log('No extensions installed.');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log('Updating all extensions...');
|
|
158
|
+
for (const ext of config.extensions) {
|
|
159
|
+
console.log(` Updating ${ext.name}...`);
|
|
160
|
+
await execAsync(`npm update ${ext.name}`, { cwd: extensionsDir });
|
|
161
|
+
const newVersion = await readInstalledVersion(extensionsDir, ext.name);
|
|
162
|
+
await addExtensionToConfig(ext.name, newVersion);
|
|
163
|
+
}
|
|
164
|
+
console.log('✓ All extensions updated');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
extension
|
|
168
|
+
.command('enable')
|
|
169
|
+
.description('Enable a disabled extension')
|
|
170
|
+
.argument('<package>', 'npm package name')
|
|
171
|
+
.action(async (packageName) => {
|
|
172
|
+
const config = await loadExtensionConfig();
|
|
173
|
+
const ext = config.extensions.find(e => e.name === packageName);
|
|
174
|
+
if (!ext) {
|
|
175
|
+
console.error(`Extension ${packageName} is not installed.`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
if (ext.enabled !== false) {
|
|
179
|
+
console.log(`Extension ${packageName} is already enabled.`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await addExtensionToConfig(packageName, ext.version, true);
|
|
183
|
+
console.log(`✓ Enabled ${packageName}`);
|
|
184
|
+
});
|
|
185
|
+
extension
|
|
186
|
+
.command('disable')
|
|
187
|
+
.description('Disable an extension without removing it')
|
|
188
|
+
.argument('<package>', 'npm package name')
|
|
189
|
+
.action(async (packageName) => {
|
|
190
|
+
const config = await loadExtensionConfig();
|
|
191
|
+
const ext = config.extensions.find(e => e.name === packageName);
|
|
192
|
+
if (!ext) {
|
|
193
|
+
console.error(`Extension ${packageName} is not installed.`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (ext.enabled === false) {
|
|
197
|
+
console.log(`Extension ${packageName} is already disabled.`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await addExtensionToConfig(packageName, ext.version, false);
|
|
201
|
+
console.log(`✓ Disabled ${packageName}`);
|
|
202
|
+
});
|
|
203
|
+
return extension;
|
|
204
|
+
}
|