byterover-cli 3.5.0 → 3.6.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 -6
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/cipher-agent.js +1 -0
- package/dist/oclif/commands/curate/view.js +5 -25
- package/dist/oclif/commands/dream.d.ts +18 -0
- package/dist/oclif/commands/dream.js +230 -0
- package/dist/oclif/commands/query-log/summary.d.ts +18 -0
- package/dist/oclif/commands/query-log/summary.js +75 -0
- package/dist/oclif/commands/query-log/view.d.ts +23 -0
- package/dist/oclif/commands/query-log/view.js +95 -0
- package/dist/oclif/lib/time-filter.d.ts +10 -0
- package/dist/oclif/lib/time-filter.js +21 -0
- package/dist/server/config/environment.d.ts +10 -3
- package/dist/server/config/environment.js +34 -15
- package/dist/server/constants.d.ts +5 -0
- package/dist/server/constants.js +7 -0
- package/dist/server/core/domain/entities/query-log-entry.d.ts +61 -0
- package/dist/server/core/domain/entities/query-log-entry.js +40 -0
- package/dist/server/core/domain/transport/schemas.d.ts +108 -7
- package/dist/server/core/domain/transport/schemas.js +34 -2
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +23 -2
- package/dist/server/core/interfaces/i-terminal.d.ts +3 -0
- package/dist/server/core/interfaces/i-terminal.js +1 -0
- package/dist/server/core/interfaces/storage/i-query-log-store.d.ts +23 -0
- package/dist/server/core/interfaces/storage/i-query-log-store.js +2 -0
- package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.d.ts +44 -0
- package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.js +1 -0
- package/dist/server/core/interfaces/usecase/i-query-log-use-case.d.ts +13 -0
- package/dist/server/core/interfaces/usecase/i-query-log-use-case.js +3 -0
- package/dist/server/infra/daemon/agent-process.js +79 -9
- package/dist/server/infra/daemon/brv-server.js +74 -5
- package/dist/server/infra/dream/dream-lock-service.d.ts +37 -0
- package/dist/server/infra/dream/dream-lock-service.js +88 -0
- package/dist/server/infra/dream/dream-log-schema.d.ts +966 -0
- package/dist/server/infra/dream/dream-log-schema.js +57 -0
- package/dist/server/infra/dream/dream-log-store.d.ts +55 -0
- package/dist/server/infra/dream/dream-log-store.js +141 -0
- package/dist/server/infra/dream/dream-response-schemas.d.ts +219 -0
- package/dist/server/infra/dream/dream-response-schemas.js +38 -0
- package/dist/server/infra/dream/dream-state-schema.d.ts +67 -0
- package/dist/server/infra/dream/dream-state-schema.js +23 -0
- package/dist/server/infra/dream/dream-state-service.d.ts +38 -0
- package/dist/server/infra/dream/dream-state-service.js +91 -0
- package/dist/server/infra/dream/dream-trigger.d.ts +46 -0
- package/dist/server/infra/dream/dream-trigger.js +65 -0
- package/dist/server/infra/dream/dream-undo.d.ts +38 -0
- package/dist/server/infra/dream/dream-undo.js +293 -0
- package/dist/server/infra/dream/operations/consolidate.d.ts +52 -0
- package/dist/server/infra/dream/operations/consolidate.js +514 -0
- package/dist/server/infra/dream/operations/prune.d.ts +45 -0
- package/dist/server/infra/dream/operations/prune.js +362 -0
- package/dist/server/infra/dream/operations/synthesize.d.ts +37 -0
- package/dist/server/infra/dream/operations/synthesize.js +278 -0
- package/dist/server/infra/dream/parse-dream-response.d.ts +11 -0
- package/dist/server/infra/dream/parse-dream-response.js +35 -0
- package/dist/server/infra/executor/curate-executor.js +10 -0
- package/dist/server/infra/executor/dream-executor.d.ts +97 -0
- package/dist/server/infra/executor/dream-executor.js +431 -0
- package/dist/server/infra/executor/query-executor.d.ts +2 -2
- package/dist/server/infra/executor/query-executor.js +92 -22
- package/dist/server/infra/mcp/mcp-server.js +3 -0
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +3 -7
- package/dist/server/infra/mcp/tools/brv-query-tool.js +3 -7
- package/dist/server/infra/mcp/tools/index.d.ts +1 -0
- package/dist/server/infra/mcp/tools/index.js +1 -0
- package/dist/server/infra/mcp/tools/shared-schema.d.ts +3 -0
- package/dist/server/infra/mcp/tools/shared-schema.js +17 -0
- package/dist/server/infra/process/feature-handlers.js +10 -6
- package/dist/server/infra/process/query-log-handler.d.ts +42 -0
- package/dist/server/infra/process/query-log-handler.js +150 -0
- package/dist/server/infra/process/task-router.d.ts +40 -0
- package/dist/server/infra/process/task-router.js +67 -9
- package/dist/server/infra/process/transport-handlers.d.ts +4 -0
- package/dist/server/infra/process/transport-handlers.js +1 -0
- package/dist/server/infra/storage/file-curate-log-store.js +1 -1
- package/dist/server/infra/storage/file-query-log-store.d.ts +81 -0
- package/dist/server/infra/storage/file-query-log-store.js +249 -0
- package/dist/server/infra/transport/handlers/config-handler.js +1 -1
- package/dist/server/infra/usecase/curate-log-use-case.js +7 -3
- package/dist/server/infra/usecase/query-log-summary-narrative-formatter.d.ts +15 -0
- package/dist/server/infra/usecase/query-log-summary-narrative-formatter.js +79 -0
- package/dist/server/infra/usecase/query-log-summary-use-case.d.ts +13 -0
- package/dist/server/infra/usecase/query-log-summary-use-case.js +217 -0
- package/dist/server/infra/usecase/query-log-use-case.d.ts +31 -0
- package/dist/server/infra/usecase/query-log-use-case.js +128 -0
- package/dist/server/utils/log-format-utils.d.ts +5 -0
- package/dist/server/utils/log-format-utils.js +23 -0
- package/dist/shared/transport/events/config-events.d.ts +1 -1
- package/oclif.manifest.json +439 -184
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
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 { TIER_DIRECT_SEARCH, TIER_EXACT_CACHE, TIER_FULL_AGENTIC, TIER_FUZZY_CACHE, TIER_OPTIMIZED_LLM, } from '../../core/domain/entities/query-log-entry.js';
|
|
3
4
|
import { loadSources } from '../../core/domain/source/source-schema.js';
|
|
4
5
|
import { isDerivedArtifact } from '../context-tree/derived-artifact.js';
|
|
5
6
|
import { FileContextTreeManifestService } from '../context-tree/file-context-tree-manifest-service.js';
|
|
@@ -7,6 +8,10 @@ import { canRespondDirectly, formatDirectResponse, formatNotFoundResponse, } fro
|
|
|
7
8
|
import { QueryResultCache } from './query-result-cache.js';
|
|
8
9
|
/** Attribution footer appended to all query responses */
|
|
9
10
|
const ATTRIBUTION_FOOTER = '\n\n---\nSource: ByteRover Knowledge Base';
|
|
11
|
+
/** Map search results to the matchedDocs shape for QueryExecutorResult. */
|
|
12
|
+
function buildMatchedDocs(sr) {
|
|
13
|
+
return (sr?.results ?? []).map((r) => ({ path: r.path, score: r.score, title: r.title }));
|
|
14
|
+
}
|
|
10
15
|
/** Minimum normalized score to consider a result high-confidence for pre-fetching */
|
|
11
16
|
const SMART_ROUTING_SCORE_THRESHOLD = 0.7;
|
|
12
17
|
/** Maximum number of documents to pre-fetch and inject into the prompt */
|
|
@@ -46,6 +51,7 @@ export class QueryExecutor {
|
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
async executeWithAgent(agent, options) {
|
|
54
|
+
const startTime = Date.now();
|
|
49
55
|
const { query, taskId, worktreeRoot } = options;
|
|
50
56
|
const workspaceScope = this.deriveWorkspaceScope(worktreeRoot);
|
|
51
57
|
// Start search early — runs in parallel with fingerprint computation (independent operations)
|
|
@@ -58,14 +64,24 @@ export class QueryExecutor {
|
|
|
58
64
|
fingerprint = await this.computeContextTreeFingerprint(worktreeRoot);
|
|
59
65
|
const cached = this.cache.get(query, fingerprint);
|
|
60
66
|
if (cached) {
|
|
61
|
-
return
|
|
67
|
+
return {
|
|
68
|
+
matchedDocs: [],
|
|
69
|
+
response: cached + ATTRIBUTION_FOOTER,
|
|
70
|
+
tier: TIER_EXACT_CACHE,
|
|
71
|
+
timing: { durationMs: Date.now() - startTime },
|
|
72
|
+
};
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
// === Tier 1: Fuzzy cache match (~50ms) ===
|
|
65
76
|
if (this.cache && fingerprint) {
|
|
66
77
|
const fuzzyHit = this.cache.findSimilar(query, fingerprint);
|
|
67
78
|
if (fuzzyHit) {
|
|
68
|
-
return
|
|
79
|
+
return {
|
|
80
|
+
matchedDocs: [],
|
|
81
|
+
response: fuzzyHit + ATTRIBUTION_FOOTER,
|
|
82
|
+
tier: TIER_FUZZY_CACHE,
|
|
83
|
+
timing: { durationMs: Date.now() - startTime },
|
|
84
|
+
};
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
87
|
// Await search result (already started in parallel with fingerprint computation)
|
|
@@ -86,7 +102,13 @@ export class QueryExecutor {
|
|
|
86
102
|
if (this.cache && fingerprint) {
|
|
87
103
|
this.cache.set(query, response, fingerprint);
|
|
88
104
|
}
|
|
89
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
matchedDocs: [],
|
|
107
|
+
response: response + ATTRIBUTION_FOOTER,
|
|
108
|
+
searchMetadata: { resultCount: 0, topScore: 0, totalFound: 0 },
|
|
109
|
+
tier: TIER_DIRECT_SEARCH,
|
|
110
|
+
timing: { durationMs: Date.now() - startTime },
|
|
111
|
+
};
|
|
90
112
|
}
|
|
91
113
|
// === Tier 2: Direct search response (~100-200ms) ===
|
|
92
114
|
if (searchResult && this.fileSystem) {
|
|
@@ -95,7 +117,18 @@ export class QueryExecutor {
|
|
|
95
117
|
if (this.cache && fingerprint) {
|
|
96
118
|
this.cache.set(query, directResult, fingerprint);
|
|
97
119
|
}
|
|
98
|
-
return
|
|
120
|
+
return {
|
|
121
|
+
matchedDocs: buildMatchedDocs(searchResult),
|
|
122
|
+
response: directResult + ATTRIBUTION_FOOTER,
|
|
123
|
+
searchMetadata: {
|
|
124
|
+
cacheFingerprint: fingerprint,
|
|
125
|
+
resultCount: searchResult.results.length,
|
|
126
|
+
topScore: searchResult.results[0]?.score ?? 0,
|
|
127
|
+
totalFound: searchResult.totalFound,
|
|
128
|
+
},
|
|
129
|
+
tier: TIER_DIRECT_SEARCH,
|
|
130
|
+
timing: { durationMs: Date.now() - startTime },
|
|
131
|
+
};
|
|
99
132
|
}
|
|
100
133
|
}
|
|
101
134
|
// === Tier 3/4: LLM call with RLM pattern (variable-based search results) ===
|
|
@@ -115,9 +148,7 @@ export class QueryExecutor {
|
|
|
115
148
|
if (manifest) {
|
|
116
149
|
const resolved = await manifestService.resolveForInjection(manifest, query, this.baseDirectory);
|
|
117
150
|
if (resolved.length > 0) {
|
|
118
|
-
manifestContext = resolved
|
|
119
|
-
.map((e) => `[${e.type} ${e.path}]\n${e.content}`)
|
|
120
|
-
.join('\n\n---\n\n');
|
|
151
|
+
manifestContext = resolved.map((e) => `[${e.type} ${e.path}]\n${e.content}`).join('\n\n---\n\n');
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
}
|
|
@@ -169,7 +200,19 @@ export class QueryExecutor {
|
|
|
169
200
|
if (this.cache && fingerprint) {
|
|
170
201
|
this.cache.set(query, response, fingerprint);
|
|
171
202
|
}
|
|
172
|
-
|
|
203
|
+
const tier = prefetchedContext ? TIER_OPTIMIZED_LLM : TIER_FULL_AGENTIC;
|
|
204
|
+
return {
|
|
205
|
+
matchedDocs: buildMatchedDocs(searchResult),
|
|
206
|
+
response: response + ATTRIBUTION_FOOTER,
|
|
207
|
+
searchMetadata: {
|
|
208
|
+
cacheFingerprint: fingerprint,
|
|
209
|
+
resultCount: searchResult?.results.length ?? 0,
|
|
210
|
+
topScore: searchResult?.results[0]?.score ?? 0,
|
|
211
|
+
totalFound: searchResult?.totalFound ?? 0,
|
|
212
|
+
},
|
|
213
|
+
tier,
|
|
214
|
+
timing: { durationMs: Date.now() - startTime },
|
|
215
|
+
};
|
|
173
216
|
}
|
|
174
217
|
finally {
|
|
175
218
|
// Clean up entire task session (sandbox + history) in one call
|
|
@@ -188,9 +231,7 @@ export class QueryExecutor {
|
|
|
188
231
|
if (highConfidenceResults.length === 0)
|
|
189
232
|
return undefined;
|
|
190
233
|
const sections = highConfidenceResults.map((r) => {
|
|
191
|
-
const source = r.origin === 'shared' && r.originAlias
|
|
192
|
-
? `[${r.originAlias}]:${r.path}`
|
|
193
|
-
: `.brv/context-tree/${r.path}`;
|
|
234
|
+
const source = r.origin === 'shared' && r.originAlias ? `[${r.originAlias}]:${r.path}` : `.brv/context-tree/${r.path}`;
|
|
194
235
|
return `### ${r.title}\n**Source**: ${source}\n\n${r.excerpt}`;
|
|
195
236
|
});
|
|
196
237
|
return sections.join('\n\n---\n\n');
|
|
@@ -274,9 +315,10 @@ ${responseFormat}`;
|
|
|
274
315
|
async computeContextTreeFingerprint(worktreeRoot) {
|
|
275
316
|
// Fast path: return cached fingerprint if still valid (avoids globFiles I/O)
|
|
276
317
|
// Invalidate if worktreeRoot changed or knowledge source validity changed
|
|
277
|
-
if (this.cachedFingerprint &&
|
|
278
|
-
|
|
279
|
-
|
|
318
|
+
if (this.cachedFingerprint &&
|
|
319
|
+
Date.now() < this.cachedFingerprint.expiresAt &&
|
|
320
|
+
this.cachedFingerprint.worktreeRoot === worktreeRoot &&
|
|
321
|
+
this.cachedFingerprint.sourceValidityHash === this.computeSourceValidityHash()) {
|
|
280
322
|
return this.cachedFingerprint.value;
|
|
281
323
|
}
|
|
282
324
|
try {
|
|
@@ -298,7 +340,7 @@ ${responseFormat}`;
|
|
|
298
340
|
}));
|
|
299
341
|
// Include shared source state in fingerprint so edits in shared
|
|
300
342
|
// projects invalidate cached query answers.
|
|
301
|
-
const loaded = this.baseDirectory ? loadSources(this.baseDirectory) :
|
|
343
|
+
const loaded = this.baseDirectory ? loadSources(this.baseDirectory) : undefined;
|
|
302
344
|
if (loaded) {
|
|
303
345
|
// sources-file mtime detects source additions/removals
|
|
304
346
|
if (loaded.mtime) {
|
|
@@ -357,7 +399,10 @@ ${responseFormat}`;
|
|
|
357
399
|
const loaded = loadSources(this.baseDirectory);
|
|
358
400
|
if (!loaded)
|
|
359
401
|
return 'no-sources';
|
|
360
|
-
return loaded.origins
|
|
402
|
+
return loaded.origins
|
|
403
|
+
.map((o) => o.originKey)
|
|
404
|
+
.sort()
|
|
405
|
+
.join(',');
|
|
361
406
|
}
|
|
362
407
|
/**
|
|
363
408
|
* Derive a workspace scope for search from the worktreeRoot.
|
|
@@ -385,9 +430,36 @@ ${responseFormat}`;
|
|
|
385
430
|
*/
|
|
386
431
|
extractQueryEntities(query) {
|
|
387
432
|
const stopwords = new Set([
|
|
388
|
-
'a',
|
|
389
|
-
'
|
|
390
|
-
'
|
|
433
|
+
'a',
|
|
434
|
+
'about',
|
|
435
|
+
'an',
|
|
436
|
+
'and',
|
|
437
|
+
'by',
|
|
438
|
+
'did',
|
|
439
|
+
'do',
|
|
440
|
+
'does',
|
|
441
|
+
'for',
|
|
442
|
+
'from',
|
|
443
|
+
'how',
|
|
444
|
+
'in',
|
|
445
|
+
'is',
|
|
446
|
+
'my',
|
|
447
|
+
'of',
|
|
448
|
+
'or',
|
|
449
|
+
'our',
|
|
450
|
+
'that',
|
|
451
|
+
'the',
|
|
452
|
+
'their',
|
|
453
|
+
'this',
|
|
454
|
+
'to',
|
|
455
|
+
'was',
|
|
456
|
+
'were',
|
|
457
|
+
'what',
|
|
458
|
+
'when',
|
|
459
|
+
'where',
|
|
460
|
+
'which',
|
|
461
|
+
'who',
|
|
462
|
+
'with',
|
|
391
463
|
]);
|
|
392
464
|
const words = query.toLowerCase().split(/\s+/);
|
|
393
465
|
return words.filter((w) => w.length >= 3 && !stopwords.has(w));
|
|
@@ -461,9 +533,7 @@ ${responseFormat}`;
|
|
|
461
533
|
// Use excerpt if full read fails
|
|
462
534
|
}
|
|
463
535
|
// Include source attribution in path for shared results
|
|
464
|
-
const displayPath = result.origin === 'shared' && result.originAlias
|
|
465
|
-
? `[${result.originAlias}]:${result.path}`
|
|
466
|
-
: result.path;
|
|
536
|
+
const displayPath = result.origin === 'shared' && result.originAlias ? `[${result.originAlias}]:${result.path}` : result.path;
|
|
467
537
|
return { content, path: displayPath, score: result.score, title: result.title };
|
|
468
538
|
}));
|
|
469
539
|
if (!canRespondDirectly(fullResults))
|
|
@@ -39,6 +39,9 @@ export class ByteRoverMcpServer {
|
|
|
39
39
|
this.server = new McpServer({
|
|
40
40
|
name: 'byterover',
|
|
41
41
|
version: config.version,
|
|
42
|
+
}, {
|
|
43
|
+
instructions: 'ByteRover MCP — curate and query project context trees. ' +
|
|
44
|
+
'See the `cwd` parameter description on each tool for how to provide the project path correctly.',
|
|
42
45
|
});
|
|
43
46
|
this.connectOptions = {
|
|
44
47
|
clientType: 'mcp',
|
|
@@ -2,19 +2,15 @@ import { waitForConnectedClient } from '@campfirein/brv-transport-client';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
|
|
5
|
-
import { associateProjectWithRetry, resolveMcpTaskContext
|
|
5
|
+
import { associateProjectWithRetry, resolveMcpTaskContext } from './mcp-project-context.js';
|
|
6
6
|
import { resolveClientCwd } from './resolve-client-cwd.js';
|
|
7
|
+
import { cwdField } from './shared-schema.js';
|
|
7
8
|
export const BrvCurateInputSchema = z.object({
|
|
8
9
|
context: z
|
|
9
10
|
.string()
|
|
10
11
|
.optional()
|
|
11
12
|
.describe('Knowledge to store: patterns, decisions, errors, or insights about the codebase. Required unless files or folder are provided.'),
|
|
12
|
-
cwd:
|
|
13
|
-
.string()
|
|
14
|
-
.optional()
|
|
15
|
-
.describe('Working directory of the project (absolute path). ' +
|
|
16
|
-
'Required when the MCP server runs in global mode (e.g., Windsurf). ' +
|
|
17
|
-
'Optional in project mode — defaults to the project directory.'),
|
|
13
|
+
cwd: cwdField,
|
|
18
14
|
files: z
|
|
19
15
|
.array(z.string())
|
|
20
16
|
.max(5)
|
|
@@ -2,16 +2,12 @@ import { waitForConnectedClient } from '@campfirein/brv-transport-client';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { TransportTaskEventNames } from '../../../core/domain/transport/schemas.js';
|
|
5
|
-
import { associateProjectWithRetry, resolveMcpTaskContext
|
|
5
|
+
import { associateProjectWithRetry, resolveMcpTaskContext } from './mcp-project-context.js';
|
|
6
6
|
import { resolveClientCwd } from './resolve-client-cwd.js';
|
|
7
|
+
import { cwdField } from './shared-schema.js';
|
|
7
8
|
import { waitForTaskResult } from './task-result-waiter.js';
|
|
8
9
|
export const BrvQueryInputSchema = z.object({
|
|
9
|
-
cwd:
|
|
10
|
-
.string()
|
|
11
|
-
.optional()
|
|
12
|
-
.describe('Working directory of the project (absolute path). ' +
|
|
13
|
-
'Required when the MCP server runs in global mode (e.g., Windsurf). ' +
|
|
14
|
-
'Optional in project mode — defaults to the project directory.'),
|
|
10
|
+
cwd: cwdField,
|
|
15
11
|
query: z.string().describe('Natural language question about the codebase or project'),
|
|
16
12
|
});
|
|
17
13
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { registerBrvCurateTool } from './brv-curate-tool.js';
|
|
2
2
|
export { registerBrvQueryTool } from './brv-query-tool.js';
|
|
3
3
|
export { resolveClientCwd } from './resolve-client-cwd.js';
|
|
4
|
+
export { CWD_DESCRIPTION, cwdField } from './shared-schema.js';
|
|
4
5
|
export { waitForTaskResult } from './task-result-waiter.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { registerBrvCurateTool } from './brv-curate-tool.js';
|
|
2
2
|
export { registerBrvQueryTool } from './brv-query-tool.js';
|
|
3
3
|
export { resolveClientCwd } from './resolve-client-cwd.js';
|
|
4
|
+
export { CWD_DESCRIPTION, cwdField } from './shared-schema.js';
|
|
4
5
|
export { waitForTaskResult } from './task-result-waiter.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const CWD_DESCRIPTION = 'Absolute path to the project root — selects which ByteRover context tree to use ' +
|
|
3
|
+
String.raw `(e.g., "/Users/me/code/myapp", "C:\\code\\myapp").` +
|
|
4
|
+
'\n\n' +
|
|
5
|
+
'When to provide:\n' +
|
|
6
|
+
'- If your runtime does NOT expose any workspace/project context to you ' +
|
|
7
|
+
'(e.g., Claude Desktop, hosted MCP, global Windsurf): you MUST provide cwd. ' +
|
|
8
|
+
'Use the path the user mentions, or ASK the user for the absolute path if unknown.\n' +
|
|
9
|
+
'- If your runtime DOES expose an open workspace/project root to you ' +
|
|
10
|
+
'(e.g., Cursor, Cline, Zed, Claude Code): you can OMIT this field — ' +
|
|
11
|
+
'the MCP server was launched from that same project and already knows the cwd. ' +
|
|
12
|
+
'Providing it is harmless but unnecessary.\n' +
|
|
13
|
+
'\n' +
|
|
14
|
+
'If you omit cwd and the tool returns an error about the project not being resolved or cwd being required, retry with cwd explicitly set to the project root path.\n' +
|
|
15
|
+
'\n' +
|
|
16
|
+
'Never guess, never invent paths, never use relative paths.';
|
|
17
|
+
export const cwdField = z.string().optional().describe(CWD_DESCRIPTION);
|
|
@@ -9,7 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { ReviewEvents } from '../../../shared/transport/events/review-events.js';
|
|
10
10
|
import { getAuthConfig } from '../../config/auth.config.js';
|
|
11
11
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
12
|
-
import { BRV_DIR } from '../../constants.js';
|
|
12
|
+
import { API_V1_PATH, BRV_DIR } from '../../constants.js';
|
|
13
13
|
import { getProjectDataDir } from '../../utils/path-utils.js';
|
|
14
14
|
import { OAuthService } from '../auth/oauth-service.js';
|
|
15
15
|
import { OidcDiscoveryService } from '../auth/oidc-discovery-service.js';
|
|
@@ -48,9 +48,12 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
48
48
|
const envConfig = getCurrentConfig();
|
|
49
49
|
const tokenStore = createTokenStore();
|
|
50
50
|
const projectConfigStore = new ProjectConfigStore();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
51
|
+
// API version paths appended at point of use.
|
|
52
|
+
// Note: IAM and Cogit currently share this version path, but may version independently in the future.
|
|
53
|
+
const iamApiV1 = `${envConfig.iamBaseUrl}${API_V1_PATH}`;
|
|
54
|
+
const userService = new HttpUserService({ apiBaseUrl: iamApiV1 });
|
|
55
|
+
const teamService = new HttpTeamService({ apiBaseUrl: iamApiV1 });
|
|
56
|
+
const spaceService = new HttpSpaceService({ apiBaseUrl: iamApiV1 });
|
|
54
57
|
// Auth handler requires async OIDC discovery
|
|
55
58
|
const discoveryService = new OidcDiscoveryService();
|
|
56
59
|
const authConfig = await getAuthConfig(discoveryService);
|
|
@@ -86,8 +89,9 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
86
89
|
const contextTreeWriterService = new FileContextTreeWriterService({ snapshotService: contextTreeSnapshotService });
|
|
87
90
|
const contextTreeMerger = new FileContextTreeMerger({ snapshotService: contextTreeSnapshotService });
|
|
88
91
|
const contextFileReader = new FileContextFileReader();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
92
|
+
const cogitApiV1 = `${envConfig.cogitBaseUrl}${API_V1_PATH}`;
|
|
93
|
+
const cogitPushService = new HttpCogitPushService({ apiBaseUrl: cogitApiV1 });
|
|
94
|
+
const cogitPullService = new HttpCogitPullService({ apiBaseUrl: cogitApiV1 });
|
|
91
95
|
// ConnectorManager factory — creates per-project instances since constructor binds to projectRoot
|
|
92
96
|
const fileService = new FsFileService();
|
|
93
97
|
const templateLoader = new FsTemplateLoader(fileService);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { TaskInfo } from '../../core/domain/transport/task-info.js';
|
|
2
|
+
import type { QueryExecutorResult } from '../../core/interfaces/executor/i-query-executor.js';
|
|
3
|
+
import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
|
|
4
|
+
import type { IQueryLogStore } from '../../core/interfaces/storage/i-query-log-store.js';
|
|
5
|
+
/** Query metadata without the response string (response arrives via task:completed). */
|
|
6
|
+
type QueryResultMetadata = Omit<QueryExecutorResult, 'response'>;
|
|
7
|
+
/**
|
|
8
|
+
* Lifecycle hook that transparently logs query task execution.
|
|
9
|
+
*
|
|
10
|
+
* Wired into TaskRouter via lifecycleHooks[]. Writes log entries to
|
|
11
|
+
* per-project FileQueryLogStore. All I/O errors are swallowed — logging
|
|
12
|
+
* must never block or affect query task execution.
|
|
13
|
+
*
|
|
14
|
+
* Key difference from CurateLogHandler: no onToolResult accumulation.
|
|
15
|
+
* Query metadata (tier, timing, matchedDocs, searchMetadata) arrives via
|
|
16
|
+
* setQueryResult() called after QueryExecutor.executeWithAgent() returns.
|
|
17
|
+
*/
|
|
18
|
+
export declare class QueryLogHandler implements ITaskLifecycleHook {
|
|
19
|
+
private readonly createStore?;
|
|
20
|
+
/** Active task count per projectPath — used to evict idle stores. */
|
|
21
|
+
private readonly activeTaskCount;
|
|
22
|
+
/** Per-project store cache (one store per projectPath). Evicted when no active tasks remain. */
|
|
23
|
+
private readonly stores;
|
|
24
|
+
/** In-memory state per active task. Cleared on cleanup(). */
|
|
25
|
+
private readonly tasks;
|
|
26
|
+
constructor(createStore?: ((projectPath: string) => IQueryLogStore) | undefined);
|
|
27
|
+
cleanup(taskId: string): void;
|
|
28
|
+
onTaskCancelled(taskId: string, _task: TaskInfo): Promise<void>;
|
|
29
|
+
onTaskCompleted(taskId: string, result: string, _task: TaskInfo): Promise<void>;
|
|
30
|
+
onTaskCreate(task: TaskInfo): Promise<void | {
|
|
31
|
+
logId?: string;
|
|
32
|
+
}>;
|
|
33
|
+
onTaskError(taskId: string, errorMessage: string, _task: TaskInfo): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Store query execution metadata for later finalization.
|
|
36
|
+
* Called by agent-process after QueryExecutor.executeWithAgent() returns.
|
|
37
|
+
* Synchronous — no I/O. Metadata is merged into the final entry on completion.
|
|
38
|
+
*/
|
|
39
|
+
setQueryResult(taskId: string, result: QueryResultMetadata): void;
|
|
40
|
+
private getOrCreateStore;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { getProjectDataDir } from '../../utils/path-utils.js';
|
|
2
|
+
import { transportLog } from '../../utils/process-logger.js';
|
|
3
|
+
import { FileQueryLogStore } from '../storage/file-query-log-store.js';
|
|
4
|
+
const QUERY_TASK_TYPES = new Set(['query']);
|
|
5
|
+
// ── QueryLogHandler ──────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Lifecycle hook that transparently logs query task execution.
|
|
8
|
+
*
|
|
9
|
+
* Wired into TaskRouter via lifecycleHooks[]. Writes log entries to
|
|
10
|
+
* per-project FileQueryLogStore. All I/O errors are swallowed — logging
|
|
11
|
+
* must never block or affect query task execution.
|
|
12
|
+
*
|
|
13
|
+
* Key difference from CurateLogHandler: no onToolResult accumulation.
|
|
14
|
+
* Query metadata (tier, timing, matchedDocs, searchMetadata) arrives via
|
|
15
|
+
* setQueryResult() called after QueryExecutor.executeWithAgent() returns.
|
|
16
|
+
*/
|
|
17
|
+
export class QueryLogHandler {
|
|
18
|
+
createStore;
|
|
19
|
+
/** Active task count per projectPath — used to evict idle stores. */
|
|
20
|
+
activeTaskCount = new Map();
|
|
21
|
+
/** Per-project store cache (one store per projectPath). Evicted when no active tasks remain. */
|
|
22
|
+
stores = new Map();
|
|
23
|
+
/** In-memory state per active task. Cleared on cleanup(). */
|
|
24
|
+
tasks = new Map();
|
|
25
|
+
constructor(createStore) {
|
|
26
|
+
this.createStore = createStore;
|
|
27
|
+
}
|
|
28
|
+
cleanup(taskId) {
|
|
29
|
+
const state = this.tasks.get(taskId);
|
|
30
|
+
this.tasks.delete(taskId);
|
|
31
|
+
if (state) {
|
|
32
|
+
const remaining = (this.activeTaskCount.get(state.projectPath) ?? 1) - 1;
|
|
33
|
+
if (remaining <= 0) {
|
|
34
|
+
this.activeTaskCount.delete(state.projectPath);
|
|
35
|
+
this.stores.delete(state.projectPath);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.activeTaskCount.set(state.projectPath, remaining);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async onTaskCancelled(taskId, _task) {
|
|
43
|
+
const state = this.tasks.get(taskId);
|
|
44
|
+
if (!state)
|
|
45
|
+
return;
|
|
46
|
+
const store = this.getOrCreateStore(state.projectPath);
|
|
47
|
+
const updated = {
|
|
48
|
+
...state.entry,
|
|
49
|
+
completedAt: Date.now(),
|
|
50
|
+
matchedDocs: state.queryResult?.matchedDocs ?? state.entry.matchedDocs,
|
|
51
|
+
searchMetadata: state.queryResult?.searchMetadata,
|
|
52
|
+
status: 'cancelled',
|
|
53
|
+
tier: state.queryResult?.tier,
|
|
54
|
+
timing: state.queryResult?.timing,
|
|
55
|
+
};
|
|
56
|
+
await store.save(updated).catch((error) => {
|
|
57
|
+
transportLog(`QueryLogHandler: failed to save cancelled entry for ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async onTaskCompleted(taskId, result, _task) {
|
|
61
|
+
const state = this.tasks.get(taskId);
|
|
62
|
+
if (!state)
|
|
63
|
+
return;
|
|
64
|
+
const store = this.getOrCreateStore(state.projectPath);
|
|
65
|
+
const updated = {
|
|
66
|
+
...state.entry,
|
|
67
|
+
completedAt: Date.now(),
|
|
68
|
+
matchedDocs: state.queryResult?.matchedDocs ?? state.entry.matchedDocs,
|
|
69
|
+
response: result.length > 0 ? result : undefined,
|
|
70
|
+
searchMetadata: state.queryResult?.searchMetadata,
|
|
71
|
+
status: 'completed',
|
|
72
|
+
tier: state.queryResult?.tier,
|
|
73
|
+
timing: state.queryResult?.timing,
|
|
74
|
+
};
|
|
75
|
+
await store.save(updated).catch((error) => {
|
|
76
|
+
transportLog(`QueryLogHandler: failed to save completed entry for ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async onTaskCreate(task) {
|
|
80
|
+
if (!QUERY_TASK_TYPES.has(task.type))
|
|
81
|
+
return;
|
|
82
|
+
if (!task.projectPath)
|
|
83
|
+
return;
|
|
84
|
+
const store = this.getOrCreateStore(task.projectPath);
|
|
85
|
+
const logId = await store.getNextId().catch((error) => {
|
|
86
|
+
transportLog(`QueryLogHandler: getNextId failed for ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
});
|
|
88
|
+
if (!logId)
|
|
89
|
+
return;
|
|
90
|
+
const entry = {
|
|
91
|
+
id: logId,
|
|
92
|
+
matchedDocs: [],
|
|
93
|
+
query: task.content,
|
|
94
|
+
startedAt: task.createdAt,
|
|
95
|
+
status: 'processing',
|
|
96
|
+
taskId: task.taskId,
|
|
97
|
+
};
|
|
98
|
+
// MEMORY-FIRST: Set in-memory state BEFORE disk write so setQueryResult can access it immediately.
|
|
99
|
+
// Caching `entry` here lets onTaskCompleted/onTaskError rebuild the final entry
|
|
100
|
+
// without a getById round-trip — so completion is never lost even if this initial
|
|
101
|
+
// save fails.
|
|
102
|
+
this.tasks.set(task.taskId, { entry, projectPath: task.projectPath });
|
|
103
|
+
this.activeTaskCount.set(task.projectPath, (this.activeTaskCount.get(task.projectPath) ?? 0) + 1);
|
|
104
|
+
// Fire-and-forget disk I/O — logId is already known and returned.
|
|
105
|
+
store.save(entry).catch((error) => {
|
|
106
|
+
transportLog(`QueryLogHandler: failed to save processing entry for ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
});
|
|
108
|
+
return { logId };
|
|
109
|
+
}
|
|
110
|
+
async onTaskError(taskId, errorMessage, _task) {
|
|
111
|
+
const state = this.tasks.get(taskId);
|
|
112
|
+
if (!state)
|
|
113
|
+
return;
|
|
114
|
+
const store = this.getOrCreateStore(state.projectPath);
|
|
115
|
+
const updated = {
|
|
116
|
+
...state.entry,
|
|
117
|
+
completedAt: Date.now(),
|
|
118
|
+
error: errorMessage,
|
|
119
|
+
matchedDocs: state.queryResult?.matchedDocs ?? state.entry.matchedDocs,
|
|
120
|
+
searchMetadata: state.queryResult?.searchMetadata,
|
|
121
|
+
status: 'error',
|
|
122
|
+
tier: state.queryResult?.tier,
|
|
123
|
+
timing: state.queryResult?.timing,
|
|
124
|
+
};
|
|
125
|
+
await store.save(updated).catch((error) => {
|
|
126
|
+
transportLog(`QueryLogHandler: failed to save error entry for ${taskId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Store query execution metadata for later finalization.
|
|
131
|
+
* Called by agent-process after QueryExecutor.executeWithAgent() returns.
|
|
132
|
+
* Synchronous — no I/O. Metadata is merged into the final entry on completion.
|
|
133
|
+
*/
|
|
134
|
+
setQueryResult(taskId, result) {
|
|
135
|
+
const state = this.tasks.get(taskId);
|
|
136
|
+
if (!state)
|
|
137
|
+
return;
|
|
138
|
+
state.queryResult = result;
|
|
139
|
+
}
|
|
140
|
+
getOrCreateStore(projectPath) {
|
|
141
|
+
const existing = this.stores.get(projectPath);
|
|
142
|
+
if (existing)
|
|
143
|
+
return existing;
|
|
144
|
+
const store = this.createStore
|
|
145
|
+
? this.createStore(projectPath)
|
|
146
|
+
: new FileQueryLogStore({ baseDir: getProjectDataDir(projectPath) });
|
|
147
|
+
this.stores.set(projectPath, store);
|
|
148
|
+
return store;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -14,18 +14,41 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Consumed by TransportHandlers (orchestrator).
|
|
16
16
|
*/
|
|
17
|
+
import type { TaskCreateRequest } from '../../core/domain/transport/schemas.js';
|
|
17
18
|
import type { IAgentPool } from '../../core/interfaces/agent/i-agent-pool.js';
|
|
18
19
|
import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
|
|
19
20
|
import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
|
|
20
21
|
import type { IProjectRouter } from '../../core/interfaces/routing/i-project-router.js';
|
|
21
22
|
import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
|
|
22
23
|
import type { TaskInfo } from './types.js';
|
|
24
|
+
/**
|
|
25
|
+
* Outcome of the daemon-side pre-dispatch check.
|
|
26
|
+
*
|
|
27
|
+
* `skipResult` is the full string sent to the client as the task:completed `result`.
|
|
28
|
+
* The callback owns the message format so task-router stays task-type-agnostic
|
|
29
|
+
* (e.g. dream uses "Dream skipped: <reason>"; future task types can use their own).
|
|
30
|
+
*/
|
|
31
|
+
export type PreDispatchCheckResult = {
|
|
32
|
+
eligible: false;
|
|
33
|
+
skipResult: string;
|
|
34
|
+
} | {
|
|
35
|
+
eligible: true;
|
|
36
|
+
};
|
|
37
|
+
export type PreDispatchCheck = (task: TaskCreateRequest, projectPath?: string) => Promise<PreDispatchCheckResult>;
|
|
23
38
|
type TaskRouterOptions = {
|
|
24
39
|
agentPool?: IAgentPool;
|
|
25
40
|
/** Function to resolve agent clientId for a given project */
|
|
26
41
|
getAgentForProject: (projectPath?: string) => string | undefined;
|
|
27
42
|
/** Lifecycle hooks for task events (e.g. CurateLogHandler). */
|
|
28
43
|
lifecycleHooks?: ITaskLifecycleHook[];
|
|
44
|
+
/**
|
|
45
|
+
* Optional daemon-side gate run before dispatching to the agent pool. If it
|
|
46
|
+
* resolves ineligible, task-router short-circuits with task:completed carrying
|
|
47
|
+
* the skip reason and never submits the task to an agent.
|
|
48
|
+
* Used for dream task type to enforce gates 1-3 (time, activity, queue) even
|
|
49
|
+
* on the CLI dispatch path — mirrors the idle-trigger pre-check pattern.
|
|
50
|
+
*/
|
|
51
|
+
preDispatchCheck?: PreDispatchCheck;
|
|
29
52
|
projectRegistry?: IProjectRegistry;
|
|
30
53
|
projectRouter?: IProjectRouter;
|
|
31
54
|
/** Resolves the projectPath a client registered with (from client:register). */
|
|
@@ -41,6 +64,7 @@ export declare class TaskRouter {
|
|
|
41
64
|
private completedTasks;
|
|
42
65
|
private readonly getAgentForProject;
|
|
43
66
|
private readonly lifecycleHooks;
|
|
67
|
+
private readonly preDispatchCheck;
|
|
44
68
|
private readonly projectRegistry;
|
|
45
69
|
private readonly projectRouter;
|
|
46
70
|
private readonly resolveClientProjectPath;
|
|
@@ -100,6 +124,22 @@ export declare class TaskRouter {
|
|
|
100
124
|
*/
|
|
101
125
|
private handleTaskCreate;
|
|
102
126
|
private handleTaskError;
|
|
127
|
+
/**
|
|
128
|
+
* Emit `task:completed` for a task that the daemon's pre-dispatch gate skipped
|
|
129
|
+
* before it ever reached `AgentPool.submitTask`.
|
|
130
|
+
*
|
|
131
|
+
* Distinct from {@link handleTaskCompleted}:
|
|
132
|
+
* - does NOT call `agentPool.notifyTaskCompleted` (the pool's `activeTasks`
|
|
133
|
+
* counter was never incremented, so decrementing here would undercount real
|
|
134
|
+
* load and let `drainQueue` dispatch an extra queued task)
|
|
135
|
+
* - does NOT fire `onTaskCompleted` lifecycle hooks (counters/metrics that
|
|
136
|
+
* act on completed tasks should not see pre-check skips as completions)
|
|
137
|
+
*
|
|
138
|
+
* Still emits the event to the client and the project room so REPL/TUI
|
|
139
|
+
* receive the skip result, and still calls `moveToCompleted` so the task is
|
|
140
|
+
* removed from the active set.
|
|
141
|
+
*/
|
|
142
|
+
private handleTaskSkippedByPreCheck;
|
|
103
143
|
private handleTaskStarted;
|
|
104
144
|
/**
|
|
105
145
|
* Move a task to the completed tasks map with grace period cleanup.
|