@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,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 {};