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,36 @@
1
+ /**
2
+ * Answers collected from the onboarding wizard.
3
+ */
4
+ export type WizardAnswers = {
5
+ budget?: {
6
+ globalMonthlyCents: number;
7
+ };
8
+ /** Whether to enable enrichment edges between providers (default: true when 2+ providers) */
9
+ enrichment?: boolean;
10
+ providers: {
11
+ config: Record<string, unknown>;
12
+ enabled: boolean;
13
+ id: string;
14
+ }[];
15
+ };
16
+ /**
17
+ * Result from scaffoldConfig — YAML string plus any warnings.
18
+ */
19
+ export type ScaffoldResult = {
20
+ /** Warnings about dropped duplicate entries (e.g. multiple obsidian vaults) */
21
+ warnings: string[];
22
+ /** YAML config string ready to write to `.brv/swarm/config.yaml` */
23
+ yaml: string;
24
+ };
25
+ /**
26
+ * Pure function: transforms wizard answers into a YAML config string.
27
+ * No side effects — fully testable.
28
+ *
29
+ * When multiple entries share the same provider ID:
30
+ * - `local-markdown`: folders arrays are merged (schema supports multiple folders)
31
+ * - All others: last entry wins, earlier entries produce a warning
32
+ *
33
+ * @param answers - Wizard answers with provider selections and config
34
+ * @returns ScaffoldResult with YAML string and any warnings
35
+ */
36
+ export declare function scaffoldConfig(answers: WizardAnswers): ScaffoldResult;
@@ -0,0 +1,96 @@
1
+ import { dump } from 'js-yaml';
2
+ /**
3
+ * Providers whose config schema supports only a single instance.
4
+ * Duplicates produce a warning and the last entry wins.
5
+ */
6
+ const SINGLE_INSTANCE_PROVIDERS = new Set(['byterover', 'gbrain', 'hindsight', 'honcho', 'obsidian']);
7
+ /**
8
+ * Map provider IDs to their YAML key names.
9
+ */
10
+ const PROVIDER_YAML_KEYS = {
11
+ 'byterover': 'byterover',
12
+ 'gbrain': 'gbrain',
13
+ 'hindsight': 'hindsight',
14
+ 'honcho': 'honcho',
15
+ 'local-markdown': 'local_markdown',
16
+ 'obsidian': 'obsidian',
17
+ };
18
+ /**
19
+ * Pure function: transforms wizard answers into a YAML config string.
20
+ * No side effects — fully testable.
21
+ *
22
+ * When multiple entries share the same provider ID:
23
+ * - `local-markdown`: folders arrays are merged (schema supports multiple folders)
24
+ * - All others: last entry wins, earlier entries produce a warning
25
+ *
26
+ * @param answers - Wizard answers with provider selections and config
27
+ * @returns ScaffoldResult with YAML string and any warnings
28
+ */
29
+ export function scaffoldConfig(answers) {
30
+ const config = {};
31
+ const warnings = [];
32
+ // Build providers section, merging duplicate entries
33
+ const providers = {};
34
+ for (const provider of answers.providers) {
35
+ if (!provider.enabled)
36
+ continue;
37
+ const yamlKey = PROVIDER_YAML_KEYS[provider.id] ?? provider.id;
38
+ const existing = providers[yamlKey];
39
+ if (existing && provider.config.folders && existing.folders) {
40
+ // Merge folders arrays for local_markdown (multiple folder entries)
41
+ existing.folders = [
42
+ ...existing.folders,
43
+ ...provider.config.folders,
44
+ ];
45
+ }
46
+ else if (existing && SINGLE_INSTANCE_PROVIDERS.has(provider.id)) {
47
+ // Single-instance provider: last entry wins, warn about earlier
48
+ const droppedDetail = existing.vault_path ?? existing.repo_path ?? existing.connection_string ?? provider.id;
49
+ warnings.push(`Multiple ${provider.id} entries selected — config only supports one. ` +
50
+ `Keeping the last selection; dropped earlier entry (${droppedDetail}).`);
51
+ Object.assign(existing, provider.config);
52
+ }
53
+ else if (existing) {
54
+ Object.assign(existing, provider.config);
55
+ }
56
+ else {
57
+ providers[yamlKey] = {
58
+ enabled: true,
59
+ ...provider.config,
60
+ };
61
+ }
62
+ }
63
+ config.providers = providers;
64
+ // Build enrichment section when user opted in (or default true for 2+ providers)
65
+ if (answers.enrichment !== false) {
66
+ const enabledIds = new Set(answers.providers.filter((p) => p.enabled).map((p) => p.id));
67
+ if (enabledIds.size >= 2 && enabledIds.has('byterover')) {
68
+ // ByteRover is the master — it feeds context to all other providers
69
+ const edges = [];
70
+ if (enabledIds.has('obsidian'))
71
+ edges.push({ from: 'byterover', to: 'obsidian' });
72
+ if (enabledIds.has('local-markdown'))
73
+ edges.push({ from: 'byterover', to: 'local-markdown' });
74
+ if (enabledIds.has('gbrain'))
75
+ edges.push({ from: 'byterover', to: 'gbrain' });
76
+ if (edges.length > 0) {
77
+ config.enrichment = { edges };
78
+ }
79
+ }
80
+ }
81
+ // Build budget section (only if specified)
82
+ if (answers.budget) {
83
+ config.budget = {
84
+ // eslint-disable-next-line camelcase -- YAML budget key
85
+ global_monthly_cap_cents: answers.budget.globalMonthlyCents,
86
+ };
87
+ }
88
+ const header = '# Memory Swarm Configuration for ByteRover-CLI\n# Generated by `brv swarm onboard`\n\n';
89
+ const body = dump(config, {
90
+ indent: 2,
91
+ lineWidth: 120,
92
+ noRefs: true,
93
+ sortKeys: false,
94
+ });
95
+ return { warnings, yaml: header + body };
96
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A provider discovered during environment scanning.
3
+ */
4
+ export type DetectedProvider = {
5
+ /** Whether this provider was auto-discovered */
6
+ detected: boolean;
7
+ /** Environment variable name (for cloud providers) */
8
+ envVar?: string;
9
+ /** Unique provider identifier */
10
+ id: string;
11
+ /** Number of .md files found (for local providers) */
12
+ noteCount?: number;
13
+ /** File system path (for local providers) */
14
+ path?: string;
15
+ /** Local or cloud */
16
+ type: 'cloud' | 'local';
17
+ };
18
+ /**
19
+ * Options for provider detection.
20
+ */
21
+ export type DetectProvidersOptions = {
22
+ /** Override environment variables (defaults to process.env) */
23
+ env?: Record<string, string | undefined>;
24
+ /**
25
+ * Roots to check for a GBrain CLI checkout (`src/cli.ts`).
26
+ * Defaults to workspace sibling, home, and known workspace paths.
27
+ * Pass a fixture path in tests; pass `[]` to force undetected.
28
+ */
29
+ gbrainCandidatePaths?: string[];
30
+ /** Explicit markdown folder paths to check */
31
+ markdownPaths?: string[];
32
+ /** Directories to scan for Obsidian vaults and .md folders */
33
+ searchPaths?: string[];
34
+ };
35
+ /**
36
+ * Return sensible default paths for scanning on the current platform.
37
+ * Exported for testability.
38
+ */
39
+ export declare function getDefaultSearchPaths(): {
40
+ markdownPaths: string[];
41
+ searchPaths: string[];
42
+ };
43
+ /**
44
+ * Scan the environment for available memory providers.
45
+ *
46
+ * Detects:
47
+ * - Obsidian vaults (by scanning for `.obsidian/` directories)
48
+ * - Local markdown folders (by checking explicit paths for .md files)
49
+ * - Cloud providers (by checking environment variables)
50
+ *
51
+ * @param options - Search paths and env var overrides for testability
52
+ * @returns Array of detected (and undetected) providers
53
+ */
54
+ export declare function detectProviders(options?: DetectProvidersOptions): Promise<DetectedProvider[]>;
@@ -0,0 +1,153 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Count .md files in a directory (non-recursive, capped at 10000).
6
+ */
7
+ function countMarkdownFiles(dirPath) {
8
+ try {
9
+ const entries = readdirSync(dirPath);
10
+ return entries.filter((e) => e.endsWith('.md')).length;
11
+ }
12
+ catch {
13
+ return 0;
14
+ }
15
+ }
16
+ /**
17
+ * Find Obsidian vaults by scanning for `.obsidian/` directories
18
+ * in immediate children of the given search paths.
19
+ */
20
+ function findObsidianVaults(searchPaths) {
21
+ const results = [];
22
+ for (const searchPath of searchPaths) {
23
+ if (!existsSync(searchPath))
24
+ continue;
25
+ try {
26
+ const entries = readdirSync(searchPath, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (!entry.isDirectory())
29
+ continue;
30
+ const candidate = join(searchPath, entry.name);
31
+ if (existsSync(join(candidate, '.obsidian'))) {
32
+ results.push({
33
+ detected: true,
34
+ id: 'obsidian',
35
+ noteCount: countMarkdownFiles(candidate),
36
+ path: candidate,
37
+ type: 'local',
38
+ });
39
+ }
40
+ }
41
+ }
42
+ catch {
43
+ // Skip inaccessible directories
44
+ }
45
+ }
46
+ return results;
47
+ }
48
+ /**
49
+ * Find markdown folders from explicit paths.
50
+ */
51
+ function findMarkdownFolders(paths) {
52
+ const results = [];
53
+ for (const mdPath of paths) {
54
+ if (!existsSync(mdPath))
55
+ continue;
56
+ const count = countMarkdownFiles(mdPath);
57
+ if (count > 0) {
58
+ results.push({
59
+ detected: true,
60
+ id: 'local-markdown',
61
+ noteCount: count,
62
+ path: mdPath,
63
+ type: 'local',
64
+ });
65
+ }
66
+ }
67
+ return results;
68
+ }
69
+ /**
70
+ * Return sensible default paths for scanning on the current platform.
71
+ * Exported for testability.
72
+ */
73
+ export function getDefaultSearchPaths() {
74
+ const home = homedir();
75
+ const searchPaths = [
76
+ join(home, 'Documents'),
77
+ home,
78
+ ].filter((p) => existsSync(p));
79
+ const markdownPaths = [
80
+ join(home, 'notes'),
81
+ join(home, 'Notes'),
82
+ join(home, 'content-skill-graph'),
83
+ join(home, 'Documents', 'notes'),
84
+ join(home, 'Documents', 'Notes'),
85
+ ].filter((p) => existsSync(p));
86
+ return { markdownPaths, searchPaths };
87
+ }
88
+ /**
89
+ * Scan the environment for available memory providers.
90
+ *
91
+ * Detects:
92
+ * - Obsidian vaults (by scanning for `.obsidian/` directories)
93
+ * - Local markdown folders (by checking explicit paths for .md files)
94
+ * - Cloud providers (by checking environment variables)
95
+ *
96
+ * @param options - Search paths and env var overrides for testability
97
+ * @returns Array of detected (and undetected) providers
98
+ */
99
+ export async function detectProviders(options) {
100
+ // const env = options?.env ?? process.env // Re-enable for honcho/hindsight detection in Phase 3
101
+ const defaults = options?.searchPaths ? undefined : getDefaultSearchPaths();
102
+ const searchPaths = options?.searchPaths ?? defaults?.searchPaths ?? [];
103
+ const markdownPaths = options?.markdownPaths ?? defaults?.markdownPaths ?? [];
104
+ const providers = [];
105
+ // ByteRover is always present
106
+ providers.push({
107
+ detected: true,
108
+ id: 'byterover',
109
+ type: 'local',
110
+ });
111
+ // Scan for Obsidian vaults
112
+ const obsidianVaults = findObsidianVaults(searchPaths);
113
+ if (obsidianVaults.length > 0) {
114
+ providers.push(...obsidianVaults);
115
+ }
116
+ else {
117
+ providers.push({
118
+ detected: false,
119
+ id: 'obsidian',
120
+ type: 'local',
121
+ });
122
+ }
123
+ // Check explicit markdown folders
124
+ const mdFolders = findMarkdownFolders(markdownPaths);
125
+ if (mdFolders.length > 0) {
126
+ providers.push(...mdFolders);
127
+ }
128
+ // Always include an undetected local-markdown entry so the user can manually add folders
129
+ // Cloud providers — check env vars
130
+ providers.push({
131
+ detected: false,
132
+ id: 'local-markdown',
133
+ type: 'local',
134
+ });
135
+ // Honcho and Hindsight temporarily disabled — adapters coming in Phase 3.
136
+ // GBrain — detect local CLI checkout by checking common locations for src/cli.ts
137
+ // (This path is the tool source tree, not `providers.gbrain.repoPath` / brain data.)
138
+ const gbrainCandidates = options?.gbrainCandidatePaths === undefined
139
+ ? [
140
+ join(process.cwd(), '..', 'gbrain'),
141
+ join(homedir(), 'gbrain'),
142
+ join(homedir(), 'Myspace', 'campfire', 'workspace', 'gbrain'),
143
+ ]
144
+ : options.gbrainCandidatePaths;
145
+ const gbrainPath = gbrainCandidates.find((p) => existsSync(join(p, 'src', 'cli.ts')));
146
+ providers.push({
147
+ detected: Boolean(gbrainPath),
148
+ id: 'gbrain',
149
+ path: gbrainPath,
150
+ type: 'cloud',
151
+ });
152
+ return providers;
153
+ }
@@ -0,0 +1,61 @@
1
+ import type { WizardAnswers } from './config-scaffolder.js';
2
+ import type { DetectedProvider } from './provider-detector.js';
3
+ /**
4
+ * Error thrown when the user cancels the wizard.
5
+ */
6
+ export declare class WizardCancelledError extends Error {
7
+ readonly name = "WizardCancelledError";
8
+ constructor(message?: string);
9
+ }
10
+ /**
11
+ * Sentinel error thrown by prompt implementations to signal ESC back-navigation.
12
+ * The wizard catches this and returns to the previous step.
13
+ */
14
+ export declare class EscBackError extends Error {
15
+ readonly name = "EscBackError";
16
+ constructor();
17
+ }
18
+ /**
19
+ * Injectable prompts interface for the onboarding wizard.
20
+ * Tests inject mock implementations; production uses real `@inquirer/prompts`.
21
+ */
22
+ export interface MemoryWizardPrompts {
23
+ /**
24
+ * Ask user to configure budget for cloud providers.
25
+ */
26
+ configureBudget(): Promise<{
27
+ globalMonthlyCents: number;
28
+ }>;
29
+ /**
30
+ * Ask user whether to enable enrichment (provider chaining).
31
+ * Shown when 2+ providers are selected. Returns true to enable, false to skip.
32
+ */
33
+ configureEnrichment(providerIds: string[]): Promise<boolean>;
34
+ /**
35
+ * Ask user to configure a specific provider (vault path, API key, etc.)
36
+ */
37
+ configureProvider(provider: DetectedProvider): Promise<Record<string, unknown>>;
38
+ /**
39
+ * Confirm that the wizard should write the config file.
40
+ */
41
+ confirmWrite(summary: string): Promise<boolean>;
42
+ /**
43
+ * Ask user to select which providers to enable from detected list.
44
+ * Returns array of indices into the detected array (as strings).
45
+ * Using indices instead of IDs supports multiple entries with the same provider type.
46
+ */
47
+ selectProviders(detected: DetectedProvider[]): Promise<string[]>;
48
+ }
49
+ /**
50
+ * Run the memory swarm onboarding wizard.
51
+ *
52
+ * Orchestrates: select → configure → budget → confirm.
53
+ * ESC back-navigation: prompts throw `EscBackError` to go back one step.
54
+ * Ctrl+C propagates as-is (handled by the command).
55
+ *
56
+ * @param prompts - Injectable prompt functions
57
+ * @param detected - Pre-scanned providers from detectProviders()
58
+ * @returns Wizard answers ready for scaffoldConfig()
59
+ * @throws WizardCancelledError if user declines to write
60
+ */
61
+ export declare function runMemoryWizard(prompts: MemoryWizardPrompts, detected: DetectedProvider[]): Promise<WizardAnswers>;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Error thrown when the user cancels the wizard.
3
+ */
4
+ export class WizardCancelledError extends Error {
5
+ name = 'WizardCancelledError';
6
+ constructor(message = 'Wizard cancelled by user') {
7
+ super(message);
8
+ }
9
+ }
10
+ /**
11
+ * Sentinel error thrown by prompt implementations to signal ESC back-navigation.
12
+ * The wizard catches this and returns to the previous step.
13
+ */
14
+ export class EscBackError extends Error {
15
+ name = 'EscBackError';
16
+ constructor() {
17
+ super('ESC back');
18
+ }
19
+ }
20
+ /**
21
+ * Providers whose config schema supports only a single instance.
22
+ * Duplicates produce a warning shown in the summary before confirm.
23
+ */
24
+ const SINGLE_INSTANCE_PROVIDERS = new Set(['byterover', 'gbrain', 'hindsight', 'honcho', 'obsidian']);
25
+ /**
26
+ * Detect duplicate single-instance providers and return user-facing warnings.
27
+ * local-markdown is excluded because its folders array supports merging.
28
+ */
29
+ function detectDuplicateWarnings(providers) {
30
+ const warnings = [];
31
+ const seen = new Map();
32
+ for (const provider of providers) {
33
+ if (!provider.enabled)
34
+ continue;
35
+ if (!SINGLE_INSTANCE_PROVIDERS.has(provider.id))
36
+ continue;
37
+ const previous = seen.get(provider.id);
38
+ if (previous) {
39
+ const droppedDetail = previous.vault_path ?? previous.repo_path ?? previous.connection_string ?? provider.id;
40
+ warnings.push(`Warning: Multiple ${provider.id} entries selected but config only supports one. ` +
41
+ `Earlier entry (${droppedDetail}) will be dropped; keeping the last.`);
42
+ }
43
+ seen.set(provider.id, provider.config);
44
+ }
45
+ return warnings;
46
+ }
47
+ /**
48
+ * Build a summary string for the confirmation step.
49
+ */
50
+ function buildSummary(providers, _budget, // Temporarily disabled — re-enable in Phase 3
51
+ duplicateWarnings, enrichment) {
52
+ const localCount = providers.filter((p) => p.enabled).length;
53
+ const lines = [
54
+ `Providers: ${localCount}`,
55
+ ...providers.filter((p) => p.enabled).map((p) => ` - ${p.id}`),
56
+ ];
57
+ // Budget temporarily disabled — re-enable in Phase 3 with cloud providers.
58
+ // if (budget) {
59
+ // lines.push(`Budget: $${(budget.globalMonthlyCents / 100).toFixed(2)}/month`)
60
+ // } else {
61
+ // lines.push('Budget: $0/month (local only)')
62
+ // }
63
+ lines.push('Strategy: adaptive routing', `Enrichment: ${enrichment === false ? 'off (all parallel)' : 'on (providers feed context to each other)'}`);
64
+ if (duplicateWarnings && duplicateWarnings.length > 0) {
65
+ lines.push('', ...duplicateWarnings);
66
+ }
67
+ return lines.join('\n');
68
+ }
69
+ /**
70
+ * Resolve selected keys (indices) to detected provider entries,
71
+ * ensuring byterover is always included.
72
+ */
73
+ function resolveSelectedProviders(selectedKeys, detected) {
74
+ const selected = selectedKeys
75
+ .map((key) => detected[Number(key)])
76
+ .filter((p) => p !== undefined);
77
+ if (!selected.some((p) => p.id === 'byterover')) {
78
+ const brvEntry = detected.find((p) => p.id === 'byterover');
79
+ if (brvEntry) {
80
+ selected.unshift(brvEntry);
81
+ }
82
+ }
83
+ return selected;
84
+ }
85
+ /**
86
+ * Configure each selected provider, returning wizard provider entries.
87
+ */
88
+ async function configureProviders(prompts, selectedProviders) {
89
+ const providers = [];
90
+ for (const detectedProvider of selectedProviders) {
91
+ if (detectedProvider.id === 'byterover') {
92
+ providers.push({ config: {}, enabled: true, id: 'byterover' });
93
+ continue;
94
+ }
95
+ // eslint-disable-next-line no-await-in-loop -- user-facing prompts must stay sequential
96
+ const config = await prompts.configureProvider(detectedProvider);
97
+ providers.push({ config, enabled: true, id: detectedProvider.id });
98
+ }
99
+ return providers;
100
+ }
101
+ /**
102
+ * Run the memory swarm onboarding wizard.
103
+ *
104
+ * Orchestrates: select → configure → budget → confirm.
105
+ * ESC back-navigation: prompts throw `EscBackError` to go back one step.
106
+ * Ctrl+C propagates as-is (handled by the command).
107
+ *
108
+ * @param prompts - Injectable prompt functions
109
+ * @param detected - Pre-scanned providers from detectProviders()
110
+ * @returns Wizard answers ready for scaffoldConfig()
111
+ * @throws WizardCancelledError if user declines to write
112
+ */
113
+ export async function runMemoryWizard(prompts, detected) {
114
+ let step = 'select';
115
+ let selectedProviders = [];
116
+ let providers = [];
117
+ let enrichment;
118
+ /** Whether enrichment step should be shown (2+ providers including byterover) */
119
+ const shouldAskEnrichment = () => selectedProviders.length >= 2 && selectedProviders.some((p) => p.id === 'byterover');
120
+ /* eslint-disable no-await-in-loop -- wizard steps are inherently sequential (user-facing prompts) */
121
+ // Step-based loop with ESC back-navigation
122
+ // Flow: select → configure → enrichment → confirm
123
+ // Budget step temporarily disabled — re-enable in Phase 3 with cloud providers.
124
+ while (true) {
125
+ try {
126
+ switch (step) {
127
+ // Budget temporarily disabled — re-enable when cloud providers are wired.
128
+ // case 'budget': {
129
+ // budget = await prompts.configureBudget()
130
+ // step = 'confirm'
131
+ // break
132
+ // }
133
+ case 'configure': {
134
+ providers = await configureProviders(prompts, selectedProviders);
135
+ step = shouldAskEnrichment() ? 'enrichment' : 'confirm';
136
+ break;
137
+ }
138
+ case 'confirm': {
139
+ const duplicateWarnings = detectDuplicateWarnings(providers);
140
+ const summary = buildSummary(providers, undefined, duplicateWarnings, enrichment);
141
+ const confirmed = await prompts.confirmWrite(summary);
142
+ if (!confirmed) {
143
+ throw new WizardCancelledError();
144
+ }
145
+ return { enrichment, providers };
146
+ }
147
+ case 'enrichment': {
148
+ const providerIds = providers.filter((p) => p.enabled).map((p) => p.id);
149
+ enrichment = await prompts.configureEnrichment(providerIds);
150
+ step = 'confirm';
151
+ break;
152
+ }
153
+ case 'select': {
154
+ const selectedKeys = await prompts.selectProviders(detected);
155
+ selectedProviders = resolveSelectedProviders(selectedKeys, detected);
156
+ step = 'configure';
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ catch (error) {
162
+ if (error instanceof EscBackError) {
163
+ // Go back one step
164
+ switch (step) {
165
+ case 'configure': {
166
+ step = 'select';
167
+ break;
168
+ }
169
+ case 'confirm': {
170
+ step = shouldAskEnrichment() ? 'enrichment' : 'configure';
171
+ break;
172
+ }
173
+ case 'enrichment': {
174
+ step = 'configure';
175
+ break;
176
+ }
177
+ default: {
178
+ // Already at first step — ignore ESC
179
+ break;
180
+ }
181
+ }
182
+ continue;
183
+ }
184
+ throw error;
185
+ }
186
+ }
187
+ }
@@ -11,3 +11,4 @@ export type { FileContributorOptions } from './file-contributor.js';
11
11
  export { MemoryContributor } from './memory-contributor.js';
12
12
  export type { MemoryContributorOptions } from './memory-contributor.js';
13
13
  export { StaticContributor } from './static-contributor.js';
14
+ export { SwarmStateContributor } from './swarm-state-contributor.js';
@@ -6,3 +6,4 @@ export { EnvironmentContributor } from './environment-contributor.js';
6
6
  export { FileContributor } from './file-contributor.js';
7
7
  export { MemoryContributor } from './memory-contributor.js';
8
8
  export { StaticContributor } from './static-contributor.js';
9
+ export { SwarmStateContributor } from './swarm-state-contributor.js';
@@ -0,0 +1,15 @@
1
+ import type { ContributorContext, SystemPromptContributor } from '../../../core/domain/system-prompt/types.js';
2
+ import type { ISwarmCoordinator } from '../../../core/interfaces/i-swarm-coordinator.js';
3
+ /**
4
+ * System prompt contributor that injects swarm state information.
5
+ *
6
+ * Only contributes when more than 1 provider is registered,
7
+ * since a single provider (ByteRover) doesn't need swarm awareness.
8
+ */
9
+ export declare class SwarmStateContributor implements SystemPromptContributor {
10
+ readonly id: string;
11
+ readonly priority: number;
12
+ private readonly coordinator;
13
+ constructor(id: string, priority: number, coordinator: ISwarmCoordinator);
14
+ getContent(_context: ContributorContext): Promise<string>;
15
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * System prompt contributor that injects swarm state information.
3
+ *
4
+ * Only contributes when more than 1 provider is registered,
5
+ * since a single provider (ByteRover) doesn't need swarm awareness.
6
+ */
7
+ export class SwarmStateContributor {
8
+ id;
9
+ priority;
10
+ coordinator;
11
+ constructor(id, priority, coordinator) {
12
+ this.id = id;
13
+ this.priority = priority;
14
+ this.coordinator = coordinator;
15
+ }
16
+ async getContent(_context) {
17
+ const providers = this.coordinator.getActiveProviders();
18
+ // No swarm awareness needed with 0 or 1 provider
19
+ if (providers.length <= 1) {
20
+ return '';
21
+ }
22
+ const lines = [
23
+ '<swarm-state>',
24
+ '## Memory Swarm',
25
+ '',
26
+ `${providers.length} memory providers are active. Use the \`swarm_query\` tool to search across all of them.`,
27
+ '',
28
+ '### Active Providers',
29
+ ];
30
+ for (const p of providers) {
31
+ const status = p.healthy ? 'healthy' : 'unhealthy';
32
+ const caps = [];
33
+ if (p.capabilities.keywordSearch)
34
+ caps.push('keyword');
35
+ if (p.capabilities.semanticSearch)
36
+ caps.push('semantic');
37
+ if (p.capabilities.graphTraversal)
38
+ caps.push('graph');
39
+ if (p.capabilities.temporalQuery)
40
+ caps.push('temporal');
41
+ if (p.capabilities.userModeling)
42
+ caps.push('user-modeling');
43
+ lines.push(`- **${p.id}** (${p.type}) — ${status} — capabilities: ${caps.join(', ')}`);
44
+ }
45
+ // Write guidance — only show if writable providers exist
46
+ const writableProviders = providers.filter((p) => p.capabilities.writeSupported && p.healthy);
47
+ if (writableProviders.length > 0) {
48
+ lines.push('', '### Writing Knowledge', 'Use `swarm_store` to write to external providers:');
49
+ for (const p of writableProviders) {
50
+ if (p.type === 'gbrain') {
51
+ lines.push(`- **${p.id}**: structured entities (people, companies, concepts)`);
52
+ }
53
+ else if (p.type === 'local-markdown') {
54
+ lines.push(`- **${p.id}**: notes, drafts, meeting summaries`);
55
+ }
56
+ else {
57
+ lines.push(`- **${p.id}**: general knowledge`);
58
+ }
59
+ }
60
+ lines.push('Use `curate` for project-specific knowledge (writes to context tree).');
61
+ }
62
+ lines.push('', '</swarm-state>');
63
+ return lines.join('\n');
64
+ }
65
+ }