@getlore/cli 0.7.0 → 0.8.1
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/README.md +2 -2
- package/dist/cli/commands/auth.js +83 -9
- package/dist/cli/commands/docs.js +14 -7
- package/dist/cli/commands/misc.js +1 -1
- package/dist/cli/commands/sync.js +91 -24
- package/dist/cli/logo.d.ts +13 -0
- package/dist/cli/logo.js +32 -0
- package/dist/cli/welcome.d.ts +7 -0
- package/dist/cli/welcome.js +57 -0
- package/dist/core/auth.js +1 -1
- package/dist/core/data-repo.js +1 -3
- package/dist/core/types.d.ts +1 -8
- package/dist/core/vector-store.js +2 -4
- package/dist/extensions/proposals.d.ts +1 -2
- package/dist/extensions/proposals.js +0 -13
- package/dist/extensions/registry.js +1 -1
- package/dist/index.js +5 -0
- package/dist/mcp/handlers/ingest.d.ts +1 -1
- package/dist/mcp/handlers/ingest.js +16 -8
- package/dist/mcp/handlers/list-sources.js +2 -1
- package/dist/mcp/handlers/research-agent.js +7 -5
- package/dist/mcp/handlers/research.js +2 -1
- package/dist/mcp/handlers/search.js +2 -1
- package/dist/mcp/server.js +1 -8
- package/dist/mcp/tools.js +4 -28
- package/dist/sync/config.js +3 -1
- package/dist/tui/browse-handlers.js +27 -8
- package/dist/tui/browse-ui.js +6 -3
- package/package.json +1 -1
- package/plugins/claude-code/skills/lore/SKILL.md +9 -14
- package/plugins/codex/SKILL.md +2 -5
- package/plugins/gemini/GEMINI.md +2 -5
- package/skills/generic-agent.md +11 -15
- package/skills/openclaw.md +5 -5
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ If the MCP host doesn't inherit your shell environment (e.g. Claude Desktop), ad
|
|
|
62
62
|
| Command | Description |
|
|
63
63
|
|---------|-------------|
|
|
64
64
|
| `lore setup` | Guided configuration wizard |
|
|
65
|
-
| `lore login` | Sign in with email OTP |
|
|
65
|
+
| `lore auth login` | Sign in with email OTP |
|
|
66
66
|
| `lore sync` | Sync all configured sources |
|
|
67
67
|
| `lore sync add` | Add a source directory |
|
|
68
68
|
| `lore search <query>` | Hybrid search |
|
|
@@ -77,7 +77,7 @@ If the MCP host doesn't inherit your shell environment (e.g. Claude Desktop), ad
|
|
|
77
77
|
- **Node.js** 18+
|
|
78
78
|
- **OpenAI API key** (embeddings)
|
|
79
79
|
- **Anthropic API key** (metadata extraction & research)
|
|
80
|
-
- **Lore account** (free — sign up via `lore login`)
|
|
80
|
+
- **Lore account** (free — sign up via `lore auth login`)
|
|
81
81
|
|
|
82
82
|
## How Sync Works
|
|
83
83
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { c } from '../colors.js';
|
|
11
|
+
import { getLogo } from '../logo.js';
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Readline helper
|
|
13
14
|
// ============================================================================
|
|
@@ -46,7 +47,7 @@ export function registerAuthCommands(program) {
|
|
|
46
47
|
const { loadAuthSession } = await import('../../core/auth.js');
|
|
47
48
|
const session = await loadAuthSession();
|
|
48
49
|
console.log(c.success(`Already logged in as ${session?.user.email}`));
|
|
49
|
-
console.log(c.dim('Run \'lore logout\' first to switch accounts.'));
|
|
50
|
+
console.log(c.dim('Run \'lore auth logout\' first to switch accounts.'));
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const email = options.email || await prompt('Email');
|
|
@@ -119,7 +120,7 @@ export function registerAuthCommands(program) {
|
|
|
119
120
|
}
|
|
120
121
|
else {
|
|
121
122
|
callback.abort();
|
|
122
|
-
console.error(c.error('Login timed out. Try again with \'lore login\'.'));
|
|
123
|
+
console.error(c.error('Login timed out. Try again with \'lore auth login\'.'));
|
|
123
124
|
process.exit(1);
|
|
124
125
|
}
|
|
125
126
|
if (verifyError || !session) {
|
|
@@ -153,7 +154,7 @@ export function registerAuthCommands(program) {
|
|
|
153
154
|
}
|
|
154
155
|
const session = await getValidSession();
|
|
155
156
|
if (!session) {
|
|
156
|
-
console.log(c.dim('Not logged in. Run \'lore login\' to sign in.'));
|
|
157
|
+
console.log(c.dim('Not logged in. Run \'lore auth login\' to sign in.'));
|
|
157
158
|
return;
|
|
158
159
|
}
|
|
159
160
|
console.log(`${c.bold('Email:')} ${session.user.email}`);
|
|
@@ -178,14 +179,21 @@ export function registerAuthCommands(program) {
|
|
|
178
179
|
.option('--data-dir <dir>', 'Data directory path (skip prompt)')
|
|
179
180
|
.option('--code <code>', 'OTP code (for non-interactive login)')
|
|
180
181
|
.option('--skip-login', 'Skip the login step (use if already authenticated)')
|
|
182
|
+
.option('--sync-path <path>', 'Sync source directory path (non-interactive)')
|
|
183
|
+
.option('--sync-project <project>', 'Default project for sync source (non-interactive)')
|
|
184
|
+
.option('--sync-name <name>', 'Name for sync source (non-interactive, defaults to project name)')
|
|
181
185
|
.action(async (options) => {
|
|
182
186
|
const { saveLoreConfig, getLoreConfigPath } = await import('../../core/config.js');
|
|
183
187
|
const { sendOTP, verifyOTP, sessionFromMagicLink, isAuthenticated, loadAuthSession } = await import('../../core/auth.js');
|
|
184
188
|
const { bridgeConfigToEnv } = await import('../../core/config.js');
|
|
185
189
|
// Non-interactive mode: skip prompts when all key flags are provided
|
|
186
190
|
const nonInteractive = !!(options.openaiKey && options.anthropicKey && options.email);
|
|
187
|
-
console.log(
|
|
188
|
-
console.log(
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(getLogo());
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(` ${c.title('Setup Wizard')}`);
|
|
195
|
+
console.log(` ${c.dim('━'.repeat(12))}`);
|
|
196
|
+
console.log('');
|
|
189
197
|
// ── Step 1: Configuration ───────────────────────────────────────
|
|
190
198
|
console.log(c.bold('Step 1: Configuration\n'));
|
|
191
199
|
const { expandPath } = await import('../../sync/config.js');
|
|
@@ -479,8 +487,74 @@ export function registerAuthCommands(program) {
|
|
|
479
487
|
console.log(c.warning(`Skipped — ${err instanceof Error ? err.message : 'could not index welcome document'}`));
|
|
480
488
|
console.log(c.dim('You can add documents later with lore sync\n'));
|
|
481
489
|
}
|
|
482
|
-
// ── Step 5:
|
|
483
|
-
console.log(c.bold('Step 5:
|
|
490
|
+
// ── Step 5: Sync Sources ──────────────────────────────────────────
|
|
491
|
+
console.log(c.bold('Step 5: Sync Sources\n'));
|
|
492
|
+
if (nonInteractive) {
|
|
493
|
+
if (options.syncPath && options.syncProject) {
|
|
494
|
+
const { addSyncSource } = await import('../../sync/config.js');
|
|
495
|
+
const syncName = options.syncName || options.syncProject.charAt(0).toUpperCase() + options.syncProject.slice(1);
|
|
496
|
+
try {
|
|
497
|
+
await addSyncSource({
|
|
498
|
+
name: syncName,
|
|
499
|
+
path: expandPath(options.syncPath),
|
|
500
|
+
glob: '**/*',
|
|
501
|
+
project: options.syncProject,
|
|
502
|
+
enabled: true,
|
|
503
|
+
});
|
|
504
|
+
console.log(c.success(`Added sync source "${syncName}" → ${options.syncPath}\n`));
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
console.log(c.warning(`Could not add sync source: ${err instanceof Error ? err.message : err}\n`));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
console.log(c.dim('Skipped (no --sync-path/--sync-project provided).\n'));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
console.log(c.dim('Sync sources are directories on your machine that Lore watches'));
|
|
516
|
+
console.log(c.dim('for new files. When files appear or change, they\'re automatically'));
|
|
517
|
+
console.log(c.dim('indexed into your knowledge base.\n'));
|
|
518
|
+
console.log(c.dim('Supported formats: Markdown, JSON, JSONL, plain text, CSV,'));
|
|
519
|
+
console.log(c.dim('HTML, XML, PDF, and images.\n'));
|
|
520
|
+
let addMore = true;
|
|
521
|
+
while (addMore) {
|
|
522
|
+
const wantsSource = await prompt('Would you like to add a sync source? (y/n)', 'y');
|
|
523
|
+
if (wantsSource.toLowerCase() !== 'y') {
|
|
524
|
+
if (wantsSource.toLowerCase() === 'n') {
|
|
525
|
+
console.log(c.dim('You can add sources anytime with: lore sync add\n'));
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
const sourceName = await prompt('Name (e.g., "Meeting Notes")');
|
|
530
|
+
const sourcePath = await prompt('Path (e.g., ~/Documents/notes)');
|
|
531
|
+
const sourceProject = await prompt('Default project');
|
|
532
|
+
if (!sourceName || !sourcePath || !sourceProject) {
|
|
533
|
+
console.log(c.warning('Name, path, and project are all required. Skipping.\n'));
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
const { addSyncSource } = await import('../../sync/config.js');
|
|
538
|
+
await addSyncSource({
|
|
539
|
+
name: sourceName,
|
|
540
|
+
path: expandPath(sourcePath),
|
|
541
|
+
glob: '**/*',
|
|
542
|
+
project: sourceProject,
|
|
543
|
+
enabled: true,
|
|
544
|
+
});
|
|
545
|
+
console.log(c.success(`Added source "${sourceName}"`));
|
|
546
|
+
console.log(c.dim(` Path: ${sourcePath}`));
|
|
547
|
+
console.log(c.dim(` Glob: **/* (all supported files)\n`));
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
console.log(c.warning(`Could not add source: ${err instanceof Error ? err.message : err}\n`));
|
|
551
|
+
}
|
|
552
|
+
const another = await prompt('Add another source? (y/n)', 'n');
|
|
553
|
+
addMore = another.toLowerCase() === 'y';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ── Step 6: Background Daemon ──────────────────────────────────────
|
|
557
|
+
console.log(c.bold('Step 6: Background Daemon\n'));
|
|
484
558
|
const startDaemon = nonInteractive ? 'y' : await prompt('Start background sync daemon? (y/n)', 'y');
|
|
485
559
|
if (startDaemon.toLowerCase() === 'y') {
|
|
486
560
|
try {
|
|
@@ -504,8 +578,8 @@ export function registerAuthCommands(program) {
|
|
|
504
578
|
else {
|
|
505
579
|
console.log(c.dim('You can start it later with: lore sync start\n'));
|
|
506
580
|
}
|
|
507
|
-
// ── Step
|
|
508
|
-
console.log(c.bold('Step
|
|
581
|
+
// ── Step 7: Agent Skills ──────────────────────────────────────────
|
|
582
|
+
console.log(c.bold('Step 7: Agent Skills\n'));
|
|
509
583
|
if (nonInteractive) {
|
|
510
584
|
console.log(c.dim('Skipped in non-interactive mode.'));
|
|
511
585
|
console.log(c.dim('Install later with: lore skills install <name>\n'));
|
|
@@ -106,7 +106,7 @@ export function registerDocsCommand(program, defaultDataDir) {
|
|
|
106
106
|
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
107
107
|
.option('--no-push', 'Skip git push')
|
|
108
108
|
.action(async (content, options) => {
|
|
109
|
-
const {
|
|
109
|
+
const { handleIngest } = await import('../../mcp/handlers/ingest.js');
|
|
110
110
|
const dataDir = options.dataDir;
|
|
111
111
|
const dbPath = path.join(dataDir, 'lore.lance');
|
|
112
112
|
const validTypes = ['insight', 'decision', 'requirement', 'note'];
|
|
@@ -114,21 +114,28 @@ export function registerDocsCommand(program, defaultDataDir) {
|
|
|
114
114
|
console.error(`Invalid type: ${options.type}. Must be one of: ${validTypes.join(', ')}`);
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
// Map CLI type to source_type
|
|
118
|
+
const sourceTypeMap = {
|
|
119
|
+
decision: 'notes',
|
|
120
|
+
requirement: 'notes',
|
|
121
|
+
insight: 'notes',
|
|
122
|
+
note: 'notes',
|
|
123
|
+
};
|
|
124
|
+
const result = await handleIngest(dbPath, dataDir, {
|
|
118
125
|
content,
|
|
119
126
|
project: options.project,
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
title: `${options.type.charAt(0).toUpperCase() + options.type.slice(1)}: ${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`,
|
|
128
|
+
source_type: sourceTypeMap[options.type] || 'notes',
|
|
122
129
|
tags: options.tags?.split(',').map((t) => t.trim()),
|
|
123
|
-
}, { autoPush: options.push !== false });
|
|
130
|
+
}, { autoPush: options.push !== false, hookContext: { mode: 'cli' } });
|
|
124
131
|
if (result.success) {
|
|
125
|
-
console.log(`\n✓ ${
|
|
132
|
+
console.log(`\n✓ Created ${options.type} for project "${options.project}"`);
|
|
126
133
|
console.log(` ID: ${result.id}`);
|
|
127
134
|
console.log(` Indexed: ${result.indexed ? 'yes' : 'no'}`);
|
|
128
135
|
console.log(` Synced: ${result.synced ? 'yes' : 'no'}`);
|
|
129
136
|
}
|
|
130
137
|
else {
|
|
131
|
-
console.error(`\nFailed to create
|
|
138
|
+
console.error(`\nFailed to create ${options.type}`);
|
|
132
139
|
process.exit(1);
|
|
133
140
|
}
|
|
134
141
|
});
|
|
@@ -148,7 +148,7 @@ Done! To use this data repository:
|
|
|
148
148
|
"env": { "LORE_DATA_DIR": "${expandedPath}" }
|
|
149
149
|
|
|
150
150
|
3. Add sync sources:
|
|
151
|
-
lore sync add --name "My Notes" --path ~/notes
|
|
151
|
+
lore sync add --name "My Notes" --path ~/notes -p myproject
|
|
152
152
|
|
|
153
153
|
Tip: Run 'lore setup' for the full guided experience (config + login + data repo).
|
|
154
154
|
`);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* All sync-related functionality: one-time sync, daemon, watch, sources.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn, spawnSync } from 'child_process';
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'fs';
|
|
8
8
|
import { mkdir } from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import os from 'os';
|
|
@@ -598,54 +598,115 @@ export function registerSyncCommand(program, defaultDataDir) {
|
|
|
598
598
|
.description('List configured sync sources')
|
|
599
599
|
.action(async () => {
|
|
600
600
|
const { loadSyncConfig, getConfigPath } = await import('../../sync/config.js');
|
|
601
|
-
console.log(
|
|
602
|
-
console.log(
|
|
603
|
-
console.log(`
|
|
601
|
+
console.log('');
|
|
602
|
+
console.log(` ${c.title('Sync Sources')}`);
|
|
603
|
+
console.log(` ${c.dim('━'.repeat(12))}`);
|
|
604
|
+
console.log('');
|
|
604
605
|
const config = await loadSyncConfig();
|
|
605
606
|
if (config.sources.length === 0) {
|
|
606
|
-
console.log('No sources configured.
|
|
607
|
+
console.log(c.dim(' No sources configured.'));
|
|
608
|
+
console.log(` Run ${c.bold('lore sync add')} to add one.`);
|
|
609
|
+
console.log('');
|
|
607
610
|
return;
|
|
608
611
|
}
|
|
609
612
|
for (const source of config.sources) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
613
|
+
if (source.enabled) {
|
|
614
|
+
console.log(` ${c.success('✓')} ${c.bold(source.name)}`);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
console.log(` ${c.dim('○')} ${c.dim(source.name + ' (disabled)')}`);
|
|
618
|
+
}
|
|
619
|
+
console.log(` Path: ${source.path}`);
|
|
620
|
+
console.log(` Glob: ${c.dim(source.glob)}`);
|
|
614
621
|
console.log(` Project: ${source.project}`);
|
|
615
622
|
console.log('');
|
|
616
623
|
}
|
|
624
|
+
console.log(c.dim(` Config: ${getConfigPath()}`));
|
|
625
|
+
console.log(c.dim(` Add more with: lore sync add`));
|
|
626
|
+
console.log('');
|
|
617
627
|
});
|
|
618
628
|
syncCmd
|
|
619
629
|
.command('add')
|
|
620
630
|
.description('Add a new sync source directory')
|
|
621
631
|
.option('-n, --name <name>', 'Source name')
|
|
622
632
|
.option('-p, --path <path>', 'Directory path')
|
|
623
|
-
.option('-g, --glob <glob>', 'File glob pattern', '
|
|
633
|
+
.option('-g, --glob <glob>', 'File glob pattern', '**/*')
|
|
624
634
|
.option('--project <project>', 'Default project')
|
|
625
635
|
.action(async (options) => {
|
|
626
|
-
const { addSyncSource } = await import('../../sync/config.js');
|
|
636
|
+
const { addSyncSource, expandPath } = await import('../../sync/config.js');
|
|
637
|
+
// Non-interactive: path and project provided
|
|
638
|
+
const nonInteractive = !!(options.path && options.project);
|
|
639
|
+
if (nonInteractive) {
|
|
640
|
+
const dirBase = path.basename(expandPath(options.path)) || 'Source';
|
|
641
|
+
const name = options.name || dirBase.charAt(0).toUpperCase() + dirBase.slice(1);
|
|
642
|
+
try {
|
|
643
|
+
await addSyncSource({
|
|
644
|
+
name,
|
|
645
|
+
path: options.path,
|
|
646
|
+
glob: options.glob || '**/*',
|
|
647
|
+
project: options.project,
|
|
648
|
+
enabled: true,
|
|
649
|
+
});
|
|
650
|
+
console.log(`\n ${c.success('✓')} Added "${name}"`);
|
|
651
|
+
console.log(` Path: ${options.path}`);
|
|
652
|
+
console.log(` Glob: ${options.glob || '**/*'}`);
|
|
653
|
+
console.log(` Project: ${options.project}`);
|
|
654
|
+
console.log(`\n Run ${c.bold("'lore sync'")} to index these files now.\n`);
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
console.error(`\nError: ${error}`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// Interactive flow: path → project → done
|
|
627
663
|
const readline = await import('readline');
|
|
628
664
|
const rl = readline.createInterface({
|
|
629
665
|
input: process.stdin,
|
|
630
666
|
output: process.stdout,
|
|
631
667
|
});
|
|
632
668
|
const ask = (question, defaultValue) => new Promise((resolve) => {
|
|
633
|
-
const prompt = defaultValue ?
|
|
669
|
+
const prompt = defaultValue ? ` ${question} [${defaultValue}]: ` : ` ${question}: `;
|
|
634
670
|
rl.question(prompt, (answer) => {
|
|
635
671
|
resolve(answer.trim() || defaultValue || '');
|
|
636
672
|
});
|
|
637
673
|
});
|
|
638
|
-
console.log(
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
rl.close();
|
|
645
|
-
if (!name || !sourcePath || !project) {
|
|
646
|
-
console.log('\nAll fields are required.');
|
|
674
|
+
console.log('');
|
|
675
|
+
// Path
|
|
676
|
+
const sourcePath = options.path || await ask('Path');
|
|
677
|
+
if (!sourcePath) {
|
|
678
|
+
rl.close();
|
|
679
|
+
console.log(c.warning('\n Path is required.\n'));
|
|
647
680
|
process.exit(1);
|
|
648
681
|
}
|
|
682
|
+
// Validate path
|
|
683
|
+
const resolved = expandPath(sourcePath);
|
|
684
|
+
if (existsSync(resolved)) {
|
|
685
|
+
try {
|
|
686
|
+
const count = readdirSync(resolved).length;
|
|
687
|
+
console.log(c.success(` ✓ Found (${count} items)`));
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
console.log(c.success(' ✓ Directory exists'));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
console.log(c.warning(' ⚠ Directory does not exist yet'));
|
|
695
|
+
}
|
|
696
|
+
// Derive name from directory basename
|
|
697
|
+
const dirName = path.basename(resolved) || 'Source';
|
|
698
|
+
const defaultName = options.name || dirName.charAt(0).toUpperCase() + dirName.slice(1);
|
|
699
|
+
const defaultProject = options.project || dirName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
700
|
+
// Project
|
|
701
|
+
const project = await ask('Project', defaultProject);
|
|
702
|
+
if (!project) {
|
|
703
|
+
rl.close();
|
|
704
|
+
console.log(c.warning('\n Project is required.\n'));
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
rl.close();
|
|
708
|
+
const name = options.name || defaultName;
|
|
709
|
+
const glob = options.glob || '**/*';
|
|
649
710
|
try {
|
|
650
711
|
await addSyncSource({
|
|
651
712
|
name,
|
|
@@ -654,11 +715,17 @@ export function registerSyncCommand(program, defaultDataDir) {
|
|
|
654
715
|
project,
|
|
655
716
|
enabled: true,
|
|
656
717
|
});
|
|
657
|
-
console.log(
|
|
658
|
-
console.log(
|
|
718
|
+
console.log('');
|
|
719
|
+
console.log(` ${c.success('✓')} Added "${c.bold(name)}"`);
|
|
720
|
+
console.log(` Path: ${sourcePath}`);
|
|
721
|
+
console.log(` Glob: ${c.dim(glob)}`);
|
|
722
|
+
console.log(` Project: ${project}`);
|
|
723
|
+
console.log('');
|
|
724
|
+
console.log(` Run ${c.bold("'lore sync'")} to index these files now.`);
|
|
725
|
+
console.log('');
|
|
659
726
|
}
|
|
660
727
|
catch (error) {
|
|
661
|
-
console.error(`\
|
|
728
|
+
console.error(`\n ${c.error(String(error))}\n`);
|
|
662
729
|
process.exit(1);
|
|
663
730
|
}
|
|
664
731
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Art Logo
|
|
3
|
+
*
|
|
4
|
+
* Renders the `> lore` brand as ASCII art:
|
|
5
|
+
* - Yellow chevron (╲ ▸ ╱) on the left
|
|
6
|
+
* - Cyan bold box-drawing "lore" on the right
|
|
7
|
+
*
|
|
8
|
+
* Used in the welcome screen and setup wizard.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Returns the colored ASCII logo with tagline.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getLogo(): string;
|
package/dist/cli/logo.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII Art Logo
|
|
3
|
+
*
|
|
4
|
+
* Renders the `> lore` brand as ASCII art:
|
|
5
|
+
* - Yellow chevron (╲ ▸ ╱) on the left
|
|
6
|
+
* - Cyan bold box-drawing "lore" on the right
|
|
7
|
+
*
|
|
8
|
+
* Used in the welcome screen and setup wizard.
|
|
9
|
+
*/
|
|
10
|
+
import { colors, c } from './colors.js';
|
|
11
|
+
// Yellow chevron Cyan "lore" in box-drawing
|
|
12
|
+
// ╲ ┃ ┏━┓ ┏━┓ ┏━━
|
|
13
|
+
// ▸ ┃ ┃ ┃ ┣┳┛ ┣━
|
|
14
|
+
// ╱ ┗━ ┗━┛ ┗┻╸ ┗━━
|
|
15
|
+
function yel(s) {
|
|
16
|
+
return `${colors.bold}${colors.yellow}${s}${colors.reset}`;
|
|
17
|
+
}
|
|
18
|
+
function lore(s) {
|
|
19
|
+
return c.title(s);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns the colored ASCII logo with tagline.
|
|
23
|
+
*/
|
|
24
|
+
export function getLogo() {
|
|
25
|
+
const lines = [
|
|
26
|
+
` ${yel('╲')} ${lore('┃ ┏━┓ ┏━┓ ┏━━')}`,
|
|
27
|
+
` ${yel('▸')} ${lore('┃ ┃ ┃ ┣┳┛ ┣━')}`,
|
|
28
|
+
` ${yel('╱')} ${lore('┗━ ┗━┛ ┗┻╸ ┗━━')}`,
|
|
29
|
+
].join('\n');
|
|
30
|
+
const tagline = c.dim(' Research Knowledge Repository');
|
|
31
|
+
return `${lines}\n${tagline}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome Screen
|
|
3
|
+
*
|
|
4
|
+
* Shown when `lore` is run with no command.
|
|
5
|
+
* Shows every command, categorized. This IS the help.
|
|
6
|
+
*/
|
|
7
|
+
import { c } from './colors.js';
|
|
8
|
+
import { getLogo } from './logo.js';
|
|
9
|
+
export function showWelcomeScreen() {
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(getLogo());
|
|
12
|
+
console.log('');
|
|
13
|
+
console.log(` ${c.bold('Setup:')}`);
|
|
14
|
+
console.log(` ${c.dim('•')} lore setup ${c.dim('— guided first-time setup')}`);
|
|
15
|
+
console.log(` ${c.dim('•')} lore auth login ${c.dim('— sign in with email')}`);
|
|
16
|
+
console.log(` ${c.dim('•')} lore auth logout ${c.dim('— sign out')}`);
|
|
17
|
+
console.log(` ${c.dim('•')} lore auth whoami ${c.dim('— check login status')}`);
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(` ${c.bold('Search & Research:')}`);
|
|
20
|
+
console.log(` ${c.dim('•')} lore search "query" ${c.dim('— semantic + keyword search')}`);
|
|
21
|
+
console.log(` ${c.dim('•')} lore research "query" ${c.dim('— AI-powered deep research')}`);
|
|
22
|
+
console.log(` ${c.dim('•')} lore ask "question" ${c.dim('— quick question answering')}`);
|
|
23
|
+
console.log(` ${c.dim('•')} lore browse ${c.dim('— interactive terminal browser')}`);
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(` ${c.bold('Content:')}`);
|
|
26
|
+
console.log(` ${c.dim('•')} lore ingest ${c.dim('— add content (CLI, pipe, or file)')}`);
|
|
27
|
+
console.log(` ${c.dim('•')} lore sync ${c.dim('— sync all sources now')}`);
|
|
28
|
+
console.log(` ${c.dim('•')} lore sync add ${c.dim('— add a source directory')}`);
|
|
29
|
+
console.log(` ${c.dim('•')} lore sync list ${c.dim('— show configured sources')}`);
|
|
30
|
+
console.log(` ${c.dim('•')} lore sync enable <name> ${c.dim('— enable a source')}`);
|
|
31
|
+
console.log(` ${c.dim('•')} lore sync disable <name>${c.dim(' — disable')}`);
|
|
32
|
+
console.log(` ${c.dim('•')} lore sync remove <name> ${c.dim('— remove')}`);
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(` ${c.bold('Background Sync:')}`);
|
|
35
|
+
console.log(` ${c.dim('•')} lore sync start ${c.dim('— start daemon')}`);
|
|
36
|
+
console.log(` ${c.dim('•')} lore sync stop ${c.dim('— stop daemon')}`);
|
|
37
|
+
console.log(` ${c.dim('•')} lore sync restart ${c.dim('— restart daemon')}`);
|
|
38
|
+
console.log(` ${c.dim('•')} lore sync status ${c.dim('— daemon status')}`);
|
|
39
|
+
console.log(` ${c.dim('•')} lore sync logs ${c.dim('— view daemon logs')}`);
|
|
40
|
+
console.log(` ${c.dim('•')} lore sync watch ${c.dim('— watch in foreground')}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(` ${c.bold('Documents & Projects:')}`);
|
|
43
|
+
console.log(` ${c.dim('•')} lore docs list ${c.dim('— list indexed documents')}`);
|
|
44
|
+
console.log(` ${c.dim('•')} lore docs get <id> ${c.dim('— view a document')}`);
|
|
45
|
+
console.log(` ${c.dim('•')} lore projects ${c.dim('— list projects')}`);
|
|
46
|
+
console.log(` ${c.dim('•')} lore projects archive ${c.dim('— archive a project')}`);
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(` ${c.bold('System:')}`);
|
|
49
|
+
console.log(` ${c.dim('•')} lore update ${c.dim('— check for and install updates')}`);
|
|
50
|
+
console.log(` ${c.dim('•')} lore skills install ${c.dim('— install agent integrations')}`);
|
|
51
|
+
console.log(` ${c.dim('•')} lore skills list ${c.dim('— available integrations')}`);
|
|
52
|
+
console.log(` ${c.dim('•')} lore mcp ${c.dim('— start MCP server')}`);
|
|
53
|
+
console.log(` ${c.dim('•')} lore init ${c.dim('— initialize data repository')}`);
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(c.dim(` Run 'lore <command> --help' for options and flags.`));
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
package/dist/core/auth.js
CHANGED
|
@@ -80,7 +80,7 @@ export async function verifyOTP(email, token) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
// All types failed — return the last error
|
|
83
|
-
return { error: 'Token has expired or is invalid. Request a new code with \'lore login\'.' };
|
|
83
|
+
return { error: 'Token has expired or is invalid. Request a new code with \'lore auth login\'.' };
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Extract session from a magic link URL (Supabase sends these for new signups).
|
package/dist/core/data-repo.js
CHANGED
|
@@ -19,7 +19,6 @@ import path from 'path';
|
|
|
19
19
|
export async function initDataRepo(dirPath) {
|
|
20
20
|
await mkdir(dirPath, { recursive: true });
|
|
21
21
|
await mkdir(path.join(dirPath, 'sources'), { recursive: true });
|
|
22
|
-
await mkdir(path.join(dirPath, 'retained'), { recursive: true });
|
|
23
22
|
// Create .gitignore if missing
|
|
24
23
|
const gitignorePath = path.join(dirPath, '.gitignore');
|
|
25
24
|
if (!existsSync(gitignorePath)) {
|
|
@@ -34,8 +33,7 @@ Your personal knowledge repository for Lore.
|
|
|
34
33
|
|
|
35
34
|
## Structure
|
|
36
35
|
|
|
37
|
-
- \`sources/\` - Ingested
|
|
38
|
-
- \`retained/\` - Explicitly saved insights
|
|
36
|
+
- \`sources/\` - Ingested content
|
|
39
37
|
|
|
40
38
|
Vector embeddings are stored in Supabase (cloud) for multi-machine access.
|
|
41
39
|
`);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export type SourceType = string;
|
|
10
10
|
export type SearchMode = 'semantic' | 'keyword' | 'hybrid' | 'regex';
|
|
11
|
-
export type ContentType = 'interview' | 'meeting' | 'conversation' | 'document' | 'note' | 'analysis' | 'survey' | 'research'
|
|
11
|
+
export type ContentType = 'interview' | 'meeting' | 'conversation' | 'document' | 'note' | 'analysis' | 'survey' | 'research';
|
|
12
12
|
export interface SourceDocument {
|
|
13
13
|
id: string;
|
|
14
14
|
source_type: SourceType;
|
|
@@ -154,13 +154,6 @@ export interface SearchArgs {
|
|
|
154
154
|
limit?: number;
|
|
155
155
|
mode?: SearchMode;
|
|
156
156
|
}
|
|
157
|
-
export interface RetainArgs {
|
|
158
|
-
content: string;
|
|
159
|
-
project: string;
|
|
160
|
-
type: 'insight' | 'decision' | 'requirement' | 'note';
|
|
161
|
-
source_context?: string;
|
|
162
|
-
tags?: string[];
|
|
163
|
-
}
|
|
164
157
|
export interface ResearchArgs {
|
|
165
158
|
task: string;
|
|
166
159
|
project?: string;
|
|
@@ -45,7 +45,7 @@ export async function getSupabase() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
// Mode 3: No auth
|
|
48
|
-
throw new Error('Not authenticated. Run \'lore login\' to sign in, or set SUPABASE_SERVICE_KEY for service mode.');
|
|
48
|
+
throw new Error('Not authenticated. Run \'lore auth login\' to sign in, or set SUPABASE_SERVICE_KEY for service mode.');
|
|
49
49
|
}
|
|
50
50
|
// ============================================================================
|
|
51
51
|
// Index Management (compatibility layer - not needed for Supabase)
|
|
@@ -111,9 +111,7 @@ export async function addSource(_dbPath, source, vector, extras) {
|
|
|
111
111
|
if (extras?.source_name) {
|
|
112
112
|
record.source_name = extras.source_name;
|
|
113
113
|
}
|
|
114
|
-
const { error } = await client.from('sources').upsert(record
|
|
115
|
-
ignoreDuplicates: true,
|
|
116
|
-
});
|
|
114
|
+
const { error } = await client.from('sources').upsert(record);
|
|
117
115
|
if (error) {
|
|
118
116
|
// Duplicate content_hash for this user — document already exists, skip silently
|
|
119
117
|
if (error.code === '23505') {
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
* Proposal-based write system for extensions
|
|
3
3
|
*/
|
|
4
4
|
export interface ProposedChange {
|
|
5
|
-
type: 'create_source' | 'update_source' | 'delete_source' | '
|
|
5
|
+
type: 'create_source' | 'update_source' | 'delete_source' | 'add_tags';
|
|
6
6
|
title?: string;
|
|
7
7
|
content?: string;
|
|
8
8
|
project?: string;
|
|
9
9
|
sourceId?: string;
|
|
10
10
|
changes?: Record<string, unknown>;
|
|
11
|
-
insight?: string;
|
|
12
11
|
tags?: string[];
|
|
13
12
|
reason: string;
|
|
14
13
|
}
|
|
@@ -7,7 +7,6 @@ import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { handleIngest } from '../mcp/handlers/ingest.js';
|
|
10
|
-
import { handleRetain } from '../mcp/handlers/retain.js';
|
|
11
10
|
import { getDatabase, getSourceById } from '../core/vector-store.js';
|
|
12
11
|
export function getPendingDir() {
|
|
13
12
|
return path.join(os.homedir(), '.config', 'lore', 'pending');
|
|
@@ -84,18 +83,6 @@ async function applyProposalChange(proposal, dbPath, dataDir) {
|
|
|
84
83
|
}, { hookContext: { mode: 'cli' } });
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
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
86
|
case 'update_source': {
|
|
100
87
|
if (!change.sourceId || !change.changes) {
|
|
101
88
|
throw new Error('update_source requires sourceId and changes');
|
|
@@ -83,7 +83,7 @@ export function createProposeFunction(extensionName, permissions) {
|
|
|
83
83
|
return async (change) => {
|
|
84
84
|
// Enforce permissions
|
|
85
85
|
const perms = permissions || {};
|
|
86
|
-
if (change.type === 'create_source'
|
|
86
|
+
if (change.type === 'create_source') {
|
|
87
87
|
if (!perms.proposeCreate) {
|
|
88
88
|
throw new Error(`Extension "${extensionName}" does not have permission to propose creating documents. Add permissions.proposeCreate = true to the extension.`);
|
|
89
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { registerIngestCommand } from './cli/commands/ingest.js';
|
|
|
34
34
|
import { getExtensionRegistry, getLoreVersionString } from './extensions/registry.js';
|
|
35
35
|
import { bridgeConfigToEnv } from './core/config.js';
|
|
36
36
|
import { expandPath } from './sync/config.js';
|
|
37
|
+
import { showWelcomeScreen } from './cli/welcome.js';
|
|
37
38
|
// Load .env files silently (without the v17 logging)
|
|
38
39
|
function loadEnvFile(filePath, override = false) {
|
|
39
40
|
if (!existsSync(filePath))
|
|
@@ -95,6 +96,10 @@ try {
|
|
|
95
96
|
catch {
|
|
96
97
|
// Extensions not loaded — fine for initial release
|
|
97
98
|
}
|
|
99
|
+
// Default action: show welcome screen when no command is given
|
|
100
|
+
program.action(() => {
|
|
101
|
+
showWelcomeScreen();
|
|
102
|
+
});
|
|
98
103
|
// Global error handler — show friendly messages instead of stack traces
|
|
99
104
|
process.on('uncaughtException', (error) => {
|
|
100
105
|
console.error(`\nError: ${error.message}`);
|
|
@@ -123,9 +123,12 @@ function mapContentType(sourceType) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
export async function handleIngest(dbPath, dataDir, args, options = {}) {
|
|
126
|
-
const { content,
|
|
126
|
+
const { content, project: rawProject, source_type: raw_source_type, date, participants = [], tags = [], source_url, source_name, } = args;
|
|
127
127
|
const { autoPush = true, hookContext } = options;
|
|
128
128
|
const source_type = normalizeSourceType(raw_source_type);
|
|
129
|
+
const project = rawProject.toLowerCase().trim();
|
|
130
|
+
// Auto-generate title if not provided
|
|
131
|
+
const title = args.title || `${source_type.charAt(0).toUpperCase() + source_type.slice(1)}: ${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`;
|
|
129
132
|
// Content hash deduplication — skip everything if already ingested
|
|
130
133
|
const contentHash = createHash('sha256').update(content).digest('hex');
|
|
131
134
|
try {
|
|
@@ -170,12 +173,17 @@ export async function handleIngest(dbPath, dataDir, args, options = {}) {
|
|
|
170
173
|
await writeFile(path.join(sourceDir, 'metadata.json'), JSON.stringify(metadata, null, 2));
|
|
171
174
|
// Save content.md
|
|
172
175
|
await writeFile(path.join(sourceDir, 'content.md'), content);
|
|
173
|
-
// Extract insights using LLM
|
|
176
|
+
// Extract insights using LLM (skip for short content)
|
|
174
177
|
let summary = content.slice(0, 200) + (content.length > 200 ? '...' : '');
|
|
175
178
|
let themes = [];
|
|
176
179
|
let quotes = [];
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
const isShortContent = content.trim().length <= 500;
|
|
181
|
+
if (isShortContent) {
|
|
182
|
+
// Short content fast path — use content as its own summary, skip LLM extraction
|
|
183
|
+
summary = content;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
try {
|
|
179
187
|
const insights = await extractInsights(content, title, id, { contentType });
|
|
180
188
|
summary = insights.summary;
|
|
181
189
|
themes = insights.themes.map((t) => ({ name: t.name, quotes: [] }));
|
|
@@ -183,10 +191,10 @@ export async function handleIngest(dbPath, dataDir, args, options = {}) {
|
|
|
183
191
|
// Save insights.json
|
|
184
192
|
await writeFile(path.join(sourceDir, 'insights.json'), JSON.stringify({ summary, themes, quotes }, null, 2));
|
|
185
193
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('Failed to extract insights:', error);
|
|
196
|
+
// Continue with basic summary
|
|
197
|
+
}
|
|
190
198
|
}
|
|
191
199
|
// Add to vector store immediately
|
|
192
200
|
try {
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { getAllSources } from '../../core/vector-store.js';
|
|
5
5
|
export async function handleListSources(dbPath, args) {
|
|
6
|
-
const { project, source_type, limit = 20 } = args;
|
|
6
|
+
const { project: rawProject, source_type, limit = 20 } = args;
|
|
7
|
+
const project = rawProject?.toLowerCase().trim();
|
|
7
8
|
const sources = await getAllSources(dbPath, {
|
|
8
9
|
project,
|
|
9
10
|
source_type,
|
|
@@ -27,9 +27,9 @@ function createLoreToolsServer(dbPath, dataDir, archivedProjects) {
|
|
|
27
27
|
tool('search', 'Semantic search across all sources in the knowledge repository. Returns summaries with relevant quotes. Use this to find information related to a topic.', {
|
|
28
28
|
query: z.string().describe('Semantic search query - describe what you\'re looking for'),
|
|
29
29
|
source_type: z
|
|
30
|
-
.
|
|
30
|
+
.string()
|
|
31
31
|
.optional()
|
|
32
|
-
.describe('Filter by source type (e.g., "
|
|
32
|
+
.describe('Filter by source type (e.g., "meeting", "slack", "document")'),
|
|
33
33
|
content_type: z
|
|
34
34
|
.enum(['interview', 'meeting', 'conversation', 'document', 'note', 'analysis'])
|
|
35
35
|
.optional()
|
|
@@ -91,6 +91,8 @@ ${quotes}`;
|
|
|
91
91
|
.slice(0, 10)
|
|
92
92
|
.map((q) => `- [${q.speaker || 'unknown'}] "${q.text}"`)
|
|
93
93
|
.join('\n');
|
|
94
|
+
const sourceUrlLine = source.source_url ? `\n**Source URL:** ${source.source_url}` : '';
|
|
95
|
+
const sourceNameLine = source.source_name ? `\n**Source:** ${source.source_name}` : '';
|
|
94
96
|
return {
|
|
95
97
|
content: [
|
|
96
98
|
{
|
|
@@ -99,7 +101,7 @@ ${quotes}`;
|
|
|
99
101
|
|
|
100
102
|
**Type:** ${source.source_type} / ${source.content_type}
|
|
101
103
|
**Created:** ${source.created_at}
|
|
102
|
-
**Projects:** ${source.projects.join(', ') || 'none'}
|
|
104
|
+
**Projects:** ${source.projects.join(', ') || 'none'}${sourceUrlLine}${sourceNameLine}
|
|
103
105
|
|
|
104
106
|
## Summary
|
|
105
107
|
${source.summary}
|
|
@@ -122,9 +124,9 @@ ${quotes || 'No quotes extracted'}`,
|
|
|
122
124
|
// List sources - browse available sources
|
|
123
125
|
tool('list_sources', 'List all sources in the repository. Use this to understand what knowledge is available before searching.', {
|
|
124
126
|
source_type: z
|
|
125
|
-
.
|
|
127
|
+
.string()
|
|
126
128
|
.optional()
|
|
127
|
-
.describe('Filter by source type'),
|
|
129
|
+
.describe('Filter by source type (e.g., "meeting", "slack", "document")'),
|
|
128
130
|
project: z.string().optional().describe('Filter to specific project'),
|
|
129
131
|
limit: z.number().optional().describe('Max results (default 20)'),
|
|
130
132
|
}, async (args) => {
|
|
@@ -240,7 +240,8 @@ Respond with only the JSON object.`;
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
export async function handleResearch(dbPath, dataDir, args, options = {}) {
|
|
243
|
-
const { task, project, include_sources = true } = args;
|
|
243
|
+
const { task, project: rawProject, include_sources = true } = args;
|
|
244
|
+
const project = rawProject?.toLowerCase().trim();
|
|
244
245
|
const { onProgress } = options;
|
|
245
246
|
// Check if we should use agentic mode (default) or simple mode (fallback)
|
|
246
247
|
const useAgenticMode = process.env.LORE_RESEARCH_MODE !== 'simple';
|
|
@@ -15,7 +15,8 @@ import { generateEmbedding } from '../../core/embedder.js';
|
|
|
15
15
|
import { searchLocalFiles, getMatchSnippet } from '../../core/local-search.js';
|
|
16
16
|
import { loadArchivedProjects } from './archive-project.js';
|
|
17
17
|
export async function handleSearch(dbPath, dataDir, args) {
|
|
18
|
-
const { query, project, source_type, content_type, limit = 10, include_archived = false, mode = 'hybrid', } = args;
|
|
18
|
+
const { query, project: rawProject, source_type, content_type, limit = 10, include_archived = false, mode = 'hybrid', } = args;
|
|
19
|
+
const project = rawProject?.toLowerCase().trim();
|
|
19
20
|
// Handle regex mode separately - uses local file search
|
|
20
21
|
if (mode === 'regex') {
|
|
21
22
|
return handleRegexSearch(dbPath, dataDir, {
|
package/dist/mcp/server.js
CHANGED
|
@@ -19,7 +19,6 @@ import { toolDefinitions } from './tools.js';
|
|
|
19
19
|
import { handleSearch } from './handlers/search.js';
|
|
20
20
|
import { handleGetSource } from './handlers/get-source.js';
|
|
21
21
|
import { handleListSources } from './handlers/list-sources.js';
|
|
22
|
-
import { handleRetain } from './handlers/retain.js';
|
|
23
22
|
import { handleIngest } from './handlers/ingest.js';
|
|
24
23
|
import { startResearchJob, getResearchJobStatus } from './handlers/research.js';
|
|
25
24
|
import { handleListProjects } from './handlers/list-projects.js';
|
|
@@ -136,7 +135,7 @@ async function main() {
|
|
|
136
135
|
}
|
|
137
136
|
const server = new Server({
|
|
138
137
|
name: 'lore',
|
|
139
|
-
version: '0.
|
|
138
|
+
version: '0.8.1',
|
|
140
139
|
}, {
|
|
141
140
|
capabilities: {
|
|
142
141
|
tools: {},
|
|
@@ -219,12 +218,6 @@ async function main() {
|
|
|
219
218
|
case 'list_projects':
|
|
220
219
|
result = await handleListProjects(DB_PATH);
|
|
221
220
|
break;
|
|
222
|
-
// Push-based retention
|
|
223
|
-
case 'retain':
|
|
224
|
-
result = await handleRetain(DB_PATH, LORE_DATA_DIR, args, {
|
|
225
|
-
autoPush: AUTO_GIT_PUSH,
|
|
226
|
-
});
|
|
227
|
-
break;
|
|
228
221
|
// Direct document ingestion
|
|
229
222
|
case 'ingest':
|
|
230
223
|
result = await handleIngest(DB_PATH, LORE_DATA_DIR, args, {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -86,18 +86,6 @@ const ListSourcesSchema = z.object({
|
|
|
86
86
|
.describe('Filter by source type (matches the source_type passed during ingest, e.g. "meeting", "slack", "github-issue")'),
|
|
87
87
|
limit: z.number().optional().describe('Max results (default 20)'),
|
|
88
88
|
});
|
|
89
|
-
const RetainSchema = z.object({
|
|
90
|
-
content: z.string().describe('The insight, decision, or note to retain'),
|
|
91
|
-
project: z.string().describe('Project this belongs to'),
|
|
92
|
-
type: z
|
|
93
|
-
.enum(['insight', 'decision', 'requirement', 'note'])
|
|
94
|
-
.describe('Type of knowledge being retained'),
|
|
95
|
-
source_context: z
|
|
96
|
-
.string()
|
|
97
|
-
.optional()
|
|
98
|
-
.describe('Where this came from (e.g., "user interview with Sarah")'),
|
|
99
|
-
tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
|
|
100
|
-
});
|
|
101
89
|
// ============================================================================
|
|
102
90
|
// Agentic Research Tool
|
|
103
91
|
// ============================================================================
|
|
@@ -116,7 +104,7 @@ const ResearchSchema = z.object({
|
|
|
116
104
|
// ============================================================================
|
|
117
105
|
const IngestSchema = z.object({
|
|
118
106
|
content: z.string().describe('The document content to ingest'),
|
|
119
|
-
title: z.string().describe('Title for the document'),
|
|
107
|
+
title: z.string().optional().describe('Title for the document. Auto-generated from content if not provided.'),
|
|
120
108
|
project: z.string().describe('Project this document belongs to'),
|
|
121
109
|
source_type: z
|
|
122
110
|
.string()
|
|
@@ -224,18 +212,6 @@ Use this to browse what exists in a project, understand the scope of available k
|
|
|
224
212
|
properties: {},
|
|
225
213
|
},
|
|
226
214
|
},
|
|
227
|
-
{
|
|
228
|
-
name: 'retain',
|
|
229
|
-
description: `Save a discrete insight, decision, requirement, or note to the knowledge base. These are short, synthesized pieces of knowledge — NOT full documents.
|
|
230
|
-
|
|
231
|
-
Examples of what to retain:
|
|
232
|
-
- A decision: "We chose JWT over session cookies because of mobile app requirements"
|
|
233
|
-
- An insight: "3 out of 5 users mentioned export speed as their top frustration"
|
|
234
|
-
- A requirement: "Must support SSO for enterprise customers"
|
|
235
|
-
|
|
236
|
-
USE 'ingest' INSTEAD for full documents, meeting notes, transcripts, or any content longer than a few paragraphs.`,
|
|
237
|
-
inputSchema: zodToJsonSchema(RetainSchema),
|
|
238
|
-
},
|
|
239
215
|
// Agentic tool
|
|
240
216
|
{
|
|
241
217
|
name: 'research',
|
|
@@ -276,7 +252,7 @@ IDEMPOTENT: Content is deduplicated by SHA256 hash. Calling ingest with identica
|
|
|
276
252
|
WHAT HAPPENS:
|
|
277
253
|
1. Content hash checked for deduplication
|
|
278
254
|
2. Document saved to disk
|
|
279
|
-
3. LLM extracts summary, themes, and key quotes
|
|
255
|
+
3. LLM extracts summary, themes, and key quotes (skipped for short content ≤500 chars)
|
|
280
256
|
4. Embedding generated for semantic search
|
|
281
257
|
5. Indexed in Supabase for instant retrieval
|
|
282
258
|
|
|
@@ -284,7 +260,7 @@ BEST PRACTICES:
|
|
|
284
260
|
- Always pass source_url when available (enables citation linking back to the original)
|
|
285
261
|
- Use source_name for human-readable origin context (e.g., "Slack #product-team")
|
|
286
262
|
- source_type is a free-form hint — use whatever describes the content (slack, email, notion, github-issue, etc.)
|
|
287
|
-
-
|
|
263
|
+
- For short insights, decisions, or notes — just pass the content. Title and source_type are optional.`,
|
|
288
264
|
inputSchema: zodToJsonSchema(IngestSchema),
|
|
289
265
|
},
|
|
290
266
|
// Sync tool
|
|
@@ -295,7 +271,7 @@ BEST PRACTICES:
|
|
|
295
271
|
Phase 1 (Discovery — free, no LLM calls): Scans configured directories, computes content hashes, identifies new files.
|
|
296
272
|
Phase 2 (Processing — only new files): Extracts metadata via LLM, generates embeddings, stores in Supabase.
|
|
297
273
|
|
|
298
|
-
Use this when source directories have been updated externally, or to refresh the index after manual file changes. Source directories are configured via 'lore
|
|
274
|
+
Use this when source directories have been updated externally, or to refresh the index after manual file changes. Source directories are configured via 'lore sync add' CLI command.
|
|
299
275
|
|
|
300
276
|
Note: For pushing content from agents, use 'ingest' instead — it's the direct path.`,
|
|
301
277
|
inputSchema: zodToJsonSchema(SyncSchema),
|
package/dist/sync/config.js
CHANGED
|
@@ -35,7 +35,7 @@ function getDefaultConfig() {
|
|
|
35
35
|
{
|
|
36
36
|
name: 'Example Source',
|
|
37
37
|
path: '~/Documents/notes',
|
|
38
|
-
glob: '
|
|
38
|
+
glob: '**/*',
|
|
39
39
|
project: 'notes',
|
|
40
40
|
enabled: false,
|
|
41
41
|
},
|
|
@@ -103,6 +103,8 @@ export async function initializeSyncConfig() {
|
|
|
103
103
|
// ============================================================================
|
|
104
104
|
export async function addSyncSource(source) {
|
|
105
105
|
const config = await loadSyncConfig();
|
|
106
|
+
// Normalize project name
|
|
107
|
+
source.project = source.project.toLowerCase().trim();
|
|
106
108
|
// Check for duplicate names
|
|
107
109
|
const existingIndex = config.sources.findIndex(s => s.name === source.name);
|
|
108
110
|
if (existingIndex !== -1) {
|
|
@@ -963,6 +963,23 @@ function showProjectDeleteConfirm(state, ui, header) {
|
|
|
963
963
|
ui.deleteConfirm.show();
|
|
964
964
|
ui.screen.render();
|
|
965
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* Start an animated spinner in the status bar.
|
|
968
|
+
* Returns a stop function that clears the interval.
|
|
969
|
+
*/
|
|
970
|
+
function startSpinner(ui, message) {
|
|
971
|
+
const frames = ['\u280b', '\u2819', '\u2838', '\u2834', '\u2826', '\u2807']; // braille dots spinner
|
|
972
|
+
let i = 0;
|
|
973
|
+
const interval = setInterval(() => {
|
|
974
|
+
ui.statusBar.setContent(` {red-fg}{bold}${frames[i % frames.length]}{/bold}{/red-fg} {black-fg}{bold}${message}{/bold}{/black-fg}`);
|
|
975
|
+
ui.screen.render();
|
|
976
|
+
i++;
|
|
977
|
+
}, 100);
|
|
978
|
+
// Show first frame immediately
|
|
979
|
+
ui.statusBar.setContent(` {red-fg}{bold}${frames[0]}{/bold}{/red-fg} {black-fg}{bold}${message}{/bold}{/black-fg}`);
|
|
980
|
+
ui.screen.render();
|
|
981
|
+
return () => clearInterval(interval);
|
|
982
|
+
}
|
|
966
983
|
/**
|
|
967
984
|
* Cancel delete operation
|
|
968
985
|
*/
|
|
@@ -996,8 +1013,7 @@ export async function confirmDelete(state, ui, dbPath, dataDir, project, sourceT
|
|
|
996
1013
|
// Hide dialog and show progress
|
|
997
1014
|
ui.deleteConfirm.hide();
|
|
998
1015
|
state.mode = 'list';
|
|
999
|
-
ui
|
|
1000
|
-
ui.screen.render();
|
|
1016
|
+
const stopSpinner = startSpinner(ui, `Deleting "${source.title}"...`);
|
|
1001
1017
|
try {
|
|
1002
1018
|
// 1. Delete from Supabase (this also handles chunks cascade)
|
|
1003
1019
|
const { sourcePath: originalPath } = await deleteSource(dbPath, source.id);
|
|
@@ -1037,11 +1053,12 @@ export async function confirmDelete(state, ui, dbPath, dataDir, project, sourceT
|
|
|
1037
1053
|
state.selectedIndex = Math.max(0, state.filtered.length - 1);
|
|
1038
1054
|
}
|
|
1039
1055
|
}
|
|
1056
|
+
stopSpinner();
|
|
1040
1057
|
// Update UI
|
|
1041
1058
|
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1042
1059
|
renderList(ui, state);
|
|
1043
1060
|
renderPreview(ui, state);
|
|
1044
|
-
ui.statusBar.setContent(` {
|
|
1061
|
+
ui.statusBar.setContent(` {black-fg}{bold}Deleted successfully{/bold}{/black-fg}`);
|
|
1045
1062
|
ui.screen.render();
|
|
1046
1063
|
// Restore normal status after delay
|
|
1047
1064
|
setTimeout(() => {
|
|
@@ -1050,6 +1067,7 @@ export async function confirmDelete(state, ui, dbPath, dataDir, project, sourceT
|
|
|
1050
1067
|
}, 2000);
|
|
1051
1068
|
}
|
|
1052
1069
|
catch (error) {
|
|
1070
|
+
stopSpinner();
|
|
1053
1071
|
ui.statusBar.setContent(` {red-fg}Delete failed: ${error}{/red-fg}`);
|
|
1054
1072
|
ui.screen.render();
|
|
1055
1073
|
}
|
|
@@ -1058,11 +1076,10 @@ export async function confirmDelete(state, ui, dbPath, dataDir, project, sourceT
|
|
|
1058
1076
|
* Confirm and execute project deletion (all documents in a project)
|
|
1059
1077
|
*/
|
|
1060
1078
|
async function confirmProjectDelete(state, ui, dbPath, dataDir, header, project, sourceType) {
|
|
1061
|
-
// Hide dialog and show
|
|
1079
|
+
// Hide dialog and show spinner
|
|
1062
1080
|
ui.deleteConfirm.hide();
|
|
1063
1081
|
state.mode = 'list';
|
|
1064
|
-
ui
|
|
1065
|
-
ui.screen.render();
|
|
1082
|
+
const stopSpinner = startSpinner(ui, `Deleting ${header.documentCount} documents from "${header.displayName}"...`);
|
|
1066
1083
|
try {
|
|
1067
1084
|
// Get all documents in this project
|
|
1068
1085
|
const docsToDelete = state.filtered.filter(s => {
|
|
@@ -1122,15 +1139,16 @@ async function confirmProjectDelete(state, ui, dbPath, dataDir, header, project,
|
|
|
1122
1139
|
state.selectedIndex = Math.max(0, state.filtered.length - 1);
|
|
1123
1140
|
}
|
|
1124
1141
|
}
|
|
1142
|
+
stopSpinner();
|
|
1125
1143
|
// Update UI
|
|
1126
1144
|
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1127
1145
|
renderList(ui, state);
|
|
1128
1146
|
renderPreview(ui, state);
|
|
1129
1147
|
if (errors.length > 0) {
|
|
1130
|
-
ui.statusBar.setContent(` {
|
|
1148
|
+
ui.statusBar.setContent(` {black-fg}{bold}Deleted ${deleted} documents, ${errors.length} failed{/bold}{/black-fg}`);
|
|
1131
1149
|
}
|
|
1132
1150
|
else {
|
|
1133
|
-
ui.statusBar.setContent(` {
|
|
1151
|
+
ui.statusBar.setContent(` {black-fg}{bold}Deleted ${deleted} documents{/bold}{/black-fg}`);
|
|
1134
1152
|
}
|
|
1135
1153
|
ui.screen.render();
|
|
1136
1154
|
// Restore normal status after delay
|
|
@@ -1140,6 +1158,7 @@ async function confirmProjectDelete(state, ui, dbPath, dataDir, header, project,
|
|
|
1140
1158
|
}, 2000);
|
|
1141
1159
|
}
|
|
1142
1160
|
catch (error) {
|
|
1161
|
+
stopSpinner();
|
|
1143
1162
|
ui.statusBar.setContent(` {red-fg}Delete failed: ${error}{/red-fg}`);
|
|
1144
1163
|
ui.screen.render();
|
|
1145
1164
|
}
|
package/dist/tui/browse-ui.js
CHANGED
|
@@ -54,6 +54,7 @@ export function createUIComponents() {
|
|
|
54
54
|
type: 'line',
|
|
55
55
|
},
|
|
56
56
|
style: {
|
|
57
|
+
bg: 'black',
|
|
57
58
|
border: {
|
|
58
59
|
fg: 'blue',
|
|
59
60
|
},
|
|
@@ -103,6 +104,7 @@ export function createUIComponents() {
|
|
|
103
104
|
type: 'line',
|
|
104
105
|
},
|
|
105
106
|
style: {
|
|
107
|
+
bg: 'black',
|
|
106
108
|
border: {
|
|
107
109
|
fg: 'blue',
|
|
108
110
|
},
|
|
@@ -148,6 +150,7 @@ export function createUIComponents() {
|
|
|
148
150
|
type: 'line',
|
|
149
151
|
},
|
|
150
152
|
style: {
|
|
153
|
+
bg: 'black',
|
|
151
154
|
border: {
|
|
152
155
|
fg: 'blue',
|
|
153
156
|
},
|
|
@@ -258,7 +261,7 @@ export function createUIComponents() {
|
|
|
258
261
|
top: 'center',
|
|
259
262
|
left: 'center',
|
|
260
263
|
width: 60,
|
|
261
|
-
height:
|
|
264
|
+
height: 11,
|
|
262
265
|
border: {
|
|
263
266
|
type: 'line',
|
|
264
267
|
},
|
|
@@ -351,7 +354,7 @@ export function createUIComponents() {
|
|
|
351
354
|
height: 3,
|
|
352
355
|
label: ' Ask Lore ',
|
|
353
356
|
border: { type: 'line' },
|
|
354
|
-
style: { border: { fg: 'cyan' }, focus: { border: { fg: 'green' } } },
|
|
357
|
+
style: { bg: 'black', fg: 'white', border: { fg: 'cyan' }, focus: { border: { fg: 'green' } } },
|
|
355
358
|
hidden: true,
|
|
356
359
|
inputOnFocus: true,
|
|
357
360
|
});
|
|
@@ -364,7 +367,7 @@ export function createUIComponents() {
|
|
|
364
367
|
height: '100%-7',
|
|
365
368
|
label: ' Response ',
|
|
366
369
|
border: { type: 'line' },
|
|
367
|
-
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
370
|
+
style: { fg: 'white', bg: 'black', border: { fg: 'cyan' } },
|
|
368
371
|
hidden: true,
|
|
369
372
|
tags: true,
|
|
370
373
|
scrollable: true,
|
package/package.json
CHANGED
|
@@ -36,8 +36,7 @@ After setup, Lore works autonomously.
|
|
|
36
36
|
| `get_source` | Low | Full document retrieval by ID |
|
|
37
37
|
| `list_sources` | Low | Browse what exists in a project |
|
|
38
38
|
| `list_projects` | Low | Discover available knowledge domains |
|
|
39
|
-
| `
|
|
40
|
-
| `ingest` | Medium | Push full documents into the knowledge base |
|
|
39
|
+
| `ingest` | Low-Medium | Push content — documents, insights, or decisions |
|
|
41
40
|
| `research` | High | Cross-reference multiple sources, synthesize findings |
|
|
42
41
|
| `sync` | Variable | Refresh from configured source directories |
|
|
43
42
|
|
|
@@ -50,6 +49,11 @@ Use `ingest` to push content into Lore when:
|
|
|
50
49
|
|
|
51
50
|
Always pass `source_url` (original URL for linking) and `source_name` (human-readable label like "GitHub PR #123") when available. Ingestion is idempotent — safe to call repeatedly with the same content.
|
|
52
51
|
|
|
52
|
+
For short insights, decisions, or notes — title and source_type are optional:
|
|
53
|
+
```
|
|
54
|
+
ingest(content: "We chose JWT for auth", project: "auth-system")
|
|
55
|
+
```
|
|
56
|
+
|
|
53
57
|
## When to Search
|
|
54
58
|
|
|
55
59
|
Before making recommendations or answering questions about past work:
|
|
@@ -57,13 +61,6 @@ Before making recommendations or answering questions about past work:
|
|
|
57
61
|
2. Only use `research` if the question genuinely needs cross-referencing multiple sources
|
|
58
62
|
3. Use `get_source(id, include_content: true)` when you need the full text
|
|
59
63
|
|
|
60
|
-
## When to Retain
|
|
61
|
-
|
|
62
|
-
Use `retain` for short synthesized knowledge (not full documents):
|
|
63
|
-
- Decisions made during a session
|
|
64
|
-
- Key insights distilled from analysis
|
|
65
|
-
- Requirements extracted from conversations
|
|
66
|
-
|
|
67
64
|
## Example: Grounding a Decision
|
|
68
65
|
|
|
69
66
|
```
|
|
@@ -73,11 +70,9 @@ search("database migration approach", project: "backend-rewrite")
|
|
|
73
70
|
# 2. If results are relevant, get full context
|
|
74
71
|
get_source("abc-123", include_content: true)
|
|
75
72
|
|
|
76
|
-
# 3. After making a decision,
|
|
77
|
-
|
|
73
|
+
# 3. After making a decision, save it
|
|
74
|
+
ingest(
|
|
78
75
|
content: "Chose pgvector over Pinecone for embeddings — lower latency, simpler ops, sufficient scale",
|
|
79
|
-
project: "backend-rewrite"
|
|
80
|
-
type: "decision",
|
|
81
|
-
source_context: "Architecture review session"
|
|
76
|
+
project: "backend-rewrite"
|
|
82
77
|
)
|
|
83
78
|
```
|
package/plugins/codex/SKILL.md
CHANGED
|
@@ -35,8 +35,7 @@ After setup, Lore works autonomously.
|
|
|
35
35
|
| `get_source` | Low | Full document retrieval by ID |
|
|
36
36
|
| `list_sources` | Low | Browse what exists in a project |
|
|
37
37
|
| `list_projects` | Low | Discover available knowledge domains |
|
|
38
|
-
| `
|
|
39
|
-
| `ingest` | Medium | Push full documents into the knowledge base |
|
|
38
|
+
| `ingest` | Low-Medium | Push content — documents, insights, or decisions |
|
|
40
39
|
| `research` | High | Cross-reference multiple sources, synthesize findings |
|
|
41
40
|
| `sync` | Variable | Refresh from configured source directories |
|
|
42
41
|
|
|
@@ -51,6 +50,4 @@ Before making recommendations or answering questions about past work:
|
|
|
51
50
|
2. Only use `research` for multi-source synthesis (10x more expensive)
|
|
52
51
|
3. Use `get_source(id, include_content: true)` for full text
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
Use `retain` for short synthesized knowledge (decisions, insights, requirements) — not full documents.
|
|
53
|
+
For short insights or decisions, just pass the content — title and source_type are optional and auto-generated from content.
|
package/plugins/gemini/GEMINI.md
CHANGED
|
@@ -30,8 +30,7 @@ After setup, Lore works autonomously.
|
|
|
30
30
|
| `get_source` | Low | Full document retrieval by ID |
|
|
31
31
|
| `list_sources` | Low | Browse what exists in a project |
|
|
32
32
|
| `list_projects` | Low | Discover available knowledge domains |
|
|
33
|
-
| `
|
|
34
|
-
| `ingest` | Medium | Push full documents into the knowledge base |
|
|
33
|
+
| `ingest` | Low-Medium | Push content — documents, insights, or decisions |
|
|
35
34
|
| `research` | High | Cross-reference multiple sources, synthesize findings |
|
|
36
35
|
| `sync` | Variable | Refresh from configured source directories |
|
|
37
36
|
|
|
@@ -46,6 +45,4 @@ Before making recommendations or answering questions about past work:
|
|
|
46
45
|
2. Only use `research` for multi-source synthesis (10x more expensive)
|
|
47
46
|
3. Use `get_source(id, include_content: true)` for full text
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Use `retain` for short synthesized knowledge (decisions, insights, requirements) — not full documents.
|
|
48
|
+
For short insights or decisions, just pass the content — title and source_type are optional and auto-generated from content.
|
package/skills/generic-agent.md
CHANGED
|
@@ -26,7 +26,6 @@ After setup, Lore works autonomously.
|
|
|
26
26
|
|
|
27
27
|
- **Sources**: Full documents (meeting notes, interviews, Slack threads, specs, etc.)
|
|
28
28
|
- **Projects**: Organizational grouping for sources
|
|
29
|
-
- **Insights**: Short retained knowledge (decisions, requirements, observations)
|
|
30
29
|
- **Citations**: Every piece of knowledge links back to its original source
|
|
31
30
|
|
|
32
31
|
## Tools Reference
|
|
@@ -47,10 +46,19 @@ The primary way to add content. Accepts any document with metadata.
|
|
|
47
46
|
}
|
|
48
47
|
```
|
|
49
48
|
|
|
49
|
+
For short insights, decisions, or notes — title and source_type are optional:
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"content": "We chose JWT over session cookies because of mobile app requirements",
|
|
53
|
+
"project": "auth-system"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
50
57
|
- **Idempotent**: Duplicate content returns `{deduplicated: true}` with no processing cost.
|
|
51
58
|
- **source_type**: Free-form string. Common values: `meeting`, `interview`, `document`, `notes`, `analysis`, `conversation`, `slack`, `email`, `github-issue`, `notion`.
|
|
52
59
|
- **source_url**: Always pass when available — enables citation linking.
|
|
53
60
|
- **source_name**: Human-readable origin label.
|
|
61
|
+
- Short content (≤500 chars) skips LLM extraction for speed.
|
|
54
62
|
|
|
55
63
|
### `search` — Find relevant sources
|
|
56
64
|
Fast lookup. Returns summaries with relevance scores.
|
|
@@ -79,18 +87,6 @@ List sources filtered by project or type. Sorted by date (newest first).
|
|
|
79
87
|
### `list_projects` — Discover projects
|
|
80
88
|
Lists all projects with source counts and activity dates.
|
|
81
89
|
|
|
82
|
-
### `retain` — Save discrete knowledge
|
|
83
|
-
For short insights, decisions, or requirements — not full documents.
|
|
84
|
-
|
|
85
|
-
```json
|
|
86
|
-
{
|
|
87
|
-
"content": "Users consistently report export takes >30s for large datasets",
|
|
88
|
-
"project": "my-project",
|
|
89
|
-
"type": "insight",
|
|
90
|
-
"source_context": "User interview synthesis — Jan batch"
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
90
|
### `research` — Deep research with citations
|
|
95
91
|
Runs an internal agent that iteratively searches, reads, and synthesizes findings.
|
|
96
92
|
|
|
@@ -101,7 +97,7 @@ Runs an internal agent that iteratively searches, reads, and synthesizes finding
|
|
|
101
97
|
}
|
|
102
98
|
```
|
|
103
99
|
|
|
104
|
-
**
|
|
100
|
+
**Async**: Returns a `job_id` immediately. Poll `research_status` for results (typically 2-8 minutes). Makes 10-30 internal LLM calls. Use `search` for simple lookups.
|
|
105
101
|
|
|
106
102
|
### `sync` — Refresh from source directories
|
|
107
103
|
Scans configured directories for new files. Use `ingest` for agent-pushed content instead.
|
|
@@ -114,6 +110,6 @@ Excludes from default search. Only use when explicitly requested.
|
|
|
114
110
|
1. **Search before you answer**: If a question might have documented context, search Lore first.
|
|
115
111
|
2. **Ingest what matters**: After meaningful conversations or when processing external content, ingest it.
|
|
116
112
|
3. **Always pass source_url**: Enables citation linking back to the original.
|
|
117
|
-
4. **
|
|
113
|
+
4. **Ingest handles both long and short content**: For short insights, decisions, or notes — just pass the content. Title and source_type are optional.
|
|
118
114
|
5. **Prefer search over research**: `search` is 10x cheaper. Only use `research` for multi-source synthesis.
|
|
119
115
|
6. **Cite your sources**: When presenting Lore results, reference the source title and date.
|
package/skills/openclaw.md
CHANGED
|
@@ -37,12 +37,12 @@ Before answering questions about past decisions, user feedback, project history,
|
|
|
37
37
|
|
|
38
38
|
3. **Use `get_source`** with `include_content=true` when you need the full original text of a specific document.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Short Content
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
For short insights, decisions, or notes — title and source_type are optional:
|
|
43
|
+
```
|
|
44
|
+
ingest(content: "We chose X because Y", project: "my-project")
|
|
45
|
+
```
|
|
46
46
|
|
|
47
47
|
## Citation Best Practices
|
|
48
48
|
|