byterover-cli 3.1.0 → 3.3.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/README.md +17 -0
- package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
- package/dist/agent/infra/agent/agent-schemas.js +1 -0
- package/dist/agent/infra/sandbox/curate-service.js +14 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +226 -103
- package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
- package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
- package/dist/agent/infra/tools/tool-registry.js +1 -1
- package/dist/agent/infra/tools/write-guard.d.ts +11 -0
- package/dist/agent/infra/tools/write-guard.js +48 -0
- package/dist/agent/resources/prompts/system-prompt.yml +9 -0
- package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
- package/dist/agent/resources/tools/search_knowledge.txt +11 -1
- package/dist/oclif/commands/curate/index.d.ts +1 -0
- package/dist/oclif/commands/curate/index.js +19 -4
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.d.ts +1 -0
- package/dist/oclif/commands/query.js +19 -4
- package/dist/oclif/commands/search.d.ts +20 -0
- package/dist/oclif/commands/search.js +186 -0
- package/dist/oclif/commands/source/add.d.ts +12 -0
- package/dist/oclif/commands/source/add.js +42 -0
- package/dist/oclif/commands/source/index.d.ts +6 -0
- package/dist/oclif/commands/source/index.js +8 -0
- package/dist/oclif/commands/source/list.d.ts +6 -0
- package/dist/oclif/commands/source/list.js +32 -0
- package/dist/oclif/commands/source/remove.d.ts +9 -0
- package/dist/oclif/commands/source/remove.js +33 -0
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +45 -6
- package/dist/oclif/commands/worktree/add.d.ts +12 -0
- package/dist/oclif/commands/worktree/add.js +44 -0
- package/dist/oclif/commands/worktree/index.d.ts +6 -0
- package/dist/oclif/commands/worktree/index.js +8 -0
- package/dist/oclif/commands/worktree/list.d.ts +6 -0
- package/dist/oclif/commands/worktree/list.js +28 -0
- package/dist/oclif/commands/worktree/remove.d.ts +9 -0
- package/dist/oclif/commands/worktree/remove.js +35 -0
- package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
- package/dist/oclif/lib/daemon-client.d.ts +4 -2
- package/dist/oclif/lib/daemon-client.js +19 -4
- 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 +7 -1
- package/dist/server/constants.js +10 -0
- package/dist/server/core/domain/client/client-info.d.ts +7 -0
- package/dist/server/core/domain/client/client-info.js +11 -0
- 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/project/worktrees-schema.d.ts +29 -0
- package/dist/server/core/domain/project/worktrees-schema.js +17 -0
- package/dist/server/core/domain/source/source-operations.d.ts +31 -0
- package/dist/server/core/domain/source/source-operations.js +201 -0
- package/dist/server/core/domain/source/source-schema.d.ts +94 -0
- package/dist/server/core/domain/source/source-schema.js +121 -0
- package/dist/server/core/domain/transport/schemas.d.ts +18 -10
- package/dist/server/core/domain/transport/schemas.js +7 -3
- package/dist/server/core/domain/transport/task-info.d.ts +2 -0
- package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
- package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
- package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
- 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/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/daemon/agent-process.js +35 -12
- package/dist/server/infra/executor/curate-executor.js +4 -2
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +23 -12
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +115 -21
- 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/mcp/mcp-mode-detector.d.ts +7 -5
- package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
- package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
- package/dist/server/infra/mcp/mcp-server.js +11 -6
- package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
- package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
- package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
- package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
- package/dist/server/infra/process/connection-coordinator.js +11 -0
- package/dist/server/infra/process/feature-handlers.js +4 -1
- package/dist/server/infra/process/task-router.d.ts +1 -0
- package/dist/server/infra/process/task-router.js +60 -5
- package/dist/server/infra/project/resolve-project.d.ts +106 -0
- package/dist/server/infra/project/resolve-project.js +473 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- 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/source-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/source-handler.js +37 -0
- package/dist/server/infra/transport/handlers/status-handler.js +76 -27
- package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
- package/dist/server/infra/transport/transport-connector.d.ts +10 -4
- package/dist/server/infra/transport/transport-connector.js +2 -2
- package/dist/server/templates/skill/SKILL.md +25 -5
- package/dist/server/utils/path-utils.d.ts +5 -0
- package/dist/server/utils/path-utils.js +11 -1
- package/dist/shared/transport/events/client-events.d.ts +3 -0
- package/dist/shared/transport/events/client-events.js +3 -0
- package/dist/shared/transport/events/index.d.ts +13 -0
- package/dist/shared/transport/events/index.js +9 -0
- package/dist/shared/transport/events/source-events.d.ts +30 -0
- package/dist/shared/transport/events/source-events.js +5 -0
- package/dist/shared/transport/events/status-events.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +4 -1
- package/dist/shared/transport/events/worktree-events.d.ts +31 -0
- package/dist/shared/transport/events/worktree-events.js +5 -0
- 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 +20 -1
- package/dist/tui/features/commands/definitions/index.js +6 -0
- package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-add.js +48 -0
- package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-list.js +47 -0
- package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-remove.js +38 -0
- package/dist/tui/features/commands/definitions/source.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source.js +8 -0
- package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
- package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
- package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
- package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree.js +8 -0
- package/dist/tui/features/curate/api/create-curate-task.js +3 -1
- package/dist/tui/features/query/api/create-query-task.js +3 -1
- package/dist/tui/features/source/api/source-api.d.ts +4 -0
- package/dist/tui/features/source/api/source-api.js +22 -0
- package/dist/tui/features/status/api/get-status.js +2 -1
- package/dist/tui/features/status/utils/format-status.js +28 -1
- package/dist/tui/features/transport/components/transport-initializer.js +36 -1
- package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
- package/dist/tui/features/worktree/api/worktree-api.js +22 -0
- package/dist/tui/repl-startup.d.ts +2 -0
- package/dist/tui/repl-startup.js +5 -3
- package/dist/tui/stores/transport-store.d.ts +6 -0
- package/dist/tui/stores/transport-store.js +6 -0
- package/dist/tui/utils/error-messages.js +2 -2
- package/oclif.manifest.json +380 -36
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
2
2
|
import { ABSTRACT_EXTENSION, BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
|
|
3
|
+
import { loadSources } from '../../core/domain/source/source-schema.js';
|
|
3
4
|
import { isDerivedArtifact } from '../context-tree/derived-artifact.js';
|
|
4
5
|
import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
|
|
5
6
|
import { canRespondDirectly, formatDirectResponse, formatNotFoundResponse, } from './direct-search-responder.js';
|
|
@@ -45,15 +46,16 @@ export class QueryExecutor {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
async executeWithAgent(agent, options) {
|
|
48
|
-
const { query, taskId } = options;
|
|
49
|
+
const { query, taskId, worktreeRoot } = options;
|
|
50
|
+
const workspaceScope = this.deriveWorkspaceScope(worktreeRoot);
|
|
49
51
|
// Start search early — runs in parallel with fingerprint computation (independent operations)
|
|
50
|
-
const searchPromise = this.searchService?.search(query, { limit: SMART_ROUTING_MAX_DOCS });
|
|
52
|
+
const searchPromise = this.searchService?.search(query, { limit: SMART_ROUTING_MAX_DOCS, scope: workspaceScope });
|
|
51
53
|
// Prevent unhandled rejection if we return early (cache hit) while search is still pending
|
|
52
54
|
searchPromise?.catch(() => { });
|
|
53
55
|
// === Tier 0: Exact cache hit (0ms) ===
|
|
54
56
|
let fingerprint;
|
|
55
57
|
if (this.cache && this.fileSystem) {
|
|
56
|
-
fingerprint = await this.computeContextTreeFingerprint();
|
|
58
|
+
fingerprint = await this.computeContextTreeFingerprint(worktreeRoot);
|
|
57
59
|
const cached = this.cache.get(query, fingerprint);
|
|
58
60
|
if (cached) {
|
|
59
61
|
return cached + ATTRIBUTION_FOOTER;
|
|
@@ -76,7 +78,7 @@ export class QueryExecutor {
|
|
|
76
78
|
}
|
|
77
79
|
// Supplementary entity-based searches for better multi-session recall
|
|
78
80
|
if (this.searchService && searchResult && searchResult.totalFound < 3) {
|
|
79
|
-
searchResult = await this.supplementEntitySearches(query, searchResult);
|
|
81
|
+
searchResult = await this.supplementEntitySearches(query, searchResult, workspaceScope);
|
|
80
82
|
}
|
|
81
83
|
// === OOD short-circuit: no results means topic not covered ===
|
|
82
84
|
if (searchResult && searchResult.results.length === 0) {
|
|
@@ -141,12 +143,18 @@ export class QueryExecutor {
|
|
|
141
143
|
// Inject search results + metadata into the TASK session's sandbox
|
|
142
144
|
agent.setSandboxVariableOnSession(taskSessionId, resultsVar, searchResult?.results ?? []);
|
|
143
145
|
agent.setSandboxVariableOnSession(taskSessionId, metaVar, metadata);
|
|
146
|
+
// Inject workspace scope so agent follow-up searches are workspace-aware
|
|
147
|
+
const scopeVar = workspaceScope ? `__query_scope_${taskIdSafe}` : undefined;
|
|
148
|
+
if (scopeVar && workspaceScope) {
|
|
149
|
+
agent.setSandboxVariableOnSession(taskSessionId, scopeVar, workspaceScope);
|
|
150
|
+
}
|
|
144
151
|
const prompt = this.buildQueryPrompt(query, {
|
|
145
152
|
manifestContext,
|
|
146
153
|
metadata,
|
|
147
154
|
metaVar,
|
|
148
155
|
prefetchedContext,
|
|
149
156
|
resultsVar,
|
|
157
|
+
scopeVar,
|
|
150
158
|
});
|
|
151
159
|
// Query-optimized LLM overrides: tokens and lower temperature
|
|
152
160
|
const queryOverrides = prefetchedContext
|
|
@@ -179,7 +187,12 @@ export class QueryExecutor {
|
|
|
179
187
|
const highConfidenceResults = searchResult.results.filter((r) => r.score >= SMART_ROUTING_SCORE_THRESHOLD);
|
|
180
188
|
if (highConfidenceResults.length === 0)
|
|
181
189
|
return undefined;
|
|
182
|
-
const sections = highConfidenceResults.map((r) =>
|
|
190
|
+
const sections = highConfidenceResults.map((r) => {
|
|
191
|
+
const source = r.origin === 'shared' && r.originAlias
|
|
192
|
+
? `[${r.originAlias}]:${r.path}`
|
|
193
|
+
: `.brv/context-tree/${r.path}`;
|
|
194
|
+
return `### ${r.title}\n**Source**: ${source}\n\n${r.excerpt}`;
|
|
195
|
+
});
|
|
183
196
|
return sections.join('\n\n---\n\n');
|
|
184
197
|
}
|
|
185
198
|
/**
|
|
@@ -192,9 +205,9 @@ export class QueryExecutor {
|
|
|
192
205
|
* @param options - Prompt options with variable names and metadata
|
|
193
206
|
*/
|
|
194
207
|
buildQueryPrompt(query, options) {
|
|
195
|
-
const { manifestContext, metadata, metaVar, prefetchedContext, resultsVar } = options;
|
|
208
|
+
const { manifestContext, metadata, metaVar, prefetchedContext, resultsVar, scopeVar } = options;
|
|
196
209
|
const groundingRules = `### Grounding Rules (CRITICAL)
|
|
197
|
-
- ONLY use information from the curated knowledge base (.brv/context-tree/)
|
|
210
|
+
- ONLY use information from the curated knowledge base (local .brv/context-tree/ plus any read-only shared sources)
|
|
198
211
|
- If no relevant knowledge is found, respond: "This topic is not covered in the knowledge base."
|
|
199
212
|
- Do NOT extrapolate, infer, or generate information beyond what is explicitly stated in sources
|
|
200
213
|
- Every claim MUST be traceable to a specific source file
|
|
@@ -202,11 +215,15 @@ export class QueryExecutor {
|
|
|
202
215
|
const responseFormat = `### Response Format
|
|
203
216
|
- **Summary**: Direct answer (2-3 sentences)
|
|
204
217
|
- **Details**: Key findings with explanations
|
|
205
|
-
- **Sources**:
|
|
218
|
+
- **Sources**: Use .brv/context-tree/... for local knowledge and [alias]:path for shared sources
|
|
206
219
|
- **Gaps**: Note any aspects not covered`;
|
|
207
220
|
const manifestSection = manifestContext
|
|
208
221
|
? `\n## Structural Context (from manifest)\nThe following provides broad structural awareness of the knowledge base:\n\n${manifestContext}\n`
|
|
209
222
|
: '';
|
|
223
|
+
// When workspace scope is active, instruct the agent to pass it to follow-up searches
|
|
224
|
+
const scopeGuidance = scopeVar
|
|
225
|
+
? `\nFor any follow-up \`tools.searchKnowledge()\` calls, pass \`{ scope: ${scopeVar} }\` to scope results to the current workspace.`
|
|
226
|
+
: '';
|
|
210
227
|
if (prefetchedContext) {
|
|
211
228
|
return `## User Query
|
|
212
229
|
${query}
|
|
@@ -224,7 +241,7 @@ Metadata: \`${metaVar}\`
|
|
|
224
241
|
|
|
225
242
|
Answer the user's question using the pre-fetched context above.
|
|
226
243
|
If the pre-fetched context does not directly address the user's query topic, respond that the topic is not covered in the knowledge base. Do not attempt to answer from tangentially related content.
|
|
227
|
-
If the context is insufficient but relevant, use \`code_exec\` with \`silent: true\` to read additional documents from the search results variable. Use \`setFinalResult(answer)\` when done
|
|
244
|
+
If the context is insufficient but relevant, use \`code_exec\` with \`silent: true\` to read additional documents from the search results variable. Use \`setFinalResult(answer)\` when done.${scopeGuidance}
|
|
228
245
|
|
|
229
246
|
${groundingRules}
|
|
230
247
|
|
|
@@ -240,7 +257,7 @@ Metadata: \`${metaVar}\`
|
|
|
240
257
|
## Instructions
|
|
241
258
|
|
|
242
259
|
Use \`code_exec\` to examine the search results in \`${resultsVar}\`, read relevant documents with \`tools.readFile()\`, and synthesize an answer.
|
|
243
|
-
Use \`silent: true\` for data-loading code_exec calls. Use \`setFinalResult(answer)\` to return the final answer immediately
|
|
260
|
+
Use \`silent: true\` for data-loading code_exec calls. Use \`setFinalResult(answer)\` to return the final answer immediately.${scopeGuidance}
|
|
244
261
|
|
|
245
262
|
${groundingRules}
|
|
246
263
|
|
|
@@ -250,10 +267,16 @@ ${responseFormat}`;
|
|
|
250
267
|
* Compute a context tree fingerprint cheaply using file mtimes.
|
|
251
268
|
* Used for cache invalidation — if any file in the context tree changes,
|
|
252
269
|
* the fingerprint changes and cached results are invalidated.
|
|
270
|
+
*
|
|
271
|
+
* Includes worktreeRoot in the hash so different workspaces produce
|
|
272
|
+
* different fingerprints, preventing cross-workspace cache bleed.
|
|
253
273
|
*/
|
|
254
|
-
async computeContextTreeFingerprint() {
|
|
274
|
+
async computeContextTreeFingerprint(worktreeRoot) {
|
|
255
275
|
// Fast path: return cached fingerprint if still valid (avoids globFiles I/O)
|
|
256
|
-
if
|
|
276
|
+
// Invalidate if worktreeRoot changed or knowledge source validity changed
|
|
277
|
+
if (this.cachedFingerprint && Date.now() < this.cachedFingerprint.expiresAt
|
|
278
|
+
&& this.cachedFingerprint.worktreeRoot === worktreeRoot
|
|
279
|
+
&& this.cachedFingerprint.sourceValidityHash === this.computeSourceValidityHash()) {
|
|
257
280
|
return this.cachedFingerprint.value;
|
|
258
281
|
}
|
|
259
282
|
try {
|
|
@@ -271,8 +294,36 @@ ${responseFormat}`;
|
|
|
271
294
|
.filter((f) => !isDerivedArtifact(f.path))
|
|
272
295
|
.map((f) => ({
|
|
273
296
|
mtime: f.modified?.getTime() ?? 0,
|
|
274
|
-
path: f.path,
|
|
297
|
+
path: worktreeRoot ? `${worktreeRoot}:${f.path}` : f.path,
|
|
275
298
|
}));
|
|
299
|
+
// Include shared source state in fingerprint so edits in shared
|
|
300
|
+
// projects invalidate cached query answers.
|
|
301
|
+
const loaded = this.baseDirectory ? loadSources(this.baseDirectory) : null;
|
|
302
|
+
if (loaded) {
|
|
303
|
+
// sources-file mtime detects source additions/removals
|
|
304
|
+
if (loaded.mtime) {
|
|
305
|
+
files.push({ mtime: loaded.mtime, path: '__sources.json__' });
|
|
306
|
+
}
|
|
307
|
+
// Glob each shared context tree for file-level change detection
|
|
308
|
+
const sharedResults = await Promise.all(loaded.origins.map(async (origin) => {
|
|
309
|
+
try {
|
|
310
|
+
const sharedGlob = await this.fileSystem.globFiles(`**/*${CONTEXT_FILE_EXTENSION}`, {
|
|
311
|
+
cwd: origin.contextTreeRoot,
|
|
312
|
+
includeMetadata: true,
|
|
313
|
+
maxResults: 10_000,
|
|
314
|
+
respectGitignore: false,
|
|
315
|
+
});
|
|
316
|
+
return sharedGlob.files
|
|
317
|
+
.filter((f) => !isDerivedArtifact(f.path))
|
|
318
|
+
.map((f) => ({ mtime: f.modified?.getTime() ?? 0, path: `shared:${origin.originKey}:${f.path}` }));
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Broken source — skip
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}));
|
|
325
|
+
files.push(...sharedResults.flat());
|
|
326
|
+
}
|
|
276
327
|
// Include .abstract.md siblings so newly-written abstracts invalidate the cache.
|
|
277
328
|
// They are excluded by isDerivedArtifact() above, so we append them separately.
|
|
278
329
|
const abstractFiles = globResult.files
|
|
@@ -284,7 +335,9 @@ ${responseFormat}`;
|
|
|
284
335
|
const fingerprint = QueryResultCache.computeFingerprint([...files, ...abstractFiles]);
|
|
285
336
|
this.cachedFingerprint = {
|
|
286
337
|
expiresAt: Date.now() + QueryExecutor.FINGERPRINT_CACHE_TTL_MS,
|
|
338
|
+
sourceValidityHash: this.computeSourceValidityHash(),
|
|
287
339
|
value: fingerprint,
|
|
340
|
+
worktreeRoot,
|
|
288
341
|
};
|
|
289
342
|
return fingerprint;
|
|
290
343
|
}
|
|
@@ -292,6 +345,40 @@ ${responseFormat}`;
|
|
|
292
345
|
return 'unknown';
|
|
293
346
|
}
|
|
294
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Lightweight hash of currently valid shared source keys.
|
|
350
|
+
* Used by the fingerprint cache fast path to detect when a source target
|
|
351
|
+
* becomes broken (directory deleted) within the TTL window.
|
|
352
|
+
* Cost: one readFileSync + existsSync per source — sub-millisecond for typical setups.
|
|
353
|
+
*/
|
|
354
|
+
computeSourceValidityHash() {
|
|
355
|
+
if (!this.baseDirectory)
|
|
356
|
+
return '';
|
|
357
|
+
const loaded = loadSources(this.baseDirectory);
|
|
358
|
+
if (!loaded)
|
|
359
|
+
return 'no-sources';
|
|
360
|
+
return loaded.origins.map((o) => o.originKey).sort().join(',');
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Derive a workspace scope for search from the worktreeRoot.
|
|
364
|
+
* Returns the relative path from projectRoot to worktreeRoot,
|
|
365
|
+
* or undefined if they are the same (no scoping needed).
|
|
366
|
+
*
|
|
367
|
+
* KNOWN LIMITATION: Workspace scoping only works if the curated context
|
|
368
|
+
* tree has a subtree matching the workspace relative path (e.g., 'packages/api').
|
|
369
|
+
* Since the context tree is organized semantically by the LLM (topic-based),
|
|
370
|
+
* not by directory structure, scope filtering typically has 0 matches and
|
|
371
|
+
* falls through to unscoped search. A proper fix requires tagging curated
|
|
372
|
+
* files with source workspace metadata during curation.
|
|
373
|
+
*/
|
|
374
|
+
deriveWorkspaceScope(worktreeRoot) {
|
|
375
|
+
if (!worktreeRoot || !this.baseDirectory)
|
|
376
|
+
return undefined;
|
|
377
|
+
if (worktreeRoot === this.baseDirectory)
|
|
378
|
+
return undefined;
|
|
379
|
+
const rel = relative(this.baseDirectory, worktreeRoot);
|
|
380
|
+
return rel || undefined;
|
|
381
|
+
}
|
|
295
382
|
/**
|
|
296
383
|
* Extract key entities from a query for supplementary searches.
|
|
297
384
|
* Simple heuristic: split query, filter stopwords, keep significant terms.
|
|
@@ -314,20 +401,21 @@ ${responseFormat}`;
|
|
|
314
401
|
* @param searchResult - Initial search result to supplement
|
|
315
402
|
* @returns Merged search result with additional entity-based matches
|
|
316
403
|
*/
|
|
317
|
-
async supplementEntitySearches(query, searchResult) {
|
|
404
|
+
async supplementEntitySearches(query, searchResult, scope) {
|
|
318
405
|
const entities = this.extractQueryEntities(query);
|
|
319
406
|
if (entities.length <= 1)
|
|
320
407
|
return searchResult;
|
|
321
408
|
try {
|
|
322
|
-
const entitySearches = await Promise.allSettled(entities.slice(0, 3).map((entity) => this.searchService.search(entity, { limit: 3 })));
|
|
409
|
+
const entitySearches = await Promise.allSettled(entities.slice(0, 3).map((entity) => this.searchService.search(entity, { limit: 3, scope })));
|
|
323
410
|
// Collect existing paths to deduplicate
|
|
324
|
-
const existingPaths = new Set(searchResult.results.map((r) => r.path));
|
|
411
|
+
const existingPaths = new Set(searchResult.results.map((r) => `${r.originAlias ?? r.origin ?? 'local'}::${r.path}`));
|
|
325
412
|
const supplementary = [];
|
|
326
413
|
for (const settled of entitySearches) {
|
|
327
414
|
if (settled.status === 'fulfilled' && settled.value.results) {
|
|
328
415
|
for (const result of settled.value.results) {
|
|
329
|
-
|
|
330
|
-
|
|
416
|
+
const resultKey = `${result.originAlias ?? result.origin ?? 'local'}::${result.path}`;
|
|
417
|
+
if (!existingPaths.has(resultKey)) {
|
|
418
|
+
existingPaths.add(resultKey);
|
|
331
419
|
supplementary.push(result);
|
|
332
420
|
}
|
|
333
421
|
}
|
|
@@ -363,14 +451,20 @@ ${responseFormat}`;
|
|
|
363
451
|
.map(async (result) => {
|
|
364
452
|
let content = result.excerpt;
|
|
365
453
|
try {
|
|
366
|
-
|
|
454
|
+
// Use originContextTreeRoot for shared results, local context tree for local
|
|
455
|
+
const ctBase = result.originContextTreeRoot ?? join(BRV_DIR, CONTEXT_TREE_DIR);
|
|
456
|
+
const ctPath = join(ctBase, result.path);
|
|
367
457
|
const { content: fullContent } = await this.fileSystem.readFile(ctPath);
|
|
368
458
|
content = fullContent;
|
|
369
459
|
}
|
|
370
460
|
catch {
|
|
371
461
|
// Use excerpt if full read fails
|
|
372
462
|
}
|
|
373
|
-
|
|
463
|
+
// Include source attribution in path for shared results
|
|
464
|
+
const displayPath = result.origin === 'shared' && result.originAlias
|
|
465
|
+
? `[${result.originAlias}]:${result.path}`
|
|
466
|
+
: result.path;
|
|
467
|
+
return { content, path: displayPath, score: result.score, title: result.title };
|
|
374
468
|
}));
|
|
375
469
|
if (!canRespondDirectly(fullResults))
|
|
376
470
|
return undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchExecutor - Executes context tree searches via SearchKnowledgeService.
|
|
3
|
+
*
|
|
4
|
+
* Unlike QueryExecutor (Tier 0-4 with LLM synthesis), SearchExecutor is
|
|
5
|
+
* pure retrieval: BM25 index lookup → scored results. No LLM, no agent
|
|
6
|
+
* session, no sandbox, no token cost.
|
|
7
|
+
*
|
|
8
|
+
* This is the engine behind `brv search`. The CLI command and transport
|
|
9
|
+
* layer handle I/O; this module handles the search logic.
|
|
10
|
+
*/
|
|
11
|
+
import type { ISearchKnowledgeService, SearchKnowledgeResult } from '../../../agent/infra/sandbox/tools-sdk.js';
|
|
12
|
+
import type { ISearchExecutor, SearchExecuteOptions } from '../../core/interfaces/executor/i-search-executor.js';
|
|
13
|
+
export declare class SearchExecutor implements ISearchExecutor {
|
|
14
|
+
private readonly searchService;
|
|
15
|
+
constructor(searchService: ISearchKnowledgeService);
|
|
16
|
+
execute(options: SearchExecuteOptions): Promise<SearchKnowledgeResult>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchExecutor - Executes context tree searches via SearchKnowledgeService.
|
|
3
|
+
*
|
|
4
|
+
* Unlike QueryExecutor (Tier 0-4 with LLM synthesis), SearchExecutor is
|
|
5
|
+
* pure retrieval: BM25 index lookup → scored results. No LLM, no agent
|
|
6
|
+
* session, no sandbox, no token cost.
|
|
7
|
+
*
|
|
8
|
+
* This is the engine behind `brv search`. The CLI command and transport
|
|
9
|
+
* layer handle I/O; this module handles the search logic.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_LIMIT = 10;
|
|
12
|
+
const MAX_LIMIT = 50;
|
|
13
|
+
export class SearchExecutor {
|
|
14
|
+
searchService;
|
|
15
|
+
constructor(searchService) {
|
|
16
|
+
this.searchService = searchService;
|
|
17
|
+
}
|
|
18
|
+
async execute(options) {
|
|
19
|
+
const query = options.query.trim();
|
|
20
|
+
if (!query) {
|
|
21
|
+
return { message: 'Empty query', results: [], totalFound: 0 };
|
|
22
|
+
}
|
|
23
|
+
const scope = options.scope?.trim() || undefined;
|
|
24
|
+
const limit = Math.min(MAX_LIMIT, Math.max(1, Math.trunc(options.limit ?? DEFAULT_LIMIT)));
|
|
25
|
+
return this.searchService.search(query, {
|
|
26
|
+
limit,
|
|
27
|
+
...(scope ? { scope } : {}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export type McpMode = 'global' | 'project';
|
|
10
10
|
export type McpModeResult = {
|
|
11
|
-
mode:
|
|
12
|
-
|
|
11
|
+
mode: 'global';
|
|
12
|
+
} | {
|
|
13
|
+
mode: 'project';
|
|
14
|
+
projectRoot: string;
|
|
15
|
+
worktreeRoot: string;
|
|
13
16
|
};
|
|
14
17
|
/**
|
|
15
18
|
* Detects whether the MCP server is running in project or global mode.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* If the filesystem root is reached without finding it, returns global mode.
|
|
20
|
+
* Uses the canonical project resolver so MCP shares workspace-link semantics
|
|
21
|
+
* with the rest of the CLI.
|
|
20
22
|
*/
|
|
21
23
|
export declare function detectMcpMode(workingDirectory: string): McpModeResult;
|
|
@@ -1,25 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
1
|
+
import { resolveProject } from '../project/resolve-project.js';
|
|
3
2
|
/**
|
|
4
3
|
* Detects whether the MCP server is running in project or global mode.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* If the filesystem root is reached without finding it, returns global mode.
|
|
5
|
+
* Uses the canonical project resolver so MCP shares workspace-link semantics
|
|
6
|
+
* with the rest of the CLI.
|
|
9
7
|
*/
|
|
10
8
|
export function detectMcpMode(workingDirectory) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (existsSync(join(current, '.brv', 'config.json'))) {
|
|
15
|
-
return { mode: 'project', projectRoot: current };
|
|
16
|
-
}
|
|
17
|
-
current = parent;
|
|
18
|
-
parent = dirname(current);
|
|
9
|
+
const resolution = resolveProject({ cwd: workingDirectory });
|
|
10
|
+
if (!resolution) {
|
|
11
|
+
return { mode: 'global' };
|
|
19
12
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
return {
|
|
14
|
+
mode: 'project',
|
|
15
|
+
projectRoot: resolution.projectRoot,
|
|
16
|
+
worktreeRoot: resolution.worktreeRoot,
|
|
17
|
+
};
|
|
25
18
|
}
|
|
@@ -29,11 +29,13 @@ export class ByteRoverMcpServer {
|
|
|
29
29
|
reconnectorHandle;
|
|
30
30
|
server;
|
|
31
31
|
transport;
|
|
32
|
+
worktreeRoot;
|
|
32
33
|
constructor(config) {
|
|
33
34
|
this.config = config;
|
|
34
|
-
const
|
|
35
|
-
this.mode = mode;
|
|
36
|
-
this.projectRoot = projectRoot;
|
|
35
|
+
const modeResult = detectMcpMode(config.workingDirectory);
|
|
36
|
+
this.mode = modeResult.mode;
|
|
37
|
+
this.projectRoot = modeResult.mode === 'project' ? modeResult.projectRoot : undefined;
|
|
38
|
+
this.worktreeRoot = modeResult.mode === 'project' ? modeResult.worktreeRoot : undefined;
|
|
37
39
|
this.server = new McpServer({
|
|
38
40
|
name: 'byterover',
|
|
39
41
|
version: config.version,
|
|
@@ -45,10 +47,13 @@ export class ByteRoverMcpServer {
|
|
|
45
47
|
version: config.version,
|
|
46
48
|
...(this.mode === 'project' && this.projectRoot ? { projectPath: this.projectRoot } : {}),
|
|
47
49
|
};
|
|
50
|
+
const getStartupProjectContext = () => this.mode === 'project' && this.projectRoot && this.worktreeRoot
|
|
51
|
+
? { projectRoot: this.projectRoot, worktreeRoot: this.worktreeRoot }
|
|
52
|
+
: undefined;
|
|
48
53
|
// Register tools with lazy client getter
|
|
49
54
|
// Client will be set when start() is called
|
|
50
|
-
registerBrvQueryTool(this.server, () => this.client, () => this.getWorkingDirectory());
|
|
51
|
-
registerBrvCurateTool(this.server, () => this.client, () => this.getWorkingDirectory());
|
|
55
|
+
registerBrvQueryTool(this.server, () => this.client, () => this.getWorkingDirectory(), getStartupProjectContext);
|
|
56
|
+
registerBrvCurateTool(this.server, () => this.client, () => this.getWorkingDirectory(), getStartupProjectContext);
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
59
|
* Starts the MCP server.
|
|
@@ -141,7 +146,7 @@ export class ByteRoverMcpServer {
|
|
|
141
146
|
* In global mode, returns undefined — each tool call must provide cwd.
|
|
142
147
|
*/
|
|
143
148
|
getWorkingDirectory() {
|
|
144
|
-
return this.mode === 'project' ? this.
|
|
149
|
+
return this.mode === 'project' ? this.worktreeRoot : undefined;
|
|
145
150
|
}
|
|
146
151
|
/**
|
|
147
152
|
* Log to stderr (stdout is reserved for MCP protocol).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ITransportClient } from '@campfirein/brv-transport-client';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { type McpStartupProjectContext } from './mcp-project-context.js';
|
|
4
5
|
export declare const BrvCurateInputSchema: z.ZodObject<{
|
|
5
6
|
context: z.ZodOptional<z.ZodString>;
|
|
6
7
|
cwd: z.ZodOptional<z.ZodString>;
|
|
@@ -26,4 +27,4 @@ export declare const BrvCurateInputSchema: z.ZodObject<{
|
|
|
26
27
|
* Uses fire-and-forget pattern: returns immediately after queueing the task.
|
|
27
28
|
* The curation is processed asynchronously by the ByteRover agent.
|
|
28
29
|
*/
|
|
29
|
-
export declare function registerBrvCurateTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined): void;
|
|
30
|
+
export declare function registerBrvCurateTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined, getStartupProjectContext: () => McpStartupProjectContext | undefined): void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { waitForConnectedClient } from '@campfirein/brv-transport-client';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
|
|
5
|
+
import { associateProjectWithRetry, resolveMcpTaskContext, } from './mcp-project-context.js';
|
|
6
6
|
import { resolveClientCwd } from './resolve-client-cwd.js';
|
|
7
7
|
export const BrvCurateInputSchema = z.object({
|
|
8
8
|
context: z
|
|
@@ -34,7 +34,7 @@ export const BrvCurateInputSchema = z.object({
|
|
|
34
34
|
* Uses fire-and-forget pattern: returns immediately after queueing the task.
|
|
35
35
|
* The curation is processed asynchronously by the ByteRover agent.
|
|
36
36
|
*/
|
|
37
|
-
export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
|
|
37
|
+
export function registerBrvCurateTool(server, getClient, getWorkingDirectory, getStartupProjectContext) {
|
|
38
38
|
server.registerTool('brv-curate', {
|
|
39
39
|
description: 'Store context to the ByteRover context tree. Save patterns, decisions, or insights. ' +
|
|
40
40
|
'Curation is processed asynchronously — the tool returns immediately after queueing.',
|
|
@@ -69,20 +69,11 @@ export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
|
|
|
69
69
|
isError: true,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
// In global mode, associate client with the walked-up project root.
|
|
73
|
-
// Walk up from clientCwd to find .brv/config.json — raw cwd may be a subdirectory.
|
|
74
|
-
// Fire-and-forget: server handler is idempotent (first association wins).
|
|
75
|
-
if (!getWorkingDirectory()) {
|
|
76
|
-
const { projectRoot } = detectMcpMode(cwdResult.clientCwd);
|
|
77
|
-
if (projectRoot) {
|
|
78
|
-
client
|
|
79
|
-
.requestWithAck(TransportClientEventNames.ASSOCIATE_PROJECT, {
|
|
80
|
-
projectPath: projectRoot,
|
|
81
|
-
})
|
|
82
|
-
.catch(() => { });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
72
|
try {
|
|
73
|
+
const taskContext = resolveMcpTaskContext(cwdResult.clientCwd, getStartupProjectContext());
|
|
74
|
+
if (!getWorkingDirectory()) {
|
|
75
|
+
await associateProjectWithRetry(client, taskContext.projectRoot);
|
|
76
|
+
}
|
|
86
77
|
const taskId = randomUUID();
|
|
87
78
|
// Create task via transport (same pattern as brv curate command)
|
|
88
79
|
// Use provided context, or empty string for file-only/folder-only mode
|
|
@@ -93,8 +84,10 @@ export function registerBrvCurateTool(server, getClient, getWorkingDirectory) {
|
|
|
93
84
|
const ack = await client.requestWithAck(TransportTaskEventNames.CREATE, {
|
|
94
85
|
clientCwd: cwdResult.clientCwd,
|
|
95
86
|
content: resolvedContent,
|
|
87
|
+
projectPath: taskContext.projectRoot,
|
|
96
88
|
taskId,
|
|
97
89
|
type: taskType,
|
|
90
|
+
worktreeRoot: taskContext.worktreeRoot,
|
|
98
91
|
...(hasFolder && folder ? { folderPath: folder } : {}),
|
|
99
92
|
...(!hasFolder && files?.length ? { files } : {}),
|
|
100
93
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ITransportClient } from '@campfirein/brv-transport-client';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { type McpStartupProjectContext } from './mcp-project-context.js';
|
|
4
5
|
export declare const BrvQueryInputSchema: z.ZodObject<{
|
|
5
6
|
cwd: z.ZodOptional<z.ZodString>;
|
|
6
7
|
query: z.ZodString;
|
|
@@ -17,4 +18,4 @@ export declare const BrvQueryInputSchema: z.ZodObject<{
|
|
|
17
18
|
* This tool allows coding agents to query the ByteRover context tree
|
|
18
19
|
* for patterns, decisions, implementation details, or any stored knowledge.
|
|
19
20
|
*/
|
|
20
|
-
export declare function registerBrvQueryTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined): void;
|
|
21
|
+
export declare function registerBrvQueryTool(server: McpServer, getClient: () => ITransportClient | undefined, getWorkingDirectory: () => string | undefined, getStartupProjectContext: () => McpStartupProjectContext | undefined): void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { waitForConnectedClient } from '@campfirein/brv-transport-client';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
|
|
5
|
+
import { associateProjectWithRetry, resolveMcpTaskContext, } from './mcp-project-context.js';
|
|
6
6
|
import { resolveClientCwd } from './resolve-client-cwd.js';
|
|
7
7
|
import { waitForTaskResult } from './task-result-waiter.js';
|
|
8
8
|
export const BrvQueryInputSchema = z.object({
|
|
@@ -20,7 +20,7 @@ export const BrvQueryInputSchema = z.object({
|
|
|
20
20
|
* This tool allows coding agents to query the ByteRover context tree
|
|
21
21
|
* for patterns, decisions, implementation details, or any stored knowledge.
|
|
22
22
|
*/
|
|
23
|
-
export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
|
|
23
|
+
export function registerBrvQueryTool(server, getClient, getWorkingDirectory, getStartupProjectContext) {
|
|
24
24
|
server.registerTool('brv-query', {
|
|
25
25
|
description: 'Query the ByteRover context tree for patterns, decisions, or implementation details.',
|
|
26
26
|
inputSchema: BrvQueryInputSchema,
|
|
@@ -47,20 +47,11 @@ export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
|
|
|
47
47
|
isError: true,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
// In global mode, associate client with the walked-up project root.
|
|
51
|
-
// Walk up from clientCwd to find .brv/config.json — raw cwd may be a subdirectory.
|
|
52
|
-
// Fire-and-forget: server handler is idempotent (first association wins).
|
|
53
|
-
if (!getWorkingDirectory()) {
|
|
54
|
-
const { projectRoot } = detectMcpMode(cwdResult.clientCwd);
|
|
55
|
-
if (projectRoot) {
|
|
56
|
-
client
|
|
57
|
-
.requestWithAck(TransportClientEventNames.ASSOCIATE_PROJECT, {
|
|
58
|
-
projectPath: projectRoot,
|
|
59
|
-
})
|
|
60
|
-
.catch(() => { });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
50
|
try {
|
|
51
|
+
const taskContext = resolveMcpTaskContext(cwdResult.clientCwd, getStartupProjectContext());
|
|
52
|
+
if (!getWorkingDirectory()) {
|
|
53
|
+
await associateProjectWithRetry(client, taskContext.projectRoot);
|
|
54
|
+
}
|
|
64
55
|
const taskId = randomUUID();
|
|
65
56
|
// Register event listeners BEFORE sending task:create to avoid race conditions.
|
|
66
57
|
// If the task completes before listeners are set up, the task:completed event is missed.
|
|
@@ -69,8 +60,10 @@ export function registerBrvQueryTool(server, getClient, getWorkingDirectory) {
|
|
|
69
60
|
await client.requestWithAck(TransportTaskEventNames.CREATE, {
|
|
70
61
|
clientCwd: cwdResult.clientCwd,
|
|
71
62
|
content: query,
|
|
63
|
+
projectPath: taskContext.projectRoot,
|
|
72
64
|
taskId,
|
|
73
65
|
type: 'query',
|
|
66
|
+
worktreeRoot: taskContext.worktreeRoot,
|
|
74
67
|
});
|
|
75
68
|
// Wait for the already-listening result promise
|
|
76
69
|
const result = await resultPromise;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ITransportClient } from '@campfirein/brv-transport-client';
|
|
2
|
+
export type McpStartupProjectContext = {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
worktreeRoot: string;
|
|
5
|
+
};
|
|
6
|
+
export type ResolvedMcpTaskContext = {
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
worktreeRoot: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function associateProjectWithRetry(client: ITransportClient, projectPath: string): Promise<void>;
|
|
11
|
+
export declare function resolveMcpTaskContext(clientCwd: string, startupProjectContext?: McpStartupProjectContext): ResolvedMcpTaskContext;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MCP_ASSOCIATE_PROJECT_MAX_ATTEMPTS, MCP_ASSOCIATE_PROJECT_TIMEOUT_MS } from '../../../constants.js';
|
|
2
|
+
import { TransportClientEventNames } from '../../../core/domain/transport/schemas.js';
|
|
3
|
+
import { resolveProject } from '../../project/resolve-project.js';
|
|
4
|
+
function describeError(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
8
|
+
let timeoutId;
|
|
9
|
+
try {
|
|
10
|
+
return await Promise.race([
|
|
11
|
+
promise,
|
|
12
|
+
new Promise((_, reject) => {
|
|
13
|
+
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
14
|
+
}),
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
if (timeoutId) {
|
|
19
|
+
clearTimeout(timeoutId);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function associateProjectWithRetry(client, projectPath) {
|
|
24
|
+
let lastError;
|
|
25
|
+
/* eslint-disable no-await-in-loop -- intentional sequential retry loop */
|
|
26
|
+
for (let attempt = 1; attempt <= MCP_ASSOCIATE_PROJECT_MAX_ATTEMPTS; attempt++) {
|
|
27
|
+
try {
|
|
28
|
+
await withTimeout(client.requestWithAck(TransportClientEventNames.ASSOCIATE_PROJECT, { projectPath }), MCP_ASSOCIATE_PROJECT_TIMEOUT_MS, `Timed out waiting for project association ack after ${MCP_ASSOCIATE_PROJECT_TIMEOUT_MS}ms`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
lastError = error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/* eslint-enable no-await-in-loop */
|
|
36
|
+
throw new Error(`Failed to associate MCP client with project "${projectPath}": ${describeError(lastError)}. ` +
|
|
37
|
+
`Retry the tool call or run 'brv restart' if the daemon is unresponsive.`);
|
|
38
|
+
}
|
|
39
|
+
export function resolveMcpTaskContext(clientCwd, startupProjectContext) {
|
|
40
|
+
const resolution = resolveProject({ cwd: clientCwd });
|
|
41
|
+
if (resolution) {
|
|
42
|
+
return projectResolutionToTaskContext(resolution);
|
|
43
|
+
}
|
|
44
|
+
if (startupProjectContext && clientCwd === startupProjectContext.worktreeRoot) {
|
|
45
|
+
return startupProjectContext;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`No ByteRover project could be resolved from cwd "${clientCwd}".`);
|
|
48
|
+
}
|
|
49
|
+
function projectResolutionToTaskContext(resolution) {
|
|
50
|
+
return {
|
|
51
|
+
projectRoot: resolution.projectRoot,
|
|
52
|
+
worktreeRoot: resolution.worktreeRoot,
|
|
53
|
+
};
|
|
54
|
+
}
|