byterover-cli 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
- package/dist/agent/infra/agent/agent-schemas.js +1 -0
- package/dist/agent/infra/sandbox/curate-service.js +14 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +226 -103
- package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
- package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
- package/dist/agent/infra/tools/tool-registry.js +1 -1
- package/dist/agent/infra/tools/write-guard.d.ts +11 -0
- package/dist/agent/infra/tools/write-guard.js +48 -0
- package/dist/agent/resources/prompts/system-prompt.yml +9 -0
- package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
- package/dist/agent/resources/tools/search_knowledge.txt +11 -1
- package/dist/oclif/commands/curate/index.d.ts +1 -0
- package/dist/oclif/commands/curate/index.js +19 -4
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.d.ts +1 -0
- package/dist/oclif/commands/query.js +19 -4
- package/dist/oclif/commands/search.d.ts +20 -0
- package/dist/oclif/commands/search.js +186 -0
- package/dist/oclif/commands/source/add.d.ts +12 -0
- package/dist/oclif/commands/source/add.js +42 -0
- package/dist/oclif/commands/source/index.d.ts +6 -0
- package/dist/oclif/commands/source/index.js +8 -0
- package/dist/oclif/commands/source/list.d.ts +6 -0
- package/dist/oclif/commands/source/list.js +32 -0
- package/dist/oclif/commands/source/remove.d.ts +9 -0
- package/dist/oclif/commands/source/remove.js +33 -0
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +45 -6
- package/dist/oclif/commands/worktree/add.d.ts +12 -0
- package/dist/oclif/commands/worktree/add.js +44 -0
- package/dist/oclif/commands/worktree/index.d.ts +6 -0
- package/dist/oclif/commands/worktree/index.js +8 -0
- package/dist/oclif/commands/worktree/list.d.ts +6 -0
- package/dist/oclif/commands/worktree/list.js +28 -0
- package/dist/oclif/commands/worktree/remove.d.ts +9 -0
- package/dist/oclif/commands/worktree/remove.js +35 -0
- package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
- package/dist/oclif/lib/daemon-client.d.ts +4 -2
- package/dist/oclif/lib/daemon-client.js +19 -4
- package/dist/oclif/lib/search-format.d.ts +10 -0
- package/dist/oclif/lib/search-format.js +25 -0
- package/dist/oclif/lib/task-client.d.ts +6 -0
- package/dist/oclif/lib/task-client.js +10 -3
- package/dist/server/constants.d.ts +7 -1
- package/dist/server/constants.js +10 -0
- package/dist/server/core/domain/client/client-info.d.ts +7 -0
- package/dist/server/core/domain/client/client-info.js +11 -0
- package/dist/server/core/domain/errors/task-error.d.ts +2 -2
- package/dist/server/core/domain/errors/task-error.js +5 -4
- package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
- package/dist/server/core/domain/project/worktrees-schema.js +17 -0
- package/dist/server/core/domain/source/source-operations.d.ts +31 -0
- package/dist/server/core/domain/source/source-operations.js +201 -0
- package/dist/server/core/domain/source/source-schema.d.ts +94 -0
- package/dist/server/core/domain/source/source-schema.js +121 -0
- package/dist/server/core/domain/transport/schemas.d.ts +18 -10
- package/dist/server/core/domain/transport/schemas.js +7 -3
- package/dist/server/core/domain/transport/task-info.d.ts +2 -0
- package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
- package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
- package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
- package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
- package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
- package/dist/server/core/interfaces/executor/index.d.ts +1 -0
- package/dist/server/core/interfaces/executor/index.js +1 -0
- package/dist/server/infra/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/daemon/agent-process.js +35 -12
- package/dist/server/infra/executor/curate-executor.js +4 -2
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +23 -12
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +115 -21
- package/dist/server/infra/executor/search-executor.d.ts +17 -0
- package/dist/server/infra/executor/search-executor.js +30 -0
- package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
- package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
- package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
- package/dist/server/infra/mcp/mcp-server.js +11 -6
- package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
- package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
- package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
- package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
- package/dist/server/infra/process/connection-coordinator.js +11 -0
- package/dist/server/infra/process/feature-handlers.js +4 -1
- package/dist/server/infra/process/task-router.d.ts +1 -0
- package/dist/server/infra/process/task-router.js +60 -5
- package/dist/server/infra/project/resolve-project.d.ts +106 -0
- package/dist/server/infra/project/resolve-project.js +473 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
- package/dist/server/infra/transport/handlers/push-handler.js +3 -3
- package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/source-handler.js +37 -0
- package/dist/server/infra/transport/handlers/status-handler.js +76 -27
- package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
- package/dist/server/infra/transport/transport-connector.d.ts +10 -4
- package/dist/server/infra/transport/transport-connector.js +2 -2
- package/dist/server/templates/skill/SKILL.md +25 -5
- package/dist/server/utils/path-utils.d.ts +5 -0
- package/dist/server/utils/path-utils.js +11 -1
- package/dist/shared/transport/events/client-events.d.ts +3 -0
- package/dist/shared/transport/events/client-events.js +3 -0
- package/dist/shared/transport/events/index.d.ts +13 -0
- package/dist/shared/transport/events/index.js +9 -0
- package/dist/shared/transport/events/source-events.d.ts +30 -0
- package/dist/shared/transport/events/source-events.js +5 -0
- package/dist/shared/transport/events/status-events.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +4 -1
- package/dist/shared/transport/events/worktree-events.d.ts +31 -0
- package/dist/shared/transport/events/worktree-events.js +5 -0
- package/dist/shared/transport/search-content.d.ts +28 -0
- package/dist/shared/transport/search-content.js +38 -0
- package/dist/shared/transport/types/dto.d.ts +20 -1
- package/dist/tui/features/commands/definitions/index.js +6 -0
- package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-add.js +48 -0
- package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-list.js +47 -0
- package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-remove.js +38 -0
- package/dist/tui/features/commands/definitions/source.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source.js +8 -0
- package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
- package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
- package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
- package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree.js +8 -0
- package/dist/tui/features/curate/api/create-curate-task.js +3 -1
- package/dist/tui/features/query/api/create-query-task.js +3 -1
- package/dist/tui/features/source/api/source-api.d.ts +4 -0
- package/dist/tui/features/source/api/source-api.js +22 -0
- package/dist/tui/features/status/api/get-status.js +2 -1
- package/dist/tui/features/status/utils/format-status.js +28 -1
- package/dist/tui/features/transport/components/transport-initializer.js +36 -1
- package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
- package/dist/tui/features/worktree/api/worktree-api.js +22 -0
- package/dist/tui/repl-startup.d.ts +2 -0
- package/dist/tui/repl-startup.js +5 -3
- package/dist/tui/stores/transport-store.d.ts +6 -0
- package/dist/tui/stores/transport-store.js +6 -0
- package/dist/tui/utils/error-messages.js +2 -2
- package/oclif.manifest.json +380 -36
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { EnvironmentContext } from '../../../core/domain/environment/types.js';
|
|
1
2
|
import type { Tool } from '../../../core/domain/tools/types.js';
|
|
2
3
|
import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
|
|
3
4
|
/**
|
|
@@ -9,4 +10,4 @@ import type { IFileSystem } from '../../../core/interfaces/i-file-system.js';
|
|
|
9
10
|
* @param fileSystemService - File system service dependency
|
|
10
11
|
* @returns Configured write file tool
|
|
11
12
|
*/
|
|
12
|
-
export declare function createWriteFileTool(fileSystemService: IFileSystem): Tool;
|
|
13
|
+
export declare function createWriteFileTool(fileSystemService: IFileSystem, environmentContext?: EnvironmentContext): Tool;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { sanitizeFolderName } from '../../../../server/utils/file-helpers.js';
|
|
3
3
|
import { ToolName } from '../../../core/domain/tools/constants.js';
|
|
4
|
+
import { validateWriteTarget } from '../write-guard.js';
|
|
4
5
|
/**
|
|
5
6
|
* Input schema for write file tool.
|
|
6
7
|
*/
|
|
@@ -29,13 +30,26 @@ const WriteFileInputSchema = z
|
|
|
29
30
|
* @param fileSystemService - File system service dependency
|
|
30
31
|
* @returns Configured write file tool
|
|
31
32
|
*/
|
|
32
|
-
export function createWriteFileTool(fileSystemService) {
|
|
33
|
+
export function createWriteFileTool(fileSystemService, environmentContext) {
|
|
33
34
|
return {
|
|
34
35
|
description: 'Write content to a file. Overwrites existing files. Can optionally create parent directories.',
|
|
35
36
|
async execute(input, _context) {
|
|
36
37
|
const { content, createDirs, encoding, filePath } = input;
|
|
38
|
+
const sanitizedPath = sanitizeFolderName(filePath);
|
|
39
|
+
// Write guard: block writes to shared source context trees
|
|
40
|
+
if (environmentContext?.workingDirectory) {
|
|
41
|
+
const writeError = validateWriteTarget(sanitizedPath, environmentContext.workingDirectory);
|
|
42
|
+
if (writeError) {
|
|
43
|
+
return {
|
|
44
|
+
bytesWritten: 0,
|
|
45
|
+
error: writeError,
|
|
46
|
+
path: sanitizedPath,
|
|
47
|
+
success: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
// Call file system service
|
|
38
|
-
const result = await fileSystemService.writeFile(
|
|
52
|
+
const result = await fileSystemService.writeFile(sanitizedPath, content, {
|
|
39
53
|
createDirs,
|
|
40
54
|
encoding: encoding,
|
|
41
55
|
});
|
|
@@ -144,7 +144,7 @@ export const TOOL_REGISTRY = {
|
|
|
144
144
|
},
|
|
145
145
|
[ToolName.WRITE_FILE]: {
|
|
146
146
|
descriptionFile: 'write_file',
|
|
147
|
-
factory: (services) => createWriteFileTool(getRequiredService(services.fileSystemService, 'fileSystemService')),
|
|
147
|
+
factory: (services) => createWriteFileTool(getRequiredService(services.fileSystemService, 'fileSystemService'), services.environmentContext),
|
|
148
148
|
markers: [ToolMarker.Modification],
|
|
149
149
|
requiredServices: ['fileSystemService'],
|
|
150
150
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that a write target path is within the local project's context tree,
|
|
3
|
+
* not inside any shared knowledge source's context tree.
|
|
4
|
+
*
|
|
5
|
+
* Shared sources are read-only — agents must never write to them.
|
|
6
|
+
*
|
|
7
|
+
* @param targetPath - Absolute path being written to
|
|
8
|
+
* @param projectRoot - The local project root (owns .brv/)
|
|
9
|
+
* @returns null if write is allowed, or an error message string if blocked
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateWriteTarget(targetPath: string, projectRoot: string): null | string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { BRV_DIR, CONTEXT_TREE_DIR } from '../../../server/constants.js';
|
|
4
|
+
import { loadSources } from '../../../server/core/domain/source/source-schema.js';
|
|
5
|
+
import { isDescendantOf } from '../../../server/utils/path-utils.js';
|
|
6
|
+
const canonicalize = (path) => {
|
|
7
|
+
try {
|
|
8
|
+
return realpathSync(resolve(path));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// For non-existent files, try canonicalizing the parent
|
|
12
|
+
try {
|
|
13
|
+
const parent = dirname(resolve(path));
|
|
14
|
+
return join(realpathSync(parent), basename(path));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return resolve(path);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validates that a write target path is within the local project's context tree,
|
|
23
|
+
* not inside any shared knowledge source's context tree.
|
|
24
|
+
*
|
|
25
|
+
* Shared sources are read-only — agents must never write to them.
|
|
26
|
+
*
|
|
27
|
+
* @param targetPath - Absolute path being written to
|
|
28
|
+
* @param projectRoot - The local project root (owns .brv/)
|
|
29
|
+
* @returns null if write is allowed, or an error message string if blocked
|
|
30
|
+
*/
|
|
31
|
+
export function validateWriteTarget(targetPath, projectRoot) {
|
|
32
|
+
const localContextTree = resolve(projectRoot, BRV_DIR, CONTEXT_TREE_DIR);
|
|
33
|
+
const canonicalLocalContextTree = canonicalize(localContextTree);
|
|
34
|
+
const canonicalTarget = canonicalize(targetPath);
|
|
35
|
+
if (isDescendantOf(canonicalTarget, canonicalLocalContextTree)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// Load sources to get shared context tree roots
|
|
39
|
+
const loaded = loadSources(projectRoot);
|
|
40
|
+
for (const origin of loaded?.origins ?? []) {
|
|
41
|
+
const canonicalSharedRoot = canonicalize(origin.contextTreeRoot);
|
|
42
|
+
if (isDescendantOf(canonicalTarget, canonicalSharedRoot)) {
|
|
43
|
+
const alias = origin.alias ?? origin.originKey;
|
|
44
|
+
return `Cannot write to shared source "${alias}" — sources are read-only. Only the local context tree (${localContextTree}) is writable.`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `Cannot write outside the local context tree. Only the local context tree (${localContextTree}) is writable.`;
|
|
48
|
+
}
|
|
@@ -38,6 +38,15 @@ prompt: |
|
|
|
38
38
|
- Maximum depth: 2 levels (domain → topic → subtopic)
|
|
39
39
|
</DIRECTORIES>
|
|
40
40
|
|
|
41
|
+
<KNOWLEDGE_SOURCES>
|
|
42
|
+
This project may have knowledge sources pointing to other projects' context trees (read-only).
|
|
43
|
+
- Search results may include `origin: "shared"` results from shared sources
|
|
44
|
+
- Shared results include `originContextTreeRoot` — use `join(originContextTreeRoot, path)` for `readFile`
|
|
45
|
+
- Local results get a slight relevance boost over shared results
|
|
46
|
+
- You CANNOT curate, write, or modify shared source context trees — they are read-only
|
|
47
|
+
- Only the local `.brv/context-tree/` is writable
|
|
48
|
+
</KNOWLEDGE_SOURCES>
|
|
49
|
+
|
|
41
50
|
<TOOL_QUICK_REFERENCE>
|
|
42
51
|
**Queries:** `code_exec` with `tools.*` SDK (searchKnowledge, glob, grep, readFile, listDirectory)
|
|
43
52
|
**Curation:** `code_exec` with `tools.curate()` using UPSERT (preferred), ADD, UPDATE, MERGE, DELETE
|
|
@@ -14,6 +14,10 @@ When searching the knowledge base, you may encounter results with `symbolKind: "
|
|
|
14
14
|
- `fullContent`: Complete original content of the archived entry
|
|
15
15
|
- `tokenCount`: Estimated token count of the full content
|
|
16
16
|
|
|
17
|
+
**Shared-source results:**
|
|
18
|
+
- For shared results (`origin: "shared"`), the `stubPath` is relative to the shared context tree
|
|
19
|
+
- Use `originContextTreeRoot` from the search result to resolve the full path
|
|
20
|
+
|
|
17
21
|
**Example:**
|
|
18
22
|
- Search returns: `{ path: "_archived/auth/jwt-tokens/refresh-flow.stub.md", symbolKind: "archive_stub" }`
|
|
19
23
|
- Call: `expand_knowledge({ stubPath: "_archived/auth/jwt-tokens/refresh-flow.stub.md" })`
|
|
@@ -17,14 +17,24 @@ This tool enables semantic/fuzzy search across all curated knowledge without nee
|
|
|
17
17
|
- `title`: Topic title
|
|
18
18
|
- `excerpt`: Relevant content snippet
|
|
19
19
|
- `score`: Relevance score (higher is better)
|
|
20
|
+
- `origin`: `"local"` or `"shared"` (shared = from a knowledge source pointing at another project)
|
|
21
|
+
- `originAlias`: Alias of the shared source (only for shared results)
|
|
22
|
+
- `originContextTreeRoot`: Absolute path to the shared context tree (only for shared results). Use `join(originContextTreeRoot, path)` to read full content.
|
|
20
23
|
- `totalFound`: Total number of matches
|
|
21
24
|
- `message`: Status message
|
|
22
25
|
|
|
26
|
+
**Knowledge sources:**
|
|
27
|
+
- Results may include entries from shared knowledge sources (read-only)
|
|
28
|
+
- Shared results have `origin: "shared"` and `originContextTreeRoot` for reading
|
|
29
|
+
- Local results are boosted slightly in relevance scoring
|
|
30
|
+
- You cannot curate or write to shared source context trees
|
|
31
|
+
|
|
23
32
|
**Usage tips:**
|
|
24
33
|
- Use descriptive queries: "authentication flow" works better than "auth"
|
|
25
34
|
- Search is fuzzy: minor typos are tolerated
|
|
26
35
|
- Results are ranked by relevance to your query
|
|
27
|
-
-
|
|
36
|
+
- For local results, use `read_file` on returned paths to view full content
|
|
37
|
+
- For shared results, use `read_file` on `join(originContextTreeRoot, path)`
|
|
28
38
|
|
|
29
39
|
**Examples:**
|
|
30
40
|
- Query: "API authentication" - finds topics about auth design
|
|
@@ -11,6 +11,7 @@ export default class Curate extends Command {
|
|
|
11
11
|
files: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
folder: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
};
|
|
15
16
|
protected getDaemonClientOptions(): DaemonClientOptions;
|
|
16
17
|
run(): Promise<void>;
|
|
@@ -6,7 +6,7 @@ import { extractCurateOperations } from '../../../server/utils/curate-result-par
|
|
|
6
6
|
import { TaskEvents } from '../../../shared/transport/events/index.js';
|
|
7
7
|
import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../../lib/daemon-client.js';
|
|
8
8
|
import { writeJsonResponse } from '../../lib/json-response.js';
|
|
9
|
-
import { waitForTaskCompletion } from '../../lib/task-client.js';
|
|
9
|
+
import { DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, waitForTaskCompletion } from '../../lib/task-client.js';
|
|
10
10
|
export default class Curate extends Command {
|
|
11
11
|
static args = {
|
|
12
12
|
context: Args.string({
|
|
@@ -38,6 +38,9 @@ Bad examples:
|
|
|
38
38
|
'# Folder pack with context',
|
|
39
39
|
'<%= config.bin %> <%= command.id %> "Analyze authentication module" -d src/auth/',
|
|
40
40
|
'',
|
|
41
|
+
'# Increase timeout for slow models (in seconds)',
|
|
42
|
+
'<%= config.bin %> <%= command.id %> "context here" --timeout 600',
|
|
43
|
+
'',
|
|
41
44
|
'# View curate history',
|
|
42
45
|
'<%= config.bin %> curate view',
|
|
43
46
|
'<%= config.bin %> curate view --status completed --since 1h',
|
|
@@ -62,6 +65,12 @@ Bad examples:
|
|
|
62
65
|
description: 'Output format (text or json)',
|
|
63
66
|
options: ['text', 'json'],
|
|
64
67
|
}),
|
|
68
|
+
timeout: Flags.integer({
|
|
69
|
+
default: DEFAULT_TIMEOUT_SECONDS,
|
|
70
|
+
description: 'Maximum seconds to wait for task completion',
|
|
71
|
+
max: MAX_TIMEOUT_SECONDS,
|
|
72
|
+
min: MIN_TIMEOUT_SECONDS,
|
|
73
|
+
}),
|
|
65
74
|
};
|
|
66
75
|
getDaemonClientOptions() {
|
|
67
76
|
return {};
|
|
@@ -73,6 +82,7 @@ Bad examples:
|
|
|
73
82
|
files: rawFlags.files,
|
|
74
83
|
folder: rawFlags.folder,
|
|
75
84
|
format: rawFlags.format === 'json' ? 'json' : rawFlags.format === 'text' ? 'text' : undefined,
|
|
85
|
+
timeout: rawFlags.timeout,
|
|
76
86
|
};
|
|
77
87
|
const format = flags.format ?? 'text';
|
|
78
88
|
if (!this.validateInput(args, flags, format))
|
|
@@ -85,7 +95,7 @@ Bad examples:
|
|
|
85
95
|
const taskType = flags.folder?.length ? 'curate-folder' : 'curate';
|
|
86
96
|
let providerContext;
|
|
87
97
|
try {
|
|
88
|
-
await withDaemonRetry(async (client, projectRoot) => {
|
|
98
|
+
await withDaemonRetry(async (client, projectRoot, worktreeRoot) => {
|
|
89
99
|
const active = await client.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
|
|
90
100
|
providerContext = { activeModel: active.activeModel, activeProvider: active.activeProvider };
|
|
91
101
|
if (!active.activeProvider) {
|
|
@@ -94,7 +104,7 @@ Bad examples:
|
|
|
94
104
|
if (active.providerKeyMissing) {
|
|
95
105
|
throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
|
|
96
106
|
}
|
|
97
|
-
await this.submitTask({ client, content: resolvedContent, flags, format, projectRoot, taskType });
|
|
107
|
+
await this.submitTask({ client, content: resolvedContent, flags, format, projectRoot, taskType, worktreeRoot });
|
|
98
108
|
}, {
|
|
99
109
|
...this.getDaemonClientOptions(),
|
|
100
110
|
onRetry: format === 'text'
|
|
@@ -221,7 +231,7 @@ Bad examples:
|
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
async submitTask(props) {
|
|
224
|
-
const { client, content, flags, format, projectRoot, taskType } = props;
|
|
234
|
+
const { client, content, flags, format, projectRoot, taskType, worktreeRoot } = props;
|
|
225
235
|
const hasFolders = Boolean(flags.folder?.length);
|
|
226
236
|
const taskId = randomUUID();
|
|
227
237
|
const taskPayload = {
|
|
@@ -232,8 +242,12 @@ Bad examples:
|
|
|
232
242
|
...(projectRoot ? { projectPath: projectRoot } : {}),
|
|
233
243
|
taskId,
|
|
234
244
|
type: taskType,
|
|
245
|
+
...(worktreeRoot ? { worktreeRoot } : {}),
|
|
235
246
|
};
|
|
236
247
|
if (flags.detach) {
|
|
248
|
+
if (flags.timeout !== DEFAULT_TIMEOUT_SECONDS && format !== 'json') {
|
|
249
|
+
this.log('Note: --timeout has no effect with --detach');
|
|
250
|
+
}
|
|
237
251
|
const ack = await client.requestWithAck(TaskEvents.CREATE, taskPayload);
|
|
238
252
|
const { logId } = ack;
|
|
239
253
|
if (format === 'json') {
|
|
@@ -298,6 +312,7 @@ Bad examples:
|
|
|
298
312
|
}
|
|
299
313
|
},
|
|
300
314
|
taskId,
|
|
315
|
+
timeoutMs: (flags.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
|
|
301
316
|
}, (msg) => this.log(msg));
|
|
302
317
|
await client.requestWithAck(TaskEvents.CREATE, taskPayload);
|
|
303
318
|
await completionPromise;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { findProjectRoot } from '@campfirein/brv-transport-client';
|
|
2
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { resolveProject } from '../../../server/infra/project/resolve-project.js';
|
|
3
3
|
import { FileCurateLogStore } from '../../../server/infra/storage/file-curate-log-store.js';
|
|
4
4
|
import { CurateLogUseCase } from '../../../server/infra/usecase/curate-log-use-case.js';
|
|
5
5
|
import { getProjectDataDir } from '../../../server/utils/path-utils.js';
|
|
@@ -77,7 +77,7 @@ export default class CurateView extends Command {
|
|
|
77
77
|
const after = flags.since ? this.parseTime(flags.since, '--since') : undefined;
|
|
78
78
|
const before = flags.before ? this.parseTime(flags.before, '--before') : undefined;
|
|
79
79
|
const format = flags.format === 'json' ? 'json' : 'text';
|
|
80
|
-
const projectRoot = (
|
|
80
|
+
const projectRoot = resolveProject()?.projectRoot ?? process.cwd();
|
|
81
81
|
const baseDir = getProjectDataDir(projectRoot);
|
|
82
82
|
const store = new FileCurateLogStore({ baseDir });
|
|
83
83
|
const useCase = new CurateLogUseCase({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ensureDaemonRunning } from '@campfirein/brv-transport-client';
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
|
+
import { resolveProject } from '../../server/infra/project/resolve-project.js';
|
|
3
4
|
import { resolveLocalServerMainPath } from '../../server/utils/server-main-resolver.js';
|
|
4
5
|
import { startRepl } from '../../tui/repl-startup.js';
|
|
5
6
|
/**
|
|
@@ -31,9 +32,21 @@ export default class Main extends Command {
|
|
|
31
32
|
const detail = daemonResult.spawnError ? `: ${daemonResult.spawnError}` : '';
|
|
32
33
|
this.error(`Failed to start daemon: timed out waiting for daemon to become ready${detail}\n\nRun 'brv restart' to force a clean restart.`);
|
|
33
34
|
}
|
|
35
|
+
// Resolve project (workspace-link-aware) before starting TUI.
|
|
36
|
+
// Gracefully handle broken/malformed workspace links so TUI still starts
|
|
37
|
+
// (user can fix via /worktree remove from within the REPL).
|
|
38
|
+
let resolution = null;
|
|
39
|
+
try {
|
|
40
|
+
resolution = resolveProject();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Broken workspace link — start TUI without resolved project (falls back to cwd)
|
|
44
|
+
}
|
|
34
45
|
// Start the interactive REPL (TUI connects via connectToDaemon internally)
|
|
35
46
|
await startRepl({
|
|
47
|
+
projectPath: resolution?.projectRoot,
|
|
36
48
|
version: this.config.version,
|
|
49
|
+
worktreeRoot: resolution?.worktreeRoot,
|
|
37
50
|
});
|
|
38
51
|
this.exit(0);
|
|
39
52
|
}
|
|
@@ -8,6 +8,7 @@ export default class Query extends Command {
|
|
|
8
8
|
static examples: string[];
|
|
9
9
|
static flags: {
|
|
10
10
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
};
|
|
12
13
|
static strict: boolean;
|
|
13
14
|
protected getDaemonClientOptions(): DaemonClientOptions;
|
|
@@ -4,7 +4,7 @@ import { TransportStateEventNames } from '../../server/core/domain/transport/sch
|
|
|
4
4
|
import { TaskEvents } from '../../shared/transport/events/index.js';
|
|
5
5
|
import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../lib/daemon-client.js';
|
|
6
6
|
import { writeJsonResponse } from '../lib/json-response.js';
|
|
7
|
-
import { waitForTaskCompletion } from '../lib/task-client.js';
|
|
7
|
+
import { DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, waitForTaskCompletion } from '../lib/task-client.js';
|
|
8
8
|
export default class Query extends Command {
|
|
9
9
|
static args = {
|
|
10
10
|
query: Args.string({
|
|
@@ -34,6 +34,12 @@ Bad:
|
|
|
34
34
|
description: 'Output format (text or json)',
|
|
35
35
|
options: ['text', 'json'],
|
|
36
36
|
}),
|
|
37
|
+
timeout: Flags.integer({
|
|
38
|
+
default: DEFAULT_TIMEOUT_SECONDS,
|
|
39
|
+
description: 'Maximum seconds to wait for task completion',
|
|
40
|
+
max: MAX_TIMEOUT_SECONDS,
|
|
41
|
+
min: MIN_TIMEOUT_SECONDS,
|
|
42
|
+
}),
|
|
37
43
|
};
|
|
38
44
|
static strict = false;
|
|
39
45
|
getDaemonClientOptions() {
|
|
@@ -47,7 +53,7 @@ Bad:
|
|
|
47
53
|
return;
|
|
48
54
|
let providerContext;
|
|
49
55
|
try {
|
|
50
|
-
await withDaemonRetry(async (client, projectRoot) => {
|
|
56
|
+
await withDaemonRetry(async (client, projectRoot, worktreeRoot) => {
|
|
51
57
|
const active = await client.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
|
|
52
58
|
providerContext = { activeModel: active.activeModel, activeProvider: active.activeProvider };
|
|
53
59
|
if (!active.activeProvider) {
|
|
@@ -56,7 +62,14 @@ Bad:
|
|
|
56
62
|
if (active.providerKeyMissing) {
|
|
57
63
|
throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
|
|
58
64
|
}
|
|
59
|
-
await this.submitTask({
|
|
65
|
+
await this.submitTask({
|
|
66
|
+
client,
|
|
67
|
+
format,
|
|
68
|
+
projectRoot,
|
|
69
|
+
query: args.query,
|
|
70
|
+
timeoutMs: (flags.timeout ?? DEFAULT_TIMEOUT_SECONDS) * 1000,
|
|
71
|
+
worktreeRoot,
|
|
72
|
+
});
|
|
60
73
|
}, {
|
|
61
74
|
...this.getDaemonClientOptions(),
|
|
62
75
|
onRetry: format === 'text'
|
|
@@ -82,7 +95,7 @@ Bad:
|
|
|
82
95
|
}
|
|
83
96
|
}
|
|
84
97
|
async submitTask(props) {
|
|
85
|
-
const { client, format, projectRoot, query } = props;
|
|
98
|
+
const { client, format, projectRoot, query, timeoutMs, worktreeRoot } = props;
|
|
86
99
|
const taskId = randomUUID();
|
|
87
100
|
const taskPayload = {
|
|
88
101
|
clientCwd: process.cwd(),
|
|
@@ -90,6 +103,7 @@ Bad:
|
|
|
90
103
|
...(projectRoot ? { projectPath: projectRoot } : {}),
|
|
91
104
|
taskId,
|
|
92
105
|
type: 'query',
|
|
106
|
+
...(worktreeRoot ? { worktreeRoot } : {}),
|
|
93
107
|
};
|
|
94
108
|
let finalResult;
|
|
95
109
|
const completionPromise = waitForTaskCompletion({
|
|
@@ -155,6 +169,7 @@ Bad:
|
|
|
155
169
|
}
|
|
156
170
|
},
|
|
157
171
|
taskId,
|
|
172
|
+
timeoutMs,
|
|
158
173
|
}, (msg) => this.log(msg));
|
|
159
174
|
await client.requestWithAck(TaskEvents.CREATE, taskPayload);
|
|
160
175
|
await completionPromise;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { type DaemonClientOptions } from '../lib/daemon-client.js';
|
|
3
|
+
export default class Search extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
query: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static examples: string[];
|
|
9
|
+
static flags: {
|
|
10
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
scope: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
static strict: boolean;
|
|
15
|
+
protected getDaemonClientOptions(): DaemonClientOptions;
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private reportError;
|
|
18
|
+
private submitTask;
|
|
19
|
+
private validateInput;
|
|
20
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { TaskEvents } from '../../shared/transport/events/index.js';
|
|
4
|
+
import { encodeSearchContent } from '../../shared/transport/search-content.js';
|
|
5
|
+
import { formatConnectionError, hasLeakedHandles, withDaemonRetry, } from '../lib/daemon-client.js';
|
|
6
|
+
import { writeJsonResponse } from '../lib/json-response.js';
|
|
7
|
+
import { formatSearchTextOutput } from '../lib/search-format.js';
|
|
8
|
+
import { waitForTaskCompletion } from '../lib/task-client.js';
|
|
9
|
+
export default class Search extends Command {
|
|
10
|
+
static args = {
|
|
11
|
+
query: Args.string({
|
|
12
|
+
description: 'Search query to find relevant knowledge in the context tree',
|
|
13
|
+
required: true,
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
static description = `Search the context tree for relevant knowledge
|
|
17
|
+
|
|
18
|
+
Returns ranked results with paths, scores, and excerpts.
|
|
19
|
+
Pure BM25 retrieval — no LLM, no token cost.
|
|
20
|
+
|
|
21
|
+
Use this for structured results with file paths.
|
|
22
|
+
Use "brv query" when you need a synthesized answer.`;
|
|
23
|
+
static examples = [
|
|
24
|
+
'# Search for knowledge about authentication',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> "authentication"',
|
|
26
|
+
'',
|
|
27
|
+
'# Limit results and scope to a domain',
|
|
28
|
+
'<%= config.bin %> <%= command.id %> "JWT tokens" --limit 5 --scope auth/',
|
|
29
|
+
'',
|
|
30
|
+
'# JSON output (for automation)',
|
|
31
|
+
'<%= config.bin %> <%= command.id %> "auth" --format json',
|
|
32
|
+
];
|
|
33
|
+
static flags = {
|
|
34
|
+
format: Flags.string({
|
|
35
|
+
default: 'text',
|
|
36
|
+
description: 'Output format (text or json)',
|
|
37
|
+
options: ['text', 'json'],
|
|
38
|
+
}),
|
|
39
|
+
limit: Flags.integer({
|
|
40
|
+
default: 10,
|
|
41
|
+
description: 'Maximum number of results (1-50)',
|
|
42
|
+
max: 50,
|
|
43
|
+
min: 1,
|
|
44
|
+
}),
|
|
45
|
+
scope: Flags.string({
|
|
46
|
+
description: 'Path prefix to scope results (e.g. "auth/" for auth domain only)',
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
static strict = false;
|
|
50
|
+
getDaemonClientOptions() {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
async run() {
|
|
54
|
+
const { args, flags } = await this.parse(Search);
|
|
55
|
+
const format = flags.format === 'json' ? 'json' : 'text';
|
|
56
|
+
if (!this.validateInput(args.query, format))
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
await withDaemonRetry(async (client, projectRoot, worktreeRoot) => {
|
|
60
|
+
// No provider validation — search is pure BM25, no LLM needed.
|
|
61
|
+
await this.submitTask({
|
|
62
|
+
client,
|
|
63
|
+
format,
|
|
64
|
+
limit: flags.limit,
|
|
65
|
+
projectRoot,
|
|
66
|
+
query: args.query,
|
|
67
|
+
scope: flags.scope,
|
|
68
|
+
worktreeRoot,
|
|
69
|
+
});
|
|
70
|
+
}, {
|
|
71
|
+
...this.getDaemonClientOptions(),
|
|
72
|
+
onRetry: format === 'text'
|
|
73
|
+
? (attempt, maxRetries) => this.log(`\nConnection lost. Restarting daemon... (attempt ${attempt}/${maxRetries})`)
|
|
74
|
+
: undefined,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.reportError(error, format);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
reportError(error, format) {
|
|
82
|
+
const errorMessage = error instanceof Error ? error.message : 'Search failed';
|
|
83
|
+
if (format === 'json') {
|
|
84
|
+
writeJsonResponse({ command: 'search', data: { error: errorMessage, status: 'error' }, success: false });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.log(formatConnectionError(error));
|
|
88
|
+
}
|
|
89
|
+
if (hasLeakedHandles(error)) {
|
|
90
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async submitTask(props) {
|
|
95
|
+
const { client, format, projectRoot, query, worktreeRoot } = props;
|
|
96
|
+
const taskId = randomUUID();
|
|
97
|
+
const contentPayload = encodeSearchContent({ limit: props.limit, query, scope: props.scope });
|
|
98
|
+
const taskPayload = {
|
|
99
|
+
clientCwd: process.cwd(),
|
|
100
|
+
content: contentPayload,
|
|
101
|
+
...(projectRoot ? { projectPath: projectRoot } : {}),
|
|
102
|
+
taskId,
|
|
103
|
+
type: 'search',
|
|
104
|
+
...(worktreeRoot ? { worktreeRoot } : {}),
|
|
105
|
+
};
|
|
106
|
+
const completionPromise = waitForTaskCompletion({
|
|
107
|
+
client,
|
|
108
|
+
command: 'search',
|
|
109
|
+
format,
|
|
110
|
+
onCompleted: ({ result }) => {
|
|
111
|
+
if (!result) {
|
|
112
|
+
if (format === 'json') {
|
|
113
|
+
writeJsonResponse({
|
|
114
|
+
command: 'search',
|
|
115
|
+
data: { results: [], status: 'completed', totalFound: 0 },
|
|
116
|
+
success: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.log('\nNo results.\n');
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const searchResult = JSON.parse(result);
|
|
126
|
+
if (format === 'json') {
|
|
127
|
+
writeJsonResponse({
|
|
128
|
+
command: 'search',
|
|
129
|
+
data: {
|
|
130
|
+
...searchResult,
|
|
131
|
+
status: 'completed',
|
|
132
|
+
},
|
|
133
|
+
success: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
for (const line of formatSearchTextOutput(searchResult)) {
|
|
138
|
+
this.log(line);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Fallback: result isn't valid JSON — display as-is
|
|
144
|
+
if (format === 'json') {
|
|
145
|
+
writeJsonResponse({
|
|
146
|
+
command: 'search',
|
|
147
|
+
data: { error: 'Invalid search result format', raw: result, status: 'error' },
|
|
148
|
+
success: false,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.log(`\n${result}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
onError({ error }) {
|
|
157
|
+
if (format === 'json') {
|
|
158
|
+
writeJsonResponse({
|
|
159
|
+
command: 'search',
|
|
160
|
+
data: { event: 'error', message: error.message, status: 'error' },
|
|
161
|
+
success: false,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
taskId,
|
|
166
|
+
}, (msg) => this.log(msg));
|
|
167
|
+
await client.requestWithAck(TaskEvents.CREATE, taskPayload);
|
|
168
|
+
await completionPromise;
|
|
169
|
+
}
|
|
170
|
+
validateInput(query, format) {
|
|
171
|
+
if (query.trim())
|
|
172
|
+
return true;
|
|
173
|
+
if (format === 'json') {
|
|
174
|
+
writeJsonResponse({
|
|
175
|
+
command: 'search',
|
|
176
|
+
data: { message: 'Search query is required.', status: 'error' },
|
|
177
|
+
success: false,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.log('Search query is required.');
|
|
182
|
+
this.log('Usage: brv search "your query here"');
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class SourceAdd extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
alias: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { SourceEvents } from '../../../shared/transport/events/source-events.js';
|
|
4
|
+
import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
|
|
5
|
+
export default class SourceAdd extends Command {
|
|
6
|
+
static args = {
|
|
7
|
+
path: Args.string({
|
|
8
|
+
description: 'Path to the target project containing .brv/',
|
|
9
|
+
required: true,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = "Add a read-only knowledge source from another project's context tree";
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> <%= command.id %> /path/to/shared-lib',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> /path/to/shared-lib --alias shared',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
alias: Flags.string({
|
|
19
|
+
description: 'Custom alias for the source (defaults to directory name)',
|
|
20
|
+
required: false,
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args, flags } = await this.parse(SourceAdd);
|
|
25
|
+
const targetPath = resolve(args.path);
|
|
26
|
+
try {
|
|
27
|
+
const result = await withDaemonRetry(async (client) => client.requestWithAck(SourceEvents.ADD, {
|
|
28
|
+
alias: flags.alias,
|
|
29
|
+
targetPath,
|
|
30
|
+
}), { projectPath: process.cwd() });
|
|
31
|
+
if (result.success) {
|
|
32
|
+
this.log(result.message);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.error(result.message, { exit: 1 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
this.error(formatConnectionError(error), { exit: 1 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|