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.
Files changed (95) hide show
  1. package/dist/agent/core/domain/swarm/types.d.ts +132 -0
  2. package/dist/agent/core/domain/swarm/types.js +128 -0
  3. package/dist/agent/core/domain/tools/constants.d.ts +2 -0
  4. package/dist/agent/core/domain/tools/constants.js +2 -0
  5. package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
  6. package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
  7. package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
  8. package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
  9. package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
  10. package/dist/agent/infra/agent/service-initializer.js +48 -0
  11. package/dist/agent/infra/map/map-shared.d.ts +2 -2
  12. package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
  13. package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
  14. package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
  15. package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
  16. package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
  17. package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
  18. package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
  19. package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
  20. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
  21. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
  22. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
  23. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
  24. package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
  25. package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
  26. package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
  27. package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
  28. package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
  29. package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
  30. package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
  31. package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
  32. package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
  33. package/dist/agent/infra/swarm/provider-factory.js +67 -0
  34. package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
  35. package/dist/agent/infra/swarm/search-precision.js +141 -0
  36. package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
  37. package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
  38. package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
  39. package/dist/agent/infra/swarm/swarm-graph.js +167 -0
  40. package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
  41. package/dist/agent/infra/swarm/swarm-merger.js +66 -0
  42. package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
  43. package/dist/agent/infra/swarm/swarm-router.js +40 -0
  44. package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
  45. package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
  46. package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
  47. package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
  48. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
  49. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
  50. package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
  51. package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
  52. package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
  53. package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
  54. package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
  55. package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
  56. package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
  57. package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
  58. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
  59. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
  60. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
  61. package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
  62. package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
  63. package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
  64. package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
  65. package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
  66. package/dist/agent/infra/tools/tool-provider.js +1 -0
  67. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  68. package/dist/agent/infra/tools/tool-registry.js +25 -1
  69. package/dist/agent/resources/tools/code_exec.txt +2 -0
  70. package/dist/agent/resources/tools/swarm_query.txt +38 -0
  71. package/dist/agent/resources/tools/swarm_store.txt +35 -0
  72. package/dist/oclif/commands/swarm/curate.d.ts +13 -0
  73. package/dist/oclif/commands/swarm/curate.js +81 -0
  74. package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
  75. package/dist/oclif/commands/swarm/onboard.js +233 -0
  76. package/dist/oclif/commands/swarm/query.d.ts +14 -0
  77. package/dist/oclif/commands/swarm/query.js +84 -0
  78. package/dist/oclif/commands/swarm/status.d.ts +41 -0
  79. package/dist/oclif/commands/swarm/status.js +278 -0
  80. package/dist/server/constants.d.ts +3 -2
  81. package/dist/server/constants.js +10 -9
  82. package/dist/server/core/domain/source/source-schema.d.ts +6 -6
  83. package/dist/server/core/domain/transport/schemas.d.ts +4 -4
  84. package/dist/server/infra/http/provider-model-fetchers.js +1 -0
  85. package/dist/server/infra/process/feature-handlers.js +13 -0
  86. package/dist/server/infra/project/project-registry.js +13 -1
  87. package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
  88. package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
  89. package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
  90. package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
  91. package/dist/server/templates/skill/SKILL.md +163 -0
  92. package/dist/server/utils/gitignore.d.ts +1 -0
  93. package/dist/server/utils/gitignore.js +36 -4
  94. package/oclif.manifest.json +259 -79
  95. package/package.json +2 -2
