byterover-cli 3.2.0 → 3.4.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 (127) hide show
  1. package/.env.production +0 -4
  2. package/dist/agent/core/domain/swarm/types.d.ts +132 -0
  3. package/dist/agent/core/domain/swarm/types.js +128 -0
  4. package/dist/agent/core/domain/tools/constants.d.ts +2 -0
  5. package/dist/agent/core/domain/tools/constants.js +2 -0
  6. package/dist/agent/core/interfaces/i-memory-provider.d.ts +45 -0
  7. package/dist/agent/core/interfaces/i-memory-provider.js +1 -0
  8. package/dist/agent/core/interfaces/i-sandbox-service.d.ts +8 -0
  9. package/dist/agent/core/interfaces/i-swarm-coordinator.d.ts +127 -0
  10. package/dist/agent/core/interfaces/i-swarm-coordinator.js +1 -0
  11. package/dist/agent/infra/agent/service-initializer.js +48 -0
  12. package/dist/agent/infra/map/map-shared.d.ts +2 -2
  13. package/dist/agent/infra/sandbox/sandbox-service.d.ts +10 -0
  14. package/dist/agent/infra/sandbox/sandbox-service.js +13 -0
  15. package/dist/agent/infra/sandbox/tools-sdk.d.ts +25 -0
  16. package/dist/agent/infra/sandbox/tools-sdk.js +24 -1
  17. package/dist/agent/infra/swarm/adapters/byterover-adapter.d.ts +39 -0
  18. package/dist/agent/infra/swarm/adapters/byterover-adapter.js +62 -0
  19. package/dist/agent/infra/swarm/adapters/gbrain-adapter.d.ts +63 -0
  20. package/dist/agent/infra/swarm/adapters/gbrain-adapter.js +209 -0
  21. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.d.ts +41 -0
  22. package/dist/agent/infra/swarm/adapters/local-markdown-adapter.js +256 -0
  23. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.d.ts +29 -0
  24. package/dist/agent/infra/swarm/adapters/memory-wiki-adapter.js +244 -0
  25. package/dist/agent/infra/swarm/adapters/obsidian-adapter.d.ts +37 -0
  26. package/dist/agent/infra/swarm/adapters/obsidian-adapter.js +201 -0
  27. package/dist/agent/infra/swarm/cli/query-renderer.d.ts +15 -0
  28. package/dist/agent/infra/swarm/cli/query-renderer.js +126 -0
  29. package/dist/agent/infra/swarm/config/swarm-config-loader.d.ts +14 -0
  30. package/dist/agent/infra/swarm/config/swarm-config-loader.js +82 -0
  31. package/dist/agent/infra/swarm/config/swarm-config-schema.d.ts +667 -0
  32. package/dist/agent/infra/swarm/config/swarm-config-schema.js +305 -0
  33. package/dist/agent/infra/swarm/provider-factory.d.ts +21 -0
  34. package/dist/agent/infra/swarm/provider-factory.js +67 -0
  35. package/dist/agent/infra/swarm/search-precision.d.ts +95 -0
  36. package/dist/agent/infra/swarm/search-precision.js +141 -0
  37. package/dist/agent/infra/swarm/swarm-coordinator.d.ts +59 -0
  38. package/dist/agent/infra/swarm/swarm-coordinator.js +436 -0
  39. package/dist/agent/infra/swarm/swarm-graph.d.ts +63 -0
  40. package/dist/agent/infra/swarm/swarm-graph.js +167 -0
  41. package/dist/agent/infra/swarm/swarm-merger.d.ts +29 -0
  42. package/dist/agent/infra/swarm/swarm-merger.js +66 -0
  43. package/dist/agent/infra/swarm/swarm-router.d.ts +12 -0
  44. package/dist/agent/infra/swarm/swarm-router.js +40 -0
  45. package/dist/agent/infra/swarm/swarm-write-router.d.ts +23 -0
  46. package/dist/agent/infra/swarm/swarm-write-router.js +45 -0
  47. package/dist/agent/infra/swarm/validation/config-validator.d.ts +16 -0
  48. package/dist/agent/infra/swarm/validation/config-validator.js +402 -0
  49. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.d.ts +33 -0
  50. package/dist/agent/infra/swarm/validation/memory-swarm-validation-error.js +27 -0
  51. package/dist/agent/infra/swarm/wizard/config-scaffolder.d.ts +36 -0
  52. package/dist/agent/infra/swarm/wizard/config-scaffolder.js +96 -0
  53. package/dist/agent/infra/swarm/wizard/provider-detector.d.ts +54 -0
  54. package/dist/agent/infra/swarm/wizard/provider-detector.js +153 -0
  55. package/dist/agent/infra/swarm/wizard/swarm-wizard.d.ts +61 -0
  56. package/dist/agent/infra/swarm/wizard/swarm-wizard.js +187 -0
  57. package/dist/agent/infra/system-prompt/contributors/index.d.ts +1 -0
  58. package/dist/agent/infra/system-prompt/contributors/index.js +1 -0
  59. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.d.ts +15 -0
  60. package/dist/agent/infra/system-prompt/contributors/swarm-state-contributor.js +65 -0
  61. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +14 -14
  62. package/dist/agent/infra/tools/implementations/curate-tool.js +2 -0
  63. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +12 -2
  64. package/dist/agent/infra/tools/implementations/swarm-query-tool.d.ts +9 -0
  65. package/dist/agent/infra/tools/implementations/swarm-query-tool.js +44 -0
  66. package/dist/agent/infra/tools/implementations/swarm-store-tool.d.ts +9 -0
  67. package/dist/agent/infra/tools/implementations/swarm-store-tool.js +43 -0
  68. package/dist/agent/infra/tools/tool-provider.js +1 -0
  69. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  70. package/dist/agent/infra/tools/tool-registry.js +25 -1
  71. package/dist/agent/resources/tools/code_exec.txt +2 -0
  72. package/dist/agent/resources/tools/swarm_query.txt +38 -0
  73. package/dist/agent/resources/tools/swarm_store.txt +35 -0
  74. package/dist/oclif/commands/curate/index.d.ts +1 -0
  75. package/dist/oclif/commands/curate/index.js +15 -1
  76. package/dist/oclif/commands/query.d.ts +1 -0
  77. package/dist/oclif/commands/query.js +17 -3
  78. package/dist/oclif/commands/search.d.ts +20 -0
  79. package/dist/oclif/commands/search.js +186 -0
  80. package/dist/oclif/commands/status.js +4 -0
  81. package/dist/oclif/commands/swarm/curate.d.ts +13 -0
  82. package/dist/oclif/commands/swarm/curate.js +81 -0
  83. package/dist/oclif/commands/swarm/onboard.d.ts +6 -0
  84. package/dist/oclif/commands/swarm/onboard.js +233 -0
  85. package/dist/oclif/commands/swarm/query.d.ts +14 -0
  86. package/dist/oclif/commands/swarm/query.js +84 -0
  87. package/dist/oclif/commands/swarm/status.d.ts +41 -0
  88. package/dist/oclif/commands/swarm/status.js +278 -0
  89. package/dist/oclif/lib/daemon-client.js +0 -1
  90. package/dist/oclif/lib/search-format.d.ts +10 -0
  91. package/dist/oclif/lib/search-format.js +25 -0
  92. package/dist/oclif/lib/task-client.d.ts +6 -0
  93. package/dist/oclif/lib/task-client.js +10 -3
  94. package/dist/server/constants.d.ts +3 -2
  95. package/dist/server/constants.js +10 -7
  96. package/dist/server/core/domain/errors/task-error.d.ts +2 -2
  97. package/dist/server/core/domain/errors/task-error.js +5 -4
  98. package/dist/server/core/domain/source/source-schema.d.ts +6 -6
  99. package/dist/server/core/domain/transport/schemas.d.ts +14 -14
  100. package/dist/server/core/domain/transport/schemas.js +3 -3
  101. package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
  102. package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
  103. package/dist/server/core/interfaces/executor/index.d.ts +1 -0
  104. package/dist/server/core/interfaces/executor/index.js +1 -0
  105. package/dist/server/infra/daemon/agent-process.js +20 -7
  106. package/dist/server/infra/executor/search-executor.d.ts +17 -0
  107. package/dist/server/infra/executor/search-executor.js +30 -0
  108. package/dist/server/infra/http/provider-model-fetchers.js +1 -0
  109. package/dist/server/infra/process/feature-handlers.js +13 -0
  110. package/dist/server/infra/project/project-registry.js +13 -1
  111. package/dist/server/infra/transport/handlers/locations-handler.d.ts +2 -0
  112. package/dist/server/infra/transport/handlers/locations-handler.js +16 -1
  113. package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
  114. package/dist/server/infra/transport/handlers/push-handler.js +3 -3
  115. package/dist/server/infra/transport/handlers/status-handler.js +25 -18
  116. package/dist/server/infra/transport/handlers/vc-handler.d.ts +0 -4
  117. package/dist/server/infra/transport/handlers/vc-handler.js +5 -16
  118. package/dist/server/templates/skill/SKILL.md +188 -5
  119. package/dist/server/utils/gitignore.d.ts +1 -0
  120. package/dist/server/utils/gitignore.js +36 -4
  121. package/dist/shared/transport/search-content.d.ts +28 -0
  122. package/dist/shared/transport/search-content.js +38 -0
  123. package/dist/shared/transport/types/dto.d.ts +1 -1
  124. package/dist/tui/features/status/utils/format-status.js +5 -0
  125. package/dist/tui/utils/error-messages.js +2 -2
  126. package/oclif.manifest.json +581 -317
  127. package/package.json +2 -2
