byterover-cli 3.1.0 → 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/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 +214 -101
- 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.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 +6 -0
- package/dist/server/constants.js +8 -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/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 +8 -0
- 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/daemon/agent-process.js +15 -5
- 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/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 +55 -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/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 +19 -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 +418 -158
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { BRV_DIR, CONTEXT_TREE_DIR, PROJECT_CONFIG_FILE, SOURCES_FILE } from '../../../constants.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Schema
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export const SourceSchema = z.object({
|
|
10
|
+
addedAt: z.string(),
|
|
11
|
+
alias: z.string().min(1),
|
|
12
|
+
projectRoot: z.string().min(1),
|
|
13
|
+
readOnly: z.literal(true),
|
|
14
|
+
});
|
|
15
|
+
export const SourcesFileSchema = z.object({
|
|
16
|
+
sources: z.array(SourceSchema),
|
|
17
|
+
version: z.literal(1),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Derives a stable, short origin key from a canonical path.
|
|
21
|
+
* Uses first 12 hex chars of SHA-256 to avoid alias-based collisions.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveOriginKey(canonicalPath) {
|
|
24
|
+
return createHash('sha256').update(canonicalPath).digest('hex').slice(0, 12);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Loads and validates `.brv/sources.json` from a project root.
|
|
28
|
+
*
|
|
29
|
+
* Returns null if the file does not exist.
|
|
30
|
+
* Broken sources (target `.brv/` missing) are included in `sources` but excluded
|
|
31
|
+
* from `origins` — callers decide how to surface them (status vs search).
|
|
32
|
+
*/
|
|
33
|
+
export function loadSources(projectRoot) {
|
|
34
|
+
const filePath = join(projectRoot, BRV_DIR, SOURCES_FILE);
|
|
35
|
+
if (!existsSync(filePath)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { mtime, origins: [], sources: [] };
|
|
45
|
+
}
|
|
46
|
+
const result = SourcesFileSchema.safeParse(raw);
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
return { mtime, origins: [], sources: [] };
|
|
49
|
+
}
|
|
50
|
+
const origins = [];
|
|
51
|
+
for (const source of result.data.sources) {
|
|
52
|
+
const targetConfigPath = join(source.projectRoot, BRV_DIR, PROJECT_CONFIG_FILE);
|
|
53
|
+
if (!existsSync(targetConfigPath)) {
|
|
54
|
+
// Broken source — skip from origins but keep in sources for status display
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
let canonicalRoot;
|
|
58
|
+
try {
|
|
59
|
+
canonicalRoot = realpathSync(source.projectRoot);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const contextTreeRoot = join(canonicalRoot, BRV_DIR, CONTEXT_TREE_DIR);
|
|
65
|
+
if (!existsSync(contextTreeRoot)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
origins.push({
|
|
69
|
+
alias: source.alias,
|
|
70
|
+
contextTreeRoot,
|
|
71
|
+
origin: 'shared',
|
|
72
|
+
originKey: deriveOriginKey(canonicalRoot),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return { mtime, origins, sources: result.data.sources };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Validates each source and returns status for display.
|
|
79
|
+
*
|
|
80
|
+
* A source is valid only when both `.brv/config.json` AND `.brv/context-tree/`
|
|
81
|
+
* exist — matching what loadSources() requires before including a source in
|
|
82
|
+
* search origins. When valid, `contextTreeSize` counts `.md` files.
|
|
83
|
+
*/
|
|
84
|
+
export function getSourceStatuses(sources) {
|
|
85
|
+
return sources.map((source) => {
|
|
86
|
+
const targetConfigPath = join(source.projectRoot, BRV_DIR, PROJECT_CONFIG_FILE);
|
|
87
|
+
const targetContextTree = join(source.projectRoot, BRV_DIR, CONTEXT_TREE_DIR);
|
|
88
|
+
const valid = existsSync(targetConfigPath) && existsSync(targetContextTree);
|
|
89
|
+
let contextTreeSize;
|
|
90
|
+
if (valid) {
|
|
91
|
+
contextTreeSize = countMarkdownFiles(targetContextTree);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
alias: source.alias,
|
|
95
|
+
contextTreeSize,
|
|
96
|
+
projectRoot: source.projectRoot,
|
|
97
|
+
valid,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Recursively counts .md files in a directory.
|
|
103
|
+
*/
|
|
104
|
+
function countMarkdownFiles(dir) {
|
|
105
|
+
let count = 0;
|
|
106
|
+
try {
|
|
107
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
count += countMarkdownFiles(join(dir, entry.name));
|
|
111
|
+
}
|
|
112
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
113
|
+
count++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Directory unreadable — return 0
|
|
119
|
+
}
|
|
120
|
+
return count;
|
|
121
|
+
}
|
|
@@ -515,6 +515,8 @@ export declare const TaskExecuteSchema: z.ZodObject<{
|
|
|
515
515
|
taskId: z.ZodString;
|
|
516
516
|
/** Task type */
|
|
517
517
|
type: z.ZodEnum<["curate", "curate-folder", "query"]>;
|
|
518
|
+
/** Workspace root for scoped query/curate */
|
|
519
|
+
worktreeRoot: z.ZodOptional<z.ZodString>;
|
|
518
520
|
}, "strip", z.ZodTypeAny, {
|
|
519
521
|
type: "curate" | "query" | "curate-folder";
|
|
520
522
|
content: string;
|
|
@@ -524,6 +526,7 @@ export declare const TaskExecuteSchema: z.ZodObject<{
|
|
|
524
526
|
clientCwd?: string | undefined;
|
|
525
527
|
folderPath?: string | undefined;
|
|
526
528
|
projectPath?: string | undefined;
|
|
529
|
+
worktreeRoot?: string | undefined;
|
|
527
530
|
}, {
|
|
528
531
|
type: "curate" | "query" | "curate-folder";
|
|
529
532
|
content: string;
|
|
@@ -533,6 +536,7 @@ export declare const TaskExecuteSchema: z.ZodObject<{
|
|
|
533
536
|
clientCwd?: string | undefined;
|
|
534
537
|
folderPath?: string | undefined;
|
|
535
538
|
projectPath?: string | undefined;
|
|
539
|
+
worktreeRoot?: string | undefined;
|
|
536
540
|
}>;
|
|
537
541
|
/**
|
|
538
542
|
* task:cancel - Transport tells Agent to cancel a task
|
|
@@ -960,6 +964,8 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
|
|
|
960
964
|
taskId: z.ZodString;
|
|
961
965
|
/** Task type */
|
|
962
966
|
type: z.ZodEnum<["curate", "curate-folder", "query"]>;
|
|
967
|
+
/** Workspace root for scoped query/curate (stable linked root or projectRoot if unlinked) */
|
|
968
|
+
worktreeRoot: z.ZodOptional<z.ZodString>;
|
|
963
969
|
}, "strip", z.ZodTypeAny, {
|
|
964
970
|
type: "curate" | "query" | "curate-folder";
|
|
965
971
|
content: string;
|
|
@@ -968,6 +974,7 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
|
|
|
968
974
|
clientCwd?: string | undefined;
|
|
969
975
|
folderPath?: string | undefined;
|
|
970
976
|
projectPath?: string | undefined;
|
|
977
|
+
worktreeRoot?: string | undefined;
|
|
971
978
|
}, {
|
|
972
979
|
type: "curate" | "query" | "curate-folder";
|
|
973
980
|
content: string;
|
|
@@ -976,6 +983,7 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
|
|
|
976
983
|
clientCwd?: string | undefined;
|
|
977
984
|
folderPath?: string | undefined;
|
|
978
985
|
projectPath?: string | undefined;
|
|
986
|
+
worktreeRoot?: string | undefined;
|
|
979
987
|
}>;
|
|
980
988
|
/**
|
|
981
989
|
* Response after task creation
|
|
@@ -315,6 +315,8 @@ export const TaskExecuteSchema = z.object({
|
|
|
315
315
|
taskId: z.string(),
|
|
316
316
|
/** Task type */
|
|
317
317
|
type: z.enum(['curate', 'curate-folder', 'query']),
|
|
318
|
+
/** Workspace root for scoped query/curate */
|
|
319
|
+
worktreeRoot: z.string().optional(),
|
|
318
320
|
});
|
|
319
321
|
/**
|
|
320
322
|
* task:cancel - Transport tells Agent to cancel a task
|
|
@@ -492,6 +494,8 @@ export const TaskCreateRequestSchema = z.object({
|
|
|
492
494
|
taskId: z.string().uuid('Invalid taskId format - must be UUID'),
|
|
493
495
|
/** Task type */
|
|
494
496
|
type: TaskTypeSchema,
|
|
497
|
+
/** Workspace root for scoped query/curate (stable linked root or projectRoot if unlinked) */
|
|
498
|
+
worktreeRoot: z.string().optional(),
|
|
495
499
|
});
|
|
496
500
|
/**
|
|
497
501
|
* Response after task creation
|
|
@@ -114,4 +114,17 @@ export interface IClientManager {
|
|
|
114
114
|
* @param clientId - The client's Socket.IO ID
|
|
115
115
|
*/
|
|
116
116
|
unregister(clientId: string): void;
|
|
117
|
+
/**
|
|
118
|
+
* Update a client's project path, even if already associated.
|
|
119
|
+
* Used for reassociation after worktree add/remove operations.
|
|
120
|
+
* Moves the client from the old project index to the new one,
|
|
121
|
+
* and fires onProjectEmpty if the old project has no remaining external clients.
|
|
122
|
+
*
|
|
123
|
+
* No-op if client is unknown.
|
|
124
|
+
*
|
|
125
|
+
* @param clientId - The client's Socket.IO ID
|
|
126
|
+
* @param newProjectPath - The new project path to associate
|
|
127
|
+
* @returns The previous project path (undefined if client not found or not previously associated)
|
|
128
|
+
*/
|
|
129
|
+
updateProjectPath(clientId: string, newProjectPath: string): string | undefined;
|
|
117
130
|
}
|
|
@@ -10,8 +10,12 @@ export interface CurateExecuteOptions {
|
|
|
10
10
|
content: string;
|
|
11
11
|
/** Optional file paths for --files flag */
|
|
12
12
|
files?: string[];
|
|
13
|
+
/** Canonical project root where .brv/ lives (for post-processing: snapshot, summary, manifest) */
|
|
14
|
+
projectRoot?: string;
|
|
13
15
|
/** Task ID for event routing (required for concurrent task isolation) */
|
|
14
16
|
taskId: string;
|
|
17
|
+
/** Workspace root — linked subdir or same as projectRoot for direct projects */
|
|
18
|
+
worktreeRoot?: string;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* ICurateExecutor - Executes curate tasks with an injected CipherAgent.
|
|
@@ -4,14 +4,18 @@ import type { ICipherAgent } from '../../../../agent/core/interfaces/i-cipher-ag
|
|
|
4
4
|
* Agent uses its default session (Single-Session pattern).
|
|
5
5
|
*/
|
|
6
6
|
export interface FolderPackExecuteOptions {
|
|
7
|
-
/** Client's working directory for resolving relative paths */
|
|
7
|
+
/** Client's working directory for resolving relative paths (shell semantics) */
|
|
8
8
|
clientCwd?: string;
|
|
9
9
|
/** Optional context to guide the analysis */
|
|
10
10
|
content?: string;
|
|
11
|
-
/** Folder path to pack (relative to clientCwd or absolute) */
|
|
12
|
-
folderPath
|
|
11
|
+
/** Folder path to pack (relative to clientCwd or absolute). When absent, defaults to worktreeRoot. */
|
|
12
|
+
folderPath?: string;
|
|
13
|
+
/** Canonical project root where .brv/ lives (for temp file location) */
|
|
14
|
+
projectRoot?: string;
|
|
13
15
|
/** Task ID for event routing (required for concurrent task isolation) */
|
|
14
16
|
taskId: string;
|
|
17
|
+
/** Workspace root — linked subdir or same as projectRoot. Used as default folderPath when none supplied. */
|
|
18
|
+
worktreeRoot?: string;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* IFolderPackExecutor - Executes folder pack + curate tasks with an injected CipherAgent.
|
|
@@ -8,6 +8,8 @@ export interface QueryExecuteOptions {
|
|
|
8
8
|
query: string;
|
|
9
9
|
/** Task ID for event routing (required for concurrent task isolation) */
|
|
10
10
|
taskId: string;
|
|
11
|
+
/** Stable workspace root for scoping search and cache isolation */
|
|
12
|
+
worktreeRoot?: string;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* IQueryExecutor - Executes query tasks with an injected CipherAgent.
|
|
@@ -41,6 +41,7 @@ export declare class ClientManager implements IClientManager {
|
|
|
41
41
|
register(clientId: string, type: ClientType, projectPath?: string): void;
|
|
42
42
|
setAgentName(clientId: string, agentName: string): void;
|
|
43
43
|
unregister(clientId: string): void;
|
|
44
|
+
updateProjectPath(clientId: string, newProjectPath: string): string | undefined;
|
|
44
45
|
private addToProjectIndex;
|
|
45
46
|
/**
|
|
46
47
|
* Check if a project has no remaining external clients.
|
|
@@ -111,6 +111,22 @@ export class ClientManager {
|
|
|
111
111
|
// Notify idle timeout policy
|
|
112
112
|
this.clientDisconnectedCallback?.();
|
|
113
113
|
}
|
|
114
|
+
updateProjectPath(clientId, newProjectPath) {
|
|
115
|
+
const client = this.clients.get(clientId);
|
|
116
|
+
if (!client)
|
|
117
|
+
return undefined;
|
|
118
|
+
const oldPath = client.updateProjectPath(newProjectPath);
|
|
119
|
+
// Move between project indexes
|
|
120
|
+
if (oldPath) {
|
|
121
|
+
this.removeFromProjectIndex(clientId, oldPath);
|
|
122
|
+
}
|
|
123
|
+
this.addToProjectIndex(clientId, newProjectPath);
|
|
124
|
+
// Check if old project is now empty
|
|
125
|
+
if (oldPath && oldPath !== newProjectPath && client.isExternalClient) {
|
|
126
|
+
this.checkProjectEmpty(oldPath);
|
|
127
|
+
}
|
|
128
|
+
return oldPath;
|
|
129
|
+
}
|
|
114
130
|
addToProjectIndex(clientId, projectPath) {
|
|
115
131
|
let members = this.projectClients.get(projectPath);
|
|
116
132
|
if (!members) {
|
|
@@ -31,6 +31,7 @@ import { AuthEvents } from '../../../shared/transport/events/auth-events.js';
|
|
|
31
31
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
32
32
|
import { DEFAULT_LLM_MODEL, PROJECT } from '../../constants.js';
|
|
33
33
|
import { serializeTaskError, TaskError, TaskErrorCode } from '../../core/domain/errors/task-error.js';
|
|
34
|
+
import { loadSources } from '../../core/domain/source/source-schema.js';
|
|
34
35
|
import { TransportAgentEventNames, TransportDaemonEventNames, TransportStateEventNames, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
|
|
35
36
|
import { CurateExecutor } from '../executor/curate-executor.js';
|
|
36
37
|
import { FolderPackExecutor } from '../executor/folder-pack-executor.js';
|
|
@@ -189,10 +190,14 @@ async function start() {
|
|
|
189
190
|
cachedProviderHeaders = providerResult.providerHeaders ? JSON.stringify(providerResult.providerHeaders) : undefined;
|
|
190
191
|
agentLog(`Provider: ${activeProvider}, Model: ${activeModel ?? 'default'}`);
|
|
191
192
|
// 5. Create CipherAgent with lazy providers + transport client
|
|
193
|
+
// Load knowledge sources early so shared context tree roots can be shared with both
|
|
194
|
+
// the agent's FileSystemService (via config) and the executor's FileSystemService
|
|
195
|
+
const sourcesData = loadSources(projectPath);
|
|
196
|
+
const sharedAllowedPaths = (sourcesData?.origins ?? []).map((o) => o.contextTreeRoot);
|
|
192
197
|
const envConfig = getCurrentConfig();
|
|
193
198
|
const agentConfig = {
|
|
194
199
|
apiBaseUrl: envConfig.llmApiBaseUrl,
|
|
195
|
-
fileSystem: { workingDirectory: projectPath },
|
|
200
|
+
fileSystem: { allowedPaths: ['.', ...sharedAllowedPaths], workingDirectory: projectPath },
|
|
196
201
|
llm: {
|
|
197
202
|
maxIterations: 10,
|
|
198
203
|
maxTokens: 4096,
|
|
@@ -279,7 +284,10 @@ async function start() {
|
|
|
279
284
|
}
|
|
280
285
|
});
|
|
281
286
|
// 6. Create FileSystemService + SearchKnowledgeService for smart query routing
|
|
282
|
-
const fileSystemService = new FileSystemService({
|
|
287
|
+
const fileSystemService = new FileSystemService({
|
|
288
|
+
allowedPaths: ['.', ...sharedAllowedPaths],
|
|
289
|
+
workingDirectory: projectPath,
|
|
290
|
+
});
|
|
283
291
|
await fileSystemService.initialize();
|
|
284
292
|
const searchService = createSearchKnowledgeService(fileSystemService, { baseDirectory: projectPath });
|
|
285
293
|
// 7. Create executors and listen for task:execute from pool
|
|
@@ -305,7 +313,7 @@ async function start() {
|
|
|
305
313
|
agentLog('Ready — listening for tasks');
|
|
306
314
|
}
|
|
307
315
|
async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor) {
|
|
308
|
-
const { clientCwd, clientId, content, files, folderPath, taskId, type } = task;
|
|
316
|
+
const { clientCwd, clientId, content, files, folderPath, taskId, type, worktreeRoot } = task;
|
|
309
317
|
if (!transport || !agent)
|
|
310
318
|
return;
|
|
311
319
|
const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
|
|
@@ -372,7 +380,7 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
372
380
|
let result;
|
|
373
381
|
switch (type) {
|
|
374
382
|
case 'curate': {
|
|
375
|
-
result = await curateExecutor.executeWithAgent(agent, { clientCwd, content, files, taskId });
|
|
383
|
+
result = await curateExecutor.executeWithAgent(agent, { clientCwd, content, files, projectRoot: projectPath, taskId, worktreeRoot });
|
|
376
384
|
break;
|
|
377
385
|
}
|
|
378
386
|
case 'curate-folder': {
|
|
@@ -380,12 +388,14 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
380
388
|
clientCwd,
|
|
381
389
|
content,
|
|
382
390
|
folderPath: folderPath,
|
|
391
|
+
projectRoot: projectPath,
|
|
383
392
|
taskId,
|
|
393
|
+
worktreeRoot,
|
|
384
394
|
});
|
|
385
395
|
break;
|
|
386
396
|
}
|
|
387
397
|
case 'query': {
|
|
388
|
-
result = await queryExecutor.executeWithAgent(agent, { query: content, taskId });
|
|
398
|
+
result = await queryExecutor.executeWithAgent(agent, { query: content, taskId, worktreeRoot });
|
|
389
399
|
break;
|
|
390
400
|
}
|
|
391
401
|
}
|
|
@@ -36,7 +36,7 @@ export class CurateExecutor {
|
|
|
36
36
|
this.fileContentReader = fileContentReader ?? createFileContentReader();
|
|
37
37
|
}
|
|
38
38
|
async executeWithAgent(agent, options) {
|
|
39
|
-
const { clientCwd, content, files, taskId } = options;
|
|
39
|
+
const { clientCwd, content, files, projectRoot, taskId } = options;
|
|
40
40
|
// --- Phase 1: Preprocessing (no sessions created yet — safe to throw) ---
|
|
41
41
|
const fileReferenceInstructions = await this.processFileReferences(files ?? [], clientCwd);
|
|
42
42
|
const fullContext = fileReferenceInstructions ? `${content}\n${fileReferenceInstructions}` : content;
|
|
@@ -45,7 +45,9 @@ export class CurateExecutor {
|
|
|
45
45
|
const effectiveContext = compactionResult.context;
|
|
46
46
|
// --- Phase 3: Curation (session created AFTER preprocessing + compaction) ---
|
|
47
47
|
// Capture pre-curation state for snapshot diff (summary propagation)
|
|
48
|
-
|
|
48
|
+
// Post-processing (snapshot, summary, manifest) operates on projectRoot where .brv/ lives.
|
|
49
|
+
// worktreeRoot is a linked subdir — .brv/ does not exist there in linked setups.
|
|
50
|
+
const baseDir = projectRoot ?? clientCwd ?? process.cwd();
|
|
49
51
|
const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: baseDir });
|
|
50
52
|
let preState;
|
|
51
53
|
try {
|
|
@@ -58,7 +58,11 @@ export function formatDirectResponse(query, results) {
|
|
|
58
58
|
return `### ${r.title}\n\n${truncatedContent}`;
|
|
59
59
|
})
|
|
60
60
|
.join('\n\n---\n\n');
|
|
61
|
-
const sources = topResults.map((r) =>
|
|
61
|
+
const sources = topResults.map((r) => {
|
|
62
|
+
// Paths starting with [ are already namespaced (linked results)
|
|
63
|
+
const displayPath = r.path.startsWith('[') ? r.path : `.brv/context-tree/${r.path}`;
|
|
64
|
+
return `- \`${displayPath}\``;
|
|
65
|
+
}).join('\n');
|
|
62
66
|
return `**Summary**: ${summary}
|
|
63
67
|
|
|
64
68
|
**Details**:
|
|
@@ -38,17 +38,28 @@ export class FolderPackExecutor {
|
|
|
38
38
|
this.folderPackService = folderPackService;
|
|
39
39
|
}
|
|
40
40
|
async executeWithAgent(agent, options) {
|
|
41
|
-
const { clientCwd, content, folderPath, taskId } = options;
|
|
41
|
+
const { clientCwd, content, folderPath, projectRoot, taskId, worktreeRoot } = options;
|
|
42
|
+
// Resolve folder path:
|
|
43
|
+
// - Absent folderPath → default to worktreeRoot (implicit workspace default)
|
|
44
|
+
// - Relative folderPath → resolve from clientCwd (shell semantics)
|
|
45
|
+
// - Absolute folderPath → use as-is
|
|
46
|
+
let absoluteFolderPath;
|
|
42
47
|
if (!folderPath) {
|
|
43
|
-
|
|
48
|
+
absoluteFolderPath = worktreeRoot ?? clientCwd ?? process.cwd();
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
else if (path.isAbsolute(folderPath)) {
|
|
51
|
+
absoluteFolderPath = folderPath;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const shellCwd = clientCwd ?? process.cwd();
|
|
55
|
+
absoluteFolderPath = path.resolve(shellCwd, folderPath);
|
|
56
|
+
}
|
|
57
|
+
// Temp file location: use projectRoot where .brv/ lives (accessible to sandbox)
|
|
58
|
+
const tempFileDir = projectRoot ?? clientCwd ?? process.cwd();
|
|
59
|
+
const snapshotService = new FileContextTreeSnapshotService({ baseDirectory: tempFileDir });
|
|
49
60
|
let preState;
|
|
50
61
|
try {
|
|
51
|
-
preState = await snapshotService.getCurrentState(
|
|
62
|
+
preState = await snapshotService.getCurrentState(tempFileDir);
|
|
52
63
|
}
|
|
53
64
|
catch {
|
|
54
65
|
// Fail-open: if snapshot fails, skip summary propagation
|
|
@@ -62,17 +73,17 @@ export class FolderPackExecutor {
|
|
|
62
73
|
// Use iterative extraction strategy (inspired by rlm)
|
|
63
74
|
// Stores packed folder in sandbox environment and lets agent iteratively query/extract
|
|
64
75
|
// This avoids token limits entirely - works for folders of any size
|
|
65
|
-
const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId,
|
|
76
|
+
const response = await this.executeIterative(agent, packResult, content, absoluteFolderPath, taskId, tempFileDir);
|
|
66
77
|
if (preState) {
|
|
67
78
|
try {
|
|
68
|
-
const postState = await snapshotService.getCurrentState(
|
|
79
|
+
const postState = await snapshotService.getCurrentState(tempFileDir);
|
|
69
80
|
const changedPaths = diffStates(preState, postState);
|
|
70
81
|
if (changedPaths.length > 0) {
|
|
71
82
|
const summaryService = new FileContextTreeSummaryService();
|
|
72
|
-
const results = await summaryService.propagateStaleness(changedPaths, agent,
|
|
83
|
+
const results = await summaryService.propagateStaleness(changedPaths, agent, tempFileDir);
|
|
73
84
|
if (results.some((result) => result.actionTaken)) {
|
|
74
|
-
const manifestService = new FileContextTreeManifestService({ baseDirectory:
|
|
75
|
-
await manifestService.buildManifest(
|
|
85
|
+
const manifestService = new FileContextTreeManifestService({ baseDirectory: tempFileDir });
|
|
86
|
+
await manifestService.buildManifest(tempFileDir);
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
}
|
|
@@ -64,8 +64,31 @@ export declare class QueryExecutor implements IQueryExecutor {
|
|
|
64
64
|
* Compute a context tree fingerprint cheaply using file mtimes.
|
|
65
65
|
* Used for cache invalidation — if any file in the context tree changes,
|
|
66
66
|
* the fingerprint changes and cached results are invalidated.
|
|
67
|
+
*
|
|
68
|
+
* Includes worktreeRoot in the hash so different workspaces produce
|
|
69
|
+
* different fingerprints, preventing cross-workspace cache bleed.
|
|
67
70
|
*/
|
|
68
71
|
private computeContextTreeFingerprint;
|
|
72
|
+
/**
|
|
73
|
+
* Lightweight hash of currently valid shared source keys.
|
|
74
|
+
* Used by the fingerprint cache fast path to detect when a source target
|
|
75
|
+
* becomes broken (directory deleted) within the TTL window.
|
|
76
|
+
* Cost: one readFileSync + existsSync per source — sub-millisecond for typical setups.
|
|
77
|
+
*/
|
|
78
|
+
private computeSourceValidityHash;
|
|
79
|
+
/**
|
|
80
|
+
* Derive a workspace scope for search from the worktreeRoot.
|
|
81
|
+
* Returns the relative path from projectRoot to worktreeRoot,
|
|
82
|
+
* or undefined if they are the same (no scoping needed).
|
|
83
|
+
*
|
|
84
|
+
* KNOWN LIMITATION: Workspace scoping only works if the curated context
|
|
85
|
+
* tree has a subtree matching the workspace relative path (e.g., 'packages/api').
|
|
86
|
+
* Since the context tree is organized semantically by the LLM (topic-based),
|
|
87
|
+
* not by directory structure, scope filtering typically has 0 matches and
|
|
88
|
+
* falls through to unscoped search. A proper fix requires tagging curated
|
|
89
|
+
* files with source workspace metadata during curation.
|
|
90
|
+
*/
|
|
91
|
+
private deriveWorkspaceScope;
|
|
69
92
|
/**
|
|
70
93
|
* Extract key entities from a query for supplementary searches.
|
|
71
94
|
* Simple heuristic: split query, filter stopwords, keep significant terms.
|