byterover-cli 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/core/domain/swarm/types.d.ts +132 -0
- package/dist/agent/core/domain/swarm/types.js +128 -0
- package/dist/agent/core/domain/tools/constants.d.ts +2 -0
- package/dist/agent/core/domain/tools/constants.js +2 -0
- package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
- package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
- package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
- package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
- package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
- package/dist/agent/infra/agent/service-initializer.js +48 -0
- package/dist/agent/infra/map/map-shared.d.ts +2 -2
- package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
- package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
- package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
- package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
- package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
- package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
- package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
- package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
- package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
- package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
- package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
- package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
- package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
- package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
- package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
- package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
- package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
- package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
- package/dist/agent/infra/swarm/provider-factory.js +67 -0
- package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
- package/dist/agent/infra/swarm/search-precision.js +141 -0
- package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
- package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
- package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
- package/dist/agent/infra/swarm/swarm-graph.js +167 -0
- package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
- package/dist/agent/infra/swarm/swarm-merger.js +66 -0
- package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
- package/dist/agent/infra/swarm/swarm-router.js +40 -0
- package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
- package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
- package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
- package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
- package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
- package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
- package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
- package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
- package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
- package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
- package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
- package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
- package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
- package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
- package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
- package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
- package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
- package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
- package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
- package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
- package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +25 -1
- package/dist/agent/resources/tools/code_exec.txt +2 -0
- package/dist/agent/resources/tools/swarm_query.txt +38 -0
- package/dist/agent/resources/tools/swarm_store.txt +35 -0
- package/dist/oclif/commands/swarm/curate.d.ts +13 -0
- package/dist/oclif/commands/swarm/curate.js +81 -0
- package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
- package/dist/oclif/commands/swarm/onboard.js +233 -0
- package/dist/oclif/commands/swarm/query.d.ts +14 -0
- package/dist/oclif/commands/swarm/query.js +84 -0
- package/dist/oclif/commands/swarm/status.d.ts +41 -0
- package/dist/oclif/commands/swarm/status.js +278 -0
- package/dist/server/constants.d.ts +3 -2
- package/dist/server/constants.js +10 -9
- package/dist/server/core/domain/source/source-schema.d.ts +6 -6
- package/dist/server/core/domain/transport/schemas.d.ts +4 -4
- package/dist/server/infra/http/provider-model-fetchers.js +1 -0
- package/dist/server/infra/process/feature-handlers.js +13 -0
- package/dist/server/infra/project/project-registry.js +13 -1
- package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
- package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
- package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
- package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
- package/dist/server/templates/skill/SKILL.md +163 -0
- package/dist/server/utils/gitignore.d.ts +1 -0
- package/dist/server/utils/gitignore.js +36 -4
- package/oclif.manifest.json +259 -79
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import type { IContentGenerator } from '../../core/interfaces/i-content-generato
|
|
|
3
3
|
import type { CurateOperation, CurateOptions, CurateResult, DetectDomainsInput, DetectDomainsResult, ICurateService } from '../../core/interfaces/i-curate-service.js';
|
|
4
4
|
import type { IFileSystem } from '../../core/interfaces/i-file-system.js';
|
|
5
5
|
import type { ISandboxService } from '../../core/interfaces/i-sandbox-service.js';
|
|
6
|
+
import type { ISwarmCoordinator } from '../../core/interfaces/i-swarm-coordinator.js';
|
|
6
7
|
import type { SessionManager } from '../session/session-manager.js';
|
|
7
8
|
import { type ChunkResult, type CurationFact, type MessageBoundary, type ReconResult } from './curation-helpers.js';
|
|
8
9
|
/**
|
|
@@ -210,6 +211,28 @@ export interface ToolsSDK {
|
|
|
210
211
|
* @returns Promise resolving to search results
|
|
211
212
|
*/
|
|
212
213
|
searchKnowledge(query: string, options?: SearchKnowledgeOptions): Promise<SearchKnowledgeResult>;
|
|
214
|
+
/**
|
|
215
|
+
* Search across all active swarm memory providers.
|
|
216
|
+
* Available in both query and curate modes (read operation).
|
|
217
|
+
* @param query - Natural language search query
|
|
218
|
+
* @param options - Optional limit and scope
|
|
219
|
+
* @returns Promise resolving to ranked results from all active providers
|
|
220
|
+
*/
|
|
221
|
+
swarmQuery(query: string, options?: {
|
|
222
|
+
limit?: number;
|
|
223
|
+
scope?: string;
|
|
224
|
+
}): Promise<unknown>;
|
|
225
|
+
/**
|
|
226
|
+
* Store knowledge in a swarm provider (GBrain, local markdown).
|
|
227
|
+
* Disabled in query (read-only) mode.
|
|
228
|
+
* @param request - Store request with content, optional contentType and provider
|
|
229
|
+
* @returns Promise resolving to store result with provider ID and latency
|
|
230
|
+
*/
|
|
231
|
+
swarmStore(request: {
|
|
232
|
+
content: string;
|
|
233
|
+
contentType?: 'entity' | 'general' | 'note';
|
|
234
|
+
provider?: string;
|
|
235
|
+
}): Promise<unknown>;
|
|
213
236
|
/**
|
|
214
237
|
* Write content to a file.
|
|
215
238
|
* @param filePath - Absolute path where the file should be written
|
|
@@ -241,6 +264,8 @@ export interface CreateToolsSDKOptions {
|
|
|
241
264
|
searchKnowledgeService?: ISearchKnowledgeService;
|
|
242
265
|
/** Session manager for sub-agent delegation (required for agentQuery) */
|
|
243
266
|
sessionManager?: SessionManager;
|
|
267
|
+
/** Swarm coordinator for cross-provider query and store (optional) */
|
|
268
|
+
swarmCoordinator?: ISwarmCoordinator;
|
|
244
269
|
}
|
|
245
270
|
/**
|
|
246
271
|
* Creates a Tools SDK instance for sandbox code execution.
|
|
@@ -12,7 +12,7 @@ import { chunk, dedup, detectMessageBoundaries, groupBySubject, recon, recordPro
|
|
|
12
12
|
* @returns ToolsSDK instance ready to be injected into sandbox context
|
|
13
13
|
*/
|
|
14
14
|
export function createToolsSDK(options) {
|
|
15
|
-
const { commandType, contentGenerator, curateService, fileSystem, parentSessionId, projectRoot, sandboxService, searchKnowledgeService, sessionManager } = options;
|
|
15
|
+
const { commandType, contentGenerator, curateService, fileSystem, parentSessionId, projectRoot, sandboxService, searchKnowledgeService, sessionManager, swarmCoordinator } = options;
|
|
16
16
|
const isReadOnly = commandType === 'query';
|
|
17
17
|
return {
|
|
18
18
|
async agentQuery(prompt, options) {
|
|
@@ -153,6 +153,29 @@ export function createToolsSDK(options) {
|
|
|
153
153
|
}
|
|
154
154
|
return searchKnowledgeService.search(query, options);
|
|
155
155
|
},
|
|
156
|
+
async swarmQuery(query, queryOptions) {
|
|
157
|
+
if (!swarmCoordinator) {
|
|
158
|
+
throw new Error('Swarm query not available — no swarm coordinator configured.');
|
|
159
|
+
}
|
|
160
|
+
return swarmCoordinator.execute({
|
|
161
|
+
maxResults: queryOptions?.limit,
|
|
162
|
+
query,
|
|
163
|
+
scope: queryOptions?.scope,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
async swarmStore(request) {
|
|
167
|
+
if (isReadOnly) {
|
|
168
|
+
throw new Error('swarmStore() is disabled in read-only (query) mode');
|
|
169
|
+
}
|
|
170
|
+
if (!swarmCoordinator) {
|
|
171
|
+
throw new Error('Swarm store not available — no swarm coordinator configured.');
|
|
172
|
+
}
|
|
173
|
+
return swarmCoordinator.store({
|
|
174
|
+
content: request.content,
|
|
175
|
+
contentType: request.contentType,
|
|
176
|
+
provider: request.provider,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
156
179
|
async writeFile(filePath, content, options) {
|
|
157
180
|
if (isReadOnly) {
|
|
158
181
|
throw new Error('writeFile() is disabled in read-only (query) mode');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CostEstimate, HealthStatus, MemoryEntry, ProviderCapabilities, QueryRequest, QueryResult, StoreResult } from '../../../core/domain/swarm/types.js';
|
|
2
|
+
import type { IMemoryProvider } from '../../../core/interfaces/i-memory-provider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal interface for the search service dependency.
|
|
5
|
+
* Matches SearchKnowledgeService.search() signature.
|
|
6
|
+
*/
|
|
7
|
+
export interface SearchService {
|
|
8
|
+
search(query: string, options?: {
|
|
9
|
+
limit?: number;
|
|
10
|
+
scope?: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
results: Array<{
|
|
13
|
+
excerpt: string;
|
|
14
|
+
path: string;
|
|
15
|
+
score: number;
|
|
16
|
+
title: string;
|
|
17
|
+
}>;
|
|
18
|
+
totalFound: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* ByteRover adapter — wraps the existing SearchKnowledgeService
|
|
23
|
+
* behind the IMemoryProvider interface.
|
|
24
|
+
*
|
|
25
|
+
* Always active (built-in). Uses BM25 keyword search over the context-tree.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ByteRoverAdapter implements IMemoryProvider {
|
|
28
|
+
private readonly searchService;
|
|
29
|
+
readonly capabilities: ProviderCapabilities;
|
|
30
|
+
readonly id = "byterover";
|
|
31
|
+
readonly type: "byterover";
|
|
32
|
+
constructor(searchService: SearchService);
|
|
33
|
+
delete(_id: string): Promise<void>;
|
|
34
|
+
estimateCost(_request: QueryRequest): CostEstimate;
|
|
35
|
+
healthCheck(): Promise<HealthStatus>;
|
|
36
|
+
query(request: QueryRequest): Promise<QueryResult[]>;
|
|
37
|
+
store(_entry: MemoryEntry): Promise<StoreResult>;
|
|
38
|
+
update(_id: string, _entry: Partial<MemoryEntry>): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ByteRover adapter — wraps the existing SearchKnowledgeService
|
|
3
|
+
* behind the IMemoryProvider interface.
|
|
4
|
+
*
|
|
5
|
+
* Always active (built-in). Uses BM25 keyword search over the context-tree.
|
|
6
|
+
*/
|
|
7
|
+
export class ByteRoverAdapter {
|
|
8
|
+
searchService;
|
|
9
|
+
capabilities = {
|
|
10
|
+
avgLatencyMs: 50,
|
|
11
|
+
graphTraversal: false,
|
|
12
|
+
keywordSearch: true,
|
|
13
|
+
localOnly: true,
|
|
14
|
+
maxTokensPerQuery: 8000,
|
|
15
|
+
semanticSearch: false,
|
|
16
|
+
temporalQuery: false,
|
|
17
|
+
userModeling: false,
|
|
18
|
+
writeSupported: false,
|
|
19
|
+
};
|
|
20
|
+
id = 'byterover';
|
|
21
|
+
type = 'byterover';
|
|
22
|
+
constructor(searchService) {
|
|
23
|
+
this.searchService = searchService;
|
|
24
|
+
}
|
|
25
|
+
async delete(_id) {
|
|
26
|
+
throw new Error('ByteRover delete not implemented — use curate tool directly.');
|
|
27
|
+
}
|
|
28
|
+
estimateCost(_request) {
|
|
29
|
+
return {
|
|
30
|
+
estimatedCostCents: 0,
|
|
31
|
+
estimatedLatencyMs: this.capabilities.avgLatencyMs,
|
|
32
|
+
estimatedTokens: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async healthCheck() {
|
|
36
|
+
return { available: true };
|
|
37
|
+
}
|
|
38
|
+
async query(request) {
|
|
39
|
+
const searchResult = await this.searchService.search(request.query, {
|
|
40
|
+
limit: request.maxResults,
|
|
41
|
+
scope: request.scope,
|
|
42
|
+
});
|
|
43
|
+
return searchResult.results.map((result, index) => ({
|
|
44
|
+
content: result.excerpt,
|
|
45
|
+
id: `brv-${index}`,
|
|
46
|
+
metadata: {
|
|
47
|
+
matchType: 'keyword',
|
|
48
|
+
path: result.path,
|
|
49
|
+
source: result.path,
|
|
50
|
+
},
|
|
51
|
+
provider: 'byterover',
|
|
52
|
+
providerType: 'byterover',
|
|
53
|
+
score: result.score,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async store(_entry) {
|
|
57
|
+
throw new Error('ByteRover store not implemented — use curate tool directly.');
|
|
58
|
+
}
|
|
59
|
+
async update(_id, _entry) {
|
|
60
|
+
throw new Error('ByteRover update not implemented — use curate tool directly.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CostEstimate, HealthStatus, MemoryEntry, ProviderCapabilities, QueryRequest, QueryResult, StoreResult } from '../../../core/domain/swarm/types.js';
|
|
2
|
+
import type { IMemoryProvider } from '../../../core/interfaces/i-memory-provider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the GBrain adapter.
|
|
5
|
+
*/
|
|
6
|
+
export interface GBrainAdapterOptions {
|
|
7
|
+
/** Explicit path to the gbrain binary or .ts entrypoint. Auto-resolved if omitted. */
|
|
8
|
+
gbrainBinPath?: string;
|
|
9
|
+
/** Path to the brain repo (also used to find local gbrain installation) */
|
|
10
|
+
repoPath: string;
|
|
11
|
+
/** Search mode: hybrid (default), keyword, or vector */
|
|
12
|
+
searchMode: 'hybrid' | 'keyword' | 'vector';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Executor function type for running gbrain CLI operations.
|
|
16
|
+
* Abstracted for testability — tests inject a mock, production uses execGbrain().
|
|
17
|
+
*/
|
|
18
|
+
export type GBrainExecutor = (operation: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the gbrain binary path.
|
|
21
|
+
*
|
|
22
|
+
* Search order:
|
|
23
|
+
* 1. Explicit `gbrainBinPath` from options
|
|
24
|
+
* 2. `gbrain` in PATH (globally installed)
|
|
25
|
+
* 3. `src/cli.ts` in `repoPath` (local Bun clone — repoPath IS the source checkout)
|
|
26
|
+
* 4. `src/cli.ts` in common workspace siblings (../gbrain relative to cwd)
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveGBrainBin(options: GBrainAdapterOptions): {
|
|
29
|
+
argsPrefix: string[];
|
|
30
|
+
command: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* GBrain adapter — queries a GBrain knowledge brain via CLI subprocess.
|
|
34
|
+
*
|
|
35
|
+
* Supports three search modes:
|
|
36
|
+
* - hybrid (default): vector + keyword + RRF + multi-query expansion
|
|
37
|
+
* - keyword: full-text search only (no API key needed)
|
|
38
|
+
* - vector: vector search without expansion
|
|
39
|
+
*/
|
|
40
|
+
export declare class GBrainAdapter implements IMemoryProvider {
|
|
41
|
+
readonly capabilities: ProviderCapabilities;
|
|
42
|
+
readonly id = "gbrain";
|
|
43
|
+
readonly type: "gbrain";
|
|
44
|
+
private cachedExecutor?;
|
|
45
|
+
private readonly injectedExecutor?;
|
|
46
|
+
private readonly options;
|
|
47
|
+
private readonly repoPath;
|
|
48
|
+
private readonly searchMode;
|
|
49
|
+
constructor(options: GBrainAdapterOptions, executor?: GBrainExecutor);
|
|
50
|
+
private get executor();
|
|
51
|
+
delete(id: string): Promise<void>;
|
|
52
|
+
estimateCost(_request: QueryRequest): CostEstimate;
|
|
53
|
+
healthCheck(): Promise<HealthStatus>;
|
|
54
|
+
query(request: QueryRequest): Promise<QueryResult[]>;
|
|
55
|
+
store(entry: MemoryEntry): Promise<StoreResult>;
|
|
56
|
+
update(id: string, entry: Partial<MemoryEntry>): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Normalize scores to 0-1 range.
|
|
59
|
+
* - Hybrid/RRF scores are already in 0-1 range (sum of 1/(60+rank))
|
|
60
|
+
* - Keyword ts_rank scores can exceed 1 — normalize with score/(1+score)
|
|
61
|
+
*/
|
|
62
|
+
private normalizeScore;
|
|
63
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Default timeout for gbrain subprocess calls.
|
|
6
|
+
* PGLite WASM startup via Node execFile is significantly slower than interactive shell
|
|
7
|
+
* (cold-start ~60s for writes vs <1s for reads). 120s covers worst-case write operations.
|
|
8
|
+
*/
|
|
9
|
+
const EXEC_TIMEOUT_MS = 120_000;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the gbrain binary path.
|
|
12
|
+
*
|
|
13
|
+
* Search order:
|
|
14
|
+
* 1. Explicit `gbrainBinPath` from options
|
|
15
|
+
* 2. `gbrain` in PATH (globally installed)
|
|
16
|
+
* 3. `src/cli.ts` in `repoPath` (local Bun clone — repoPath IS the source checkout)
|
|
17
|
+
* 4. `src/cli.ts` in common workspace siblings (../gbrain relative to cwd)
|
|
18
|
+
*/
|
|
19
|
+
export function resolveGBrainBin(options) {
|
|
20
|
+
// 1. Explicit path
|
|
21
|
+
if (options.gbrainBinPath) {
|
|
22
|
+
if (options.gbrainBinPath.endsWith('.ts')) {
|
|
23
|
+
return { argsPrefix: ['run', options.gbrainBinPath], command: 'bun' };
|
|
24
|
+
}
|
|
25
|
+
return { argsPrefix: [], command: options.gbrainBinPath };
|
|
26
|
+
}
|
|
27
|
+
// 2. Check PATH (sync probe — runs lazily on first executor access, not at construction)
|
|
28
|
+
try {
|
|
29
|
+
execFileSync('gbrain', ['--version'], { encoding: 'utf8', stdio: 'pipe', timeout: 5000 });
|
|
30
|
+
return { argsPrefix: [], command: 'gbrain' };
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Not in PATH — continue to fallback
|
|
34
|
+
}
|
|
35
|
+
// 3. Local clone at repoPath
|
|
36
|
+
const localScript = join(options.repoPath, 'src', 'cli.ts');
|
|
37
|
+
if (existsSync(localScript)) {
|
|
38
|
+
return { argsPrefix: ['run', localScript], command: 'bun' };
|
|
39
|
+
}
|
|
40
|
+
// 4. Common workspace siblings — look for ../gbrain relative to cwd
|
|
41
|
+
const workspaceSibling = join(process.cwd(), '..', 'gbrain', 'src', 'cli.ts');
|
|
42
|
+
if (existsSync(workspaceSibling)) {
|
|
43
|
+
return { argsPrefix: ['run', workspaceSibling], command: 'bun' };
|
|
44
|
+
}
|
|
45
|
+
// 5. Fallback — try 'gbrain' anyway (will fail with ENOENT at runtime)
|
|
46
|
+
return { argsPrefix: [], command: 'gbrain' };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Execute a gbrain CLI operation via subprocess.
|
|
50
|
+
* Runs: <command> [argsPrefix...] call <operation> '<json params>'
|
|
51
|
+
*/
|
|
52
|
+
function createDefaultExecutor(resolved) {
|
|
53
|
+
return (operation, params) => new Promise((resolve, reject) => {
|
|
54
|
+
const jsonArgs = JSON.stringify(params);
|
|
55
|
+
const args = [...resolved.argsPrefix, 'call', operation, jsonArgs];
|
|
56
|
+
execFile(resolved.command, args, { encoding: 'utf8', timeout: EXEC_TIMEOUT_MS }, (error, stdout, stderr) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
const message = stderr?.trim() || error.message;
|
|
59
|
+
reject(new Error(`gbrain ${operation} failed: ${message}`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
resolve(JSON.parse(stdout));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
reject(new Error(`gbrain ${operation} returned invalid JSON: ${stdout.slice(0, 200)}`));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive a slug from content for gbrain page creation.
|
|
73
|
+
* Extracts title from first heading or uses timestamp fallback.
|
|
74
|
+
*/
|
|
75
|
+
function deriveSlug(content) {
|
|
76
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
77
|
+
const title = titleMatch?.[1] ?? `note-${Date.now()}`;
|
|
78
|
+
const slug = title
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replaceAll(/[^\w\s-]/g, '')
|
|
81
|
+
.replaceAll(/\s+/g, '-')
|
|
82
|
+
.replaceAll(/-+/g, '-')
|
|
83
|
+
.replaceAll(/^-+|-+$/g, '');
|
|
84
|
+
return `concept/${slug || `note-${Date.now()}`}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* GBrain adapter — queries a GBrain knowledge brain via CLI subprocess.
|
|
88
|
+
*
|
|
89
|
+
* Supports three search modes:
|
|
90
|
+
* - hybrid (default): vector + keyword + RRF + multi-query expansion
|
|
91
|
+
* - keyword: full-text search only (no API key needed)
|
|
92
|
+
* - vector: vector search without expansion
|
|
93
|
+
*/
|
|
94
|
+
export class GBrainAdapter {
|
|
95
|
+
capabilities = {
|
|
96
|
+
avgLatencyMs: 300,
|
|
97
|
+
graphTraversal: false,
|
|
98
|
+
keywordSearch: true,
|
|
99
|
+
localOnly: false,
|
|
100
|
+
maxTokensPerQuery: 10_000,
|
|
101
|
+
semanticSearch: true,
|
|
102
|
+
temporalQuery: true,
|
|
103
|
+
userModeling: false,
|
|
104
|
+
writeSupported: true,
|
|
105
|
+
};
|
|
106
|
+
id = 'gbrain';
|
|
107
|
+
type = 'gbrain';
|
|
108
|
+
cachedExecutor;
|
|
109
|
+
injectedExecutor;
|
|
110
|
+
options;
|
|
111
|
+
repoPath;
|
|
112
|
+
searchMode;
|
|
113
|
+
constructor(options, executor) {
|
|
114
|
+
this.repoPath = options.repoPath;
|
|
115
|
+
this.searchMode = options.searchMode;
|
|
116
|
+
this.options = options;
|
|
117
|
+
this.injectedExecutor = executor;
|
|
118
|
+
}
|
|
119
|
+
get executor() {
|
|
120
|
+
if (this.injectedExecutor)
|
|
121
|
+
return this.injectedExecutor;
|
|
122
|
+
this.cachedExecutor ??= createDefaultExecutor(resolveGBrainBin(this.options));
|
|
123
|
+
return this.cachedExecutor;
|
|
124
|
+
}
|
|
125
|
+
async delete(id) {
|
|
126
|
+
await this.executor('delete_page', { slug: id });
|
|
127
|
+
}
|
|
128
|
+
estimateCost(_request) {
|
|
129
|
+
// Keyword mode is free (no embedding API call)
|
|
130
|
+
// Hybrid/vector use OpenAI embedding (~$0.001 per query)
|
|
131
|
+
const costCents = this.searchMode === 'keyword' ? 0 : 0.1;
|
|
132
|
+
return {
|
|
133
|
+
estimatedCostCents: costCents,
|
|
134
|
+
estimatedLatencyMs: this.capabilities.avgLatencyMs,
|
|
135
|
+
estimatedTokens: 0,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async healthCheck() {
|
|
139
|
+
if (!existsSync(this.repoPath)) {
|
|
140
|
+
return {
|
|
141
|
+
available: false,
|
|
142
|
+
error: `GBrain repo not found at ${this.repoPath}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await this.executor('get_stats', {});
|
|
147
|
+
return { available: true };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return {
|
|
151
|
+
available: false,
|
|
152
|
+
error: error instanceof Error ? error.message : String(error),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async query(request) {
|
|
157
|
+
const limit = request.maxResults ?? 10;
|
|
158
|
+
let results;
|
|
159
|
+
switch (this.searchMode) {
|
|
160
|
+
case 'keyword': {
|
|
161
|
+
results = await this.executor('search', { limit, query: request.query });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'vector': {
|
|
165
|
+
results = await this.executor('query', { expand: false, limit, query: request.query });
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
default: {
|
|
169
|
+
// hybrid
|
|
170
|
+
results = await this.executor('query', { expand: true, limit, query: request.query });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return results.map((r) => ({
|
|
174
|
+
content: r.chunk_text,
|
|
175
|
+
id: r.slug,
|
|
176
|
+
metadata: {
|
|
177
|
+
matchType: this.searchMode === 'keyword' ? 'keyword' : 'semantic',
|
|
178
|
+
path: r.slug,
|
|
179
|
+
source: r.slug,
|
|
180
|
+
},
|
|
181
|
+
provider: 'gbrain',
|
|
182
|
+
providerType: 'gbrain',
|
|
183
|
+
score: this.normalizeScore(r.score),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
async store(entry) {
|
|
187
|
+
const slug = deriveSlug(entry.content);
|
|
188
|
+
const content = `---\ntype: concept\ntitle: ${entry.content.match(/^#\s+(.+)$/m)?.[1] ?? slug}\n---\n${entry.content}`;
|
|
189
|
+
await this.executor('put_page', { content, slug });
|
|
190
|
+
return { id: slug, provider: 'gbrain', success: true };
|
|
191
|
+
}
|
|
192
|
+
async update(id, entry) {
|
|
193
|
+
if (!entry.content)
|
|
194
|
+
return;
|
|
195
|
+
const content = `---\ntype: concept\ntitle: ${entry.content.match(/^#\s+(.+)$/m)?.[1] ?? id}\n---\n${entry.content}`;
|
|
196
|
+
await this.executor('put_page', { content, slug: id });
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Normalize scores to 0-1 range.
|
|
200
|
+
* - Hybrid/RRF scores are already in 0-1 range (sum of 1/(60+rank))
|
|
201
|
+
* - Keyword ts_rank scores can exceed 1 — normalize with score/(1+score)
|
|
202
|
+
*/
|
|
203
|
+
normalizeScore(score) {
|
|
204
|
+
if (this.searchMode === 'keyword' && score > 1) {
|
|
205
|
+
return score / (1 + score);
|
|
206
|
+
}
|
|
207
|
+
return Math.min(score, 1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CostEstimate, HealthStatus, MemoryEntry, ProviderCapabilities, QueryRequest, QueryResult, StoreResult } from '../../../core/domain/swarm/types.js';
|
|
2
|
+
import type { IMemoryProvider } from '../../../core/interfaces/i-memory-provider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for LocalMarkdownAdapter behavior.
|
|
5
|
+
*/
|
|
6
|
+
export interface LocalMarkdownAdapterOptions {
|
|
7
|
+
/** Whether to follow [[wikilinks]] for graph expansion (default: true) */
|
|
8
|
+
followWikilinks?: boolean;
|
|
9
|
+
/** Whether the folder is read-only (default: false) */
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
/** Whether to rescan files on each query (default: true). When false, the index is built once and frozen. */
|
|
12
|
+
watchForChanges?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Local markdown folder adapter — indexes .md files via MiniSearch,
|
|
16
|
+
* optionally follows [[wikilinks]] one hop, supports write unless read-only.
|
|
17
|
+
*
|
|
18
|
+
* Each folder instance has a unique id: `local-markdown:{name}`.
|
|
19
|
+
*/
|
|
20
|
+
export declare class LocalMarkdownAdapter implements IMemoryProvider {
|
|
21
|
+
private readonly folderPath;
|
|
22
|
+
private readonly name;
|
|
23
|
+
readonly capabilities: ProviderCapabilities;
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly type: "local-markdown";
|
|
26
|
+
private documents;
|
|
27
|
+
private readonly followWikilinks;
|
|
28
|
+
private index?;
|
|
29
|
+
private indexSignature?;
|
|
30
|
+
private pathToDoc;
|
|
31
|
+
private readonly readOnly;
|
|
32
|
+
private readonly watchForChanges;
|
|
33
|
+
constructor(folderPath: string, name: string, options?: LocalMarkdownAdapterOptions);
|
|
34
|
+
delete(_id: string): Promise<void>;
|
|
35
|
+
estimateCost(_request: QueryRequest): CostEstimate;
|
|
36
|
+
healthCheck(): Promise<HealthStatus>;
|
|
37
|
+
query(request: QueryRequest): Promise<QueryResult[]>;
|
|
38
|
+
store(entry: MemoryEntry): Promise<StoreResult>;
|
|
39
|
+
update(_id: string, _entry: Partial<MemoryEntry>): Promise<void>;
|
|
40
|
+
private ensureIndex;
|
|
41
|
+
}
|