byterover-cli 0.3.3 → 0.3.5
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/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/core/domain/knowledge/directory-manager.js +2 -0
- 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 +43 -0
- 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-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 +1 -1
- package/package.json +7 -2
- 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
|
}
|
|
@@ -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);
|
|
@@ -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,43 @@
|
|
|
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
|
+
const shouldUpdate = await confirmPrompt({
|
|
18
|
+
default: true,
|
|
19
|
+
message: `Update available: ${current} → ${latest}. Would you like to update now?`,
|
|
20
|
+
});
|
|
21
|
+
if (shouldUpdate) {
|
|
22
|
+
log('Updating byterover-cli...');
|
|
23
|
+
try {
|
|
24
|
+
execSyncFn('npm update -g byterover-cli', { stdio: 'inherit' });
|
|
25
|
+
log(`✓ Successfully updated to ${latest}`);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
log('⚠️ Automatic update failed. Please run manually: npm update -g byterover-cli');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const hook = async function () {
|
|
33
|
+
const pkgInfo = { name: this.config.name, version: this.config.version };
|
|
34
|
+
const notifier = updateNotifier({ pkg: pkgInfo, updateCheckInterval: UPDATE_CHECK_INTERVAL_MS });
|
|
35
|
+
await handleUpdateNotification({
|
|
36
|
+
confirmPrompt: confirm,
|
|
37
|
+
execSyncFn: execSync,
|
|
38
|
+
isTTY: process.stdout.isTTY ?? false,
|
|
39
|
+
log: this.log.bind(this),
|
|
40
|
+
notifier,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
export default hook;
|
|
@@ -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,
|
|
@@ -10,21 +10,21 @@ export const mapToPushContexts = (params) => {
|
|
|
10
10
|
const addedContextFiles = params.addedFiles.map((file) => new CogitPushContext({
|
|
11
11
|
content: file.content,
|
|
12
12
|
operation: 'add',
|
|
13
|
-
path:
|
|
13
|
+
path: file.path,
|
|
14
14
|
tags: [],
|
|
15
15
|
title: file.title,
|
|
16
16
|
}));
|
|
17
17
|
const editedContextFiles = params.modifiedFiles.map((file) => new CogitPushContext({
|
|
18
18
|
content: file.content,
|
|
19
19
|
operation: 'edit',
|
|
20
|
-
path:
|
|
20
|
+
path: file.path,
|
|
21
21
|
tags: [],
|
|
22
22
|
title: file.title,
|
|
23
23
|
}));
|
|
24
|
-
const deletedContextFiles = params.deletedPaths.map((
|
|
24
|
+
const deletedContextFiles = params.deletedPaths.map((deletedPath) => new CogitPushContext({
|
|
25
25
|
content: '',
|
|
26
26
|
operation: 'delete',
|
|
27
|
-
path:
|
|
27
|
+
path: deletedPath,
|
|
28
28
|
tags: [],
|
|
29
29
|
title: '',
|
|
30
30
|
}));
|
|
@@ -40,7 +40,7 @@ export class HttpCogitPushService {
|
|
|
40
40
|
const response = await this.makeRequest({
|
|
41
41
|
accessToken: params.accessToken,
|
|
42
42
|
branch: params.branch,
|
|
43
|
-
currentSha: '',
|
|
43
|
+
currentSha: 'sha_placeholder',
|
|
44
44
|
memories,
|
|
45
45
|
sessionKey: params.sessionKey,
|
|
46
46
|
url,
|
|
@@ -79,8 +79,8 @@ export class HttpCogitPushService {
|
|
|
79
79
|
typeof error.response === 'object' &&
|
|
80
80
|
'data' in error.response) {
|
|
81
81
|
const errorResponse = error.response.data;
|
|
82
|
-
if (errorResponse.
|
|
83
|
-
return extractShaFromErrorDetails(errorResponse.
|
|
82
|
+
if (errorResponse.error) {
|
|
83
|
+
return extractShaFromErrorDetails(errorResponse.error);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
return undefined;
|
|
@@ -17,6 +17,8 @@ export declare class FileContextTreeWriterService implements IContextTreeWriterS
|
|
|
17
17
|
sync(params: SyncParams): Promise<SyncResult>;
|
|
18
18
|
/**
|
|
19
19
|
* Normalizes a file path by removing leading slashes.
|
|
20
|
+
* Retained for backwards compatibility with legacy API responses
|
|
21
|
+
* that may still include leading slashes in paths.
|
|
20
22
|
*/
|
|
21
23
|
private normalizePath;
|
|
22
24
|
}
|
|
@@ -54,6 +54,8 @@ export class FileContextTreeWriterService {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Normalizes a file path by removing leading slashes.
|
|
57
|
+
* Retained for backwards compatibility with legacy API responses
|
|
58
|
+
* that may still include leading slashes in paths.
|
|
57
59
|
*/
|
|
58
60
|
normalizePath(path) {
|
|
59
61
|
return path.replace(/^\/+/, '');
|
|
@@ -3,6 +3,7 @@ import { type IFileService, type WriteMode } from '../../core/interfaces/i-file-
|
|
|
3
3
|
* File service implementation using Node.js fs module.
|
|
4
4
|
*/
|
|
5
5
|
export declare class FsFileService implements IFileService {
|
|
6
|
+
createBackup(filePath: string): Promise<string>;
|
|
6
7
|
/**
|
|
7
8
|
* Checks if a file exists at the specified path.
|
|
8
9
|
*
|
|
@@ -17,6 +18,7 @@ export declare class FsFileService implements IFileService {
|
|
|
17
18
|
* @returns A promise that resolves with the content of the file.
|
|
18
19
|
*/
|
|
19
20
|
read(filePath: string): Promise<string>;
|
|
21
|
+
replaceContent(filePath: string, oldContent: string, newContent: string): Promise<void>;
|
|
20
22
|
/**
|
|
21
23
|
* Writes content to the specified file.
|
|
22
24
|
* @param content The content to write.
|