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.
Files changed (158) hide show
  1. package/README.md +17 -0
  2. package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
  3. package/dist/agent/infra/agent/agent-schemas.js +1 -0
  4. package/dist/agent/infra/sandbox/curate-service.js +14 -0
  5. package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
  6. package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
  7. package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
  8. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +226 -103
  9. package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
  10. package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
  11. package/dist/agent/infra/tools/tool-registry.js +1 -1
  12. package/dist/agent/infra/tools/write-guard.d.ts +11 -0
  13. package/dist/agent/infra/tools/write-guard.js +48 -0
  14. package/dist/agent/resources/prompts/system-prompt.yml +9 -0
  15. package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
  16. package/dist/agent/resources/tools/search_knowledge.txt +11 -1
  17. package/dist/oclif/commands/curate/index.d.ts +1 -0
  18. package/dist/oclif/commands/curate/index.js +19 -4
  19. package/dist/oclif/commands/curate/view.js +2 -2
  20. package/dist/oclif/commands/main.js +13 -0
  21. package/dist/oclif/commands/query.d.ts +1 -0
  22. package/dist/oclif/commands/query.js +19 -4
  23. package/dist/oclif/commands/search.d.ts +20 -0
  24. package/dist/oclif/commands/search.js +186 -0
  25. package/dist/oclif/commands/source/add.d.ts +12 -0
  26. package/dist/oclif/commands/source/add.js +42 -0
  27. package/dist/oclif/commands/source/index.d.ts +6 -0
  28. package/dist/oclif/commands/source/index.js +8 -0
  29. package/dist/oclif/commands/source/list.d.ts +6 -0
  30. package/dist/oclif/commands/source/list.js +32 -0
  31. package/dist/oclif/commands/source/remove.d.ts +9 -0
  32. package/dist/oclif/commands/source/remove.js +33 -0
  33. package/dist/oclif/commands/status.d.ts +5 -1
  34. package/dist/oclif/commands/status.js +45 -6
  35. package/dist/oclif/commands/worktree/add.d.ts +12 -0
  36. package/dist/oclif/commands/worktree/add.js +44 -0
  37. package/dist/oclif/commands/worktree/index.d.ts +6 -0
  38. package/dist/oclif/commands/worktree/index.js +8 -0
  39. package/dist/oclif/commands/worktree/list.d.ts +6 -0
  40. package/dist/oclif/commands/worktree/list.js +28 -0
  41. package/dist/oclif/commands/worktree/remove.d.ts +9 -0
  42. package/dist/oclif/commands/worktree/remove.js +35 -0
  43. package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
  44. package/dist/oclif/lib/daemon-client.d.ts +4 -2
  45. package/dist/oclif/lib/daemon-client.js +19 -4
  46. package/dist/oclif/lib/search-format.d.ts +10 -0
  47. package/dist/oclif/lib/search-format.js +25 -0
  48. package/dist/oclif/lib/task-client.d.ts +6 -0
  49. package/dist/oclif/lib/task-client.js +10 -3
  50. package/dist/server/constants.d.ts +7 -1
  51. package/dist/server/constants.js +10 -0
  52. package/dist/server/core/domain/client/client-info.d.ts +7 -0
  53. package/dist/server/core/domain/client/client-info.js +11 -0
  54. package/dist/server/core/domain/errors/task-error.d.ts +2 -2
  55. package/dist/server/core/domain/errors/task-error.js +5 -4
  56. package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
  57. package/dist/server/core/domain/project/worktrees-schema.js +17 -0
  58. package/dist/server/core/domain/source/source-operations.d.ts +31 -0
  59. package/dist/server/core/domain/source/source-operations.js +201 -0
  60. package/dist/server/core/domain/source/source-schema.d.ts +94 -0
  61. package/dist/server/core/domain/source/source-schema.js +121 -0
  62. package/dist/server/core/domain/transport/schemas.d.ts +18 -10
  63. package/dist/server/core/domain/transport/schemas.js +7 -3
  64. package/dist/server/core/domain/transport/task-info.d.ts +2 -0
  65. package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
  66. package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
  67. package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
  68. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
  69. package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
  70. package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
  71. package/dist/server/core/interfaces/executor/index.d.ts +1 -0
  72. package/dist/server/core/interfaces/executor/index.js +1 -0
  73. package/dist/server/infra/client/client-manager.d.ts +1 -0
  74. package/dist/server/infra/client/client-manager.js +16 -0
  75. package/dist/server/infra/daemon/agent-process.js +35 -12
  76. package/dist/server/infra/executor/curate-executor.js +4 -2
  77. package/dist/server/infra/executor/direct-search-responder.js +5 -1
  78. package/dist/server/infra/executor/folder-pack-executor.js +23 -12
  79. package/dist/server/infra/executor/query-executor.d.ts +23 -0
  80. package/dist/server/infra/executor/query-executor.js +115 -21
  81. package/dist/server/infra/executor/search-executor.d.ts +17 -0
  82. package/dist/server/infra/executor/search-executor.js +30 -0
  83. package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
  84. package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
  85. package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
  86. package/dist/server/infra/mcp/mcp-server.js +11 -6
  87. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
  88. package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
  89. package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
  90. package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
  91. package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
  92. package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
  93. package/dist/server/infra/process/connection-coordinator.js +11 -0
  94. package/dist/server/infra/process/feature-handlers.js +4 -1
  95. package/dist/server/infra/process/task-router.d.ts +1 -0
  96. package/dist/server/infra/process/task-router.js +60 -5
  97. package/dist/server/infra/project/resolve-project.d.ts +106 -0
  98. package/dist/server/infra/project/resolve-project.js +473 -0
  99. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  100. package/dist/server/infra/transport/handlers/index.js +2 -0
  101. package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
  102. package/dist/server/infra/transport/handlers/push-handler.js +3 -3
  103. package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
  104. package/dist/server/infra/transport/handlers/source-handler.js +37 -0
  105. package/dist/server/infra/transport/handlers/status-handler.js +76 -27
  106. package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
  107. package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
  108. package/dist/server/infra/transport/transport-connector.d.ts +10 -4
  109. package/dist/server/infra/transport/transport-connector.js +2 -2
  110. package/dist/server/templates/skill/SKILL.md +25 -5
  111. package/dist/server/utils/path-utils.d.ts +5 -0
  112. package/dist/server/utils/path-utils.js +11 -1
  113. package/dist/shared/transport/events/client-events.d.ts +3 -0
  114. package/dist/shared/transport/events/client-events.js +3 -0
  115. package/dist/shared/transport/events/index.d.ts +13 -0
  116. package/dist/shared/transport/events/index.js +9 -0
  117. package/dist/shared/transport/events/source-events.d.ts +30 -0
  118. package/dist/shared/transport/events/source-events.js +5 -0
  119. package/dist/shared/transport/events/status-events.d.ts +5 -0
  120. package/dist/shared/transport/events/task-events.d.ts +4 -1
  121. package/dist/shared/transport/events/worktree-events.d.ts +31 -0
  122. package/dist/shared/transport/events/worktree-events.js +5 -0
  123. package/dist/shared/transport/search-content.d.ts +28 -0
  124. package/dist/shared/transport/search-content.js +38 -0
  125. package/dist/shared/transport/types/dto.d.ts +20 -1
  126. package/dist/tui/features/commands/definitions/index.js +6 -0
  127. package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
  128. package/dist/tui/features/commands/definitions/source-add.js +48 -0
  129. package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
  130. package/dist/tui/features/commands/definitions/source-list.js +47 -0
  131. package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
  132. package/dist/tui/features/commands/definitions/source-remove.js +38 -0
  133. package/dist/tui/features/commands/definitions/source.d.ts +2 -0
  134. package/dist/tui/features/commands/definitions/source.js +8 -0
  135. package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
  136. package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
  137. package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
  138. package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
  139. package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
  140. package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
  141. package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
  142. package/dist/tui/features/commands/definitions/worktree.js +8 -0
  143. package/dist/tui/features/curate/api/create-curate-task.js +3 -1
  144. package/dist/tui/features/query/api/create-query-task.js +3 -1
  145. package/dist/tui/features/source/api/source-api.d.ts +4 -0
  146. package/dist/tui/features/source/api/source-api.js +22 -0
  147. package/dist/tui/features/status/api/get-status.js +2 -1
  148. package/dist/tui/features/status/utils/format-status.js +28 -1
  149. package/dist/tui/features/transport/components/transport-initializer.js +36 -1
  150. package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
  151. package/dist/tui/features/worktree/api/worktree-api.js +22 -0
  152. package/dist/tui/repl-startup.d.ts +2 -0
  153. package/dist/tui/repl-startup.js +5 -3
  154. package/dist/tui/stores/transport-store.d.ts +6 -0
  155. package/dist/tui/stores/transport-store.js +6 -0
  156. package/dist/tui/utils/error-messages.js +2 -2
  157. package/oclif.manifest.json +380 -36
  158. package/package.json +1 -1
