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.
Files changed (29) hide show
  1. package/dist/commands/gen-rules.d.ts +42 -4
  2. package/dist/commands/gen-rules.js +235 -39
  3. package/dist/commands/init.d.ts +43 -3
  4. package/dist/commands/init.js +262 -24
  5. package/dist/core/domain/knowledge/directory-manager.js +2 -0
  6. package/dist/core/interfaces/i-file-service.d.ts +16 -0
  7. package/dist/core/interfaces/i-legacy-rule-detector.d.ts +56 -0
  8. package/dist/hooks/init/update-notifier.d.ts +39 -0
  9. package/dist/hooks/init/update-notifier.js +43 -0
  10. package/dist/infra/cipher/llm/generators/byterover-content-generator.js +6 -0
  11. package/dist/infra/cogit/context-tree-to-push-context-mapper.js +4 -4
  12. package/dist/infra/cogit/http-cogit-push-service.js +3 -3
  13. package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +2 -0
  14. package/dist/infra/context-tree/file-context-tree-writer-service.js +2 -0
  15. package/dist/infra/file/fs-file-service.d.ts +2 -0
  16. package/dist/infra/file/fs-file-service.js +23 -1
  17. package/dist/infra/rule/constants.d.ts +9 -1
  18. package/dist/infra/rule/constants.js +9 -1
  19. package/dist/infra/rule/legacy-rule-detector.d.ts +21 -0
  20. package/dist/infra/rule/legacy-rule-detector.js +106 -0
  21. package/dist/infra/rule/rule-template-service.js +14 -6
  22. package/oclif.manifest.json +1 -1
  23. package/package.json +7 -2
  24. package/dist/core/domain/errors/rule-error.d.ts +0 -6
  25. package/dist/core/domain/errors/rule-error.js +0 -12
  26. package/dist/core/interfaces/i-rule-writer-service.d.ts +0 -13
  27. package/dist/infra/rule/rule-writer-service.d.ts +0 -19
  28. package/dist/infra/rule/rule-writer-service.js +0 -39
  29. /package/dist/core/interfaces/{i-rule-writer-service.js → i-legacy-rule-detector.js} +0 -0
@@ -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(ruleWriterService, agent) {
154
- this.log(`Generating rules for: ${agent}`);
155
- try {
156
- await ruleWriterService.writeRule(agent, false);
157
- this.log(`✅ Successfully generated rule file for ${agent}`);
158
- }
159
- catch (error) {
160
- if (error instanceof RuleExistsError) {
161
- const overwrite = await this.promptForOverwriteConfirmation(agent);
162
- if (overwrite) {
163
- await ruleWriterService.writeRule(agent, true);
164
- this.log(`✅ Successfully generated rule file for ${agent}`);
165
- }
166
- else {
167
- this.log(`Skipping rule file generation for ${agent}`);
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
- else {
171
- throw error;
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, projectConfigStore, ruleWriterService, spaceService, teamService, tokenStore, trackingService, } = this.createServices();
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(ruleWriterService, selectedAgent);
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 === '/README.md');
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: `/${file.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: `/${file.path}`,
20
+ path: file.path,
21
21
  tags: [],
22
22
  title: file.title,
23
23
  }));
24
- const deletedContextFiles = params.deletedPaths.map((path) => new CogitPushContext({
24
+ const deletedContextFiles = params.deletedPaths.map((deletedPath) => new CogitPushContext({
25
25
  content: '',
26
26
  operation: 'delete',
27
- path: `/${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.details) {
83
- return extractShaFromErrorDetails(errorResponse.details);
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.