byterover-cli 3.2.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/.env.production +0 -4
- 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/search-knowledge-service.js +12 -2
- 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/curate/index.d.ts +1 -0
- package/dist/oclif/commands/curate/index.js +15 -1
- package/dist/oclif/commands/query.d.ts +1 -0
- package/dist/oclif/commands/query.js +17 -3
- package/dist/oclif/commands/search.d.ts +20 -0
- package/dist/oclif/commands/search.js +186 -0
- package/dist/oclif/commands/status.js +4 -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/oclif/lib/daemon-client.js +0 -1
- package/dist/oclif/lib/search-format.d.ts +10 -0
- package/dist/oclif/lib/search-format.js +25 -0
- package/dist/oclif/lib/task-client.d.ts +6 -0
- package/dist/oclif/lib/task-client.js +10 -3
- package/dist/server/constants.d.ts +3 -2
- package/dist/server/constants.js +10 -7
- package/dist/server/core/domain/errors/task-error.d.ts +2 -2
- package/dist/server/core/domain/errors/task-error.js +5 -4
- package/dist/server/core/domain/source/source-schema.d.ts +6 -6
- package/dist/server/core/domain/transport/schemas.d.ts +14 -14
- package/dist/server/core/domain/transport/schemas.js +3 -3
- package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
- package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
- package/dist/server/core/interfaces/executor/index.d.ts +1 -0
- package/dist/server/core/interfaces/executor/index.js +1 -0
- package/dist/server/infra/daemon/agent-process.js +20 -7
- package/dist/server/infra/executor/search-executor.d.ts +17 -0
- package/dist/server/infra/executor/search-executor.js +30 -0
- 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/pull-handler.js +3 -3
- package/dist/server/infra/transport/handlers/push-handler.js +3 -3
- package/dist/server/infra/transport/handlers/status-handler.js +25 -18
- 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 +188 -5
- package/dist/server/utils/gitignore.d.ts +1 -0
- package/dist/server/utils/gitignore.js +36 -4
- package/dist/shared/transport/search-content.d.ts +28 -0
- package/dist/shared/transport/search-content.js +38 -0
- package/dist/shared/transport/types/dto.d.ts +1 -1
- package/dist/tui/features/status/utils/format-status.js +5 -0
- package/dist/tui/utils/error-messages.js +2 -2
- package/oclif.manifest.json +581 -317
- package/package.json +2 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { ADAPTER_CONTENT_LIMIT, applyGapRatio, POST_EXPANSION_GAP_RATIO, searchWithPrecision } from '../search-precision.js';
|
|
5
|
+
/** Wikilink decay factor for graph-expanded results */
|
|
6
|
+
const WIKILINK_DECAY = 0.7;
|
|
7
|
+
/** Default patterns to ignore when scanning the vault */
|
|
8
|
+
const DEFAULT_IGNORE = new Set(['.git', '.obsidian', '.trash', 'templates']);
|
|
9
|
+
/**
|
|
10
|
+
* Extract `[[target]]` and `[[target|alias]]` wikilinks from markdown content.
|
|
11
|
+
*/
|
|
12
|
+
function extractWikilinks(content) {
|
|
13
|
+
const links = [];
|
|
14
|
+
const regex = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
15
|
+
let match;
|
|
16
|
+
while ((match = regex.exec(content)) !== null) {
|
|
17
|
+
links.push(match[1].trim());
|
|
18
|
+
}
|
|
19
|
+
return links;
|
|
20
|
+
}
|
|
21
|
+
function findMarkdownFiles(dirPath, basePath, ignoreSet) {
|
|
22
|
+
const results = [];
|
|
23
|
+
try {
|
|
24
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (ignoreSet.has(entry.name))
|
|
27
|
+
continue;
|
|
28
|
+
const fullPath = join(dirPath, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
results.push(...findMarkdownFiles(fullPath, basePath, ignoreSet));
|
|
31
|
+
}
|
|
32
|
+
else if (entry.name.endsWith('.md')) {
|
|
33
|
+
const stats = statSync(fullPath);
|
|
34
|
+
results.push({
|
|
35
|
+
fullPath,
|
|
36
|
+
relativePath: relative(basePath, fullPath),
|
|
37
|
+
signature: `${stats.mtimeMs}:${stats.size}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Skip inaccessible directories
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
function buildIndexSignature(files) {
|
|
48
|
+
return files
|
|
49
|
+
.map((file) => `${file.relativePath}:${file.signature}`)
|
|
50
|
+
.sort()
|
|
51
|
+
.join('|');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Obsidian vault adapter — indexes .md files via MiniSearch,
|
|
55
|
+
* follows [[wikilinks]] one hop for graph expansion.
|
|
56
|
+
*
|
|
57
|
+
* Read-only by default (does not modify the user's vault).
|
|
58
|
+
*/
|
|
59
|
+
export class ObsidianAdapter {
|
|
60
|
+
vaultPath;
|
|
61
|
+
capabilities = {
|
|
62
|
+
avgLatencyMs: 100,
|
|
63
|
+
graphTraversal: true,
|
|
64
|
+
keywordSearch: true,
|
|
65
|
+
localOnly: true,
|
|
66
|
+
maxTokensPerQuery: 8000,
|
|
67
|
+
semanticSearch: false,
|
|
68
|
+
temporalQuery: false,
|
|
69
|
+
userModeling: false,
|
|
70
|
+
writeSupported: false,
|
|
71
|
+
};
|
|
72
|
+
id = 'obsidian';
|
|
73
|
+
type = 'obsidian';
|
|
74
|
+
documents = [];
|
|
75
|
+
ignoreSet;
|
|
76
|
+
index;
|
|
77
|
+
indexSignature;
|
|
78
|
+
pathToDoc = new Map();
|
|
79
|
+
watchForChanges;
|
|
80
|
+
constructor(vaultPath, options) {
|
|
81
|
+
this.vaultPath = vaultPath;
|
|
82
|
+
this.ignoreSet = new Set([...(options?.ignorePatterns ?? []), ...DEFAULT_IGNORE]);
|
|
83
|
+
this.watchForChanges = options?.watchForChanges ?? true;
|
|
84
|
+
}
|
|
85
|
+
async delete(_id) {
|
|
86
|
+
throw new Error('Obsidian vault is read-only.');
|
|
87
|
+
}
|
|
88
|
+
estimateCost(_request) {
|
|
89
|
+
return {
|
|
90
|
+
estimatedCostCents: 0,
|
|
91
|
+
estimatedLatencyMs: this.capabilities.avgLatencyMs,
|
|
92
|
+
estimatedTokens: 0,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async healthCheck() {
|
|
96
|
+
return { available: existsSync(this.vaultPath) };
|
|
97
|
+
}
|
|
98
|
+
async query(request) {
|
|
99
|
+
this.ensureIndex();
|
|
100
|
+
const maxResults = request.maxResults ?? 10;
|
|
101
|
+
if (!this.index)
|
|
102
|
+
return [];
|
|
103
|
+
// T1/T2/T3: Precision-filtered search (stop words, AND-first, score floor, gap ratio)
|
|
104
|
+
const precisionResults = searchWithPrecision(this.index, request.query, { maxResults });
|
|
105
|
+
if (precisionResults.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
// Collect direct matches
|
|
108
|
+
const resultMap = new Map();
|
|
109
|
+
for (const pr of precisionResults) {
|
|
110
|
+
const doc = this.documents[pr.id];
|
|
111
|
+
if (!doc)
|
|
112
|
+
continue;
|
|
113
|
+
resultMap.set(doc.path, { doc, matchType: 'keyword', score: pr.normalizedScore });
|
|
114
|
+
}
|
|
115
|
+
// Expand wikilinks one hop from direct matches
|
|
116
|
+
for (const [, entry] of resultMap) {
|
|
117
|
+
for (const linkTarget of entry.doc.wikilinks) {
|
|
118
|
+
const candidates = [
|
|
119
|
+
`${linkTarget}.md`,
|
|
120
|
+
linkTarget,
|
|
121
|
+
...[...this.pathToDoc.keys()].filter((p) => p.toLowerCase().endsWith(`${linkTarget.toLowerCase()}.md`) ||
|
|
122
|
+
p.toLowerCase() === linkTarget.toLowerCase()),
|
|
123
|
+
];
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
const linkedDoc = this.pathToDoc.get(candidate);
|
|
126
|
+
if (linkedDoc && !resultMap.has(linkedDoc.path)) {
|
|
127
|
+
resultMap.set(linkedDoc.path, {
|
|
128
|
+
doc: linkedDoc,
|
|
129
|
+
matchType: 'graph',
|
|
130
|
+
score: entry.score * WIKILINK_DECAY,
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Second gap-ratio pass on combined direct + expanded results (T3 only)
|
|
138
|
+
const combined = [...resultMap.entries()].map(([path, entry]) => ({
|
|
139
|
+
doc: entry.doc,
|
|
140
|
+
matchType: entry.matchType,
|
|
141
|
+
normalizedScore: entry.score,
|
|
142
|
+
path,
|
|
143
|
+
}));
|
|
144
|
+
const gapFiltered = applyGapRatio(combined.map((c) => ({ id: c.path, normalizedScore: c.normalizedScore, queryTerms: [], rawScore: 0 })), POST_EXPANSION_GAP_RATIO);
|
|
145
|
+
const keptPaths = new Set(gapFiltered.map((r) => r.id));
|
|
146
|
+
const sorted = combined
|
|
147
|
+
.filter((c) => keptPaths.has(c.path))
|
|
148
|
+
.sort((a, b) => b.normalizedScore - a.normalizedScore)
|
|
149
|
+
.slice(0, maxResults);
|
|
150
|
+
return sorted.map((entry, index) => ({
|
|
151
|
+
content: entry.doc.content.slice(0, ADAPTER_CONTENT_LIMIT),
|
|
152
|
+
id: `obsidian-${index}`,
|
|
153
|
+
metadata: {
|
|
154
|
+
matchType: entry.matchType,
|
|
155
|
+
path: entry.path,
|
|
156
|
+
source: entry.path,
|
|
157
|
+
},
|
|
158
|
+
provider: 'obsidian',
|
|
159
|
+
providerType: 'obsidian',
|
|
160
|
+
score: entry.normalizedScore,
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
async store(_entry) {
|
|
164
|
+
throw new Error('Obsidian vault is read-only.');
|
|
165
|
+
}
|
|
166
|
+
async update(_id, _entry) {
|
|
167
|
+
throw new Error('Obsidian vault is read-only.');
|
|
168
|
+
}
|
|
169
|
+
ensureIndex() {
|
|
170
|
+
// When watchForChanges is false, reuse the existing index after initial build
|
|
171
|
+
if (this.index && !this.watchForChanges) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const files = findMarkdownFiles(this.vaultPath, this.vaultPath, this.ignoreSet);
|
|
175
|
+
const nextSignature = buildIndexSignature(files);
|
|
176
|
+
if (this.index && this.indexSignature === nextSignature) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.documents = files.map((file, index) => {
|
|
180
|
+
const content = readFileSync(file.fullPath, 'utf8');
|
|
181
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
182
|
+
return {
|
|
183
|
+
content,
|
|
184
|
+
id: index,
|
|
185
|
+
path: file.relativePath,
|
|
186
|
+
title: titleMatch?.[1] ?? file.relativePath,
|
|
187
|
+
wikilinks: extractWikilinks(content),
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
this.index = new MiniSearch({
|
|
191
|
+
fields: ['title', 'content'],
|
|
192
|
+
storeFields: ['title', 'path'],
|
|
193
|
+
});
|
|
194
|
+
this.index.addAll(this.documents);
|
|
195
|
+
this.pathToDoc.clear();
|
|
196
|
+
for (const doc of this.documents) {
|
|
197
|
+
this.pathToDoc.set(doc.path, doc);
|
|
198
|
+
}
|
|
199
|
+
this.indexSignature = nextSignature;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderType } from '../../../core/domain/swarm/types.js';
|
|
2
|
+
import type { SwarmQueryResult } from '../../../core/interfaces/i-swarm-coordinator.js';
|
|
3
|
+
export declare function providerTypeToLabel(type: ProviderType, id: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Format swarm query results for terminal display.
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatQueryResults(result: SwarmQueryResult, query: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Format swarm query results with detailed explain output.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatQueryResultsExplain(result: SwarmQueryResult, query: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Format swarm query results as JSON.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatQueryResultsJson(result: SwarmQueryResult): string;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const DISPLAY_CONTENT_LIMIT = 2000;
|
|
3
|
+
export function providerTypeToLabel(type, id) {
|
|
4
|
+
switch (type) {
|
|
5
|
+
case 'byterover': {
|
|
6
|
+
return 'context-tree';
|
|
7
|
+
}
|
|
8
|
+
case 'gbrain': {
|
|
9
|
+
return 'gbrain';
|
|
10
|
+
}
|
|
11
|
+
case 'hindsight': {
|
|
12
|
+
return 'hindsight';
|
|
13
|
+
}
|
|
14
|
+
case 'honcho': {
|
|
15
|
+
return 'honcho';
|
|
16
|
+
}
|
|
17
|
+
case 'local-markdown': {
|
|
18
|
+
const name = id.split(':')[1] ?? 'files';
|
|
19
|
+
return `notes:${name}`;
|
|
20
|
+
}
|
|
21
|
+
case 'memory-wiki': {
|
|
22
|
+
return 'memory-wiki';
|
|
23
|
+
}
|
|
24
|
+
case 'obsidian': {
|
|
25
|
+
return 'obsidian';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const LABEL_COLORS = {
|
|
30
|
+
byterover: chalk.cyan,
|
|
31
|
+
gbrain: chalk.yellow,
|
|
32
|
+
hindsight: chalk.blueBright,
|
|
33
|
+
honcho: chalk.blue,
|
|
34
|
+
'local-markdown': chalk.green,
|
|
35
|
+
'memory-wiki': chalk.whiteBright,
|
|
36
|
+
obsidian: chalk.magenta,
|
|
37
|
+
};
|
|
38
|
+
function colorLabel(providerType, provider) {
|
|
39
|
+
const label = providerTypeToLabel(providerType, provider);
|
|
40
|
+
const colorFn = LABEL_COLORS[providerType];
|
|
41
|
+
return colorFn(`[${label}]`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Format swarm query results for terminal display.
|
|
45
|
+
*/
|
|
46
|
+
export function formatQueryResults(result, query) {
|
|
47
|
+
const providerEntries = Object.entries(result.meta.providers);
|
|
48
|
+
const queriedCount = providerEntries.filter(([, meta]) => meta.selected !== false).length;
|
|
49
|
+
const lines = [
|
|
50
|
+
chalk.bold(`\nSwarm Query: "${query}"`),
|
|
51
|
+
`Type: ${chalk.cyan(result.meta.queryType)} | Providers: ${chalk.yellow(`${queriedCount} queried`)} | Latency: ${chalk.yellow(`${result.meta.totalLatencyMs}ms`)}`,
|
|
52
|
+
'─'.repeat(50),
|
|
53
|
+
];
|
|
54
|
+
if (result.results.length === 0) {
|
|
55
|
+
lines.push(chalk.dim('No results found.'));
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
for (const [i, r] of result.results.entries()) {
|
|
59
|
+
const scoreStr = chalk.green(r.score.toFixed(4));
|
|
60
|
+
const sourceStr = chalk.dim(r.metadata.source);
|
|
61
|
+
const matchStr = chalk.dim(`[${r.metadata.matchType}]`);
|
|
62
|
+
const label = r.providerType ? colorLabel(r.providerType, r.provider) : '';
|
|
63
|
+
const content = r.content.length > DISPLAY_CONTENT_LIMIT ? `${r.content.slice(0, DISPLAY_CONTENT_LIMIT)}…` : r.content;
|
|
64
|
+
lines.push(`${chalk.bold(`${i + 1}.`)} ${label} ${sourceStr} score: ${scoreStr} ${matchStr}`, ` ${content}`, '');
|
|
65
|
+
}
|
|
66
|
+
if (result.meta.costCents > 0) {
|
|
67
|
+
lines.push(chalk.dim(`Cost: $${(result.meta.costCents / 100).toFixed(4)}`));
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Format swarm query results with detailed explain output.
|
|
73
|
+
*/
|
|
74
|
+
export function formatQueryResultsExplain(result, query) {
|
|
75
|
+
const providerEntries = Object.entries(result.meta.providers);
|
|
76
|
+
const selected = providerEntries.filter(([, m]) => m.selected !== false);
|
|
77
|
+
const excluded = providerEntries.filter(([, m]) => m.selected === false);
|
|
78
|
+
const lines = [
|
|
79
|
+
chalk.bold(`\nSwarm Query: "${query}"`),
|
|
80
|
+
`Classification: ${chalk.cyan(result.meta.queryType)}`,
|
|
81
|
+
`Provider selection: ${selected.length} of ${providerEntries.length} available`,
|
|
82
|
+
];
|
|
83
|
+
for (const [id, meta] of selected) {
|
|
84
|
+
lines.push(` ${chalk.green('✓')} ${id} (healthy, selected, ${meta.resultCount} results, ${meta.latencyMs}ms)`);
|
|
85
|
+
}
|
|
86
|
+
for (const [id, meta] of excluded) {
|
|
87
|
+
lines.push(` ${chalk.red('✗')} ${id} (excluded — ${meta.excludeReason ?? 'unknown'})`);
|
|
88
|
+
}
|
|
89
|
+
// Enrichment
|
|
90
|
+
const enriched = providerEntries.filter(([, m]) => m.enrichedBy);
|
|
91
|
+
if (enriched.length > 0) {
|
|
92
|
+
lines.push('Enrichment:');
|
|
93
|
+
for (const [id, meta] of enriched) {
|
|
94
|
+
const excerpts = meta.enrichmentExcerpts?.length
|
|
95
|
+
? ` (context: ${meta.enrichmentExcerpts.map((k) => `"${k.slice(0, 30)}"`).join(', ')})`
|
|
96
|
+
: '';
|
|
97
|
+
lines.push(` ${meta.enrichedBy} → ${id}${excerpts}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Result count
|
|
101
|
+
const totalRaw = providerEntries.reduce((sum, [, m]) => sum + m.resultCount, 0);
|
|
102
|
+
lines.push(`Results: ${totalRaw} raw → ${result.results.length} after RRF fusion + precision filtering`, '─'.repeat(50));
|
|
103
|
+
if (result.results.length === 0) {
|
|
104
|
+
lines.push(chalk.dim('No results found.'));
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
for (const [i, r] of result.results.entries()) {
|
|
108
|
+
const scoreStr = chalk.green(r.score.toFixed(4));
|
|
109
|
+
const sourceStr = chalk.dim(r.metadata.source);
|
|
110
|
+
const matchStr = chalk.dim(`[${r.metadata.matchType}]`);
|
|
111
|
+
const label = r.providerType ? colorLabel(r.providerType, r.provider) : '';
|
|
112
|
+
const content = r.content.length > DISPLAY_CONTENT_LIMIT ? `${r.content.slice(0, DISPLAY_CONTENT_LIMIT)}…` : r.content;
|
|
113
|
+
lines.push(`${chalk.bold(`${i + 1}.`)} ${label} ${sourceStr} score: ${scoreStr} ${matchStr}`, ` ${content}`, '');
|
|
114
|
+
}
|
|
115
|
+
if (result.meta.costCents > 0) {
|
|
116
|
+
lines.push(chalk.dim(`Cost: $${(result.meta.costCents / 100).toFixed(4)}`));
|
|
117
|
+
}
|
|
118
|
+
lines.push(`Latency: ${result.meta.totalLatencyMs}ms`);
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format swarm query results as JSON.
|
|
123
|
+
*/
|
|
124
|
+
export function formatQueryResultsJson(result) {
|
|
125
|
+
return JSON.stringify(result, undefined, 2);
|
|
126
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SwarmConfig } from './swarm-config-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load swarm config from `.brv/swarm/config.yaml`.
|
|
4
|
+
*
|
|
5
|
+
* 1. Read YAML file
|
|
6
|
+
* 2. Resolve `${VAR}` env var references
|
|
7
|
+
* 3. Parse and validate through Zod schema
|
|
8
|
+
*
|
|
9
|
+
* @param projectRoot - Project root directory containing `.brv/`
|
|
10
|
+
* @param env - Environment variables (defaults to `process.env`)
|
|
11
|
+
* @returns Validated swarm config
|
|
12
|
+
* @throws Error if file not found, YAML invalid, or schema validation fails
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadSwarmConfig(projectRoot: string, env?: Record<string, string | undefined>): Promise<SwarmConfig>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { load } from 'js-yaml';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ZodError } from 'zod';
|
|
6
|
+
import { resolveEnvVars, validateSwarmConfig } from './swarm-config-schema.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default path to the swarm config file relative to project root.
|
|
9
|
+
*/
|
|
10
|
+
const CONFIG_PATH = join('.brv', 'swarm', 'config.yaml');
|
|
11
|
+
/**
|
|
12
|
+
* Expand a leading `~` to the user's home directory.
|
|
13
|
+
* Handles both Unix (`~/`) and Windows (`~\\`) separators.
|
|
14
|
+
* Node's fs APIs do not expand tilde, so we must do it ourselves.
|
|
15
|
+
*/
|
|
16
|
+
function expandTilde(value) {
|
|
17
|
+
if (value === '~')
|
|
18
|
+
return homedir();
|
|
19
|
+
if (value.startsWith('~/') || value.startsWith('~\\'))
|
|
20
|
+
return join(homedir(), value.slice(2));
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Recursively resolve `${VAR}` env vars and expand `~/` in all string values.
|
|
25
|
+
*/
|
|
26
|
+
function resolveStringsDeep(obj, env) {
|
|
27
|
+
if (typeof obj === 'string') {
|
|
28
|
+
return expandTilde(resolveEnvVars(obj, env));
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(obj)) {
|
|
31
|
+
return obj.map((item) => resolveStringsDeep(item, env));
|
|
32
|
+
}
|
|
33
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
36
|
+
result[key] = resolveStringsDeep(value, env);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
return obj;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load swarm config from `.brv/swarm/config.yaml`.
|
|
44
|
+
*
|
|
45
|
+
* 1. Read YAML file
|
|
46
|
+
* 2. Resolve `${VAR}` env var references
|
|
47
|
+
* 3. Parse and validate through Zod schema
|
|
48
|
+
*
|
|
49
|
+
* @param projectRoot - Project root directory containing `.brv/`
|
|
50
|
+
* @param env - Environment variables (defaults to `process.env`)
|
|
51
|
+
* @returns Validated swarm config
|
|
52
|
+
* @throws Error if file not found, YAML invalid, or schema validation fails
|
|
53
|
+
*/
|
|
54
|
+
export async function loadSwarmConfig(projectRoot, env) {
|
|
55
|
+
const configPath = join(projectRoot, CONFIG_PATH);
|
|
56
|
+
let rawYaml;
|
|
57
|
+
try {
|
|
58
|
+
rawYaml = readFileSync(configPath, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error(`Swarm config not found at ${configPath}. Run \`brv swarm onboard\` to create one.`);
|
|
62
|
+
}
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = load(rawYaml);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
69
|
+
throw new Error(`Failed to parse ${configPath}: ${detail}`);
|
|
70
|
+
}
|
|
71
|
+
const resolved = resolveStringsDeep(parsed, env ?? process.env);
|
|
72
|
+
try {
|
|
73
|
+
return validateSwarmConfig(resolved);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof ZodError) {
|
|
77
|
+
const issues = error.issues.map((issue) => ` - ${issue.path.join('.')}: ${issue.message}`).join('\n');
|
|
78
|
+
throw new Error(`Invalid swarm config in ${configPath}:\n${issues}`);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|