@ -19,10 +19,15 @@ import * as fs from "fs";
import * as path from "path" ;
import * as path from "path" ;
// Configuration - can be overridden via environment variables
// Configuration - can be overridden via environment variables
const API_BASE_URL = process . env . SECOND_BRAIN_API_URL || "https://2brain.coer.nl/api" ;
// API_BASE_URL should point to the base (e.g., http://192.168.1.16:8001)
// The /api/v1 prefix is added by apiRequest
const API_BASE_URL = ( process . env . SECOND_BRAIN_API_URL || "http://192.168.1.16:8001" ) . replace ( /\/+$/ , "" ) ;
const API_USERNAME = process . env . SECOND_BRAIN_USERNAME || "" ;
const API_USERNAME = process . env . SECOND_BRAIN_USERNAME || "" ;
const API_PASSWORD = process . env . SECOND_BRAIN_PASSWORD || "" ;
const API_PASSWORD = process . env . SECOND_BRAIN_PASSWORD || "" ;
// Default search threshold - embedding scores are often low, so use a low default
const DEFAULT_SEARCH_THRESHOLD = 0.01 ;
// Build auth header if credentials provided
// Build auth header if credentials provided
function getAuthHeader ( ) : Record < string , string > {
function getAuthHeader ( ) : Record < string , string > {
if ( API_USERNAME && API_PASSWORD ) {
if ( API_USERNAME && API_PASSWORD ) {
@ -37,7 +42,11 @@ async function apiRequest(
endpoint : string ,
endpoint : string ,
options : RequestInit = { }
options : RequestInit = { }
) : Promise < Response > {
) : Promise < Response > {
const url = ` ${ API_BASE_URL } ${ endpoint } ` ;
// Ensure endpoint starts with /api/v1
const normalizedEndpoint = endpoint . startsWith ( "/api/v1" )
? endpoint
: ` /api ${ endpoint . startsWith ( "/v1" ) ? endpoint : ` /v1 ${ endpoint } ` } ` ;
const url = ` ${ API_BASE_URL } ${ normalizedEndpoint } ` ;
const headers = {
const headers = {
. . . getAuthHeader ( ) ,
. . . getAuthHeader ( ) ,
. . . options . headers ,
. . . options . headers ,
@ -321,12 +330,17 @@ async function handleSearch(args: {
query : string ;
query : string ;
top_k? : number ;
top_k? : number ;
} ) : Promise < string > {
} ) : Promise < string > {
const params = new URLSearchParams ( {
// Use POST with JSON body for more control over search parameters
q : args.query ,
const response = await apiRequest ( ` /v1/search ` , {
top_k : String ( args . top_k || 5 ) ,
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
query : args.query ,
limit : args.top_k || 5 ,
threshold : DEFAULT_SEARCH_THRESHOLD ,
hybrid : true ,
} ) ,
} ) ;
} ) ;
const response = await apiRequest ( ` /v1/search? ${ params } ` ) ;
const data = await response . json ( ) ;
const data = await response . json ( ) ;
if ( ! response . ok ) {
if ( ! response . ok ) {
@ -450,30 +464,34 @@ async function handleListDocuments(args: {
folder? : string ;
folder? : string ;
limit? : number ;
limit? : number ;
} ) : Promise < string > {
} ) : Promise < string > {
const params = new URLSearchParams ( {
// The /documents endpoint doesn't exist, so we use search with a broad query
limit : String ( args . limit || 20 ) ,
// or get stats to show document count
} ) ;
const statsResponse = await apiRequest ( "/v1/stats" ) ;
if ( args . folder ) params . append ( "folder" , args . folder ) ;
const stats = await statsResponse . json ( ) ;
const response = await apiRequest ( ` /v1/documents? ${ params } ` ) ;
const data = await response . json ( ) ;
if ( ! r esponse. ok ) {
if ( ! statsResponse . ok ) {
throw new Error (
throw new Error (
` Failed to list documents: ${ data . detail || r esponse. statusText } `
` Failed to get stats: ${ stats . detail || statsR esponse. statusText } `
) ;
) ;
}
}
if ( ! data . documents || data . documents . length === 0 ) {
// Also get tags to show what's in the vault
return "No documents found." ;
const tagsResponse = await apiRequest ( "/v1/tags" ) ;
}
const tags = await tagsResponse . json ( ) ;
let output = ` 📚 Knowledge Base Overview: \ n \ n ` ;
output += ` - **Documents:** ${ stats . total_documents || 0 } \ n ` ;
output += ` - **Chunks:** ${ stats . total_chunks || 0 } \ n ` ;
output += ` - **Last indexed:** ${ stats . last_indexed || "Never" } \ n \ n ` ;
let output = ` 📚 Documents ( ${ data . total } ): \ n \ n ` ;
if ( Array . isArray ( tags ) && tags . length > 0 ) {
for ( const doc of data . documents ) {
output += ` **Tags:** \ n ` ;
output += ` - ** ${ doc . title || doc . path } ** ` ;
for ( const tag of tags ) {
if ( doc . tags ? . length ) output += ` [ ${ doc . tags . join ( ", " ) } ] ` ;
output += ` - # ${ tag . tag } ( ${ tag . count } docs) \ n ` ;
output += "\n" ;
}
}
}
output += ` \ n_Use search to find specific documents._ ` ;
return output ;
return output ;
}
}
@ -489,10 +507,12 @@ async function handleGetStats(): Promise<string> {
}
}
return ` 📊 Second Brain Stats:
return ` 📊 Second Brain Stats:
- Documents : $ { data . document_count || 0 }
- Documents : $ { data . total_documents || data . document_count || 0 }
- Chunks : $ { data . chunk_count || 0 }
- Chunks : $ { data . total_chunks || data . chunk_count || 0 }
- Vault size : $ { data . vault_size_mb ? . toFixed ( 2 ) || 0 } MB
- Vault size : $ { data . vault_size_mb ? . toFixed ( 2 ) || "N/A" } MB
- Last indexed : $ { data . last_indexed || "Never" } ` ;
- Last indexed : $ { data . last_indexed || "Never" }
- Embedding model : $ { data . embedding_model || "N/A" }
- Chat model : $ { data . chat_model || "N/A" } ` ;
}
}
async function handleReindex ( ) : Promise < string > {
async function handleReindex ( ) : Promise < string > {