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,28 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { WorktreeEvents } from '../../../shared/transport/events/worktree-events.js';
|
|
3
|
+
import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
|
|
4
|
+
export default class WorktreeList extends Command {
|
|
5
|
+
static description = 'Show the current worktree link and list all registered worktrees';
|
|
6
|
+
static examples = ['<%= config.bin %> <%= command.id %>'];
|
|
7
|
+
async run() {
|
|
8
|
+
try {
|
|
9
|
+
const result = await withDaemonRetry(async (client) => client.requestWithAck(WorktreeEvents.LIST), { projectPath: process.cwd() });
|
|
10
|
+
if (result.source === 'linked') {
|
|
11
|
+
this.log(`Worktree: ${result.worktreeRoot}`);
|
|
12
|
+
this.log(`Linked to: ${result.projectRoot}`);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.log(`Project: ${result.projectRoot}`);
|
|
16
|
+
}
|
|
17
|
+
if (result.worktrees.length > 0) {
|
|
18
|
+
this.log('\nRegistered worktrees:');
|
|
19
|
+
for (const wt of result.worktrees) {
|
|
20
|
+
this.log(` ${wt.name} → ${wt.worktreePath}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
this.error(formatConnectionError(error), { exit: 1 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class WorktreeRemove extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { WorktreeEvents } from '../../../shared/transport/events/worktree-events.js';
|
|
4
|
+
import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
|
|
5
|
+
export default class WorktreeRemove extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
path: Args.string({
|
|
8
|
+
description: 'Path to the worktree to remove (defaults to cwd)',
|
|
9
|
+
required: false,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Remove a worktree registration and its .brv pointer';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> <%= command.id %> (remove cwd as worktree)',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> packages/api (remove from parent)',
|
|
16
|
+
];
|
|
17
|
+
async run() {
|
|
18
|
+
const { args } = await this.parse(WorktreeRemove);
|
|
19
|
+
const targetPath = args.path ? resolve(args.path) : resolve(process.cwd());
|
|
20
|
+
try {
|
|
21
|
+
const result = await withDaemonRetry(async (client) => client.requestWithAck(WorktreeEvents.REMOVE, {
|
|
22
|
+
worktreePath: targetPath,
|
|
23
|
+
}), { projectPath: process.cwd() });
|
|
24
|
+
if (result.success) {
|
|
25
|
+
this.log(result.message);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.error(result.message, { exit: 1 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
this.error(formatConnectionError(error), { exit: 1 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -2,6 +2,7 @@ import { BRV_CONFIG_VERSION } from '../../../server/constants.js';
|
|
|
2
2
|
import { ensureProjectInitialized } from '../../../server/infra/config/auto-init.js';
|
|
3
3
|
import { ProjectConfigStore } from '../../../server/infra/config/file-config-store.js';
|
|
4
4
|
import { FileContextTreeService } from '../../../server/infra/context-tree/file-context-tree-service.js';
|
|
5
|
+
import { isWorktreePointer } from '../../../server/infra/project/resolve-project.js';
|
|
5
6
|
import { syncConfigToXdg } from '../../../server/utils/config-xdg-sync.js';
|
|
6
7
|
/**
|
|
7
8
|
* Commands that should skip auto-init and config version validation.
|
|
@@ -22,6 +23,9 @@ const HELP_FLAGS = new Set(['--h', '--help', '-h', '-help']);
|
|
|
22
23
|
export const validateBrvConfigVersion = async (commandId, configStore, argv = [], autoInitDeps) => {
|
|
23
24
|
if (argv.some((arg) => HELP_FLAGS.has(arg)))
|
|
24
25
|
return;
|
|
26
|
+
// Skip auto-init if .brv is a pointer file (worktree) — the project lives elsewhere
|
|
27
|
+
if (isWorktreePointer(process.cwd()))
|
|
28
|
+
return;
|
|
25
29
|
const exists = await configStore.exists();
|
|
26
30
|
if (!exists && COMMANDS_NEED_AUTO_INIT.has(commandId)) {
|
|
27
31
|
const deps = autoInitDeps ?? {
|
|
@@ -5,6 +5,8 @@ export interface DaemonClientOptions {
|
|
|
5
5
|
maxRetries?: number;
|
|
6
6
|
/** Explicit project path — bypasses walk-up discovery. Use for `init` where .brv/ doesn't exist yet. */
|
|
7
7
|
projectPath?: string;
|
|
8
|
+
/** Explicit --project-root flag value to override auto-detection */
|
|
9
|
+
projectRootFlag?: string;
|
|
8
10
|
/** Delay between retries in ms. Default: 2000. Set to 0 in tests. */
|
|
9
11
|
retryDelayMs?: number;
|
|
10
12
|
/** Optional transport connector for DI/testing */
|
|
@@ -13,14 +15,14 @@ export interface DaemonClientOptions {
|
|
|
13
15
|
/**
|
|
14
16
|
* Connects to the daemon, auto-starting it if needed.
|
|
15
17
|
*/
|
|
16
|
-
export declare function connectToDaemonClient(options?: Pick<DaemonClientOptions, 'transportConnector'>): Promise<ConnectionResult>;
|
|
18
|
+
export declare function connectToDaemonClient(options?: Pick<DaemonClientOptions, 'projectRootFlag' | 'transportConnector'>): Promise<ConnectionResult>;
|
|
17
19
|
/**
|
|
18
20
|
* Executes an operation against the daemon with retry logic.
|
|
19
21
|
*
|
|
20
22
|
* Retries on infrastructure failures (daemon spawn timeout, connection dropped,
|
|
21
23
|
* agent disconnected). Does NOT retry on business errors (auth, validation, etc.).
|
|
22
24
|
*/
|
|
23
|
-
export declare function withDaemonRetry<T>(fn: (client: ITransportClient, projectRoot?: string) => Promise<T>, options?: DaemonClientOptions & {
|
|
25
|
+
export declare function withDaemonRetry<T>(fn: (client: ITransportClient, projectRoot?: string, worktreeRoot?: string) => Promise<T>, options?: DaemonClientOptions & {
|
|
24
26
|
/** Called before each retry with attempt number (1-indexed) */
|
|
25
27
|
onRetry?: (attempt: number, maxRetries: number) => void;
|
|
26
28
|
}): Promise<T>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConnectionError, ConnectionFailedError, DaemonSpawnError, InstanceCrashedError, NoInstanceRunningError, TransportRequestError, TransportRequestTimeoutError, } from '@campfirein/brv-transport-client';
|
|
2
2
|
import { TaskErrorCode } from '../../server/core/domain/errors/task-error.js';
|
|
3
|
+
import { resolveProject } from '../../server/infra/project/resolve-project.js';
|
|
3
4
|
import { createDaemonAwareConnector } from '../../server/infra/transport/transport-connector.js';
|
|
4
5
|
import { getSandboxEnvironmentName, isSandboxEnvironment, isSandboxNetworkError, } from '../../server/utils/sandbox-detector.js';
|
|
5
6
|
import { VcErrorCode } from '../../shared/transport/events/vc-events.js';
|
|
@@ -34,12 +35,22 @@ const USER_FRIENDLY_MESSAGES = {
|
|
|
34
35
|
// UNCOMMITTED_CHANGES intentionally omitted: fall through to server's detailed message with file paths
|
|
35
36
|
// USER_NOT_CONFIGURED intentionally omitted: fall through to server's specific hint with actual values
|
|
36
37
|
};
|
|
38
|
+
function resolveRequiredProjectPath(projectRootFlag) {
|
|
39
|
+
const resolution = resolveProject({ projectRootFlag });
|
|
40
|
+
if (!resolution) {
|
|
41
|
+
// No .brv found at cwd — fall back to cwd. The daemon will auto-init .brv/ on first connection.
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
return { projectRoot: cwd, worktreeRoot: cwd };
|
|
44
|
+
}
|
|
45
|
+
return resolution;
|
|
46
|
+
}
|
|
37
47
|
/**
|
|
38
48
|
* Connects to the daemon, auto-starting it if needed.
|
|
39
49
|
*/
|
|
40
50
|
export async function connectToDaemonClient(options) {
|
|
41
51
|
const connector = options?.transportConnector ?? createDaemonAwareConnector();
|
|
42
|
-
|
|
52
|
+
const resolution = resolveRequiredProjectPath(options?.projectRootFlag);
|
|
53
|
+
return connector(undefined, resolution.projectRoot);
|
|
43
54
|
}
|
|
44
55
|
/**
|
|
45
56
|
* Executes an operation against the daemon with retry logic.
|
|
@@ -51,14 +62,19 @@ export async function withDaemonRetry(fn, options) {
|
|
|
51
62
|
const maxRetries = options?.maxRetries ?? MAX_RETRIES;
|
|
52
63
|
const retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
53
64
|
const connector = options?.transportConnector ?? createDaemonAwareConnector(options?.projectPath);
|
|
65
|
+
// Pre-resolve project (workspace-link-aware) so the connector registers
|
|
66
|
+
// with the correct projectPath and callers get the resolved worktreeRoot.
|
|
67
|
+
const resolution = resolveRequiredProjectPath(options?.projectRootFlag);
|
|
68
|
+
const resolvedProjectPath = resolution.projectRoot;
|
|
69
|
+
const resolvedWorkspaceRoot = resolution.worktreeRoot;
|
|
54
70
|
let lastError;
|
|
55
71
|
/* eslint-disable no-await-in-loop -- intentional sequential retry loop */
|
|
56
72
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
57
73
|
let client;
|
|
58
74
|
try {
|
|
59
|
-
const { client: connectedClient, projectRoot } = await connector();
|
|
75
|
+
const { client: connectedClient, projectRoot } = await connector(undefined, resolvedProjectPath);
|
|
60
76
|
client = connectedClient;
|
|
61
|
-
const value = await fn(client, projectRoot);
|
|
77
|
+
const value = await fn(client, projectRoot ?? resolvedProjectPath, resolvedWorkspaceRoot);
|
|
62
78
|
await client.disconnect().catch(() => { });
|
|
63
79
|
return value;
|
|
64
80
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export declare const BRV_DIR = ".brv";
|
|
2
2
|
export declare const PROJECT_CONFIG_FILE = "config.json";
|
|
3
3
|
export declare const BRV_CONFIG_VERSION = "0.0.1";
|
|
4
|
+
export declare const WORKTREES_DIR = "worktrees";
|
|
5
|
+
export declare const WORKTREE_LINK_METADATA = "link.json";
|
|
6
|
+
export declare const SOURCES_FILE = "sources.json";
|
|
7
|
+
export declare const SHARED_SOURCE_LOCAL_SCORE_BOOST = 0.1;
|
|
8
|
+
export declare const MCP_ASSOCIATE_PROJECT_TIMEOUT_MS = 3000;
|
|
9
|
+
export declare const MCP_ASSOCIATE_PROJECT_MAX_ATTEMPTS = 2;
|
|
4
10
|
export declare const GLOBAL_CONFIG_DIR = "brv";
|
|
5
11
|
export declare const GLOBAL_CONFIG_FILE = "config.json";
|
|
6
12
|
export declare const GLOBAL_CONFIG_VERSION = "0.0.1";
|
package/dist/server/constants.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export const BRV_DIR = '.brv';
|
|
2
2
|
export const PROJECT_CONFIG_FILE = 'config.json';
|
|
3
3
|
export const BRV_CONFIG_VERSION = '0.0.1';
|
|
4
|
+
// Worktree linking (git-style: .brv is a file pointing to parent project)
|
|
5
|
+
export const WORKTREES_DIR = 'worktrees';
|
|
6
|
+
export const WORKTREE_LINK_METADATA = 'link.json';
|
|
7
|
+
// Knowledge sources (read-only references to other projects)
|
|
8
|
+
export const SOURCES_FILE = 'sources.json';
|
|
9
|
+
export const SHARED_SOURCE_LOCAL_SCORE_BOOST = 0.1;
|
|
10
|
+
export const MCP_ASSOCIATE_PROJECT_TIMEOUT_MS = 3000;
|
|
11
|
+
export const MCP_ASSOCIATE_PROJECT_MAX_ATTEMPTS = 2;
|
|
4
12
|
// Global config constants (user-level, stored in XDG config directory)
|
|
5
13
|
export const GLOBAL_CONFIG_DIR = 'brv';
|
|
6
14
|
export const GLOBAL_CONFIG_FILE = 'config.json';
|
|
@@ -78,5 +78,12 @@ export declare class ClientInfo {
|
|
|
78
78
|
* Called after MCP initialize handshake provides clientInfo.
|
|
79
79
|
*/
|
|
80
80
|
setAgentName(agentName: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* Update this client's project path, even if already associated.
|
|
83
|
+
* Used for reassociation after worktree add/remove operations.
|
|
84
|
+
*
|
|
85
|
+
* @returns The previous project path (undefined if not previously associated)
|
|
86
|
+
*/
|
|
87
|
+
updateProjectPath(projectPath: string): string | undefined;
|
|
81
88
|
}
|
|
82
89
|
export {};
|
|
@@ -84,4 +84,15 @@ export class ClientInfo {
|
|
|
84
84
|
setAgentName(agentName) {
|
|
85
85
|
this._agentName = agentName;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Update this client's project path, even if already associated.
|
|
89
|
+
* Used for reassociation after worktree add/remove operations.
|
|
90
|
+
*
|
|
91
|
+
* @returns The previous project path (undefined if not previously associated)
|
|
92
|
+
*/
|
|
93
|
+
updateProjectPath(projectPath) {
|
|
94
|
+
const oldPath = this._projectPath;
|
|
95
|
+
this._projectPath = projectPath;
|
|
96
|
+
return oldPath;
|
|
97
|
+
}
|
|
87
98
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for the `.brv` pointer file (when .brv is a FILE, not a directory).
|
|
4
|
+
* Like git's `.git` file in worktrees: contains a single `projectRoot` field
|
|
5
|
+
* pointing to the parent project's absolute path.
|
|
6
|
+
*/
|
|
7
|
+
export declare const WorktreePointerSchema: z.ZodObject<{
|
|
8
|
+
projectRoot: z.ZodString;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
}, {
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type WorktreePointer = z.infer<typeof WorktreePointerSchema>;
|
|
15
|
+
/**
|
|
16
|
+
* Schema for `.brv/worktrees/<name>/link.json` — metadata about a registered worktree.
|
|
17
|
+
* Stored in the parent project's `.brv/worktrees/` directory (like `.git/worktrees/`).
|
|
18
|
+
*/
|
|
19
|
+
export declare const WorktreeLinkMetadataSchema: z.ZodObject<{
|
|
20
|
+
addedAt: z.ZodOptional<z.ZodString>;
|
|
21
|
+
worktreePath: z.ZodString;
|
|
22
|
+
}, "strip", z.ZodTypeAny, {
|
|
23
|
+
worktreePath: string;
|
|
24
|
+
addedAt?: string | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
worktreePath: string;
|
|
27
|
+
addedAt?: string | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
export type WorktreeLinkMetadata = z.infer<typeof WorktreeLinkMetadataSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for the `.brv` pointer file (when .brv is a FILE, not a directory).
|
|
4
|
+
* Like git's `.git` file in worktrees: contains a single `projectRoot` field
|
|
5
|
+
* pointing to the parent project's absolute path.
|
|
6
|
+
*/
|
|
7
|
+
export const WorktreePointerSchema = z.object({
|
|
8
|
+
projectRoot: z.string().min(1),
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Schema for `.brv/worktrees/<name>/link.json` — metadata about a registered worktree.
|
|
12
|
+
* Stored in the parent project's `.brv/worktrees/` directory (like `.git/worktrees/`).
|
|
13
|
+
*/
|
|
14
|
+
export const WorktreeLinkMetadataSchema = z.object({
|
|
15
|
+
addedAt: z.string().optional(),
|
|
16
|
+
worktreePath: z.string().min(1),
|
|
17
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type SourceStatus } from './source-schema.js';
|
|
2
|
+
export interface OperationResult {
|
|
3
|
+
message: string;
|
|
4
|
+
success: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Adds a read-only knowledge source from another project's context tree.
|
|
8
|
+
*
|
|
9
|
+
* Validates: target is a brv project, not self, not duplicate, not circular.
|
|
10
|
+
* Writes to `.brv/sources.json`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function addSource(projectRoot: string, targetPath: string, alias?: string): OperationResult;
|
|
13
|
+
/**
|
|
14
|
+
* Removes a knowledge source by alias or path.
|
|
15
|
+
*/
|
|
16
|
+
export declare function removeSource(projectRoot: string, aliasOrPath: string): OperationResult;
|
|
17
|
+
export interface ListSourcesResult {
|
|
18
|
+
error?: string;
|
|
19
|
+
statuses: SourceStatus[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns status for all sources in the project.
|
|
23
|
+
* Surfaces malformed file errors instead of silently returning empty.
|
|
24
|
+
*/
|
|
25
|
+
export declare function listSourceStatuses(projectRoot: string): ListSourcesResult;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if adding projectRoot → targetRoot would create a circular dependency.
|
|
28
|
+
* A circular reference exists if the target project already has a source pointing
|
|
29
|
+
* back to the current project (direct cycle only — no transitive check in v1).
|
|
30
|
+
*/
|
|
31
|
+
export declare function detectCircularSource(projectRoot: string, targetRoot: string): boolean;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { BRV_DIR, PROJECT_CONFIG_FILE, SOURCES_FILE } from '../../../constants.js';
|
|
4
|
+
import { getSourceStatuses, loadSources, SourcesFileSchema, } from './source-schema.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Add source
|
|
7
|
+
// ============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* Adds a read-only knowledge source from another project's context tree.
|
|
10
|
+
*
|
|
11
|
+
* Validates: target is a brv project, not self, not duplicate, not circular.
|
|
12
|
+
* Writes to `.brv/sources.json`.
|
|
13
|
+
*/
|
|
14
|
+
export function addSource(projectRoot, targetPath, alias) {
|
|
15
|
+
// 1. Local project must have .brv/
|
|
16
|
+
const localConfigPath = join(projectRoot, BRV_DIR, PROJECT_CONFIG_FILE);
|
|
17
|
+
if (!existsSync(localConfigPath)) {
|
|
18
|
+
return { message: `Current project has no .brv/ — run 'brv' first to initialize.`, success: false };
|
|
19
|
+
}
|
|
20
|
+
// 2. Resolve target to canonical path
|
|
21
|
+
let targetRoot;
|
|
22
|
+
try {
|
|
23
|
+
targetRoot = realpathSync(targetPath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { message: `Target path does not exist: ${targetPath}`, success: false };
|
|
27
|
+
}
|
|
28
|
+
// 3. Target must be a brv project
|
|
29
|
+
const targetConfigPath = join(targetRoot, BRV_DIR, PROJECT_CONFIG_FILE);
|
|
30
|
+
if (!existsSync(targetConfigPath)) {
|
|
31
|
+
return { message: `Target "${targetRoot}" is not a ByteRover project (no .brv/config.json).`, success: false };
|
|
32
|
+
}
|
|
33
|
+
// 4. Not self
|
|
34
|
+
let canonicalProjectRoot;
|
|
35
|
+
try {
|
|
36
|
+
canonicalProjectRoot = realpathSync(projectRoot);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
canonicalProjectRoot = projectRoot;
|
|
40
|
+
}
|
|
41
|
+
if (targetRoot === canonicalProjectRoot) {
|
|
42
|
+
return { message: 'Cannot add a source pointing to the current project.', success: false };
|
|
43
|
+
}
|
|
44
|
+
// 5. Read existing file — refuse to mutate if malformed
|
|
45
|
+
const existing = readSourcesFile(projectRoot);
|
|
46
|
+
if (existing.error) {
|
|
47
|
+
return { message: existing.error, success: false };
|
|
48
|
+
}
|
|
49
|
+
// 6. Not duplicate
|
|
50
|
+
const isDuplicate = existing.data.sources.some((source) => {
|
|
51
|
+
try {
|
|
52
|
+
return realpathSync(source.projectRoot) === targetRoot;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return source.projectRoot === targetRoot;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (isDuplicate) {
|
|
59
|
+
return { message: `Source "${targetRoot}" already added.`, success: false };
|
|
60
|
+
}
|
|
61
|
+
// 7. Not circular
|
|
62
|
+
if (detectCircularSource(canonicalProjectRoot, targetRoot)) {
|
|
63
|
+
return {
|
|
64
|
+
message: `Circular source detected: "${basename(targetRoot)}" already references this project as a source.`,
|
|
65
|
+
success: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 8. Derive alias — reject empty/whitespace-only
|
|
69
|
+
if (alias !== undefined && alias.trim() === '') {
|
|
70
|
+
return { message: 'Alias must not be empty.', success: false };
|
|
71
|
+
}
|
|
72
|
+
const derivedAlias = alias ?? basename(targetRoot);
|
|
73
|
+
// 9. Ensure alias uniqueness — append suffix if collision
|
|
74
|
+
const finalAlias = ensureUniqueAlias(derivedAlias, existing.data.sources);
|
|
75
|
+
// 10. Append and write
|
|
76
|
+
const newSource = {
|
|
77
|
+
addedAt: new Date().toISOString(),
|
|
78
|
+
alias: finalAlias,
|
|
79
|
+
projectRoot: targetRoot,
|
|
80
|
+
readOnly: true,
|
|
81
|
+
};
|
|
82
|
+
existing.data.sources.push(newSource);
|
|
83
|
+
writeSourcesFile(projectRoot, existing.data);
|
|
84
|
+
return { message: `Added source "${targetRoot}" as "${finalAlias}".`, success: true };
|
|
85
|
+
}
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Remove source
|
|
88
|
+
// ============================================================================
|
|
89
|
+
/**
|
|
90
|
+
* Removes a knowledge source by alias or path.
|
|
91
|
+
*/
|
|
92
|
+
export function removeSource(projectRoot, aliasOrPath) {
|
|
93
|
+
const existing = readSourcesFile(projectRoot);
|
|
94
|
+
if (existing.error) {
|
|
95
|
+
return { message: existing.error, success: false };
|
|
96
|
+
}
|
|
97
|
+
if (existing.data.sources.length === 0) {
|
|
98
|
+
return { message: 'No knowledge sources configured.', success: false };
|
|
99
|
+
}
|
|
100
|
+
// Try match by alias first, then by canonical path
|
|
101
|
+
let matchIndex = existing.data.sources.findIndex((source) => source.alias === aliasOrPath);
|
|
102
|
+
if (matchIndex === -1) {
|
|
103
|
+
// Try matching by path
|
|
104
|
+
let canonicalTarget;
|
|
105
|
+
try {
|
|
106
|
+
canonicalTarget = realpathSync(aliasOrPath);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
canonicalTarget = aliasOrPath;
|
|
110
|
+
}
|
|
111
|
+
matchIndex = existing.data.sources.findIndex((source) => {
|
|
112
|
+
try {
|
|
113
|
+
return realpathSync(source.projectRoot) === canonicalTarget;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return source.projectRoot === canonicalTarget;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (matchIndex === -1) {
|
|
121
|
+
return { message: `No source found matching "${aliasOrPath}".`, success: false };
|
|
122
|
+
}
|
|
123
|
+
const removed = existing.data.sources.splice(matchIndex, 1)[0];
|
|
124
|
+
writeSourcesFile(projectRoot, existing.data);
|
|
125
|
+
return { message: `Removed source "${removed.alias}" (${removed.projectRoot}).`, success: true };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Returns status for all sources in the project.
|
|
129
|
+
* Surfaces malformed file errors instead of silently returning empty.
|
|
130
|
+
*/
|
|
131
|
+
export function listSourceStatuses(projectRoot) {
|
|
132
|
+
const existing = readSourcesFile(projectRoot);
|
|
133
|
+
if (existing.error) {
|
|
134
|
+
return { error: existing.error, statuses: [] };
|
|
135
|
+
}
|
|
136
|
+
if (existing.data.sources.length === 0) {
|
|
137
|
+
return { statuses: [] };
|
|
138
|
+
}
|
|
139
|
+
return { statuses: getSourceStatuses(existing.data.sources) };
|
|
140
|
+
}
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// Circular source detection
|
|
143
|
+
// ============================================================================
|
|
144
|
+
/**
|
|
145
|
+
* Checks if adding projectRoot → targetRoot would create a circular dependency.
|
|
146
|
+
* A circular reference exists if the target project already has a source pointing
|
|
147
|
+
* back to the current project (direct cycle only — no transitive check in v1).
|
|
148
|
+
*/
|
|
149
|
+
export function detectCircularSource(projectRoot, targetRoot) {
|
|
150
|
+
const targetSources = loadSources(targetRoot);
|
|
151
|
+
if (!targetSources) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return targetSources.sources.some((source) => {
|
|
155
|
+
try {
|
|
156
|
+
return realpathSync(source.projectRoot) === projectRoot;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return source.projectRoot === projectRoot;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function readSourcesFile(projectRoot) {
|
|
164
|
+
const filePath = join(projectRoot, BRV_DIR, SOURCES_FILE);
|
|
165
|
+
if (!existsSync(filePath)) {
|
|
166
|
+
return { data: { sources: [], version: 1 } };
|
|
167
|
+
}
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return {
|
|
174
|
+
data: { sources: [], version: 1 },
|
|
175
|
+
error: `Malformed ${SOURCES_FILE}: file is not valid JSON. Back up or delete the file to recover.`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const result = SourcesFileSchema.safeParse(raw);
|
|
179
|
+
if (!result.success) {
|
|
180
|
+
return {
|
|
181
|
+
data: { sources: [], version: 1 },
|
|
182
|
+
error: `Malformed ${SOURCES_FILE}: schema validation failed. Back up or delete the file to recover.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { data: result.data };
|
|
186
|
+
}
|
|
187
|
+
function writeSourcesFile(projectRoot, data) {
|
|
188
|
+
const filePath = join(projectRoot, BRV_DIR, SOURCES_FILE);
|
|
189
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
190
|
+
}
|
|
191
|
+
function ensureUniqueAlias(baseAlias, existingSources) {
|
|
192
|
+
const existingAliases = new Set(existingSources.map((source) => source.alias));
|
|
193
|
+
if (!existingAliases.has(baseAlias)) {
|
|
194
|
+
return baseAlias;
|
|
195
|
+
}
|
|
196
|
+
let counter = 2;
|
|
197
|
+
while (existingAliases.has(`${baseAlias}-${counter}`)) {
|
|
198
|
+
counter++;
|
|
199
|
+
}
|
|
200
|
+
return `${baseAlias}-${counter}`;
|
|
201
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SourceSchema: z.ZodObject<{
|
|
3
|
+
addedAt: z.ZodString;
|
|
4
|
+
alias: z.ZodString;
|
|
5
|
+
projectRoot: z.ZodString;
|
|
6
|
+
readOnly: z.ZodLiteral<true>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
addedAt: string;
|
|
9
|
+
alias: string;
|
|
10
|
+
projectRoot: string;
|
|
11
|
+
readOnly: true;
|
|
12
|
+
}, {
|
|
13
|
+
addedAt: string;
|
|
14
|
+
alias: string;
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
readOnly: true;
|
|
17
|
+
}>;
|
|
18
|
+
export declare const SourcesFileSchema: z.ZodObject<{
|
|
19
|
+
sources: z.ZodArray<z.ZodObject<{
|
|
20
|
+
addedAt: z.ZodString;
|
|
21
|
+
alias: z.ZodString;
|
|
22
|
+
projectRoot: z.ZodString;
|
|
23
|
+
readOnly: z.ZodLiteral<true>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
addedAt: string;
|
|
26
|
+
alias: string;
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
readOnly: true;
|
|
29
|
+
}, {
|
|
30
|
+
addedAt: string;
|
|
31
|
+
alias: string;
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
readOnly: true;
|
|
34
|
+
}>, "many">;
|
|
35
|
+
version: z.ZodLiteral<1>;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
version: 1;
|
|
38
|
+
sources: {
|
|
39
|
+
addedAt: string;
|
|
40
|
+
alias: string;
|
|
41
|
+
projectRoot: string;
|
|
42
|
+
readOnly: true;
|
|
43
|
+
}[];
|
|
44
|
+
}, {
|
|
45
|
+
version: 1;
|
|
46
|
+
sources: {
|
|
47
|
+
addedAt: string;
|
|
48
|
+
alias: string;
|
|
49
|
+
projectRoot: string;
|
|
50
|
+
readOnly: true;
|
|
51
|
+
}[];
|
|
52
|
+
}>;
|
|
53
|
+
export type Source = z.infer<typeof SourceSchema>;
|
|
54
|
+
export type SourcesFile = z.infer<typeof SourcesFileSchema>;
|
|
55
|
+
export interface SearchOrigin {
|
|
56
|
+
alias?: string;
|
|
57
|
+
contextTreeRoot: string;
|
|
58
|
+
origin: 'local' | 'shared';
|
|
59
|
+
originKey: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Derives a stable, short origin key from a canonical path.
|
|
63
|
+
* Uses first 12 hex chars of SHA-256 to avoid alias-based collisions.
|
|
64
|
+
*/
|
|
65
|
+
export declare function deriveOriginKey(canonicalPath: string): string;
|
|
66
|
+
export interface LoadedSources {
|
|
67
|
+
mtime: number;
|
|
68
|
+
/** Search origins derived from valid sources (callers can search them) */
|
|
69
|
+
origins: SearchOrigin[];
|
|
70
|
+
/** All configured sources (including broken ones — for status display) */
|
|
71
|
+
sources: Source[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Loads and validates `.brv/sources.json` from a project root.
|
|
75
|
+
*
|
|
76
|
+
* Returns null if the file does not exist.
|
|
77
|
+
* Broken sources (target `.brv/` missing) are included in `sources` but excluded
|
|
78
|
+
* from `origins` — callers decide how to surface them (status vs search).
|
|
79
|
+
*/
|
|
80
|
+
export declare function loadSources(projectRoot: string): LoadedSources | null;
|
|
81
|
+
export interface SourceStatus {
|
|
82
|
+
alias: string;
|
|
83
|
+
contextTreeSize?: number;
|
|
84
|
+
projectRoot: string;
|
|
85
|
+
valid: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validates each source and returns status for display.
|
|
89
|
+
*
|
|
90
|
+
* A source is valid only when both `.brv/config.json` AND `.brv/context-tree/`
|
|
91
|
+
* exist — matching what loadSources() requires before including a source in
|
|
92
|
+
* search origins. When valid, `contextTreeSize` counts `.md` files.
|
|
93
|
+
*/
|
|
94
|
+
export declare function getSourceStatuses(sources: Source[]): SourceStatus[];
|