@@ -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
+ }
@@ -139,6 +139,10 @@ export default class Status extends Command {
139
139
  this.log('Context Tree: No changes');
140
140
  break;
141
141
  }
142
+ case 'no_vc': {
143
+ this.log('Context Tree: Managed by Byterover version control (use brv vc commands)');
144
+ break;
145
+ }
142
146
  case 'not_initialized': {
143
147
  this.log('Context Tree: Not initialized');
144
148
  break;
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SwarmCurate extends Command {
3
+ static args: {
4
+ content: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ provider: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,81 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { FileSystemService } from '../../../agent/infra/file-system/file-system-service.js';
3
+ import { loadSwarmConfig } from '../../../agent/infra/swarm/config/swarm-config-loader.js';
4
+ import { buildProvidersFromConfig } from '../../../agent/infra/swarm/provider-factory.js';
5
+ import { SwarmCoordinator } from '../../../agent/infra/swarm/swarm-coordinator.js';
6
+ import { validateSwarmProviders } from '../../../agent/infra/swarm/validation/config-validator.js';
7
+ import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
8
+ export default class SwarmCurate extends Command {
9
+ static args = {
10
+ content: Args.string({ description: 'Knowledge content to store in a swarm provider', required: true }),
11
+ };
12
+ static description = 'Store knowledge in a swarm provider (GBrain, local markdown)';
13
+ static examples = [
14
+ '<%= config.bin %> swarm curate "Dario Amodei is CEO of Anthropic"',
15
+ '<%= config.bin %> swarm curate "meeting notes: decided on JWT" --provider local-markdown:notes',
16
+ '<%= config.bin %> swarm curate "Architecture uses event sourcing" --provider gbrain',
17
+ ];
18
+ static flags = {
19
+ format: Flags.string({
20
+ char: 'f',
21
+ default: 'text',
22
+ description: 'Output format',
23
+ options: ['text', 'json'],
24
+ }),
25
+ provider: Flags.string({
26
+ char: 'p',
27
+ description: 'Target provider ID (e.g., gbrain, local-markdown:notes)',
28
+ }),
29
+ };
30
+ async run() {
31
+ const { args, flags } = await this.parse(SwarmCurate);
32
+ const isJson = flags.format === 'json';
33
+ try {
34
+ const config = await loadSwarmConfig(process.cwd());
35
+ // Validate enrichment topology only (provider errors handled by health checks)
36
+ const validation = await validateSwarmProviders(config);
37
+ const topologyErrors = validation.errors.filter((e) => e.provider === 'enrichment');
38
+ if (topologyErrors.length > 0) {
39
+ const messages = topologyErrors.map((e) => e.message);
40
+ throw new Error(`Invalid enrichment topology:\n ${messages.join('\n ')}`);
41
+ }
42
+ const workingDirectory = process.cwd();
43
+ const fileSystemService = new FileSystemService({ workingDirectory });
44
+ await fileSystemService.initialize();
45
+ const searchService = createSearchKnowledgeService(fileSystemService, {
46
+ baseDirectory: workingDirectory,
47
+ });
48
+ const providers = buildProvidersFromConfig(config, { searchService });
49
+ const coordinator = new SwarmCoordinator(providers, config);
50
+ await coordinator.refreshHealth();
51
+ const result = await coordinator.store({
52
+ content: args.content,
53
+ provider: flags.provider,
54
+ });
55
+ if (isJson) {
56
+ this.log(JSON.stringify(result, undefined, 2));
57
+ }
58
+ else if (result.success && result.fallback) {
59
+ const idPart = result.id ? ` as ${result.id}` : '';
60
+ this.log(`Stored to ${result.provider} (fallback — no external providers available)${idPart}`);
61
+ }
62
+ else if (result.success) {
63
+ this.log(`Stored to ${result.provider} as ${result.id}`);
64
+ }
65
+ else {
66
+ this.logToStderr(`Error: ${result.error ?? 'Store failed'}`);
67
+ this.exit(2);
68
+ }
69
+ }
70
+ catch (error) {
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ if (isJson) {
73
+ this.log(JSON.stringify({ error: message, success: false }));
74
+ }
75
+ else {
76
+ this.logToStderr(`Error: ${message}`);
77
+ this.exit(2);
78
+ }
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SwarmOnboard extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,233 @@
1
+ /* eslint-disable camelcase -- wizard builds YAML-shaped config objects (snake_case keys) */
2
+ import { checkbox, confirm, input, select } from '@inquirer/prompts';
3
+ import { Command } from '@oclif/core';
4
+ import chalk from 'chalk';
5
+ import { load } from 'js-yaml';
6
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { safeValidateSwarmConfig } from '../../../agent/infra/swarm/config/swarm-config-schema.js';
9
+ import { scaffoldConfig } from '../../../agent/infra/swarm/wizard/config-scaffolder.js';
10
+ import { detectProviders } from '../../../agent/infra/swarm/wizard/provider-detector.js';
11
+ import { EscBackError, runMemoryWizard, WizardCancelledError } from '../../../agent/infra/swarm/wizard/swarm-wizard.js';
12
+ import { createEscapeSignal, isEscBack, isForceExit, isPromptCancelled, wizardInputTheme, wizardSelectTheme } from '../../lib/prompt-utils.js';
13
+ /**
14
+ * Wrap a prompt call with ESC back-navigation support.
15
+ * If the user presses ESC, throws EscBackError so the wizard goes back one step.
16
+ * If the user presses Ctrl+C, re-throws so the command exits.
17
+ */
18
+ async function withEscBack(esc, fn) {
19
+ try {
20
+ return await fn(esc.signal);
21
+ }
22
+ catch (error) {
23
+ if (isEscBack(error)) {
24
+ esc.reset();
25
+ throw new EscBackError();
26
+ }
27
+ if (isForceExit(error)) {
28
+ throw error;
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ /**
34
+ * Create the real wizard prompts backed by @inquirer/prompts.
35
+ * Wires createEscapeSignal() for ESC back-navigation on every prompt.
36
+ */
37
+ function createWizardPrompts(esc) {
38
+ return {
39
+ async configureBudget() {
40
+ const amount = await withEscBack(esc, (signal) => input({
41
+ default: '50',
42
+ message: 'Monthly budget cap in dollars:',
43
+ theme: wizardInputTheme,
44
+ }, { signal }));
45
+ const cents = Math.round(Number.parseFloat(amount) * 100);
46
+ return { globalMonthlyCents: cents };
47
+ },
48
+ async configureEnrichment(providerIds) {
49
+ const providerList = providerIds.filter((id) => id !== 'byterover').join(', ');
50
+ console.log('');
51
+ console.log(chalk.bold('Enrichment Topology'));
52
+ console.log(chalk.dim('─'.repeat(50)));
53
+ console.log('');
54
+ console.log('The swarm can run providers in two modes:');
55
+ console.log('');
56
+ console.log(` ${chalk.cyan('Parallel')} (enrichment off)`);
57
+ console.log(' All providers search independently at the same time.');
58
+ console.log(' Fastest, but each provider only sees your original query.');
59
+ console.log('');
60
+ console.log(` ${chalk.cyan('Chained')} (enrichment on) ${chalk.green('← recommended')}`);
61
+ console.log(` ByteRover searches first, then feeds its results to ${providerList}.`);
62
+ console.log(' Slightly slower, but downstream providers get richer context');
63
+ console.log(' from ByteRover\'s structured knowledge.');
64
+ console.log('');
65
+ console.log(chalk.dim('How routing interacts with enrichment:'));
66
+ console.log(chalk.dim(' The router decides WHICH providers handle each query type.'));
67
+ console.log(chalk.dim(' Enrichment decides the ORDER they run in.'));
68
+ console.log(chalk.dim(' If a provider is excluded by routing, its enrichment edge is skipped.'));
69
+ console.log('');
70
+ return withEscBack(esc, (signal) => confirm({
71
+ default: true,
72
+ message: 'Enable enrichment (recommended)?',
73
+ }, { signal }));
74
+ },
75
+ async configureProvider(provider) {
76
+ const config = {};
77
+ switch (provider.id) {
78
+ case 'gbrain': {
79
+ config.repo_path = await withEscBack(esc, (signal) => input({
80
+ message: 'GBrain brain repository path (your notes/data repo, not the CLI checkout):',
81
+ theme: wizardInputTheme,
82
+ }, { signal }));
83
+ config.search_mode = await withEscBack(esc, (signal) => select({
84
+ choices: [
85
+ { description: 'Vector + keyword + RRF fusion (best recall, needs OPENAI_API_KEY)', name: 'hybrid (recommended)', value: 'hybrid' },
86
+ { description: 'Full-text search only (fast, no API key needed)', name: 'keyword', value: 'keyword' },
87
+ { description: 'Vector similarity only (needs OPENAI_API_KEY)', name: 'vector', value: 'vector' },
88
+ ],
89
+ default: 'hybrid',
90
+ message: 'Search mode:',
91
+ theme: wizardSelectTheme,
92
+ }, { signal }));
93
+ break;
94
+ }
95
+ case 'hindsight': {
96
+ config.connection_string = await withEscBack(esc, (signal) => input({
97
+ // eslint-disable-next-line no-template-curly-in-string -- literal ${VAR} placeholder for resolveEnvVars
98
+ default: '${HINDSIGHT_DB_URL}',
99
+ message: 'Hindsight connection string:',
100
+ theme: wizardInputTheme,
101
+ }, { signal }));
102
+ break;
103
+ }
104
+ case 'honcho': {
105
+ config.api_key = await withEscBack(esc, (signal) => input({
106
+ // eslint-disable-next-line no-template-curly-in-string -- literal ${VAR} placeholder for resolveEnvVars
107
+ default: '${HONCHO_API_KEY}',
108
+ message: 'Honcho API key (or env var reference):',
109
+ theme: wizardInputTheme,
110
+ }, { signal }));
111
+ config.app_id = await withEscBack(esc, (signal) => input({
112
+ message: 'Honcho app ID:',
113
+ theme: wizardInputTheme,
114
+ }, { signal }));
115
+ break;
116
+ }
117
+ case 'local-markdown': {
118
+ const path = await withEscBack(esc, (signal) => input({
119
+ default: provider.path,
120
+ message: 'Markdown folder path:',
121
+ theme: wizardInputTheme,
122
+ }, { signal }));
123
+ const name = await withEscBack(esc, (signal) => input({
124
+ message: 'Human-readable name for this folder:',
125
+ theme: wizardInputTheme,
126
+ }, { signal }));
127
+ config.folders = [{
128
+ follow_wikilinks: true,
129
+ name,
130
+ path,
131
+ read_only: true,
132
+ }];
133
+ break;
134
+ }
135
+ case 'obsidian': {
136
+ config.vault_path = await withEscBack(esc, (signal) => input({
137
+ default: provider.path,
138
+ message: 'Obsidian vault path:',
139
+ theme: wizardInputTheme,
140
+ }, { signal }));
141
+ break;
142
+ }
143
+ }
144
+ return config;
145
+ },
146
+ async confirmWrite(summary) {
147
+ console.log('\n' + chalk.bold('Memory Swarm Configuration Summary:'));
148
+ console.log(summary);
149
+ console.log('');
150
+ return withEscBack(esc, (signal) => confirm({
151
+ default: true,
152
+ message: 'Write to .brv/swarm/config.yaml?',
153
+ }, { signal }));
154
+ },
155
+ async selectProviders(detected) {
156
+ const choices = detected.map((p, index) => {
157
+ const status = p.detected
158
+ ? chalk.green('detected')
159
+ : chalk.dim('not found');
160
+ const detail = p.path ? ` — ${p.path}` : p.envVar ? ` (${p.envVar})` : '';
161
+ const count = p.noteCount ? ` (${p.noteCount} files)` : '';
162
+ return {
163
+ checked: p.detected,
164
+ disabled: p.id === 'byterover' ? '(always on)' : false,
165
+ name: `${p.id}${detail}${count} [${status}]`,
166
+ value: String(index),
167
+ };
168
+ });
169
+ return withEscBack(esc, (signal) => checkbox({
170
+ choices,
171
+ message: 'Select providers to enable:',
172
+ theme: wizardSelectTheme,
173
+ }, { signal }));
174
+ },
175
+ };
176
+ }
177
+ export default class SwarmOnboard extends Command {
178
+ static description = 'Set up memory swarm with interactive onboarding wizard';
179
+ static examples = [
180
+ '<%= config.bin %> swarm onboard',
181
+ ];
182
+ async run() {
183
+ this.log(chalk.bold('\nMemory Swarm Onboarding'));
184
+ this.log('Scanning for memory providers...\n');
185
+ const esc = createEscapeSignal();
186
+ try {
187
+ // Step 1: Detect providers
188
+ const detected = await detectProviders();
189
+ // Step 2: Run wizard
190
+ const prompts = createWizardPrompts(esc);
191
+ const answers = await runMemoryWizard(prompts, detected);
192
+ // Step 3: Scaffold config
193
+ const { warnings, yaml: configYaml } = scaffoldConfig(answers);
194
+ // Surface scaffolding warnings (e.g. duplicate vaults dropped)
195
+ for (const warning of warnings) {
196
+ this.log(chalk.yellow(`⚠ ${warning}`));
197
+ }
198
+ // Step 4: Write config file
199
+ const configDir = join(process.cwd(), '.brv', 'swarm');
200
+ if (!existsSync(configDir)) {
201
+ mkdirSync(configDir, { recursive: true });
202
+ }
203
+ const configPath = join(configDir, 'config.yaml');
204
+ writeFileSync(configPath, configYaml);
205
+ this.log(chalk.green(`\n✓ Config written to ${configPath}`));
206
+ // Step 5: Validate roundtrip
207
+ const parsed = load(configYaml);
208
+ const validation = safeValidateSwarmConfig(parsed);
209
+ if (validation.success) {
210
+ this.log(chalk.green('✓ Config validated successfully'));
211
+ }
212
+ else {
213
+ this.log(chalk.yellow('⚠ Config validation warning — check the generated file.'));
214
+ }
215
+ // Step 6: Suggest next step
216
+ this.log(`\nRun ${chalk.cyan('brv swarm status')} to verify provider health.`);
217
+ }
218
+ catch (error) {
219
+ if (error instanceof WizardCancelledError) {
220
+ this.log('\nWizard cancelled. No files were written.');
221
+ return;
222
+ }
223
+ if (isPromptCancelled(error)) {
224
+ this.log('\nWizard cancelled.');
225
+ return;
226
+ }
227
+ throw error;
228
+ }
229
+ finally {
230
+ esc.cleanup();
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SwarmQuery extends Command {
3
+ static args: {
4
+ query: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ explain: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'max-results': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,84 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { FileSystemService } from '../../../agent/infra/file-system/file-system-service.js';
3
+ import { formatQueryResults, formatQueryResultsExplain, formatQueryResultsJson } from '../../../agent/infra/swarm/cli/query-renderer.js';
4
+ import { loadSwarmConfig } from '../../../agent/infra/swarm/config/swarm-config-loader.js';
5
+ import { buildProvidersFromConfig } from '../../../agent/infra/swarm/provider-factory.js';
6
+ import { SwarmCoordinator } from '../../../agent/infra/swarm/swarm-coordinator.js';
7
+ import { validateSwarmProviders } from '../../../agent/infra/swarm/validation/config-validator.js';
8
+ import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
9
+ export default class SwarmQuery extends Command {
10
+ static args = {
11
+ query: Args.string({ description: 'Natural language query to search across memory providers', required: true }),
12
+ };
13
+ static description = 'Query the memory swarm across all active providers';
14
+ static examples = [
15
+ '<%= config.bin %> swarm query "auth tokens"',
16
+ '<%= config.bin %> swarm query "what changed yesterday" --format json',
17
+ ];
18
+ static flags = {
19
+ explain: Flags.boolean({
20
+ description: 'Show classification, routing, and enrichment details (ignored with --format json, which always includes all metadata)',
21
+ }),
22
+ format: Flags.string({
23
+ char: 'f',
24
+ default: 'text',
25
+ description: 'Output format',
26
+ options: ['text', 'json'],
27
+ }),
28
+ 'max-results': Flags.integer({
29
+ char: 'n',
30
+ description: 'Maximum number of results',
31
+ }),
32
+ };
33
+ async run() {
34
+ const { args, flags } = await this.parse(SwarmQuery);
35
+ const isJson = flags.format === 'json';
36
+ try {
37
+ const config = await loadSwarmConfig(process.cwd());
38
+ // Build a real SearchKnowledgeService so ByteRover can search the context tree.
39
+ // FileSystemService is lightweight — safe to construct here for CLI use.
40
+ const workingDirectory = process.cwd();
41
+ const fileSystemService = new FileSystemService({ workingDirectory });
42
+ await fileSystemService.initialize();
43
+ const searchService = createSearchKnowledgeService(fileSystemService, {
44
+ baseDirectory: workingDirectory,
45
+ });
46
+ // Validate enrichment topology — structural errors block execution.
47
+ // Provider-specific errors (bad paths, missing API keys) are handled
48
+ // by health checks at runtime, preserving degraded-mode semantics.
49
+ const validation = await validateSwarmProviders(config);
50
+ const topologyErrors = validation.errors.filter((e) => e.provider === 'enrichment');
51
+ if (topologyErrors.length > 0) {
52
+ const messages = topologyErrors.map((e) => e.message);
53
+ throw new Error(`Invalid enrichment topology:\n ${messages.join('\n ')}`);
54
+ }
55
+ const providers = buildProvidersFromConfig(config, { searchService });
56
+ const coordinator = new SwarmCoordinator(providers, config);
57
+ // Run health checks so unhealthy providers are skipped
58
+ await coordinator.refreshHealth();
59
+ const result = await coordinator.execute({
60
+ maxResults: flags['max-results'],
61
+ query: args.query,
62
+ });
63
+ if (isJson) {
64
+ this.log(formatQueryResultsJson(result));
65
+ }
66
+ else if (flags.explain) {
67
+ this.log(formatQueryResultsExplain(result, args.query));
68
+ }
69
+ else {
70
+ this.log(formatQueryResults(result, args.query));
71
+ }
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ if (isJson) {
76
+ this.log(JSON.stringify({ error: message, success: false }));
77
+ }
78
+ else {
79
+ this.logToStderr(`Error: ${message}`);
80
+ this.exit(2);
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,41 @@
1
+ import { Command } from '@oclif/core';
2
+ import { loadSwarmConfig } from '../../../agent/infra/swarm/config/swarm-config-loader.js';
3
+ import { detectProviders } from '../../../agent/infra/swarm/wizard/provider-detector.js';
4
+ type DetectedProviders = Awaited<ReturnType<typeof detectProviders>>;
5
+ type SwarmConfig = Awaited<ReturnType<typeof loadSwarmConfig>>;
6
+ /**
7
+ * Format enrichment edges for display.
8
+ */
9
+ export declare function formatEnrichmentEdges(edges: Array<{
10
+ from: string;
11
+ to: string;
12
+ }>): string[];
13
+ /**
14
+ * Collect configured paths for path-based providers so we can detect
15
+ * newly discovered paths that aren't in the config yet.
16
+ */
17
+ export declare function getConfiguredSwarmPaths(config: SwarmConfig): Set<string>;
18
+ /**
19
+ * Build user-facing suggestions for detected providers that are not yet configured.
20
+ */
21
+ export declare function findSwarmStatusSuggestions(config: SwarmConfig, detected: DetectedProviders): string[];
22
+ export default class SwarmStatus extends Command {
23
+ static description: string;
24
+ static examples: string[];
25
+ static flags: {
26
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
27
+ };
28
+ run(): Promise<void>;
29
+ private findSuggestions;
30
+ private renderCascadeNote;
31
+ private renderEnrichmentTopology;
32
+ private renderProviderLine;
33
+ private renderProviderStatusLines;
34
+ private renderSuggestionLines;
35
+ private renderSwarmSummary;
36
+ private renderTextOutput;
37
+ private renderValidationErrors;
38
+ private renderValidationWarnings;
39
+ private renderWriteTargets;
40
+ }
41
+ export {};