byterover-cli 3.0.1 → 3.2.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 +4 -0
- package/README.md +17 -0
- package/dist/agent/core/domain/tools/constants.d.ts +1 -0
- package/dist/agent/core/domain/tools/constants.js +1 -0
- package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error-codes.js +0 -1
- package/dist/agent/infra/agent/agent-error.d.ts +0 -1
- package/dist/agent/infra/agent/agent-error.js +0 -1
- 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/agent/agent-state-manager.d.ts +1 -3
- package/dist/agent/infra/agent/agent-state-manager.js +1 -3
- package/dist/agent/infra/agent/base-agent.d.ts +1 -1
- package/dist/agent/infra/agent/base-agent.js +1 -1
- package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
- package/dist/agent/infra/agent/cipher-agent.js +188 -3
- package/dist/agent/infra/agent/index.d.ts +1 -1
- package/dist/agent/infra/agent/index.js +1 -1
- package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
- package/dist/agent/infra/agent/service-initializer.js +14 -8
- package/dist/agent/infra/agent/types.d.ts +0 -1
- package/dist/agent/infra/file-system/file-system-service.js +6 -5
- package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
- package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
- package/dist/agent/infra/llm/providers/openai.js +12 -0
- package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
- package/dist/agent/infra/llm/stream-to-text.js +14 -0
- package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
- package/dist/agent/infra/map/abstract-generator.js +67 -0
- package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
- package/dist/agent/infra/map/abstract-queue.js +218 -0
- package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
- package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
- package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
- package/dist/agent/infra/memory/memory-manager.js +6 -5
- package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
- package/dist/agent/infra/sandbox/curate-service.js +20 -7
- package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
- package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +13 -1
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/session/session-compressor.d.ts +43 -0
- package/dist/agent/infra/session/session-compressor.js +296 -0
- package/dist/agent/infra/session/session-manager.d.ts +7 -0
- package/dist/agent/infra/session/session-manager.js +9 -0
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
- package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
- package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
- package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +392 -106
- package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
- 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-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
- package/dist/agent/infra/tools/tool-registry.js +16 -5
- 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.js +4 -3
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.js +4 -3
- 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 +41 -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 -3
- package/dist/server/constants.d.ts +8 -0
- 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/knowledge/memory-scoring.d.ts +3 -3
- package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
- package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
- 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 +4 -0
- 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/infra/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/context-tree/derived-artifact.js +5 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
- package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
- package/dist/server/infra/daemon/agent-process.js +15 -5
- package/dist/server/infra/executor/curate-executor.js +6 -3
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +88 -7
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +125 -23
- 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/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 +65 -13
- 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/utils/curate-result-parser.d.ts +4 -4
- 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/types/dto.d.ts +26 -0
- 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 +23 -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/oclif.manifest.json +261 -1
- package/package.json +10 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR } from '../../constants.js';
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
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) {
|
|
@@ -124,7 +126,7 @@ export class QueryExecutor {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
// Create per-task session for parallel isolation (own sandbox + history + LLM service)
|
|
127
|
-
const taskSessionId = await agent.createTaskSession(taskId, 'query');
|
|
129
|
+
const taskSessionId = await agent.createTaskSession(taskId, 'query', { userFacing: true });
|
|
128
130
|
// Task-scoped variable names for sandbox injection (RLM pattern).
|
|
129
131
|
// Replace hyphens with underscores: UUIDs have hyphens which are invalid in JS identifiers,
|
|
130
132
|
// so the LLM uses underscores when writing code-exec calls — matching curate-executor pattern.
|
|
@@ -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,12 +294,50 @@ ${responseFormat}`;
|
|
|
271
294
|
.filter((f) => !isDerivedArtifact(f.path))
|
|
272
295
|
.map((f) => ({
|
|
273
296
|
mtime: f.modified?.getTime() ?? 0,
|
|
297
|
+
path: worktreeRoot ? `${worktreeRoot}:${f.path}` : f.path,
|
|
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
|
+
}
|
|
327
|
+
// Include .abstract.md siblings so newly-written abstracts invalidate the cache.
|
|
328
|
+
// They are excluded by isDerivedArtifact() above, so we append them separately.
|
|
329
|
+
const abstractFiles = globResult.files
|
|
330
|
+
.filter((f) => f.path.endsWith(ABSTRACT_EXTENSION))
|
|
331
|
+
.map((f) => ({
|
|
332
|
+
mtime: f.modified?.getTime() ?? 0,
|
|
274
333
|
path: f.path,
|
|
275
334
|
}));
|
|
276
|
-
const fingerprint = QueryResultCache.computeFingerprint(files);
|
|
335
|
+
const fingerprint = QueryResultCache.computeFingerprint([...files, ...abstractFiles]);
|
|
277
336
|
this.cachedFingerprint = {
|
|
278
337
|
expiresAt: Date.now() + QueryExecutor.FINGERPRINT_CACHE_TTL_MS,
|
|
338
|
+
sourceValidityHash: this.computeSourceValidityHash(),
|
|
279
339
|
value: fingerprint,
|
|
340
|
+
worktreeRoot,
|
|
280
341
|
};
|
|
281
342
|
return fingerprint;
|
|
282
343
|
}
|
|
@@ -284,6 +345,40 @@ ${responseFormat}`;
|
|
|
284
345
|
return 'unknown';
|
|
285
346
|
}
|
|
286
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
|
+
}
|
|
287
382
|
/**
|
|
288
383
|
* Extract key entities from a query for supplementary searches.
|
|
289
384
|
* Simple heuristic: split query, filter stopwords, keep significant terms.
|
|
@@ -306,20 +401,21 @@ ${responseFormat}`;
|
|
|
306
401
|
* @param searchResult - Initial search result to supplement
|
|
307
402
|
* @returns Merged search result with additional entity-based matches
|
|
308
403
|
*/
|
|
309
|
-
async supplementEntitySearches(query, searchResult) {
|
|
404
|
+
async supplementEntitySearches(query, searchResult, scope) {
|
|
310
405
|
const entities = this.extractQueryEntities(query);
|
|
311
406
|
if (entities.length <= 1)
|
|
312
407
|
return searchResult;
|
|
313
408
|
try {
|
|
314
|
-
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 })));
|
|
315
410
|
// Collect existing paths to deduplicate
|
|
316
|
-
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}`));
|
|
317
412
|
const supplementary = [];
|
|
318
413
|
for (const settled of entitySearches) {
|
|
319
414
|
if (settled.status === 'fulfilled' && settled.value.results) {
|
|
320
415
|
for (const result of settled.value.results) {
|
|
321
|
-
|
|
322
|
-
|
|
416
|
+
const resultKey = `${result.originAlias ?? result.origin ?? 'local'}::${result.path}`;
|
|
417
|
+
if (!existingPaths.has(resultKey)) {
|
|
418
|
+
existingPaths.add(resultKey);
|
|
323
419
|
supplementary.push(result);
|
|
324
420
|
}
|
|
325
421
|
}
|
|
@@ -355,14 +451,20 @@ ${responseFormat}`;
|
|
|
355
451
|
.map(async (result) => {
|
|
356
452
|
let content = result.excerpt;
|
|
357
453
|
try {
|
|
358
|
-
|
|
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);
|
|
359
457
|
const { content: fullContent } = await this.fileSystem.readFile(ctPath);
|
|
360
458
|
content = fullContent;
|
|
361
459
|
}
|
|
362
460
|
catch {
|
|
363
461
|
// Use excerpt if full read fails
|
|
364
462
|
}
|
|
365
|
-
|
|
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 };
|
|
366
468
|
}));
|
|
367
469
|
if (!canRespondDirectly(fullResults))
|
|
368
470
|
return undefined;
|
|
@@ -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
|
+
}
|
|
@@ -154,6 +154,17 @@ export class ConnectionCoordinator {
|
|
|
154
154
|
return { error: 'Client not registered', success: false };
|
|
155
155
|
}
|
|
156
156
|
if (client.hasProject) {
|
|
157
|
+
// Idempotent: same path → no-op
|
|
158
|
+
if (client.projectPath === data.projectPath) {
|
|
159
|
+
return { success: true };
|
|
160
|
+
}
|
|
161
|
+
// Reassociation: path changed (e.g. after worktree add/remove)
|
|
162
|
+
const oldPath = this.clientManager.updateProjectPath(clientId, data.projectPath);
|
|
163
|
+
transportLog(`Client ${clientId} reassociated: ${oldPath} → ${data.projectPath}`);
|
|
164
|
+
if (oldPath) {
|
|
165
|
+
this.removeFromProjectRoom(clientId, oldPath);
|
|
166
|
+
}
|
|
167
|
+
this.addToProjectRoom(clientId, data.projectPath);
|
|
157
168
|
return { success: true };
|
|
158
169
|
}
|
|
159
170
|
this.clientManager.associateProject(clientId, data.projectPath);
|
|
@@ -36,7 +36,7 @@ import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
|
|
|
36
36
|
import { createTokenStore } from '../storage/token-store.js';
|
|
37
37
|
import { HttpTeamService } from '../team/http-team-service.js';
|
|
38
38
|
import { FsTemplateLoader } from '../template/fs-template-loader.js';
|
|
39
|
-
import { AuthHandler, ConfigHandler, ConnectorsHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SpaceHandler, StatusHandler, VcHandler, } from '../transport/handlers/index.js';
|
|
39
|
+
import { AuthHandler, ConfigHandler, ConnectorsHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SourceHandler, SpaceHandler, StatusHandler, VcHandler, WorktreeHandler, } from '../transport/handlers/index.js';
|
|
40
40
|
import { HttpUserService } from '../user/http-user-service.js';
|
|
41
41
|
import { FileVcGitConfigStore } from '../vc/file-vc-git-config-store.js';
|
|
42
42
|
/**
|
|
@@ -211,5 +211,8 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
211
211
|
vcGitConfigStore: new FileVcGitConfigStore(),
|
|
212
212
|
webAppUrl: envConfig.webAppUrl,
|
|
213
213
|
}).setup();
|
|
214
|
+
// Worktree & source handlers
|
|
215
|
+
new WorktreeHandler({ resolveProjectPath, transport }).setup();
|
|
216
|
+
new SourceHandler({ resolveProjectPath, transport }).setup();
|
|
214
217
|
log('Feature handlers registered');
|
|
215
218
|
}
|