@@ -0,0 +1,305 @@
1
+ /* eslint-disable camelcase -- mapKeys maps on-disk YAML snake_case to camelCase */
2
+ import { z } from 'zod';
3
+ // ============================================================
4
+ // Helper: snake_case → camelCase key mapping
5
+ // ============================================================
6
+ function mapKeys(obj, mapping) {
7
+ const result = {};
8
+ for (const [key, value] of Object.entries(obj)) {
9
+ const mappedKey = mapping[key] ?? key;
10
+ result[mappedKey] = value;
11
+ }
12
+ return result;
13
+ }
14
+ // ============================================================
15
+ // Environment variable resolution
16
+ // ============================================================
17
+ /**
18
+ * Resolve `${VAR}` patterns in a string using the given environment.
19
+ * Returns the original string if the variable is not found.
20
+ */
21
+ export function resolveEnvVars(value, env) {
22
+ return value.replaceAll(/\$\{(\w+)\}/g, (_match, varName) => {
23
+ const envValue = env[varName];
24
+ return envValue ?? `\${${varName}}`;
25
+ });
26
+ }
27
+ // ============================================================
28
+ // Provider sub-schemas
29
+ // ============================================================
30
+ const ByteRoverProviderSchema = z.object({
31
+ enabled: z.boolean(),
32
+ });
33
+ const ObsidianProviderSchema = z.preprocess((data) => {
34
+ if (typeof data !== 'object' || data === null)
35
+ return data;
36
+ return mapKeys(data, {
37
+ ignore_patterns: 'ignorePatterns',
38
+ index_on_startup: 'indexOnStartup',
39
+ read_only: 'readOnly',
40
+ vault_path: 'vaultPath',
41
+ watch_for_changes: 'watchForChanges',
42
+ });
43
+ }, z.object({
44
+ enabled: z.boolean(),
45
+ ignorePatterns: z.array(z.string()).optional(),
46
+ indexOnStartup: z.boolean().optional().default(true),
47
+ readOnly: z.boolean().optional().default(true),
48
+ vaultPath: z.string(),
49
+ watchForChanges: z.boolean().optional().default(true),
50
+ }));
51
+ const LocalMarkdownFolderSchema = z.preprocess((data) => {
52
+ if (typeof data !== 'object' || data === null)
53
+ return data;
54
+ return mapKeys(data, {
55
+ follow_wikilinks: 'followWikilinks',
56
+ read_only: 'readOnly',
57
+ });
58
+ }, z.object({
59
+ followWikilinks: z.boolean().optional().default(true),
60
+ name: z.string(),
61
+ path: z.string(),
62
+ readOnly: z.boolean().optional().default(true),
63
+ }));
64
+ const LocalMarkdownProviderSchema = z.preprocess((data) => {
65
+ if (typeof data !== 'object' || data === null)
66
+ return data;
67
+ return mapKeys(data, {
68
+ watch_for_changes: 'watchForChanges',
69
+ });
70
+ }, z.object({
71
+ enabled: z.boolean(),
72
+ folders: z.array(LocalMarkdownFolderSchema),
73
+ watchForChanges: z.boolean().optional().default(true),
74
+ }));
75
+ const HonchoProviderSchema = z.preprocess((data) => {
76
+ if (typeof data !== 'object' || data === null)
77
+ return data;
78
+ return mapKeys(data, {
79
+ api_key: 'apiKey',
80
+ app_id: 'appId',
81
+ max_tokens_per_query: 'maxTokensPerQuery',
82
+ user_id: 'userId',
83
+ });
84
+ }, z.object({
85
+ apiKey: z.string(),
86
+ appId: z.string(),
87
+ enabled: z.boolean(),
88
+ maxTokensPerQuery: z.number().int().positive().optional().default(4000),
89
+ userId: z.string().optional().default('default'),
90
+ }));
91
+ const HindsightProviderSchema = z.preprocess((data) => {
92
+ if (typeof data !== 'object' || data === null)
93
+ return data;
94
+ return mapKeys(data, {
95
+ cara_params: 'caraParams',
96
+ connection_string: 'connectionString',
97
+ });
98
+ }, z.object({
99
+ caraParams: z.object({
100
+ empathy: z.number().min(0).max(1).optional().default(0.5),
101
+ literalism: z.number().min(0).max(1).optional().default(0.5),
102
+ skepticism: z.number().min(0).max(1).optional().default(0.5),
103
+ }).optional(),
104
+ connectionString: z.string(),
105
+ enabled: z.boolean(),
106
+ networks: z.array(z.string()).optional().default(['world', 'experience']),
107
+ }));
108
+ const GBrainProviderSchema = z.preprocess((data) => {
109
+ if (typeof data !== 'object' || data === null)
110
+ return data;
111
+ return mapKeys(data, {
112
+ connection_string: 'connectionString',
113
+ repo_path: 'repoPath',
114
+ search_mode: 'searchMode',
115
+ });
116
+ }, z.object({
117
+ connectionString: z.string().optional(),
118
+ enabled: z.boolean(),
119
+ repoPath: z.string(),
120
+ searchMode: z.enum(['hybrid', 'keyword', 'vector']).optional().default('hybrid'),
121
+ }));
122
+ const MemoryWikiProviderSchema = z.preprocess((data) => {
123
+ if (typeof data !== 'object' || data === null)
124
+ return data;
125
+ return mapKeys(data, {
126
+ boost_fresh: 'boostFresh',
127
+ vault_path: 'vaultPath',
128
+ write_page_type: 'writePageType',
129
+ });
130
+ }, z.object({
131
+ boostFresh: z.boolean().optional().default(true),
132
+ enabled: z.boolean(),
133
+ vaultPath: z.string(),
134
+ writePageType: z.enum(['concept', 'entity']).optional().default('concept'),
135
+ }));
136
+ // ============================================================
137
+ // Top-level sections
138
+ // ============================================================
139
+ const ProvidersSchema = z.preprocess((data) => {
140
+ if (typeof data !== 'object' || data === null)
141
+ return data;
142
+ return mapKeys(data, {
143
+ local_markdown: 'localMarkdown',
144
+ memory_wiki: 'memoryWiki',
145
+ });
146
+ }, z.object({
147
+ byterover: ByteRoverProviderSchema,
148
+ gbrain: GBrainProviderSchema.optional(),
149
+ hindsight: HindsightProviderSchema.optional(),
150
+ honcho: HonchoProviderSchema.optional(),
151
+ localMarkdown: LocalMarkdownProviderSchema.optional(),
152
+ memoryWiki: MemoryWikiProviderSchema.optional(),
153
+ obsidian: ObsidianProviderSchema.optional(),
154
+ }));
155
+ const RoutingSchema = z.preprocess((data) => {
156
+ if (typeof data !== 'object' || data === null)
157
+ return data;
158
+ return mapKeys(data, {
159
+ classification_method: 'classificationMethod',
160
+ default_max_results: 'defaultMaxResults',
161
+ default_strategy: 'defaultStrategy',
162
+ min_rrf_score: 'minRrfScore',
163
+ rrf_gap_ratio: 'rrfGapRatio',
164
+ rrf_k: 'rrfK',
165
+ });
166
+ }, z.object({
167
+ classificationMethod: z.enum(['auto', 'llm']).optional().default('auto'),
168
+ defaultMaxResults: z.number().int().positive().optional().default(10),
169
+ defaultStrategy: z.enum(['adaptive', 'all', 'manual']).optional().default('adaptive'),
170
+ minRrfScore: z.number().min(0).optional().default(0.005),
171
+ rrfGapRatio: z.number().gt(0).max(1).optional().default(0.5),
172
+ rrfK: z.number().int().positive().optional().default(60),
173
+ }));
174
+ const PerProviderBudgetSchema = z.preprocess((data) => {
175
+ if (typeof data !== 'object' || data === null)
176
+ return data;
177
+ return mapKeys(data, {
178
+ max_queries_per_minute: 'maxQueriesPerMinute',
179
+ monthly_cap_cents: 'monthlyCapCents',
180
+ });
181
+ }, z.object({
182
+ maxQueriesPerMinute: z.number().int().positive().optional(),
183
+ monthlyCapCents: z.number().int().nonnegative(),
184
+ }));
185
+ const BudgetSchema = z.preprocess((data) => {
186
+ if (typeof data !== 'object' || data === null)
187
+ return data;
188
+ return mapKeys(data, {
189
+ global_monthly_cap_cents: 'globalMonthlyCapCents',
190
+ per_provider: 'perProvider',
191
+ warning_threshold_pct: 'warningThresholdPct',
192
+ weight_reduction_threshold_pct: 'weightReductionThresholdPct',
193
+ });
194
+ }, z.object({
195
+ globalMonthlyCapCents: z.number().int().nonnegative().optional().default(5000),
196
+ perProvider: z.record(PerProviderBudgetSchema).optional(),
197
+ warningThresholdPct: z.number().int().min(0).max(100).optional().default(80),
198
+ weightReductionThresholdPct: z.number().int().min(0).max(100).optional().default(90),
199
+ }));
200
+ const OptimizationSchema = z.preprocess((data) => {
201
+ if (typeof data !== 'object' || data === null)
202
+ return data;
203
+ return mapKeys(data, {
204
+ edge_learning: 'edgeLearning',
205
+ template_optimization: 'templateOptimization',
206
+ });
207
+ }, z.object({
208
+ edgeLearning: z.preprocess((data) => {
209
+ if (typeof data !== 'object' || data === null)
210
+ return data;
211
+ return mapKeys(data, {
212
+ exploration_rate: 'explorationRate',
213
+ fix_threshold: 'fixThreshold',
214
+ min_observations_to_prune: 'minObservationsToPrune',
215
+ prune_threshold: 'pruneThreshold',
216
+ });
217
+ }, z.object({
218
+ enabled: z.boolean().optional().default(true),
219
+ explorationRate: z.number().min(0).max(1).optional().default(0.05),
220
+ fixThreshold: z.number().min(0).max(1).optional().default(0.95),
221
+ minObservationsToPrune: z.number().int().positive().optional().default(100),
222
+ pruneThreshold: z.number().min(0).max(1).optional().default(0.05),
223
+ })).optional().default({}),
224
+ templateOptimization: z.preprocess((data) => {
225
+ if (typeof data !== 'object' || data === null)
226
+ return data;
227
+ return mapKeys(data, {
228
+ ab_test_size: 'abTestSize',
229
+ failure_rate_trigger: 'failureRateTrigger',
230
+ });
231
+ }, z.object({
232
+ abTestSize: z.number().int().positive().optional().default(5),
233
+ enabled: z.boolean().optional().default(true),
234
+ failureRateTrigger: z.number().min(0).max(1).optional().default(0.3),
235
+ frequency: z.number().int().positive().optional().default(20),
236
+ })).optional().default({}),
237
+ }));
238
+ const ProvenanceSchema = z.preprocess((data) => {
239
+ if (typeof data !== 'object' || data === null)
240
+ return data;
241
+ return mapKeys(data, {
242
+ full_retention_days: 'fullRetentionDays',
243
+ keep_summaries: 'keepSummaries',
244
+ storage_path: 'storagePath',
245
+ });
246
+ }, z.object({
247
+ enabled: z.boolean().optional().default(true),
248
+ fullRetentionDays: z.number().int().positive().optional().default(30),
249
+ keepSummaries: z.boolean().optional().default(true),
250
+ storagePath: z.string().optional().default('swarm/provenance'),
251
+ }));
252
+ const PerformanceSchema = z.preprocess((data) => {
253
+ if (typeof data !== 'object' || data === null)
254
+ return data;
255
+ return mapKeys(data, {
256
+ file_watcher_debounce_ms: 'fileWatcherDebounceMs',
257
+ index_cache_ttl_seconds: 'indexCacheTtlSeconds',
258
+ max_concurrent_providers: 'maxConcurrentProviders',
259
+ max_query_latency_ms: 'maxQueryLatencyMs',
260
+ result_cache_ttl_ms: 'resultCacheTtlMs',
261
+ });
262
+ }, z.object({
263
+ fileWatcherDebounceMs: z.number().int().positive().optional().default(1000),
264
+ indexCacheTtlSeconds: z.number().int().optional().default(300),
265
+ maxConcurrentProviders: z.number().int().positive().optional().default(4),
266
+ maxQueryLatencyMs: z.number().int().positive().optional().default(2000),
267
+ resultCacheTtlMs: z.number().int().min(0).optional().default(10_000),
268
+ }));
269
+ const EnrichmentEdgeSchema = z.object({
270
+ from: z.string(),
271
+ to: z.string(),
272
+ });
273
+ const EnrichmentSchema = z.object({
274
+ edges: z.array(EnrichmentEdgeSchema).optional().default([]),
275
+ });
276
+ // ============================================================
277
+ // Root schema
278
+ // ============================================================
279
+ /**
280
+ * Full Zod schema for `.brv/swarm/config.yaml`.
281
+ * Accepts snake_case YAML input and outputs camelCase TypeScript.
282
+ */
283
+ export const SwarmConfigSchema = z.object({
284
+ budget: BudgetSchema.optional(),
285
+ enrichment: EnrichmentSchema.optional().default({}),
286
+ optimization: OptimizationSchema.optional().default({}),
287
+ performance: PerformanceSchema.optional().default({}),
288
+ provenance: ProvenanceSchema.optional().default({}),
289
+ providers: ProvidersSchema,
290
+ routing: RoutingSchema.optional().default({}),
291
+ });
292
+ /**
293
+ * Validate swarm config (throwing).
294
+ * @throws ZodError on invalid input
295
+ */
296
+ export function validateSwarmConfig(config) {
297
+ return SwarmConfigSchema.parse(config);
298
+ }
299
+ /**
300
+ * Validate swarm config (non-throwing).
301
+ * Returns a SafeParseResult with success/error.
302
+ */
303
+ export function safeValidateSwarmConfig(config) {
304
+ return SwarmConfigSchema.safeParse(config);
305
+ }
@@ -0,0 +1,21 @@
1
+ import type { IMemoryProvider } from '../../core/interfaces/i-memory-provider.js';
2
+ import type { SwarmConfig } from './config/swarm-config-schema.js';
3
+ import { type SearchService } from './adapters/byterover-adapter.js';
4
+ /**
5
+ * Options for building providers from config.
6
+ */
7
+ export interface ProviderFactoryOptions {
8
+ /**
9
+ * Search service for ByteRover adapter.
10
+ * In agent mode, this is the real SearchKnowledgeService.
11
+ * In CLI mode, this can be omitted (a no-op stub is used).
12
+ */
13
+ searchService?: SearchService;
14
+ }
15
+ /**
16
+ * Build IMemoryProvider instances from a validated SwarmConfig.
17
+ *
18
+ * Used by both the CLI command and the agent runtime to avoid duplicating
19
+ * adapter construction logic.
20
+ */
21
+ export declare function buildProvidersFromConfig(config: SwarmConfig, options?: ProviderFactoryOptions): IMemoryProvider[];
@@ -0,0 +1,67 @@
1
+ import { ByteRoverAdapter } from './adapters/byterover-adapter.js';
2
+ import { GBrainAdapter } from './adapters/gbrain-adapter.js';
3
+ import { LocalMarkdownAdapter } from './adapters/local-markdown-adapter.js';
4
+ import { MemoryWikiAdapter } from './adapters/memory-wiki-adapter.js';
5
+ import { ObsidianAdapter } from './adapters/obsidian-adapter.js';
6
+ /**
7
+ * No-op search service for CLI-only mode when no real search service is available.
8
+ * ByteRover adapter will return empty results, but other providers (Obsidian, local-markdown)
9
+ * will still work with their own indexes.
10
+ */
11
+ const STUB_SEARCH_SERVICE = {
12
+ async search() {
13
+ return { results: [], totalFound: 0 };
14
+ },
15
+ };
16
+ /**
17
+ * Build IMemoryProvider instances from a validated SwarmConfig.
18
+ *
19
+ * Used by both the CLI command and the agent runtime to avoid duplicating
20
+ * adapter construction logic.
21
+ */
22
+ export function buildProvidersFromConfig(config, options) {
23
+ const providers = [];
24
+ // ByteRover — always first, always the "home" provider
25
+ if (config.providers.byterover.enabled) {
26
+ const searchService = options?.searchService ?? STUB_SEARCH_SERVICE;
27
+ providers.push(new ByteRoverAdapter(searchService));
28
+ }
29
+ // Obsidian
30
+ if (config.providers.obsidian?.enabled) {
31
+ providers.push(new ObsidianAdapter(config.providers.obsidian.vaultPath, {
32
+ ignorePatterns: config.providers.obsidian.ignorePatterns,
33
+ watchForChanges: config.providers.obsidian.watchForChanges,
34
+ }));
35
+ }
36
+ // Local Markdown — one adapter per folder, with deduplication of names
37
+ if (config.providers.localMarkdown?.enabled) {
38
+ const nameCount = new Map();
39
+ for (const folder of config.providers.localMarkdown.folders) {
40
+ // Deduplicate: if two folders share the same name, suffix with index
41
+ const count = nameCount.get(folder.name) ?? 0;
42
+ nameCount.set(folder.name, count + 1);
43
+ const uniqueName = count === 0 ? folder.name : `${folder.name}-${count}`;
44
+ providers.push(new LocalMarkdownAdapter(folder.path, uniqueName, {
45
+ followWikilinks: folder.followWikilinks,
46
+ readOnly: folder.readOnly,
47
+ watchForChanges: config.providers.localMarkdown.watchForChanges,
48
+ }));
49
+ }
50
+ }
51
+ // GBrain
52
+ if (config.providers.gbrain?.enabled) {
53
+ providers.push(new GBrainAdapter({
54
+ repoPath: config.providers.gbrain.repoPath,
55
+ searchMode: config.providers.gbrain.searchMode,
56
+ }));
57
+ }
58
+ if (config.providers.memoryWiki?.enabled) {
59
+ providers.push(new MemoryWikiAdapter({
60
+ boostFresh: config.providers.memoryWiki.boostFresh,
61
+ vaultPath: config.providers.memoryWiki.vaultPath,
62
+ writePageType: config.providers.memoryWiki.writePageType,
63
+ }));
64
+ }
65
+ // Cloud providers (honcho, hindsight) are temporarily disabled — adapters coming in Phase 3.
66
+ return providers;
67
+ }
@@ -0,0 +1,95 @@
1
+ import type MiniSearch from 'minisearch';
2
+ /**
3
+ * Maximum content length (in chars) returned per result from adapters.
4
+ * The renderer applies a separate display limit; this controls payload size.
5
+ */
6
+ export declare const ADAPTER_CONTENT_LIMIT = 5000;
7
+ /**
8
+ * Default absolute score floor for adapter-level OOD detection.
9
+ * Lower than ByteRover's 0.45 because adapters use pure BM25 normalization
10
+ * without compound scoring (importance/recency/maturity).
11
+ */
12
+ export declare const DEFAULT_SCORE_FLOOR = 0.3;
13
+ /**
14
+ * Default gap ratio for adapter-level tail filtering.
15
+ * Slightly tighter than ByteRover's 0.7 to compensate for the
16
+ * absence of compound scoring differentiation.
17
+ */
18
+ export declare const DEFAULT_GAP_RATIO = 0.75;
19
+ /**
20
+ * Gap ratio for second pass after wikilink expansion.
21
+ * Must be <= WIKILINK_DECAY (0.7) so one-hop expansions are not automatically filtered.
22
+ * Uses 0.6 to give expanded results some headroom.
23
+ */
24
+ export declare const POST_EXPANSION_GAP_RATIO = 0.6;
25
+ /**
26
+ * Normalized search result from MiniSearch with precision metadata.
27
+ */
28
+ export type NormalizedResult = {
29
+ /** Document ID from MiniSearch */
30
+ id: number | string;
31
+ /** Score normalized to [0, 1) via score / (1 + score) */
32
+ normalizedScore: number;
33
+ /** Query terms that matched (from MiniSearch, for future unmatched-term detection) */
34
+ queryTerms: string[];
35
+ /** Raw MiniSearch BM25 score before normalization */
36
+ rawScore: number;
37
+ };
38
+ /**
39
+ * Options for precision-filtered MiniSearch queries.
40
+ */
41
+ export type PrecisionSearchOptions = {
42
+ /** Field boost weights. Default: {title: 2} */
43
+ boost?: Record<string, number>;
44
+ /** Fuzzy matching tolerance. Default: 0.2 */
45
+ fuzzy?: number;
46
+ /** Gap ratio: only keep results >= topScore * ratio. Default: 0.75 */
47
+ gapRatio?: number;
48
+ /** Maximum results to return. Default: 10 */
49
+ maxResults?: number;
50
+ /** Enable prefix matching. Default: true */
51
+ prefix?: boolean;
52
+ /** Absolute score floor: if top result < threshold, return empty. Default: 0.3 */
53
+ scoreFloor?: number;
54
+ };
55
+ /**
56
+ * Remove common stop words from a query string.
57
+ * Returns the original query if all tokens are stop words (never returns empty).
58
+ * Matches ByteRover behavior at search-knowledge-service.ts:291-295.
59
+ */
60
+ export declare function filterStopWords(query: string): string;
61
+ /**
62
+ * Drop all results if the best normalized score is below the threshold.
63
+ * This is an absolute OOD (out-of-domain) gate.
64
+ * Input may be sorted or unsorted — uses Math.max to find the best score.
65
+ *
66
+ * @param results - Results (sorted or unsorted)
67
+ * @param threshold - Minimum acceptable top score
68
+ * @returns Original results if best >= threshold, empty otherwise
69
+ */
70
+ export declare function applyScoreFloor(results: NormalizedResult[], threshold: number): NormalizedResult[];
71
+ /**
72
+ * Keep only results whose score is >= topScore * ratio.
73
+ * Sorts internally to find the true top score, then early-breaks on sorted input.
74
+ * Matches ByteRover's gap filter pattern at search-knowledge-service.ts:1234-1242.
75
+ *
76
+ * @param results - Results (sorted or unsorted)
77
+ * @param ratio - Gap ratio (0, 1]. Only results >= topScore * ratio are kept.
78
+ * @returns Filtered results sorted descending by normalizedScore
79
+ */
80
+ export declare function applyGapRatio(results: NormalizedResult[], ratio: number): NormalizedResult[];
81
+ /**
82
+ * Execute a precision-filtered MiniSearch query combining T1/T2/T3:
83
+ *
84
+ * 1. Filter stop words from query (T1)
85
+ * 2. Try AND for multi-word queries, fall back to OR if empty (T1)
86
+ * 3. Normalize scores: score / (1 + score)
87
+ * 4. Apply absolute score floor gate (T2)
88
+ * 5. Apply relative gap ratio filter (T3)
89
+ *
90
+ * @param index - MiniSearch index to search
91
+ * @param query - Raw query string
92
+ * @param options - Precision search options
93
+ * @returns Filtered, normalized results sorted descending by score
94
+ */
95
+ export declare function searchWithPrecision<T>(index: MiniSearch<T>, query: string, options?: PrecisionSearchOptions): NormalizedResult[];
@@ -0,0 +1,141 @@
1
+ import { removeStopwords } from 'stopword';
2
+ // ============================================================
3
+ // Constants
4
+ // ============================================================
5
+ /**
6
+ * Maximum content length (in chars) returned per result from adapters.
7
+ * The renderer applies a separate display limit; this controls payload size.
8
+ */
9
+ export const ADAPTER_CONTENT_LIMIT = 5000;
10
+ /**
11
+ * Default absolute score floor for adapter-level OOD detection.
12
+ * Lower than ByteRover's 0.45 because adapters use pure BM25 normalization
13
+ * without compound scoring (importance/recency/maturity).
14
+ */
15
+ export const DEFAULT_SCORE_FLOOR = 0.3;
16
+ /**
17
+ * Default gap ratio for adapter-level tail filtering.
18
+ * Slightly tighter than ByteRover's 0.7 to compensate for the
19
+ * absence of compound scoring differentiation.
20
+ */
21
+ export const DEFAULT_GAP_RATIO = 0.75;
22
+ /**
23
+ * Gap ratio for second pass after wikilink expansion.
24
+ * Must be <= WIKILINK_DECAY (0.7) so one-hop expansions are not automatically filtered.
25
+ * Uses 0.6 to give expanded results some headroom.
26
+ */
27
+ export const POST_EXPANSION_GAP_RATIO = 0.6;
28
+ // ============================================================
29
+ // T1: Stop word filtering
30
+ // ============================================================
31
+ /**
32
+ * Remove common stop words from a query string.
33
+ * Returns the original query if all tokens are stop words (never returns empty).
34
+ * Matches ByteRover behavior at search-knowledge-service.ts:291-295.
35
+ */
36
+ export function filterStopWords(query) {
37
+ if (!query.trim())
38
+ return query;
39
+ const words = query.toLowerCase().split(/\s+/);
40
+ const filtered = removeStopwords(words);
41
+ return filtered.length > 0 ? filtered.join(' ') : query;
42
+ }
43
+ // ============================================================
44
+ // T2: Absolute score floor gate
45
+ // ============================================================
46
+ /**
47
+ * Drop all results if the best normalized score is below the threshold.
48
+ * This is an absolute OOD (out-of-domain) gate.
49
+ * Input may be sorted or unsorted — uses Math.max to find the best score.
50
+ *
51
+ * @param results - Results (sorted or unsorted)
52
+ * @param threshold - Minimum acceptable top score
53
+ * @returns Original results if best >= threshold, empty otherwise
54
+ */
55
+ export function applyScoreFloor(results, threshold) {
56
+ if (results.length === 0)
57
+ return [];
58
+ // Use max score regardless of input order
59
+ const topScore = Math.max(...results.map((r) => r.normalizedScore));
60
+ return topScore >= threshold ? results : [];
61
+ }
62
+ // ============================================================
63
+ // T3: Relative gap ratio filter
64
+ // ============================================================
65
+ /**
66
+ * Keep only results whose score is >= topScore * ratio.
67
+ * Sorts internally to find the true top score, then early-breaks on sorted input.
68
+ * Matches ByteRover's gap filter pattern at search-knowledge-service.ts:1234-1242.
69
+ *
70
+ * @param results - Results (sorted or unsorted)
71
+ * @param ratio - Gap ratio (0, 1]. Only results >= topScore * ratio are kept.
72
+ * @returns Filtered results sorted descending by normalizedScore
73
+ */
74
+ export function applyGapRatio(results, ratio) {
75
+ if (results.length === 0)
76
+ return [];
77
+ // Sort descending to find top score and enable early break
78
+ const sorted = [...results].sort((a, b) => b.normalizedScore - a.normalizedScore);
79
+ const floor = sorted[0].normalizedScore * ratio;
80
+ const filtered = [];
81
+ for (const r of sorted) {
82
+ if (r.normalizedScore < floor)
83
+ break;
84
+ filtered.push(r);
85
+ }
86
+ return filtered;
87
+ }
88
+ // ============================================================
89
+ // Combined precision search
90
+ // ============================================================
91
+ /**
92
+ * Execute a precision-filtered MiniSearch query combining T1/T2/T3:
93
+ *
94
+ * 1. Filter stop words from query (T1)
95
+ * 2. Try AND for multi-word queries, fall back to OR if empty (T1)
96
+ * 3. Normalize scores: score / (1 + score)
97
+ * 4. Apply absolute score floor gate (T2)
98
+ * 5. Apply relative gap ratio filter (T3)
99
+ *
100
+ * @param index - MiniSearch index to search
101
+ * @param query - Raw query string
102
+ * @param options - Precision search options
103
+ * @returns Filtered, normalized results sorted descending by score
104
+ */
105
+ export function searchWithPrecision(index, query, options) {
106
+ const { boost = { title: 2 }, fuzzy = 0.2, gapRatio = DEFAULT_GAP_RATIO, maxResults = 10, prefix = true, scoreFloor = DEFAULT_SCORE_FLOOR, } = options ?? {};
107
+ // T1: Filter stop words
108
+ const filteredQuery = filterStopWords(query);
109
+ const words = filteredQuery.split(/\s+/).filter(Boolean);
110
+ // T1: AND-first for multi-word queries, OR fallback
111
+ const searchOpts = { boost, fuzzy, prefix };
112
+ let rawResults;
113
+ if (words.length >= 2) {
114
+ rawResults = index.search(filteredQuery, { combineWith: 'AND', ...searchOpts });
115
+ if (rawResults.length === 0) {
116
+ rawResults = index.search(filteredQuery, { combineWith: 'OR', ...searchOpts });
117
+ // For 2-word queries minTerms=2, effectively requiring both terms.
118
+ // This is intentional: AND failed on exact match, but OR with prefix/fuzzy
119
+ // may still find docs matching both terms via stemming or partial overlap.
120
+ const minTerms = Math.floor(words.length / 2) + 1;
121
+ rawResults = rawResults.filter((r) => (r.queryTerms ?? []).length >= minTerms);
122
+ }
123
+ }
124
+ else {
125
+ rawResults = index.search(filteredQuery, { combineWith: 'OR', ...searchOpts });
126
+ }
127
+ // Cap candidate set by MiniSearch rank order before applying T2/T3.
128
+ // This limits the precision pipeline to the top-N MiniSearch hits.
129
+ const normalized = rawResults.slice(0, maxResults).map((r) => ({
130
+ id: r.id,
131
+ normalizedScore: r.score / (1 + r.score),
132
+ queryTerms: r.queryTerms ?? [],
133
+ rawScore: r.score,
134
+ }));
135
+ // T2: Absolute score floor
136
+ const afterFloor = applyScoreFloor(normalized, scoreFloor);
137
+ if (afterFloor.length === 0)
138
+ return [];
139
+ // T3: Relative gap ratio
140
+ return applyGapRatio(afterFloor, gapRatio);
141
+ }
@@ -0,0 +1,59 @@
1
+ import type { QueryRequest } from '../../core/domain/swarm/types.js';
2
+ import type { IMemoryProvider } from '../../core/interfaces/i-memory-provider.js';
3
+ import type { ISwarmCoordinator, ProviderInfo, SwarmQueryResult, SwarmStoreRequest, SwarmStoreResult, SwarmSummary } from '../../core/interfaces/i-swarm-coordinator.js';
4
+ import type { SwarmConfig } from './config/swarm-config-schema.js';
5
+ export type BrvCurateResult = {
6
+ data?: {
7
+ logId?: string;
8
+ taskId?: string;
9
+ };
10
+ error?: string;
11
+ success?: boolean;
12
+ };
13
+ /**
14
+ * SwarmCoordinator — orchestrates query classification, provider selection,
15
+ * parallel execution, and result fusion.
16
+ *
17
+ * Implements ISwarmCoordinator to serve the CLI command and agent tool.
18
+ */
19
+ export type CurateFallbackFn = (content: string) => Promise<BrvCurateResult>;
20
+ export declare class SwarmCoordinator implements ISwarmCoordinator {
21
+ private readonly config;
22
+ private readonly curateFallback;
23
+ private readonly graph;
24
+ private readonly healthCache;
25
+ private readonly maxCacheSize;
26
+ private readonly providers;
27
+ private readonly resultCache;
28
+ private readonly resultCacheTtlMs;
29
+ private totalQueries;
30
+ constructor(providers: IMemoryProvider[], config: SwarmConfig, curateFallback?: CurateFallbackFn);
31
+ /**
32
+ * Execute a swarm query: classify → select providers → execute in parallel → fuse results.
33
+ */
34
+ execute(request: QueryRequest): Promise<SwarmQueryResult>;
35
+ /**
36
+ * Get info about all registered providers and their cached health status.
37
+ */
38
+ getActiveProviders(): ProviderInfo[];
39
+ /**
40
+ * Get a presentation-oriented summary of the swarm state.
41
+ */
42
+ getSummary(): SwarmSummary;
43
+ /**
44
+ * Run health checks on all providers and update the cache.
45
+ */
46
+ refreshHealth(): Promise<ProviderInfo[]>;
47
+ /**
48
+ * Store knowledge in the best writable provider.
49
+ *
50
+ * Routing:
51
+ * 1. If request.provider is set → use that provider (verify writable + healthy)
52
+ * 2. If request.contentType is set → use it as write type (skip classification)
53
+ * 3. Otherwise → classifyWrite(content) → selectWriteTarget()
54
+ */
55
+ store(request: SwarmStoreRequest): Promise<SwarmStoreResult>;
56
+ private buildCacheKey;
57
+ private evictIfOverSize;
58
+ private fallbackToByterover;
59
+ }