byterover-cli 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/cipher-agent/run.d.ts +3 -2
- package/dist/commands/gen-rules.d.ts +42 -4
- package/dist/commands/gen-rules.js +235 -39
- package/dist/commands/init.d.ts +43 -3
- package/dist/commands/init.js +262 -24
- package/dist/config/environment.js +1 -1
- package/dist/core/domain/entities/brv-config.js +15 -6
- package/dist/core/domain/knowledge/directory-manager.js +2 -0
- package/dist/core/interfaces/cipher/i-chat-session.d.ts +2 -1
- package/dist/core/interfaces/i-file-service.d.ts +16 -0
- package/dist/core/interfaces/i-legacy-rule-detector.d.ts +56 -0
- package/dist/hooks/init/update-notifier.d.ts +39 -0
- package/dist/hooks/init/update-notifier.js +47 -0
- package/dist/infra/cipher/blob/sqlite-blob-storage.js +1 -1
- package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -0
- package/dist/infra/cogit/context-tree-to-push-context-mapper.js +4 -4
- package/dist/infra/cogit/http-cogit-push-service.js +3 -3
- package/dist/infra/context-tree/file-context-tree-service.js +1 -2
- package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +2 -0
- package/dist/infra/context-tree/file-context-tree-writer-service.js +2 -0
- package/dist/infra/file/fs-file-service.d.ts +2 -0
- package/dist/infra/file/fs-file-service.js +23 -1
- package/dist/infra/rule/constants.d.ts +9 -1
- package/dist/infra/rule/constants.js +9 -1
- package/dist/infra/rule/legacy-rule-detector.d.ts +21 -0
- package/dist/infra/rule/legacy-rule-detector.js +106 -0
- package/dist/infra/rule/rule-template-service.js +14 -6
- package/oclif.manifest.json +10 -97
- package/package.json +8 -3
- package/dist/core/domain/errors/rule-error.d.ts +0 -6
- package/dist/core/domain/errors/rule-error.js +0 -12
- package/dist/core/interfaces/i-rule-writer-service.d.ts +0 -13
- package/dist/infra/rule/rule-writer-service.d.ts +0 -19
- package/dist/infra/rule/rule-writer-service.js +0 -39
- /package/dist/core/interfaces/{i-rule-writer-service.js → i-legacy-rule-detector.js} +0 -0
package/dist/commands/init.js
CHANGED
|
@@ -7,15 +7,16 @@ import { ACE_DIR, BRV_CONFIG_VERSION, BRV_DIR, DEFAULT_BRANCH, PROJECT_CONFIG_FI
|
|
|
7
7
|
import { AGENT_VALUES } from '../core/domain/entities/agent.js';
|
|
8
8
|
import { BrvConfig } from '../core/domain/entities/brv-config.js';
|
|
9
9
|
import { BrvConfigVersionError } from '../core/domain/errors/brv-config-version-error.js';
|
|
10
|
-
import { RuleExistsError } from '../core/domain/errors/rule-error.js';
|
|
11
10
|
import { HttpCogitPullService } from '../infra/cogit/http-cogit-pull-service.js';
|
|
12
11
|
import { ProjectConfigStore } from '../infra/config/file-config-store.js';
|
|
13
12
|
import { FileContextTreeService } from '../infra/context-tree/file-context-tree-service.js';
|
|
14
13
|
import { FileContextTreeSnapshotService } from '../infra/context-tree/file-context-tree-snapshot-service.js';
|
|
15
14
|
import { FileContextTreeWriterService } from '../infra/context-tree/file-context-tree-writer-service.js';
|
|
16
15
|
import { FsFileService } from '../infra/file/fs-file-service.js';
|
|
16
|
+
import { AGENT_RULE_CONFIGS } from '../infra/rule/agent-rule-config.js';
|
|
17
|
+
import { BRV_RULE_MARKERS, BRV_RULE_TAG } from '../infra/rule/constants.js';
|
|
18
|
+
import { LegacyRuleDetector } from '../infra/rule/legacy-rule-detector.js';
|
|
17
19
|
import { RuleTemplateService } from '../infra/rule/rule-template-service.js';
|
|
18
|
-
import { RuleWriterService } from '../infra/rule/rule-writer-service.js';
|
|
19
20
|
import { HttpSpaceService } from '../infra/space/http-space-service.js';
|
|
20
21
|
import { KeychainTokenStore } from '../infra/storage/keychain-token-store.js';
|
|
21
22
|
import { HttpTeamService } from '../infra/team/http-team-service.js';
|
|
@@ -88,6 +89,7 @@ export default class Init extends Command {
|
|
|
88
89
|
const fileService = new FsFileService();
|
|
89
90
|
const templateLoader = new FsTemplateLoader(fileService);
|
|
90
91
|
const ruleTemplateService = new RuleTemplateService(templateLoader);
|
|
92
|
+
const legacyRuleDetector = new LegacyRuleDetector();
|
|
91
93
|
const contextTreeSnapshotService = new FileContextTreeSnapshotService();
|
|
92
94
|
return {
|
|
93
95
|
cogitPullService: new HttpCogitPullService({
|
|
@@ -96,14 +98,17 @@ export default class Init extends Command {
|
|
|
96
98
|
contextTreeService: new FileContextTreeService(),
|
|
97
99
|
contextTreeSnapshotService,
|
|
98
100
|
contextTreeWriterService: new FileContextTreeWriterService({ snapshotService: contextTreeSnapshotService }),
|
|
101
|
+
fileService,
|
|
102
|
+
legacyRuleDetector,
|
|
99
103
|
projectConfigStore: new ProjectConfigStore(),
|
|
100
|
-
ruleWriterService: new RuleWriterService(fileService, ruleTemplateService),
|
|
104
|
+
// ruleWriterService: new RuleWriterService(fileService, ruleTemplateService),
|
|
101
105
|
spaceService: new HttpSpaceService({
|
|
102
106
|
apiBaseUrl: envConfig.apiBaseUrl,
|
|
103
107
|
}),
|
|
104
108
|
teamService: new HttpTeamService({
|
|
105
109
|
apiBaseUrl: envConfig.apiBaseUrl,
|
|
106
110
|
}),
|
|
111
|
+
templateService: ruleTemplateService,
|
|
107
112
|
tokenStore,
|
|
108
113
|
trackingService,
|
|
109
114
|
};
|
|
@@ -150,27 +155,86 @@ export default class Init extends Command {
|
|
|
150
155
|
this.log();
|
|
151
156
|
return this.promptForTeamSelection(teams);
|
|
152
157
|
}
|
|
153
|
-
async generateRulesForAgent(
|
|
154
|
-
this.log(`Generating rules for: ${
|
|
155
|
-
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
async generateRulesForAgent(selectedAgent, fileService, templateService, legacyRuleDetector) {
|
|
159
|
+
this.log(`Generating rules for: ${selectedAgent}`);
|
|
160
|
+
// try {
|
|
161
|
+
// await ruleWriterService.writeRule(agent, false)
|
|
162
|
+
// this.log(`✅ Successfully generated rule file for ${agent}`)
|
|
163
|
+
// } catch (error) {
|
|
164
|
+
// if (error instanceof RuleExistsError) {
|
|
165
|
+
// const overwrite = await this.promptForOverwriteConfirmation(agent)
|
|
166
|
+
// if (overwrite) {
|
|
167
|
+
// await ruleWriterService.writeRule(agent, true)
|
|
168
|
+
// this.log(`✅ Successfully generated rule file for ${agent}`)
|
|
169
|
+
// } else {
|
|
170
|
+
// this.log(`Skipping rule file generation for ${agent}`)
|
|
171
|
+
// }
|
|
172
|
+
// } else {
|
|
173
|
+
// throw error
|
|
174
|
+
// }
|
|
175
|
+
// }
|
|
176
|
+
const { filePath, writeMode } = AGENT_RULE_CONFIGS[selectedAgent];
|
|
177
|
+
// STEP 1: Check if file exists
|
|
178
|
+
const fileExists = await fileService.exists(filePath);
|
|
179
|
+
if (!fileExists) {
|
|
180
|
+
// Scenario A: File doesn't exist
|
|
181
|
+
const shouldCreate = await this.promptForFileCreation(selectedAgent, filePath);
|
|
182
|
+
if (!shouldCreate) {
|
|
183
|
+
this.log(`Skipped rule file creation for ${selectedAgent}`);
|
|
184
|
+
return;
|
|
169
185
|
}
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
await this.createNewRuleFile({
|
|
187
|
+
agent: selectedAgent,
|
|
188
|
+
filePath,
|
|
189
|
+
fileService,
|
|
190
|
+
templateService,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// STEP 2: File exists - read content
|
|
195
|
+
const content = await fileService.read(filePath);
|
|
196
|
+
// STEP 3: Check for LEGACY rules (priority: clean these up first)
|
|
197
|
+
const hasFooterTag = content.includes(`${BRV_RULE_TAG} ${selectedAgent}`);
|
|
198
|
+
const hasBoundaryMarkers = content.includes(BRV_RULE_MARKERS.START) && content.includes(BRV_RULE_MARKERS.END);
|
|
199
|
+
const hasLegacyRules = hasFooterTag && !hasBoundaryMarkers;
|
|
200
|
+
if (hasLegacyRules) {
|
|
201
|
+
// Scenario B: Legacy rules detected - handle cleanup
|
|
202
|
+
await this.handleLegacyRulesCleanup({
|
|
203
|
+
agent: selectedAgent,
|
|
204
|
+
content,
|
|
205
|
+
filePath,
|
|
206
|
+
fileService,
|
|
207
|
+
legacyRuleDetector,
|
|
208
|
+
templateService,
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// STEP 4: Check for NEW rules (boundary markers)
|
|
213
|
+
if (hasBoundaryMarkers) {
|
|
214
|
+
// Scenario C: New rules exist - prompt for overwrite
|
|
215
|
+
const shouldOverwrite = await this.promptForOverwriteConfirmation(selectedAgent);
|
|
216
|
+
if (!shouldOverwrite) {
|
|
217
|
+
this.log(`Skipped rule file update for ${selectedAgent}`);
|
|
218
|
+
return;
|
|
172
219
|
}
|
|
220
|
+
await this.replaceExistingRules({
|
|
221
|
+
agent: selectedAgent,
|
|
222
|
+
content,
|
|
223
|
+
filePath,
|
|
224
|
+
fileService,
|
|
225
|
+
templateService,
|
|
226
|
+
writeMode,
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
173
229
|
}
|
|
230
|
+
// STEP 5: No ByteRover content - append rules
|
|
231
|
+
await this.appendRulesToFile({
|
|
232
|
+
agent: selectedAgent,
|
|
233
|
+
filePath,
|
|
234
|
+
fileService,
|
|
235
|
+
templateService,
|
|
236
|
+
writeMode,
|
|
237
|
+
});
|
|
174
238
|
}
|
|
175
239
|
async getExistingConfig(projectConfigStore) {
|
|
176
240
|
const exists = await projectConfigStore.exists();
|
|
@@ -214,6 +278,14 @@ export default class Init extends Command {
|
|
|
214
278
|
isLegacyProjectConfig(config) {
|
|
215
279
|
return 'type' in config && config.type === 'legacy';
|
|
216
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Checks if the given path represents a README.md placeholder file.
|
|
283
|
+
* Handles both legacy paths with leading slash and new paths without.
|
|
284
|
+
*/
|
|
285
|
+
isReadmePlaceholder(path) {
|
|
286
|
+
const normalizedPath = path.replace(/^\/+/, '');
|
|
287
|
+
return normalizedPath === 'README.md';
|
|
288
|
+
}
|
|
217
289
|
async promptAceDeprecationRemoval() {
|
|
218
290
|
this.log('\n The ACE system is being deprecated.');
|
|
219
291
|
this.log(' ByteRover is migrating to the new Context Tree system for improved');
|
|
@@ -247,6 +319,41 @@ export default class Init extends Command {
|
|
|
247
319
|
});
|
|
248
320
|
return answer;
|
|
249
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Prompts the user to choose cleanup strategy for legacy rules.
|
|
324
|
+
* This method is protected to allow test overrides.
|
|
325
|
+
* @returns The chosen cleanup strategy
|
|
326
|
+
*/
|
|
327
|
+
async promptForCleanupStrategy() {
|
|
328
|
+
return select({
|
|
329
|
+
choices: [
|
|
330
|
+
{
|
|
331
|
+
description: 'New rules will be added with boundary markers. You manually remove old sections at your convenience.',
|
|
332
|
+
name: 'Manual cleanup (recommended)',
|
|
333
|
+
value: 'manual',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
description: '⚠️ We will remove all detected old sections. May cause content loss if detection is imperfect. A backup will be created.',
|
|
337
|
+
name: 'Automatic cleanup',
|
|
338
|
+
value: 'automatic',
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
message: 'How would you like to proceed?',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Prompts the user to create a new rule file.
|
|
346
|
+
* This method is protected to allow test overrides.
|
|
347
|
+
* @param agent The agent for which the rule file doesn't exist
|
|
348
|
+
* @param filePath The path where the file would be created
|
|
349
|
+
* @returns True if the user wants to create the file, false otherwise
|
|
350
|
+
*/
|
|
351
|
+
async promptForFileCreation(agent, filePath) {
|
|
352
|
+
return confirm({
|
|
353
|
+
default: true,
|
|
354
|
+
message: `Rule file '${filePath}' doesn't exist. Create it with ByteRover rules?`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
250
357
|
/**
|
|
251
358
|
* Prompts the user to confirm overwriting an existing rule file.
|
|
252
359
|
* This method is protected to allow test overrides.
|
|
@@ -293,7 +400,9 @@ export default class Init extends Command {
|
|
|
293
400
|
async run() {
|
|
294
401
|
try {
|
|
295
402
|
const { flags } = await this.parse(Init);
|
|
296
|
-
const { cogitPullService, contextTreeService, contextTreeSnapshotService, contextTreeWriterService,
|
|
403
|
+
const { cogitPullService, contextTreeService, contextTreeSnapshotService, contextTreeWriterService, fileService, legacyRuleDetector, projectConfigStore,
|
|
404
|
+
// ruleWriterService,
|
|
405
|
+
spaceService, teamService, templateService, tokenStore, trackingService, } = this.createServices();
|
|
297
406
|
const authToken = await this.ensureAuthenticated(tokenStore);
|
|
298
407
|
const existingConfig = await this.getExistingConfig(projectConfigStore);
|
|
299
408
|
if (existingConfig) {
|
|
@@ -346,7 +455,7 @@ export default class Init extends Command {
|
|
|
346
455
|
await projectConfigStore.write(config);
|
|
347
456
|
this.log(`\nGenerate rule instructions for coding agents to work with ByteRover correctly`);
|
|
348
457
|
this.log();
|
|
349
|
-
await this.generateRulesForAgent(
|
|
458
|
+
await this.generateRulesForAgent(selectedAgent, fileService, templateService, legacyRuleDetector);
|
|
350
459
|
await trackingService.track('rule:generate');
|
|
351
460
|
await trackingService.track('space:init');
|
|
352
461
|
this.logSuccess(selectedSpace);
|
|
@@ -369,7 +478,7 @@ export default class Init extends Command {
|
|
|
369
478
|
// Check if space is "empty" (no files, or only README.md placeholder)
|
|
370
479
|
// CoGit follows Git semantics - empty repos have a README.md placeholder
|
|
371
480
|
const isEmptySpace = coGitSnapshot.files.length === 0 ||
|
|
372
|
-
(coGitSnapshot.files.length === 1 && coGitSnapshot.files[0].path
|
|
481
|
+
(coGitSnapshot.files.length === 1 && this.isReadmePlaceholder(coGitSnapshot.files[0].path));
|
|
373
482
|
if (isEmptySpace) {
|
|
374
483
|
// Remote is empty - ignore placeholder, create templates with empty snapshot
|
|
375
484
|
await this.initializeMemoryContextDir('context tree', () => params.contextTreeService.initialize());
|
|
@@ -387,10 +496,139 @@ export default class Init extends Command {
|
|
|
387
496
|
throw new Error(`Failed to sync from ByteRover: ${error instanceof Error ? error.message : 'Unknown error'}. Please try again.`);
|
|
388
497
|
}
|
|
389
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Appends ByteRover rules to a file that has no ByteRover content.
|
|
501
|
+
*/
|
|
502
|
+
async appendRulesToFile(params) {
|
|
503
|
+
const { agent, filePath, fileService, templateService, writeMode } = params;
|
|
504
|
+
const ruleContent = await templateService.generateRuleContent(agent);
|
|
505
|
+
// For dedicated ByteRover files, overwrite; for shared instruction files, append
|
|
506
|
+
const mode = writeMode === 'overwrite' ? 'overwrite' : 'append';
|
|
507
|
+
await fileService.write(ruleContent, filePath, mode);
|
|
508
|
+
this.log(`✅ Successfully added rule file for ${agent}`);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Creates a new rule file with ByteRover rules.
|
|
512
|
+
*/
|
|
513
|
+
async createNewRuleFile(params) {
|
|
514
|
+
const { agent, filePath, fileService, templateService } = params;
|
|
515
|
+
const ruleContent = await templateService.generateRuleContent(agent);
|
|
516
|
+
await fileService.write(ruleContent, filePath, 'overwrite');
|
|
517
|
+
this.log(`✅ Successfully created rule file for ${agent} at ${filePath}`);
|
|
518
|
+
}
|
|
519
|
+
async handleLegacyRulesCleanup(params) {
|
|
520
|
+
const { agent, content, filePath, fileService, legacyRuleDetector, templateService } = params;
|
|
521
|
+
const detectionResult = legacyRuleDetector.detectLegacyRules(content, agent);
|
|
522
|
+
const { reliableMatches, uncertainMatches } = detectionResult;
|
|
523
|
+
this.log(`\n⚠️ Detected ${reliableMatches.length + uncertainMatches.length} old ByteRover rule section(s) in ${filePath}:\n`);
|
|
524
|
+
if (reliableMatches.length > 0) {
|
|
525
|
+
this.log('Reliable matches:');
|
|
526
|
+
for (const [index, match] of reliableMatches.entries()) {
|
|
527
|
+
this.log(` Section ${index + 1}: lines ${match.startLine}-${match.endLine}`);
|
|
528
|
+
}
|
|
529
|
+
this.log();
|
|
530
|
+
}
|
|
531
|
+
if (uncertainMatches.length > 0) {
|
|
532
|
+
this.log(' ⚠️ Uncertain matches (cannot determine start):');
|
|
533
|
+
for (const match of uncertainMatches) {
|
|
534
|
+
this.log(` Footer found at line ${match.footerLine}`);
|
|
535
|
+
this.log(` Reason: ${match.reason}`);
|
|
536
|
+
}
|
|
537
|
+
this.log();
|
|
538
|
+
this.log('⚠️ Due to uncertain matches, only manual cleanup is available.\n');
|
|
539
|
+
await this.performManualCleanup({
|
|
540
|
+
agent,
|
|
541
|
+
filePath,
|
|
542
|
+
fileService,
|
|
543
|
+
reliableMatches,
|
|
544
|
+
templateService,
|
|
545
|
+
uncertainMatches,
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const selectedStrategy = await this.promptForCleanupStrategy();
|
|
550
|
+
await (selectedStrategy === 'manual'
|
|
551
|
+
? this.performManualCleanup({
|
|
552
|
+
agent,
|
|
553
|
+
filePath,
|
|
554
|
+
fileService,
|
|
555
|
+
reliableMatches,
|
|
556
|
+
templateService,
|
|
557
|
+
uncertainMatches,
|
|
558
|
+
})
|
|
559
|
+
: this.performAutomaticCleanup({
|
|
560
|
+
agent,
|
|
561
|
+
filePath,
|
|
562
|
+
fileService,
|
|
563
|
+
reliableMatches,
|
|
564
|
+
templateService,
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
390
567
|
logSuccess(space) {
|
|
391
568
|
this.log(`\n✓ Project initialized successfully!`);
|
|
392
569
|
this.log(`✓ Connected to space: ${space.getDisplayName()}`);
|
|
393
570
|
this.log(`✓ Configuration saved to: ${BRV_DIR}/${PROJECT_CONFIG_FILE}`);
|
|
394
571
|
this.log("NOTE: It's recommended to add .brv/ to your .gitignore file since ByteRover already takes care of memory/context versioning for you.");
|
|
395
572
|
}
|
|
573
|
+
async performAutomaticCleanup(params) {
|
|
574
|
+
const { agent, filePath, fileService, reliableMatches, templateService } = params;
|
|
575
|
+
const backupPath = await fileService.createBackup(filePath);
|
|
576
|
+
this.log(`📦 Backup created: ${backupPath}`);
|
|
577
|
+
let content = await fileService.read(filePath);
|
|
578
|
+
// Remove all reliable matches (in reverse order to preserve line numbers)
|
|
579
|
+
const sortedMatches = [...reliableMatches].sort((a, b) => b.startLine - a.startLine);
|
|
580
|
+
for (const match of sortedMatches) {
|
|
581
|
+
content = content.replace(match.content, '');
|
|
582
|
+
}
|
|
583
|
+
// Write cleaned content
|
|
584
|
+
await fileService.write(content, filePath, 'overwrite');
|
|
585
|
+
// Append new rules
|
|
586
|
+
const ruleContent = await templateService.generateRuleContent(agent);
|
|
587
|
+
await fileService.write(ruleContent, filePath, 'append');
|
|
588
|
+
this.log(`✅ Removed ${reliableMatches.length} old ByteRover section(s)`);
|
|
589
|
+
this.log(`✅ Added new rules with boundary markers`);
|
|
590
|
+
this.log(`\nYou can safely delete the backup file once verified.`);
|
|
591
|
+
}
|
|
592
|
+
async performManualCleanup(params) {
|
|
593
|
+
const { agent, filePath, fileService, reliableMatches, templateService, uncertainMatches } = params;
|
|
594
|
+
const ruleContent = await templateService.generateRuleContent(agent);
|
|
595
|
+
await fileService.write(ruleContent, filePath, 'append');
|
|
596
|
+
this.log(`✅ New ByteRover rules added with boundary markers\n`);
|
|
597
|
+
this.log('Please manually remove old sections:');
|
|
598
|
+
for (const [index, match] of reliableMatches.entries()) {
|
|
599
|
+
this.log(` - Section ${index + 1}: lines ${match.startLine}-${match.endLine} in ${filePath}`);
|
|
600
|
+
}
|
|
601
|
+
for (const match of uncertainMatches) {
|
|
602
|
+
this.log(` - Section ending at line ${match.footerLine} in ${filePath}`);
|
|
603
|
+
}
|
|
604
|
+
this.log('\nKeep only the section between:');
|
|
605
|
+
this.log(' <!-- BEGIN BYTEROVER RULES -->');
|
|
606
|
+
this.log(' <!-- END BYTEROVER RULES -->');
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Replaces existing ByteRover rules (with boundary markers) with new rules.
|
|
610
|
+
*/
|
|
611
|
+
async replaceExistingRules(params) {
|
|
612
|
+
const { agent, content, filePath, fileService, templateService, writeMode } = params;
|
|
613
|
+
const ruleContent = await templateService.generateRuleContent(agent);
|
|
614
|
+
if (writeMode === 'overwrite') {
|
|
615
|
+
// For dedicated ByteRover files, just overwrite the entire file
|
|
616
|
+
await fileService.write(ruleContent, filePath, 'overwrite');
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
// For shared instruction files, replace the section between markers
|
|
620
|
+
const startMarker = BRV_RULE_MARKERS.START;
|
|
621
|
+
const endMarker = BRV_RULE_MARKERS.END;
|
|
622
|
+
const startIndex = content.indexOf(startMarker);
|
|
623
|
+
const endIndex = content.indexOf(endMarker, startIndex);
|
|
624
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
625
|
+
this.error('Could not find boundary markers in the file');
|
|
626
|
+
}
|
|
627
|
+
const before = content.slice(0, startIndex);
|
|
628
|
+
const after = content.slice(endIndex + endMarker.length);
|
|
629
|
+
const newContent = before + ruleContent + after;
|
|
630
|
+
await fileService.write(newContent, filePath, 'overwrite');
|
|
631
|
+
}
|
|
632
|
+
this.log(`✅ Successfully updated rule file for ${agent}`);
|
|
633
|
+
}
|
|
396
634
|
}
|
|
@@ -13,7 +13,7 @@ export const ENV_CONFIG = {
|
|
|
13
13
|
apiBaseUrl: 'https://dev-beta-iam.byterover.dev/api/v1',
|
|
14
14
|
authorizationUrl: 'https://dev-beta-iam.byterover.dev/api/v1/oidc/authorize',
|
|
15
15
|
clientId: 'byterover-cli-client',
|
|
16
|
-
cogitApiBaseUrl: 'https://dev-beta-
|
|
16
|
+
cogitApiBaseUrl: 'https://dev-beta-cgit.byterover.dev/api/v1',
|
|
17
17
|
issuerUrl: 'https://dev-beta-iam.byterover.dev/api/v1/oidc',
|
|
18
18
|
llmGrpcEndpoint: 'dev-beta-llm-grpc.byterover.dev',
|
|
19
19
|
memoraApiBaseUrl: 'https://dev-beta-memora-retrieve.byterover.dev/api/v3',
|
|
@@ -101,22 +101,31 @@ export class BrvConfig {
|
|
|
101
101
|
* @throws BrvConfigVersionError if version is missing or mismatched.
|
|
102
102
|
*/
|
|
103
103
|
static fromJson(json) {
|
|
104
|
-
if
|
|
105
|
-
|
|
104
|
+
// Minimal check if json is an object
|
|
105
|
+
if (typeof json !== 'object' || json === null || json === undefined) {
|
|
106
|
+
throw new Error('BrvConfig JSON must be an object');
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
// Check version FIRST (before full structure validation)
|
|
109
|
+
// This ensures outdated configs get a helpful version error
|
|
110
|
+
// instead of a generic structure error
|
|
111
|
+
const jsonObj = json;
|
|
112
|
+
const version = typeof jsonObj.version === 'string' ? jsonObj.version : undefined;
|
|
113
|
+
if (version === undefined) {
|
|
108
114
|
throw new BrvConfigVersionError({
|
|
109
115
|
currentVersion: undefined,
|
|
110
116
|
expectedVersion: BRV_CONFIG_VERSION,
|
|
111
117
|
});
|
|
112
118
|
}
|
|
113
|
-
if (
|
|
119
|
+
if (version !== BRV_CONFIG_VERSION) {
|
|
114
120
|
throw new BrvConfigVersionError({
|
|
115
|
-
currentVersion:
|
|
121
|
+
currentVersion: version,
|
|
116
122
|
expectedVersion: BRV_CONFIG_VERSION,
|
|
117
123
|
});
|
|
118
124
|
}
|
|
119
|
-
|
|
125
|
+
if (!isBrvConfigJson(json)) {
|
|
126
|
+
throw new Error('Invalid BrvConfig JSON structure');
|
|
127
|
+
}
|
|
128
|
+
return new BrvConfig({ ...json, version });
|
|
120
129
|
}
|
|
121
130
|
/**
|
|
122
131
|
* Creates a BrvConfig from a Space entity.
|
|
@@ -138,6 +138,8 @@ export const DirectoryManager = {
|
|
|
138
138
|
* @param content - Content to write
|
|
139
139
|
*/
|
|
140
140
|
async writeFileAtomic(filePath, content) {
|
|
141
|
+
// Ensure parent directory exists before writing
|
|
142
|
+
await this.ensureParentDirectory(filePath);
|
|
141
143
|
const tempPath = `${filePath}.tmp`;
|
|
142
144
|
await fs.writeFile(tempPath, content, 'utf8');
|
|
143
145
|
await fs.rename(tempPath, filePath);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from '../../domain/cipher/session/types.js';
|
|
2
|
+
import type { ExecutionContext } from './i-cipher-agent.js';
|
|
2
3
|
import type { ILLMService } from './i-llm-service.js';
|
|
3
4
|
/**
|
|
4
5
|
* Interface for a chat session.
|
|
@@ -56,7 +57,7 @@ export interface IChatSession {
|
|
|
56
57
|
* @throws LLMError if LLM call fails
|
|
57
58
|
*/
|
|
58
59
|
run(input: string, options?: {
|
|
59
|
-
executionContext?:
|
|
60
|
+
executionContext?: ExecutionContext;
|
|
60
61
|
mode?: 'autonomous' | 'default' | 'query';
|
|
61
62
|
}): Promise<string>;
|
|
62
63
|
}
|
|
@@ -8,6 +8,13 @@ export type WriteMode = 'append' | 'overwrite';
|
|
|
8
8
|
* Interface for file service operations.
|
|
9
9
|
*/
|
|
10
10
|
export interface IFileService {
|
|
11
|
+
/**
|
|
12
|
+
* Creates a timestamped backup copy of a file.
|
|
13
|
+
*
|
|
14
|
+
* @param filePath The path to the file to backup.
|
|
15
|
+
* @returns A promise that resolves with the path to the backup file.
|
|
16
|
+
*/
|
|
17
|
+
createBackup: (filePath: string) => Promise<string>;
|
|
11
18
|
/**
|
|
12
19
|
* Checks if a file exists at the specified path.
|
|
13
20
|
*
|
|
@@ -22,6 +29,15 @@ export interface IFileService {
|
|
|
22
29
|
* @returns A promise that resolves with the content of the file.
|
|
23
30
|
*/
|
|
24
31
|
read: (filePath: string) => Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Replaces specific content within a file with new content.
|
|
34
|
+
*
|
|
35
|
+
* @param filePath The path to the file.
|
|
36
|
+
* @param oldContent The content to be replaced.
|
|
37
|
+
* @param newContent The new content to insert.
|
|
38
|
+
* @returns A promise that resolves when the replacement is complete.
|
|
39
|
+
*/
|
|
40
|
+
replaceContent: (filePath: string, oldContent: string, newContent: string) => Promise<void>;
|
|
25
41
|
/**
|
|
26
42
|
* Writes content to the specified file.
|
|
27
43
|
*
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Agent } from '../domain/entities/agent.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a reliably detected legacy ByteRover rule section.
|
|
4
|
+
*/
|
|
5
|
+
export type LegacyRuleMatch = {
|
|
6
|
+
/**
|
|
7
|
+
* Content of the detected section.
|
|
8
|
+
*/
|
|
9
|
+
content: string;
|
|
10
|
+
/**
|
|
11
|
+
* Ending line number (1-indexed)
|
|
12
|
+
*/
|
|
13
|
+
endLine: number;
|
|
14
|
+
/**
|
|
15
|
+
* Starting line number (1-indexed)
|
|
16
|
+
*/
|
|
17
|
+
startLine: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Represents an uncertain detection where the footer was found but start couldn't be reliably determined.
|
|
21
|
+
*/
|
|
22
|
+
export type UncertainMatch = {
|
|
23
|
+
/**
|
|
24
|
+
* Line number where the footer tag was found (1-indexed).
|
|
25
|
+
*/
|
|
26
|
+
footerLine: number;
|
|
27
|
+
/**
|
|
28
|
+
* Reason why the start couldn't be determined.
|
|
29
|
+
*/
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Result of detecting legacy ByteRover rules in a file.
|
|
34
|
+
*/
|
|
35
|
+
export type LegacyRuleDetectionResult = {
|
|
36
|
+
/**
|
|
37
|
+
* Reliably detected rule sections with known start and end positions.
|
|
38
|
+
*/
|
|
39
|
+
reliableMatches: LegacyRuleMatch[];
|
|
40
|
+
/**
|
|
41
|
+
* Uncertain matches where only the footer was found.
|
|
42
|
+
*/
|
|
43
|
+
uncertainMatches: UncertainMatch[];
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Service for detecting legacy ByteRover rules (without boundary markers) in instruction files.
|
|
47
|
+
*/
|
|
48
|
+
export interface ILegacyRuleDetector {
|
|
49
|
+
/**
|
|
50
|
+
* Detects legacy ByteRover rule sections in file content.
|
|
51
|
+
* @param content The file content to analyze.
|
|
52
|
+
* @param agentName The agent name to look for in the footer tag.
|
|
53
|
+
* @returns Detection result with reliable and uncertain matches.
|
|
54
|
+
*/
|
|
55
|
+
detectLegacyRules: (content: string, agentName: Agent) => LegacyRuleDetectionResult;
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Hook } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* Check interval for update notifications (24 hours)
|
|
4
|
+
*/
|
|
5
|
+
export declare const UPDATE_CHECK_INTERVAL_MS: number;
|
|
6
|
+
/**
|
|
7
|
+
* Narrowed notifier type for dependency injection
|
|
8
|
+
*/
|
|
9
|
+
export type NarrowedUpdateNotifier = {
|
|
10
|
+
notify: (options: {
|
|
11
|
+
defer: boolean;
|
|
12
|
+
message: string;
|
|
13
|
+
}) => void;
|
|
14
|
+
update?: {
|
|
15
|
+
current: string;
|
|
16
|
+
latest: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Dependencies that can be injected for testing
|
|
21
|
+
*/
|
|
22
|
+
export type UpdateNotifierDeps = {
|
|
23
|
+
confirmPrompt: (options: {
|
|
24
|
+
default: boolean;
|
|
25
|
+
message: string;
|
|
26
|
+
}) => Promise<boolean>;
|
|
27
|
+
execSyncFn: (command: string, options: {
|
|
28
|
+
stdio: 'inherit';
|
|
29
|
+
}) => void;
|
|
30
|
+
isTTY: boolean;
|
|
31
|
+
log: (message: string) => void;
|
|
32
|
+
notifier: NarrowedUpdateNotifier;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Core update notification logic, extracted for testability
|
|
36
|
+
*/
|
|
37
|
+
export declare function handleUpdateNotification(deps: UpdateNotifierDeps): Promise<void>;
|
|
38
|
+
declare const hook: Hook<'init'>;
|
|
39
|
+
export default hook;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import updateNotifier from 'update-notifier';
|
|
4
|
+
/**
|
|
5
|
+
* Check interval for update notifications (24 hours)
|
|
6
|
+
*/
|
|
7
|
+
export const UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 24;
|
|
8
|
+
/**
|
|
9
|
+
* Core update notification logic, extracted for testability
|
|
10
|
+
*/
|
|
11
|
+
export async function handleUpdateNotification(deps) {
|
|
12
|
+
const { confirmPrompt, execSyncFn, isTTY, log, notifier } = deps;
|
|
13
|
+
if (!notifier.update || !isTTY) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const { current, latest } = notifier.update;
|
|
17
|
+
notifier.notify({
|
|
18
|
+
defer: false,
|
|
19
|
+
message: `Update available: ${current} → ${latest}`,
|
|
20
|
+
});
|
|
21
|
+
const shouldUpdate = await confirmPrompt({
|
|
22
|
+
default: true,
|
|
23
|
+
message: 'Would you like to update now?',
|
|
24
|
+
});
|
|
25
|
+
if (shouldUpdate) {
|
|
26
|
+
log('Updating byterover-cli...');
|
|
27
|
+
try {
|
|
28
|
+
execSyncFn('npm update -g byterover-cli', { stdio: 'inherit' });
|
|
29
|
+
log(`✓ Successfully updated to ${latest}`);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
log('⚠️ Automatic update failed. Please run manually: npm update -g byterover-cli');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const hook = async function () {
|
|
37
|
+
const pkgInfo = { name: this.config.name, version: this.config.version };
|
|
38
|
+
const notifier = updateNotifier({ pkg: pkgInfo, updateCheckInterval: UPDATE_CHECK_INTERVAL_MS });
|
|
39
|
+
await handleUpdateNotification({
|
|
40
|
+
confirmPrompt: confirm,
|
|
41
|
+
execSyncFn: execSync,
|
|
42
|
+
isTTY: process.stdout.isTTY ?? false,
|
|
43
|
+
log: this.log.bind(this),
|
|
44
|
+
notifier,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
export default hook;
|
|
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
|
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { BlobError } from '../../../core/domain/cipher/errors/blob-error.js';
|
|
5
|
+
import { runMigrations } from './migrations.js';
|
|
5
6
|
/**
|
|
6
7
|
* SQLite-based blob storage implementation
|
|
7
8
|
*
|
|
@@ -149,7 +150,6 @@ export class SqliteBlobStorage {
|
|
|
149
150
|
// Enable WAL mode for better concurrent performance
|
|
150
151
|
this.db.pragma('journal_mode = WAL');
|
|
151
152
|
// Run migrations to ensure schema is up-to-date
|
|
152
|
-
const { runMigrations } = await import('./migrations.js');
|
|
153
153
|
const appliedCount = runMigrations(this.db, this.logger);
|
|
154
154
|
if (appliedCount > 0) {
|
|
155
155
|
this.logger.info(`💾 Initializing storage...`);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Implements IContentGenerator using ByteRover gRPC service.
|
|
5
5
|
* Supports both Claude and Gemini models through the unified gRPC interface.
|
|
6
6
|
*/
|
|
7
|
+
import { FunctionCallingConfigMode } from '@google/genai';
|
|
7
8
|
import { ClaudeMessageFormatter } from '../formatters/claude-formatter.js';
|
|
8
9
|
import { GeminiMessageFormatter } from '../formatters/gemini-formatter.js';
|
|
9
10
|
import { ThinkingConfigManager } from '../thought-parser.js';
|
|
@@ -164,6 +165,11 @@ export class ByteRoverContentGenerator {
|
|
|
164
165
|
topP: 1,
|
|
165
166
|
...(systemPrompt && { systemInstruction: { parts: [{ text: systemPrompt }] } }),
|
|
166
167
|
...(toolDefinitions.length > 0 && {
|
|
168
|
+
toolConfig: {
|
|
169
|
+
functionCallingConfig: {
|
|
170
|
+
mode: FunctionCallingConfigMode.VALIDATED,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
167
173
|
tools: [
|
|
168
174
|
{
|
|
169
175
|
functionDeclarations: toolDefinitions,
|