@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,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Daemon Runner
|
|
4
|
+
*
|
|
5
|
+
* This script runs as a background process, handling file watching and periodic sync.
|
|
6
|
+
* It writes logs to ~/.config/lore/daemon.log and updates status in daemon.status.json.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import { existsSync, appendFileSync, writeFileSync, readFileSync } from 'fs';
|
|
11
|
+
import { mkdir } from 'fs/promises';
|
|
12
|
+
// Config paths
|
|
13
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
|
|
14
|
+
const PID_FILE = path.join(CONFIG_DIR, 'daemon.pid');
|
|
15
|
+
const STATUS_FILE = path.join(CONFIG_DIR, 'daemon.status.json');
|
|
16
|
+
const LOG_FILE = path.join(CONFIG_DIR, 'daemon.log');
|
|
17
|
+
// Get data directory from command line arg
|
|
18
|
+
const dataDir = process.argv[2] || process.env.LORE_DATA_DIR || '~/.lore';
|
|
19
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
20
|
+
function log(level, message) {
|
|
21
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
22
|
+
const line = `[${timestamp}] ${level.padEnd(5)} ${message}\n`;
|
|
23
|
+
appendFileSync(LOG_FILE, line);
|
|
24
|
+
}
|
|
25
|
+
function updateStatus(updates) {
|
|
26
|
+
try {
|
|
27
|
+
let status = {
|
|
28
|
+
pid: process.pid,
|
|
29
|
+
started_at: new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
if (existsSync(STATUS_FILE)) {
|
|
32
|
+
const existing = JSON.parse(readFileSync(STATUS_FILE, 'utf-8'));
|
|
33
|
+
// Only preserve last_sync info, always use current pid/started_at
|
|
34
|
+
if (existing.last_sync)
|
|
35
|
+
status.last_sync = existing.last_sync;
|
|
36
|
+
if (existing.last_sync_result)
|
|
37
|
+
status.last_sync_result = existing.last_sync_result;
|
|
38
|
+
}
|
|
39
|
+
Object.assign(status, updates);
|
|
40
|
+
writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
log('ERROR', `Failed to update status: ${error}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function runSync(gitPull = true) {
|
|
47
|
+
const { handleSync } = await import('./mcp/handlers/sync.js');
|
|
48
|
+
const result = await handleSync(dbPath, dataDir, {
|
|
49
|
+
git_pull: gitPull,
|
|
50
|
+
git_push: true,
|
|
51
|
+
}, { hookContext: { mode: 'cli' } });
|
|
52
|
+
return {
|
|
53
|
+
files_scanned: result.discovery?.total_files || 0,
|
|
54
|
+
files_processed: result.processing?.processed || 0,
|
|
55
|
+
errors: result.processing?.errors || 0,
|
|
56
|
+
titles: result.processing?.titles || [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
61
|
+
// Bridge config.json → process.env (critical for launchd which has no shell env)
|
|
62
|
+
const { bridgeConfigToEnv } = await import('./core/config.js');
|
|
63
|
+
await bridgeConfigToEnv();
|
|
64
|
+
// Write PID file immediately so parent knows we started
|
|
65
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
66
|
+
// Also initialize status file with correct PID
|
|
67
|
+
updateStatus({});
|
|
68
|
+
log('START', `Daemon starting (PID: ${process.pid})`);
|
|
69
|
+
log('INFO', `Data directory: ${dataDir}`);
|
|
70
|
+
// Load sync config
|
|
71
|
+
const { loadSyncConfig, getEnabledSources, expandPath } = await import('./sync/config.js');
|
|
72
|
+
const { matchesGlob } = await import('./sync/discover.js');
|
|
73
|
+
const config = await loadSyncConfig();
|
|
74
|
+
const sources = getEnabledSources(config);
|
|
75
|
+
if (sources.length === 0) {
|
|
76
|
+
log('WARN', 'No local sync sources configured');
|
|
77
|
+
log('INFO', 'Will still sync from remote');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
for (const source of sources) {
|
|
81
|
+
log('INFO', `Watching: ${source.name} (${expandPath(source.path)})`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Initial sync
|
|
85
|
+
log('SYNC', 'Running initial sync...');
|
|
86
|
+
try {
|
|
87
|
+
const result = await runSync(true);
|
|
88
|
+
log('SYNC', `Initial sync complete: ${result.files_scanned} scanned, ${result.files_processed} processed`);
|
|
89
|
+
for (const title of result.titles) {
|
|
90
|
+
log('INDEX', title);
|
|
91
|
+
}
|
|
92
|
+
updateStatus({
|
|
93
|
+
last_sync: new Date().toISOString(),
|
|
94
|
+
last_sync_result: {
|
|
95
|
+
files_scanned: result.files_scanned,
|
|
96
|
+
files_processed: result.files_processed,
|
|
97
|
+
errors: result.errors,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
log('ERROR', `Initial sync failed: ${error}`);
|
|
103
|
+
}
|
|
104
|
+
// Set up file watcher if we have local sources
|
|
105
|
+
let isSyncing = false;
|
|
106
|
+
let pendingSync = false;
|
|
107
|
+
const debounceMs = 2000;
|
|
108
|
+
let syncTimeout = null;
|
|
109
|
+
async function debouncedSync() {
|
|
110
|
+
if (isSyncing) {
|
|
111
|
+
pendingSync = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
isSyncing = true;
|
|
115
|
+
log('SYNC', 'File change detected, syncing...');
|
|
116
|
+
try {
|
|
117
|
+
const result = await runSync(false); // Don't git pull on file change
|
|
118
|
+
log('SYNC', `Sync complete: ${result.files_processed} files processed`);
|
|
119
|
+
for (const title of result.titles) {
|
|
120
|
+
log('INDEX', title);
|
|
121
|
+
}
|
|
122
|
+
updateStatus({
|
|
123
|
+
last_sync: new Date().toISOString(),
|
|
124
|
+
last_sync_result: {
|
|
125
|
+
files_scanned: result.files_scanned,
|
|
126
|
+
files_processed: result.files_processed,
|
|
127
|
+
errors: result.errors,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
log('ERROR', `Sync failed: ${error}`);
|
|
133
|
+
}
|
|
134
|
+
isSyncing = false;
|
|
135
|
+
// If another sync was requested while we were syncing, run it
|
|
136
|
+
if (pendingSync) {
|
|
137
|
+
pendingSync = false;
|
|
138
|
+
debouncedSync();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function scheduleSync() {
|
|
142
|
+
if (syncTimeout) {
|
|
143
|
+
clearTimeout(syncTimeout);
|
|
144
|
+
}
|
|
145
|
+
syncTimeout = setTimeout(debouncedSync, debounceMs);
|
|
146
|
+
}
|
|
147
|
+
// Check if a file matches any configured source glob
|
|
148
|
+
function fileMatchesAnySource(filePath) {
|
|
149
|
+
for (const source of sources) {
|
|
150
|
+
const expanded = expandPath(source.path);
|
|
151
|
+
if (filePath.startsWith(expanded)) {
|
|
152
|
+
const relativePath = path.relative(expanded, filePath);
|
|
153
|
+
if (matchesGlob(relativePath, source.glob)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// Set up file watcher
|
|
161
|
+
if (sources.length > 0) {
|
|
162
|
+
const chokidar = await import('chokidar');
|
|
163
|
+
const watchPaths = sources.map(s => expandPath(s.path));
|
|
164
|
+
const watcher = chokidar.watch(watchPaths, {
|
|
165
|
+
ignored: [
|
|
166
|
+
/(^|[\\/])\../, // Ignore dotfiles
|
|
167
|
+
/node_modules/,
|
|
168
|
+
/__pycache__/,
|
|
169
|
+
/\.lance$/,
|
|
170
|
+
/vectors\.lance/,
|
|
171
|
+
/\.db$/,
|
|
172
|
+
/\.sqlite$/,
|
|
173
|
+
],
|
|
174
|
+
persistent: true,
|
|
175
|
+
ignoreInitial: true,
|
|
176
|
+
awaitWriteFinish: {
|
|
177
|
+
stabilityThreshold: 500,
|
|
178
|
+
pollInterval: 100,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
watcher
|
|
182
|
+
.on('add', (filePath) => {
|
|
183
|
+
if (!fileMatchesAnySource(filePath))
|
|
184
|
+
return;
|
|
185
|
+
log('FILE', `Added: ${path.basename(filePath)}`);
|
|
186
|
+
scheduleSync();
|
|
187
|
+
})
|
|
188
|
+
.on('change', (filePath) => {
|
|
189
|
+
if (!fileMatchesAnySource(filePath))
|
|
190
|
+
return;
|
|
191
|
+
log('FILE', `Changed: ${path.basename(filePath)}`);
|
|
192
|
+
scheduleSync();
|
|
193
|
+
})
|
|
194
|
+
.on('error', (error) => {
|
|
195
|
+
log('ERROR', `Watcher error: ${error}`);
|
|
196
|
+
});
|
|
197
|
+
log('INFO', 'File watcher started');
|
|
198
|
+
}
|
|
199
|
+
// Periodic sync (git pull + full sync)
|
|
200
|
+
const PULL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
201
|
+
async function periodicSync() {
|
|
202
|
+
if (isSyncing)
|
|
203
|
+
return;
|
|
204
|
+
log('PULL', 'Periodic sync starting...');
|
|
205
|
+
try {
|
|
206
|
+
const result = await runSync(true);
|
|
207
|
+
if (result.files_processed > 0) {
|
|
208
|
+
log('PULL', `Found ${result.files_processed} new file(s)`);
|
|
209
|
+
for (const title of result.titles) {
|
|
210
|
+
log('INDEX', title);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
log('PULL', 'Up to date');
|
|
215
|
+
}
|
|
216
|
+
updateStatus({
|
|
217
|
+
last_sync: new Date().toISOString(),
|
|
218
|
+
last_sync_result: {
|
|
219
|
+
files_scanned: result.files_scanned,
|
|
220
|
+
files_processed: result.files_processed,
|
|
221
|
+
errors: result.errors,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
log('ERROR', `Periodic sync failed: ${error}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Run periodic sync
|
|
230
|
+
setInterval(periodicSync, PULL_INTERVAL_MS);
|
|
231
|
+
log('INFO', `Periodic sync every ${PULL_INTERVAL_MS / 60000} minutes`);
|
|
232
|
+
// Handle shutdown
|
|
233
|
+
process.on('SIGTERM', () => {
|
|
234
|
+
log('STOP', 'Daemon stopping (SIGTERM)');
|
|
235
|
+
process.exit(0);
|
|
236
|
+
});
|
|
237
|
+
process.on('SIGINT', () => {
|
|
238
|
+
log('STOP', 'Daemon stopping (SIGINT)');
|
|
239
|
+
process.exit(0);
|
|
240
|
+
});
|
|
241
|
+
log('INFO', 'Daemon ready');
|
|
242
|
+
}
|
|
243
|
+
main().catch((error) => {
|
|
244
|
+
log('FATAL', `Daemon crashed: ${error}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lore - Extension Configuration
|
|
3
|
+
*
|
|
4
|
+
* Stores installed extensions in ~/.config/lore/extensions.json
|
|
5
|
+
* Packages are installed under ~/.config/lore/extensions
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtensionConfigEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ExtensionConfig {
|
|
13
|
+
version: number;
|
|
14
|
+
extensions: ExtensionConfigEntry[];
|
|
15
|
+
}
|
|
16
|
+
export declare function getExtensionsDir(): string;
|
|
17
|
+
export declare function getExtensionsConfigPath(): string;
|
|
18
|
+
export declare function loadExtensionConfig(): Promise<ExtensionConfig>;
|
|
19
|
+
export declare function saveExtensionConfig(config: ExtensionConfig): Promise<void>;
|
|
20
|
+
export declare function ensureExtensionsDir(): Promise<void>;
|
|
21
|
+
export declare function addExtensionToConfig(name: string, version?: string, enabled?: boolean): Promise<ExtensionConfig>;
|
|
22
|
+
export declare function removeExtensionFromConfig(name: string): Promise<ExtensionConfig>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lore - Extension Configuration
|
|
3
|
+
*
|
|
4
|
+
* Stores installed extensions in ~/.config/lore/extensions.json
|
|
5
|
+
* Packages are installed under ~/.config/lore/extensions
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Paths
|
|
13
|
+
// ============================================================================
|
|
14
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
|
|
15
|
+
const EXTENSIONS_DIR = path.join(CONFIG_DIR, 'extensions');
|
|
16
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'extensions.json');
|
|
17
|
+
export function getExtensionsDir() {
|
|
18
|
+
return EXTENSIONS_DIR;
|
|
19
|
+
}
|
|
20
|
+
export function getExtensionsConfigPath() {
|
|
21
|
+
return CONFIG_FILE;
|
|
22
|
+
}
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Defaults
|
|
25
|
+
// ============================================================================
|
|
26
|
+
function getDefaultConfig() {
|
|
27
|
+
return {
|
|
28
|
+
version: 1,
|
|
29
|
+
extensions: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Config IO
|
|
34
|
+
// ============================================================================
|
|
35
|
+
export async function loadExtensionConfig() {
|
|
36
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
37
|
+
return getDefaultConfig();
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const content = await readFile(CONFIG_FILE, 'utf-8');
|
|
41
|
+
const config = JSON.parse(content);
|
|
42
|
+
if (config.version !== 1) {
|
|
43
|
+
console.warn(`[extensions] Unknown config version: ${config.version}, expected 1`);
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(config.extensions)) {
|
|
46
|
+
throw new Error('Invalid extensions config: extensions must be an array');
|
|
47
|
+
}
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT') {
|
|
52
|
+
return getDefaultConfig();
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function saveExtensionConfig(config) {
|
|
58
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
59
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
60
|
+
}
|
|
61
|
+
export async function ensureExtensionsDir() {
|
|
62
|
+
await mkdir(EXTENSIONS_DIR, { recursive: true });
|
|
63
|
+
const packageJsonPath = path.join(EXTENSIONS_DIR, 'package.json');
|
|
64
|
+
if (!existsSync(packageJsonPath)) {
|
|
65
|
+
const packageJson = {
|
|
66
|
+
name: 'lore-extensions',
|
|
67
|
+
private: true,
|
|
68
|
+
version: '0.0.0',
|
|
69
|
+
description: 'Installed Lore extensions',
|
|
70
|
+
};
|
|
71
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Config Manipulation
|
|
76
|
+
// ============================================================================
|
|
77
|
+
export async function addExtensionToConfig(name, version, enabled = true) {
|
|
78
|
+
const config = await loadExtensionConfig();
|
|
79
|
+
const existingIndex = config.extensions.findIndex((ext) => ext.name === name);
|
|
80
|
+
if (existingIndex !== -1) {
|
|
81
|
+
config.extensions[existingIndex] = {
|
|
82
|
+
...config.extensions[existingIndex],
|
|
83
|
+
version: version ?? config.extensions[existingIndex].version,
|
|
84
|
+
enabled,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
config.extensions.push({ name, version, enabled });
|
|
89
|
+
}
|
|
90
|
+
await saveExtensionConfig(config);
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
export async function removeExtensionFromConfig(name) {
|
|
94
|
+
const config = await loadExtensionConfig();
|
|
95
|
+
const index = config.extensions.findIndex((ext) => ext.name === name);
|
|
96
|
+
if (index === -1) {
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
config.extensions.splice(index, 1);
|
|
100
|
+
await saveExtensionConfig(config);
|
|
101
|
+
return config;
|
|
102
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposal-based write system for extensions
|
|
3
|
+
*/
|
|
4
|
+
export interface ProposedChange {
|
|
5
|
+
type: 'create_source' | 'update_source' | 'delete_source' | 'retain_insight' | 'add_tags';
|
|
6
|
+
title?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
project?: string;
|
|
9
|
+
sourceId?: string;
|
|
10
|
+
changes?: Record<string, unknown>;
|
|
11
|
+
insight?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PendingProposal {
|
|
16
|
+
id: string;
|
|
17
|
+
extensionName: string;
|
|
18
|
+
change: ProposedChange;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
status: 'pending' | 'approved' | 'rejected';
|
|
21
|
+
reviewedAt?: string;
|
|
22
|
+
rejectionReason?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function getPendingDir(): string;
|
|
25
|
+
export declare function ensurePendingDir(): Promise<void>;
|
|
26
|
+
export declare function createProposal(extensionName: string, change: ProposedChange): Promise<PendingProposal>;
|
|
27
|
+
export declare function listPendingProposals(): Promise<PendingProposal[]>;
|
|
28
|
+
export declare function getProposal(id: string): Promise<PendingProposal | null>;
|
|
29
|
+
export declare function approveProposal(id: string, dbPath: string, dataDir: string): Promise<void>;
|
|
30
|
+
export declare function rejectProposal(id: string, reason: string): Promise<void>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposal-based write system for extensions
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { handleIngest } from '../mcp/handlers/ingest.js';
|
|
10
|
+
import { handleRetain } from '../mcp/handlers/retain.js';
|
|
11
|
+
import { getDatabase, getSourceById } from '../core/vector-store.js';
|
|
12
|
+
export function getPendingDir() {
|
|
13
|
+
return path.join(os.homedir(), '.config', 'lore', 'pending');
|
|
14
|
+
}
|
|
15
|
+
export async function ensurePendingDir() {
|
|
16
|
+
await mkdir(getPendingDir(), { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
function proposalPath(id) {
|
|
19
|
+
return path.join(getPendingDir(), `${id}.json`);
|
|
20
|
+
}
|
|
21
|
+
async function writeProposal(proposal) {
|
|
22
|
+
await ensurePendingDir();
|
|
23
|
+
await writeFile(proposalPath(proposal.id), JSON.stringify(proposal, null, 2));
|
|
24
|
+
}
|
|
25
|
+
export async function createProposal(extensionName, change) {
|
|
26
|
+
const proposal = {
|
|
27
|
+
id: randomUUID(),
|
|
28
|
+
extensionName,
|
|
29
|
+
change,
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
status: 'pending',
|
|
32
|
+
};
|
|
33
|
+
await writeProposal(proposal);
|
|
34
|
+
return proposal;
|
|
35
|
+
}
|
|
36
|
+
export async function listPendingProposals() {
|
|
37
|
+
const dir = getPendingDir();
|
|
38
|
+
if (!existsSync(dir)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const entries = await readdir(dir);
|
|
42
|
+
const proposals = [];
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!entry.endsWith('.json'))
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
const content = await readFile(path.join(dir, entry), 'utf-8');
|
|
48
|
+
const parsed = JSON.parse(content);
|
|
49
|
+
if (parsed?.id) {
|
|
50
|
+
proposals.push(parsed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Skip unreadable proposals
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
proposals.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
58
|
+
return proposals;
|
|
59
|
+
}
|
|
60
|
+
export async function getProposal(id) {
|
|
61
|
+
const filePath = proposalPath(id);
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const content = await readFile(filePath, 'utf-8');
|
|
67
|
+
return JSON.parse(content);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function applyProposalChange(proposal, dbPath, dataDir) {
|
|
74
|
+
const change = proposal.change;
|
|
75
|
+
switch (change.type) {
|
|
76
|
+
case 'create_source': {
|
|
77
|
+
if (!change.title || !change.content || !change.project) {
|
|
78
|
+
throw new Error('create_source requires title, content, and project');
|
|
79
|
+
}
|
|
80
|
+
await handleIngest(dbPath, dataDir, {
|
|
81
|
+
title: change.title,
|
|
82
|
+
content: change.content,
|
|
83
|
+
project: change.project,
|
|
84
|
+
}, { hookContext: { mode: 'cli' } });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
case 'retain_insight': {
|
|
88
|
+
if (!change.insight) {
|
|
89
|
+
throw new Error('retain_insight requires insight');
|
|
90
|
+
}
|
|
91
|
+
const project = change.project || proposal.extensionName;
|
|
92
|
+
await handleRetain(dbPath, dataDir, {
|
|
93
|
+
content: change.insight,
|
|
94
|
+
project,
|
|
95
|
+
type: 'insight',
|
|
96
|
+
}, {});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
case 'update_source': {
|
|
100
|
+
if (!change.sourceId || !change.changes) {
|
|
101
|
+
throw new Error('update_source requires sourceId and changes');
|
|
102
|
+
}
|
|
103
|
+
const client = await getDatabase(dbPath);
|
|
104
|
+
const { error } = await client
|
|
105
|
+
.from('sources')
|
|
106
|
+
.update(change.changes)
|
|
107
|
+
.eq('id', change.sourceId);
|
|
108
|
+
if (error) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case 'add_tags': {
|
|
114
|
+
if (!change.sourceId || !change.tags) {
|
|
115
|
+
throw new Error('add_tags requires sourceId and tags');
|
|
116
|
+
}
|
|
117
|
+
const source = await getSourceById(dbPath, change.sourceId);
|
|
118
|
+
if (!source) {
|
|
119
|
+
throw new Error(`Source not found: ${change.sourceId}`);
|
|
120
|
+
}
|
|
121
|
+
const existing = Array.isArray(source.tags) ? source.tags : [];
|
|
122
|
+
const merged = Array.from(new Set([...existing, ...change.tags]));
|
|
123
|
+
const client = await getDatabase(dbPath);
|
|
124
|
+
const { error } = await client
|
|
125
|
+
.from('sources')
|
|
126
|
+
.update({ tags: merged })
|
|
127
|
+
.eq('id', change.sourceId);
|
|
128
|
+
if (error) {
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case 'delete_source': {
|
|
134
|
+
if (!change.sourceId) {
|
|
135
|
+
throw new Error('delete_source requires sourceId');
|
|
136
|
+
}
|
|
137
|
+
const client = await getDatabase(dbPath);
|
|
138
|
+
const { error } = await client
|
|
139
|
+
.from('sources')
|
|
140
|
+
.delete()
|
|
141
|
+
.eq('id', change.sourceId);
|
|
142
|
+
if (error) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
default: {
|
|
148
|
+
const exhaustive = change.type;
|
|
149
|
+
throw new Error(`Unknown proposal type: ${exhaustive}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function approveProposal(id, dbPath, dataDir) {
|
|
154
|
+
const proposal = await getProposal(id);
|
|
155
|
+
if (!proposal) {
|
|
156
|
+
throw new Error(`Proposal not found: ${id}`);
|
|
157
|
+
}
|
|
158
|
+
if (proposal.status !== 'pending') {
|
|
159
|
+
throw new Error(`Proposal ${id} is already ${proposal.status}`);
|
|
160
|
+
}
|
|
161
|
+
await applyProposalChange(proposal, dbPath, dataDir);
|
|
162
|
+
proposal.status = 'approved';
|
|
163
|
+
proposal.reviewedAt = new Date().toISOString();
|
|
164
|
+
await writeProposal(proposal);
|
|
165
|
+
}
|
|
166
|
+
export async function rejectProposal(id, reason) {
|
|
167
|
+
const proposal = await getProposal(id);
|
|
168
|
+
if (!proposal) {
|
|
169
|
+
throw new Error(`Proposal not found: ${id}`);
|
|
170
|
+
}
|
|
171
|
+
if (proposal.status !== 'pending') {
|
|
172
|
+
throw new Error(`Proposal ${id} is already ${proposal.status}`);
|
|
173
|
+
}
|
|
174
|
+
proposal.status = 'rejected';
|
|
175
|
+
proposal.reviewedAt = new Date().toISOString();
|
|
176
|
+
proposal.rejectionReason = reason;
|
|
177
|
+
await writeProposal(proposal);
|
|
178
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lore Extension Registry + Loader
|
|
3
|
+
*/
|
|
4
|
+
import type { LoreExtension, ExtensionToolContext, ExtensionCommandContext, ExtensionPermissions, LoreEventType } from './types.js';
|
|
5
|
+
import type { Command } from 'commander';
|
|
6
|
+
interface LoadedExtension {
|
|
7
|
+
extension: LoreExtension;
|
|
8
|
+
packageName: string;
|
|
9
|
+
modulePath: string;
|
|
10
|
+
}
|
|
11
|
+
interface ExtensionRegistryOptions {
|
|
12
|
+
extensionsDir?: string;
|
|
13
|
+
logger?: (message: string) => void;
|
|
14
|
+
loreVersion?: string;
|
|
15
|
+
cacheBust?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function createProposeFunction(extensionName: string, permissions?: ExtensionPermissions): (change: import('./proposals.js').ProposedChange) => Promise<import('./proposals.js').PendingProposal>;
|
|
18
|
+
export declare class ExtensionRegistry {
|
|
19
|
+
private extensions;
|
|
20
|
+
private readonly logger;
|
|
21
|
+
private readonly options;
|
|
22
|
+
constructor(extensions: LoadedExtension[], logger: (message: string) => void, options: ExtensionRegistryOptions);
|
|
23
|
+
listExtensions(): LoadedExtension[];
|
|
24
|
+
private collectMiddleware;
|
|
25
|
+
private collectEventHandlers;
|
|
26
|
+
emitEvent(type: LoreEventType, payload: unknown, context: ExtensionToolContext): Promise<void>;
|
|
27
|
+
registerCommands(program: Command, context: ExtensionCommandContext): void;
|
|
28
|
+
runHook(hookName: keyof NonNullable<LoreExtension['hooks']>, payload: unknown, context: ExtensionToolContext): Promise<void>;
|
|
29
|
+
reload(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function loadExtensionRegistry(options?: ExtensionRegistryOptions): Promise<ExtensionRegistry>;
|
|
32
|
+
export declare function getExtensionRegistry(options?: ExtensionRegistryOptions): Promise<ExtensionRegistry>;
|
|
33
|
+
export declare function clearExtensionRegistry(): void;
|
|
34
|
+
export declare function getLoreVersionString(): Promise<string | undefined>;
|
|
35
|
+
export {};
|