Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ OLLAMA_PORT=11434
OLLAMA_MODEL=llama3.2:3b

# Centralized Sync (optional, for multi-machine knowledge sharing)
ODIN_SYNC_ENABLED=false
# ODIN_URL=http://your-server:8080
# ODIN_API_TOKEN=your-token
REMOTE_SYNC_ENABLED=false
# REMOTE_SYNC_URL=http://your-server:8080
# REMOTE_SYNC_TOKEN=your-token

# Cloud API Sync (optional)
# PREFRONTAL_API_URL=http://your-api:8080
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ vendor/bin/pest tests/Feature/Commands/KnowledgeSearchCommandTest.php
| `QdrantService` | All vector DB operations (upsert, search, delete, collections) |
| `EmbeddingService` | Text-to-vector conversion |
| `KnowledgeCacheService` | Redis caching for sub-200ms queries |
| `OdinSyncService` | Background sync to centralized Odin server |
| `RemoteSyncService` | Background sync to centralized remote server |
| `WriteGateService` | Filters knowledge quality before persistence |
| `EntryMetadataService` | Staleness detection, confidence degradation |
| `CorrectionService` | Multi-tier correction propagation |
Expand Down
8 changes: 4 additions & 4 deletions MISSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Knowledge is a command-line tool that captures technical decisions, learnings, a

### 2. Offline-First, Sync-Later
- **Local Qdrant**: Full functionality without network
- **Background sync**: Queue writes, sync to Odin every N minutes
- **Background sync**: Queue writes, sync to remote server every N minutes
- **Read-optimized**: Instant local reads, async writes to central DB
- **Graceful degradation**: Always functional, network optional

Expand Down Expand Up @@ -82,10 +82,10 @@ Knowledge is a command-line tool that captures technical decisions, learnings, a
│ Background sync
┌─────────────────────────────────────────┐
Odin (Centralized)
Remote Server (Centralized) │
│ - Same stack as local │
│ - Team knowledge repository
│ - Exposed via Tailscale mesh
│ - Team/shared knowledge repository │
│ - Exposed via VPN, Tailscale, or LAN
└─────────────────────────────────────────┘
```

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Embedding Server (sentence-transformers)
Ollama (optional - async auto-tagging via background queue)
Odin Sync (background sync to centralized server)
Remote Sync (optional - background sync to centralized server)
```

No SQLite. No schema migrations. Pure vector storage. Per-project isolation via auto-detected git namespaces.
Expand Down Expand Up @@ -96,7 +96,7 @@ All commands support `--project=<name>` to target a specific project namespace a
| Command | Description |
|---------|-------------|
| `sync` | Bidirectional sync (--push / --pull) |
| `sync:odin` | Background sync to Odin central server |
| `sync:remote` | Background sync to centralized remote server |
| `sync:purge` | Purge sync queue |