@@ -7,6 +7,7 @@ export declare const TaskErrorCode: {
7
7
  readonly AGENT_NOT_AVAILABLE: "ERR_AGENT_NOT_AVAILABLE";
8
8
  readonly AGENT_NOT_INITIALIZED: "ERR_AGENT_NOT_INITIALIZED";
9
9
  readonly CONTEXT_TREE_NOT_INITIALIZED: "ERR_CONTEXT_TREE_NOT_INIT";
10
+ readonly LEGACY_SYNC_UNAVAILABLE: "ERR_LEGACY_SYNC_UNAVAILABLE";
10
11
  readonly LLM_ERROR: "ERR_LLM_ERROR";
11
12
  readonly LLM_RATE_LIMIT: "ERR_LLM_RATE_LIMIT";
12
13
  readonly LOCAL_CHANGES_EXIST: "ERR_LOCAL_CHANGES_EXIST";
@@ -15,7 +16,6 @@ export declare const TaskErrorCode: {
15
16
  readonly OAUTH_TOKEN_EXPIRED: "ERR_OAUTH_TOKEN_EXPIRED";
16
17
  readonly PROJECT_NOT_INIT: "ERR_PROJECT_NOT_INIT";
17
18
  readonly PROVIDER_NOT_CONFIGURED: "ERR_PROVIDER_NOT_CONFIGURED";
18
- readonly SPACE_NOT_CONFIGURED: "ERR_SPACE_NOT_CONFIGURED";
19
19
  readonly SPACE_NOT_FOUND: "ERR_SPACE_NOT_FOUND";
20
20
  readonly TASK_CANCELLED: "ERR_TASK_CANCELLED";
21
21
  readonly TASK_EXECUTION: "ERR_TASK_EXECUTION";
@@ -73,7 +73,7 @@ export declare class FileValidationError extends Error {
73
73
  export declare class LocalChangesExistError extends TaskError {
74
74
  constructor(message?: string);
75
75
  }
76
- export declare class SpaceNotConfiguredError extends TaskError {
76
+ export declare class LegacySyncUnavailableError extends TaskError {
77
77
  constructor();
78
78
  }
79
79
  export declare class GitVcInitializedError extends TaskError {
@@ -9,6 +9,8 @@ export const TaskErrorCode = {
9
9
  AGENT_NOT_INITIALIZED: 'ERR_AGENT_NOT_INITIALIZED',
10
10
  // Context tree errors
11
11
  CONTEXT_TREE_NOT_INITIALIZED: 'ERR_CONTEXT_TREE_NOT_INIT',
12
+ // Legacy sync (brv push/pull) no longer available because project has no team+space configured
13
+ LEGACY_SYNC_UNAVAILABLE: 'ERR_LEGACY_SYNC_UNAVAILABLE',
12
14
  // LLM errors
13
15
  LLM_ERROR: 'ERR_LLM_ERROR',
14
16
  LLM_RATE_LIMIT: 'ERR_LLM_RATE_LIMIT',
@@ -21,7 +23,6 @@ export const TaskErrorCode = {
21
23
  // Execution errors
22
24
  PROJECT_NOT_INIT: 'ERR_PROJECT_NOT_INIT',
23
25
  PROVIDER_NOT_CONFIGURED: 'ERR_PROVIDER_NOT_CONFIGURED',
24
- SPACE_NOT_CONFIGURED: 'ERR_SPACE_NOT_CONFIGURED',
25
26
  SPACE_NOT_FOUND: 'ERR_SPACE_NOT_FOUND',
26
27
  TASK_CANCELLED: 'ERR_TASK_CANCELLED',
27
28
  TASK_EXECUTION: 'ERR_TASK_EXECUTION',
@@ -153,10 +154,10 @@ export class LocalChangesExistError extends TaskError {
153
154
  this.name = 'LocalChangesExistError';
154
155
  }
155
156
  }
156
- export class SpaceNotConfiguredError extends TaskError {
157
+ export class LegacySyncUnavailableError extends TaskError {
157
158
  constructor() {
158
- super('No space configured. Run "brv space list" to see available spaces, then "brv space switch --team <team> --name <space>" to select one.', TaskErrorCode.SPACE_NOT_CONFIGURED);
159
- this.name = 'SpaceNotConfiguredError';
159
+ super('Command brv push and brv pull are deprecated. Run `brv vc init` to start using Byterover version control.', TaskErrorCode.LEGACY_SYNC_UNAVAILABLE);
160
+ this.name = 'LegacySyncUnavailableError';
160
161
  }
161
162
  }
162
163
  export class GitVcInitializedError extends TaskError {
@@ -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[];
@@ -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
+ }
@@ -514,9 +514,11 @@ export declare const TaskExecuteSchema: z.ZodObject<{
514
514
  /** Unique task identifier */
515
515
  taskId: z.ZodString;
516
516
  /** Task type */
517
- type: z.ZodEnum<["curate", "curate-folder", "query"]>;
517
+ type: z.ZodEnum<["curate", "curate-folder", "query", "search"]>;
518
+ /** Workspace root for scoped query/curate */
519
+ worktreeRoot: z.ZodOptional<z.ZodString>;
518
520
  }, "strip", z.ZodTypeAny, {
519
- type: "curate" | "query" | "curate-folder";
521
+ type: "curate" | "query" | "search" | "curate-folder";
520
522
  content: string;
521
523
  taskId: string;
522
524
  clientId: string;
@@ -524,8 +526,9 @@ 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
- type: "curate" | "query" | "curate-folder";
531
+ type: "curate" | "query" | "search" | "curate-folder";
529
532
  content: string;
530
533
  taskId: string;
531
534
  clientId: 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
@@ -683,16 +687,16 @@ export declare const TaskCreatedSchema: z.ZodObject<{
683
687
  /** Unique task identifier */
684
688
  taskId: z.ZodString;
685
689
  /** Task type */
686
- type: z.ZodEnum<["curate", "curate-folder", "query"]>;
690
+ type: z.ZodEnum<["curate", "curate-folder", "query", "search"]>;
687
691
  }, "strip", z.ZodTypeAny, {
688
- type: "curate" | "query" | "curate-folder";
692
+ type: "curate" | "query" | "search" | "curate-folder";
689
693
  content: string;
690
694
  taskId: string;
691
695
  files?: string[] | undefined;
692
696
  clientCwd?: string | undefined;
693
697
  folderPath?: string | undefined;
694
698
  }, {
695
- type: "curate" | "query" | "curate-folder";
699
+ type: "curate" | "query" | "search" | "curate-folder";
696
700
  content: string;
697
701
  taskId: string;
698
702
  files?: string[] | undefined;
@@ -941,7 +945,7 @@ export type TaskStartedEvent = z.infer<typeof TaskStartedEventSchema>;
941
945
  export type TaskCompletedEvent = z.infer<typeof TaskCompletedEventSchema>;
942
946
  export type TaskErrorData = z.infer<typeof TaskErrorDataSchema>;
943
947
  export type TaskErrorEvent = z.infer<typeof TaskErrorEventSchema>;
944
- export declare const TaskTypeSchema: z.ZodEnum<["curate", "curate-folder", "query"]>;
948
+ export declare const TaskTypeSchema: z.ZodEnum<["curate", "curate-folder", "query", "search"]>;
945
949
  /**
946
950
  * Request to create a new task
947
951
  */
@@ -959,23 +963,27 @@ export declare const TaskCreateRequestSchema: z.ZodObject<{
959
963
  /** Task ID - generated by Client UseCase (UUID v4) */
960
964
  taskId: z.ZodString;
961
965
  /** Task type */
962
- type: z.ZodEnum<["curate", "curate-folder", "query"]>;
966
+ type: z.ZodEnum<["curate", "curate-folder", "query", "search"]>;
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
- type: "curate" | "query" | "curate-folder";
970
+ type: "curate" | "query" | "search" | "curate-folder";
965
971
  content: string;
966
972
  taskId: string;
967
973
  files?: string[] | undefined;
968
974
  clientCwd?: string | undefined;
969
975
  folderPath?: string | undefined;
970
976
  projectPath?: string | undefined;
977
+ worktreeRoot?: string | undefined;
971
978
  }, {
972
- type: "curate" | "query" | "curate-folder";
979
+ type: "curate" | "query" | "search" | "curate-folder";
973
980
  content: string;
974
981
  taskId: string;
975
982
  files?: string[] | undefined;
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
@@ -314,7 +314,9 @@ export const TaskExecuteSchema = z.object({
314
314
  /** Unique task identifier */
315
315
  taskId: z.string(),
316
316
  /** Task type */
317
- type: z.enum(['curate', 'curate-folder', 'query']),
317
+ type: z.enum(['curate', 'curate-folder', 'query', 'search']),
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
@@ -372,7 +374,7 @@ export const TaskCreatedSchema = z.object({
372
374
  /** Unique task identifier */
373
375
  taskId: z.string(),
374
376
  /** Task type */
375
- type: z.enum(['curate', 'curate-folder', 'query']),
377
+ type: z.enum(['curate', 'curate-folder', 'query', 'search']),
376
378
  });
377
379
  /**
378
380
  * task:started - Agent begins processing the task
@@ -473,7 +475,7 @@ export const LlmToolResultEventSchema = z.object({
473
475
  // ============================================================================
474
476
  // Request/Response Schemas (for client → server commands)
475
477
  // ============================================================================
476
- export const TaskTypeSchema = z.enum(['curate', 'curate-folder', 'query']);
478
+ export const TaskTypeSchema = z.enum(['curate', 'curate-folder', 'query', 'search']);
477
479
  /**
478
480
  * Request to create a new task
479
481
  */
@@ -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