### Code Intelligence
Expand Down Expand Up @@ -162,9 +162,9 @@ REDIS_PORT=6379
OLLAMA_HOST=http://localhost:11434
```

### Odin (Production)
### Remote Server (Production)

Uses `docker-compose.odin.yml` with Tailscale networking for centralized knowledge sync.
Uses `docker-compose.remote.yml` to bind services to a specific network interface (e.g. Tailscale, VPN, LAN) for centralized knowledge sync across multiple machines.

## Development

Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Replaced SQLite entirely with Qdrant-only architecture. No schema migrations, no
### Redis Caching Layer
KnowledgeCacheService provides sub-200ms query responses through aggressive caching of embeddings, search results, and collection stats.

### Odin Background Sync
OdinSyncService syncs knowledge to centralized Odin server. Includes deletion propagation, sync purge, and bidirectional push/pull.
### Remote Background Sync
RemoteSyncService syncs knowledge to a centralized remote server. Includes deletion propagation, sync purge, and bidirectional push/pull. Useful for home labs and teams sharing a knowledge base.

### Entry Metadata & Staleness Detection
EntryMetadataService tracks entry freshness with confidence degradation over time. Superseded marking instead of destructive overwrites.
Expand Down
14 changes: 7 additions & 7 deletions app/Commands/KnowledgeStatsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

use App\Commands\Concerns\ResolvesProject;
use App\Services\KnowledgeCacheService;
use App\Services\OdinSyncService;
use App\Services\QdrantService;
use App\Services\RemoteSyncService;
use Illuminate\Support\Collection;
use LaravelZero\Framework\Commands\Command;

Expand All @@ -25,7 +25,7 @@ class KnowledgeStatsCommand extends Command

protected $description = 'Display analytics dashboard for knowledge entries';

public function handle(QdrantService $qdrant, OdinSyncService $odinSync): int
public function handle(QdrantService $qdrant, RemoteSyncService $remoteSync): int
{
$project = $this->resolveProject();

Expand All @@ -44,7 +44,7 @@ public function handle(QdrantService $qdrant, OdinSyncService $odinSync): int
$this->renderCacheMetrics($cacheService);
}

$this->renderSyncStatus($odinSync);
$this->renderSyncStatus($remoteSync);

return self::SUCCESS;
}
Expand Down Expand Up @@ -138,13 +138,13 @@ private function renderCacheMetrics(KnowledgeCacheService $cacheService): void
table(['Cache', 'Hits', 'Misses', 'Hit Rate'], $rows);
}

private function renderSyncStatus(OdinSyncService $odinSync): void
private function renderSyncStatus(RemoteSyncService $remoteSync): void
{
if (! $odinSync->isEnabled()) {
if (! $remoteSync->isEnabled()) {
return;
}

$status = $odinSync->getStatus();
$status = $remoteSync->getStatus();

$statusColor = match ($status['status']) {
'synced' => 'green',
Expand All @@ -154,7 +154,7 @@ private function renderSyncStatus(OdinSyncService $odinSync): void
};

$this->newLine();
$this->line('<fg=gray>Odin Sync</>');
$this->line('<fg=gray>Remote Sync</>');
table(
['Property', 'Value'],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,46 @@

namespace App\Commands;

use App\Services\OdinSyncService;
use App\Services\QdrantService;
use App\Services\RemoteSyncService;
use Illuminate\Support\Str;
use LaravelZero\Framework\Commands\Command;

class OdinSyncCommand extends Command
class RemoteSyncCommand extends Command
{
/**
* @var string
*/
protected $signature = 'sync:odin
{--push : Push queued entries to Odin}
{--pull : Pull entries from Odin}
protected $signature = 'sync:remote
{--push : Push queued entries to remote server}
{--pull : Pull entries from remote server}
{--status : Show sync status only}
{--clear : Clear the sync queue}
{--project=default : Project namespace to sync}';

/**
* @var string
*/
protected $description = 'Synchronize knowledge with Odin centralized server';
protected $description = 'Synchronize knowledge with remote centralized server';

public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int
public function handle(RemoteSyncService $remoteSync, QdrantService $qdrant): int
{
if (! $odinSync->isEnabled()) {
$this->error('Odin sync is disabled. Set ODIN_SYNC_ENABLED=true to enable.');
if (! $remoteSync->isEnabled()) {
$this->error('Remote sync is disabled. Set REMOTE_SYNC_ENABLED=true to enable.');

return self::FAILURE;
}

// Status-only mode
if ((bool) $this->option('status')) {
$this->displayStatus($odinSync);
$this->displayStatus($remoteSync);

return self::SUCCESS;
}

// Clear queue
if ((bool) $this->option('clear')) {
$odinSync->clearQueue();
$remoteSync->clearQueue();
$this->info('Sync queue cleared.');

return self::SUCCESS;
Expand All @@ -61,34 +61,34 @@ public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int
}

// Check connectivity
$this->line('Checking Odin connectivity...');
$available = $odinSync->isAvailable();
$this->line('Checking remote server connectivity...');
$available = $remoteSync->isAvailable();

if (! $available) {
$this->warn('Odin server is not reachable. Operations will remain queued for later sync.');
$this->warn('Remote server is not reachable. Operations will remain queued for later sync.');

$status = $odinSync->getStatus();
$status = $remoteSync->getStatus();
$this->line("Pending operations: {$status['pending']}");

return self::SUCCESS;
}

$this->info('Odin server connected.');
$this->info('Remote server connected.');

$pushResult = ['synced' => 0, 'failed' => 0, 'remaining' => 0];
$pullCount = 0;

// Push queued items
if ($push) {
$this->line('Processing sync queue...');
$pushResult = $odinSync->processQueue();
$pushResult = $remoteSync->processQueue();
}

// Pull from Odin
// Pull from remote server
if ($pull) {
$this->line("Pulling entries from Odin for project '{$project}'...");
$entries = $odinSync->pullFromOdin($project);
$pullCount = $this->mergeEntries($entries, $qdrant, $odinSync, $project);
$this->line("Pulling entries from remote server for project '{$project}'...");
$entries = $remoteSync->pullFromRemote($project);
$pullCount = $this->mergeEntries($entries, $qdrant, $remoteSync, $project);
}

$this->displaySyncSummary($pushResult, $pullCount, $pull);
Expand All @@ -104,7 +104,7 @@ public function handle(OdinSyncService $odinSync, QdrantService $qdrant): int
private function mergeEntries(
array $remoteEntries,
QdrantService $qdrant,
OdinSyncService $odinSync,
RemoteSyncService $remoteSync,
string $project,
): int {
$merged = 0;
Expand All @@ -121,14 +121,14 @@ private function mergeEntries(

if ($local !== null) {
// Conflict resolution: last-write-wins
$winner = $odinSync->resolveConflict($local, $remote);
$winner = $remoteSync->resolveConflict($local, $remote);
if ($winner === $remote) {
$entry = $this->buildEntryFromRemote($remote, $local['id']);
$qdrant->upsert($entry, $project);
$merged++;
}
} else {
// New entry from Odin
// New entry from remote server
$entry = $this->buildEntryFromRemote($remote, Str::uuid()->toString());
$qdrant->upsert($entry, $project);
$merged++;
Expand Down Expand Up @@ -173,9 +173,9 @@ private function buildEntryFromRemote(array $remote, string|int $id): array
return $entry;
}

private function displayStatus(OdinSyncService $odinSync): void
private function displayStatus(RemoteSyncService $remoteSync): void
{
$status = $odinSync->getStatus();
$status = $remoteSync->getStatus();

$statusColor = match ($status['status']) {
'synced' => 'green',
Expand All @@ -185,15 +185,15 @@ private function displayStatus(OdinSyncService $odinSync): void
};

$this->newLine();
$this->line('<fg=gray>Odin Sync Status</>');
$this->line('<fg=gray>Remote Sync Status</>');
$this->table(
['Property', 'Value'],
[
['Status', "<fg={$statusColor}>{$status['status']}</>"],
['Pending Operations', (string) $status['pending']],
['Last Synced', $status['last_synced'] ?? 'Never'],
['Last Error', $status['last_error'] ?? 'None'],
['Odin URL', (string) config('services.odin.url', 'Not configured')],
['Remote URL', (string) config('services.remote.url', 'Not configured')],
]
);
}
Expand All @@ -204,7 +204,7 @@ private function displayStatus(OdinSyncService $odinSync): void
private function displaySyncSummary(array $pushResult, int $pullCount, bool $pulled): void
{
$this->newLine();
$this->info('=== Odin Sync Summary ===');
$this->info('=== Remote Sync Summary ===');

$rows = [
['Pushed (synced)', (string) $pushResult['synced']],
Expand Down
8 changes: 4 additions & 4 deletions app/Commands/Service/DownCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ class DownCommand extends Command
{
protected $signature = 'service:down
{--volumes : Remove volumes}
{--odin : Use Odin (remote) configuration}
{--remote : Use remote configuration}
{--force : Skip confirmation prompts}';

protected $description = 'Stop knowledge services';

public function handle(): int
{
$composeFile = $this->option('odin') === true
? 'docker-compose.odin.yml'
$composeFile = $this->option('remote') === true
? 'docker-compose.remote.yml'
: 'docker-compose.yml';

$environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local';
$environment = $this->option('remote') === true ? 'Remote' : 'Local';

if (! file_exists(base_path($composeFile))) {
render(<<<HTML
Expand Down
8 changes: 4 additions & 4 deletions app/Commands/Service/LogsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ class LogsCommand extends Command
{service? : Specific service (qdrant, redis, embeddings, ollama)}
{--f|follow : Follow log output}
{--tail=50 : Number of lines to show}
{--odin : Use Odin (remote) configuration}';
{--remote : Use remote configuration}';

protected $description = 'View service logs';

public function handle(): int
{
$composeFile = $this->option('odin') === true
? 'docker-compose.odin.yml'
$composeFile = $this->option('remote') === true
? 'docker-compose.remote.yml'
: 'docker-compose.yml';

$environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local';
$environment = $this->option('remote') === true ? 'Remote' : 'Local';

if (! file_exists(base_path($composeFile))) {
render(<<<HTML
Expand Down
8 changes: 4 additions & 4 deletions app/Commands/Service/StatusCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class StatusCommand extends Command
{
protected $signature = 'service:status
{--odin : Use Odin (remote) configuration}';
{--remote : Use remote configuration}';

protected $description = 'Check service health status';

Expand All @@ -26,11 +26,11 @@ public function __construct(

public function handle(): int
{
$composeFile = $this->option('odin') === true
? 'docker-compose.odin.yml'
$composeFile = $this->option('remote') === true
? 'docker-compose.remote.yml'
: 'docker-compose.yml';

$environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local';
$environment = $this->option('remote') === true ? 'Remote' : 'Local';

// Perform health checks with spinner
$healthData = spin(
Expand Down
8 changes: 4 additions & 4 deletions app/Commands/Service/UpCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ class UpCommand extends Command
{
protected $signature = 'service:up
{--d|detach : Run in detached mode}
{--odin : Use Odin (remote) configuration}';
{--remote : Use remote configuration}';

protected $description = 'Start knowledge services (Qdrant, Redis, Embeddings)';

public function handle(): int
{
$composeFile = $this->option('odin') === true
? 'docker-compose.odin.yml'
$composeFile = $this->option('remote') === true
? 'docker-compose.remote.yml'
: 'docker-compose.yml';

$environment = $this->option('odin') === true ? 'Odin (Remote)' : 'Local';
$environment = $this->option('remote') === true ? 'Remote' : 'Local';

if (! file_exists(base_path($composeFile))) {
render(<<<HTML
Expand Down
Loading
Loading