Skip to content

Commit ff16990

Browse files
authored
fix(github): persist GitHub indexer data between CLI invocations (#37)
* fix(github): persist GitHub indexer data between CLI invocations Fixes #36 * fix(lint): use proper type casting for private members in tests - Cast through 'unknown' to access private properties in tests - Fix template literal usage in CLI (auto-fixed by biome) - Remove biome-ignore comments in favor of proper type safety All tests passing, lint clean.
1 parent 7774fa0 commit ff16990

File tree

9 files changed

+604
-144
lines changed

9 files changed

+604
-144
lines changed

packages/cli/src/commands/gh.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* CLI commands for indexing and searching GitHub data
44
*/
55

6-
import { RepositoryIndexer } from '@lytics/dev-agent-core';
76
import { GitHubIndexer } from '@lytics/dev-agent-subagents';
87
import chalk from 'chalk';
98
import { Command } from 'commander';
@@ -34,12 +33,15 @@ export const ghCommand = new Command('gh')
3433

3534
spinner.text = 'Initializing indexers...';
3635

37-
// Initialize code indexer
38-
const codeIndexer = new RepositoryIndexer(config);
39-
await codeIndexer.initialize();
36+
// Create GitHub indexer with vector storage
37+
const ghIndexer = new GitHubIndexer({
38+
vectorStorePath: `${config.vectorStorePath}-github`, // Separate storage for GitHub data
39+
statePath: '.dev-agent/github-state.json',
40+
autoUpdate: true,
41+
staleThreshold: 15 * 60 * 1000, // 15 minutes
42+
});
4043

41-
// Create GitHub indexer
42-
const ghIndexer = new GitHubIndexer(codeIndexer);
44+
await ghIndexer.initialize();
4345

4446
spinner.text = 'Fetching GitHub data...';
4547

@@ -120,10 +122,14 @@ export const ghCommand = new Command('gh')
120122

121123
spinner.text = 'Initializing...';
122124

123-
// Initialize indexers
124-
const codeIndexer = new RepositoryIndexer(config);
125-
await codeIndexer.initialize();
126-
const ghIndexer = new GitHubIndexer(codeIndexer);
125+
// Initialize GitHub indexer
126+
const ghIndexer = new GitHubIndexer({
127+
vectorStorePath: `${config.vectorStorePath}-github`,
128+
statePath: '.dev-agent/github-state.json',
129+
autoUpdate: true,
130+
staleThreshold: 15 * 60 * 1000,
131+
});
132+
await ghIndexer.initialize();
127133

128134
// Check if indexed
129135
if (!ghIndexer.isIndexed()) {
@@ -216,9 +222,13 @@ export const ghCommand = new Command('gh')
216222

217223
spinner.text = 'Initializing...';
218224

219-
const codeIndexer = new RepositoryIndexer(config);
220-
await codeIndexer.initialize();
221-
const ghIndexer = new GitHubIndexer(codeIndexer);
225+
const ghIndexer = new GitHubIndexer({
226+
vectorStorePath: `${config.vectorStorePath}-github`,
227+
statePath: '.dev-agent/github-state.json',
228+
autoUpdate: true,
229+
staleThreshold: 15 * 60 * 1000,
230+
});
231+
await ghIndexer.initialize();
222232

223233
if (!ghIndexer.isIndexed()) {
224234
spinner.warn('GitHub data not indexed');
@@ -301,9 +311,13 @@ export const ghCommand = new Command('gh')
301311
return;
302312
}
303313

304-
const codeIndexer = new RepositoryIndexer(config);
305-
await codeIndexer.initialize();
306-
const ghIndexer = new GitHubIndexer(codeIndexer);
314+
const ghIndexer = new GitHubIndexer({
315+
vectorStorePath: `${config.vectorStorePath}-github`,
316+
statePath: '.dev-agent/github-state.json',
317+
autoUpdate: true,
318+
staleThreshold: 15 * 60 * 1000,
319+
});
320+
await ghIndexer.initialize();
307321

308322
const stats = ghIndexer.getStats();
309323

packages/cli/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CoreService, type CoreConfig } from '@lytics/dev-agent-core';
1+
import { type CoreConfig, CoreService } from '@lytics/dev-agent-core';
22

33
export interface CliConfig {
44
coreConfig: CoreConfig;

packages/core/src/context/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export interface ContextProviderOptions {
55
}
66

77
export class ContextProvider {
8-
98
constructor(_options: ContextProviderOptions) {
109
// Placeholder constructor
1110
}

packages/core/src/github/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export interface GitHubOptions {
44
}
55

66
export class GitHubIntegration {
7-
87
constructor(_options: GitHubOptions) {
98
// Placeholder constructor
109
}

packages/subagents/src/coordinator/github-coordinator.integration.test.ts

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,58 @@
66
import { mkdtemp, rm } from 'node:fs/promises';
77
import { tmpdir } from 'node:os';
88
import { join } from 'node:path';
9-
import { RepositoryIndexer } from '@lytics/dev-agent-core';
10-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1110
import type { GitHubAgentConfig } from '../github/agent';
1211
import { GitHubAgent } from '../github/agent';
13-
import type { GitHubContextRequest, GitHubContextResult } from '../github/types';
12+
import type { GitHubContextRequest, GitHubContextResult, GitHubDocument } from '../github/types';
1413
import { SubagentCoordinator } from './coordinator';
1514

15+
// Mock GitHub utilities to avoid actual gh CLI calls
16+
vi.mock('../github/utils/index', () => ({
17+
fetchAllDocuments: vi.fn(() => [
18+
{
19+
type: 'issue',
20+
number: 1,
21+
title: 'Test Issue',
22+
body: 'Test body',
23+
state: 'open',
24+
author: 'testuser',
25+
labels: [],
26+
createdAt: '2024-01-01T00:00:00Z',
27+
updatedAt: '2024-01-01T00:00:00Z',
28+
url: 'https://github.com/test/repo/issues/1',
29+
relatedIssues: [],
30+
relatedPRs: [],
31+
linkedFiles: [],
32+
mentions: [],
33+
},
34+
]),
35+
enrichDocument: vi.fn((doc: GitHubDocument) => doc),
36+
getCurrentRepository: vi.fn(() => 'lytics/dev-agent'),
37+
calculateRelevance: vi.fn(() => 0.8),
38+
matchesQuery: vi.fn(() => true),
39+
}));
40+
1641
describe('Coordinator → GitHub Integration', () => {
1742
let coordinator: SubagentCoordinator;
1843
let github: GitHubAgent;
1944
let tempDir: string;
20-
let codeIndexer: RepositoryIndexer;
2145

2246
beforeEach(async () => {
2347
// Create temp directory
2448
tempDir = await mkdtemp(join(tmpdir(), 'gh-coordinator-test-'));
2549

26-
// Initialize code indexer
27-
codeIndexer = new RepositoryIndexer({
28-
repositoryPath: process.cwd(),
29-
vectorStorePath: join(tempDir, '.vectors'),
30-
});
31-
await codeIndexer.initialize();
32-
3350
// Create coordinator
3451
coordinator = new SubagentCoordinator({
3552
logLevel: 'error', // Reduce noise in tests
3653
});
3754

38-
// Create GitHub agent
55+
// Create GitHub agent with vector storage config
3956
const config: GitHubAgentConfig = {
4057
repositoryPath: process.cwd(),
41-
codeIndexer,
42-
storagePath: join(tempDir, '.github-index'),
58+
vectorStorePath: join(tempDir, '.github-vectors'),
59+
statePath: join(tempDir, 'github-state.json'),
60+
autoUpdate: false, // Disable for tests
4361
};
4462
github = new GitHubAgent(config);
4563

@@ -49,7 +67,6 @@ describe('Coordinator → GitHub Integration', () => {
4967

5068
afterEach(async () => {
5169
await coordinator.stop();
52-
await codeIndexer.close();
5370
await rm(tempDir, { recursive: true, force: true });
5471
});
5572

@@ -67,7 +84,7 @@ describe('Coordinator → GitHub Integration', () => {
6784
it('should prevent duplicate registration', async () => {
6885
const duplicate = new GitHubAgent({
6986
repositoryPath: process.cwd(),
70-
codeIndexer,
87+
vectorStorePath: join(tempDir, '.github-vectors-dup'),
7188
});
7289
await expect(coordinator.registerAgent(duplicate)).rejects.toThrow('already registered');
7390
});
@@ -101,6 +118,17 @@ describe('Coordinator → GitHub Integration', () => {
101118
});
102119

103120
it('should route search request to GitHub agent', async () => {
121+
// Index first (required for search)
122+
await coordinator.sendMessage({
123+
type: 'request',
124+
sender: 'test',
125+
recipient: 'github',
126+
payload: {
127+
action: 'index',
128+
indexOptions: {},
129+
} as GitHubContextRequest,
130+
});
131+
104132
const response = await coordinator.sendMessage({
105133
type: 'request',
106134
sender: 'test',

packages/subagents/src/github/agent.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* Provides rich context from GitHub issues, PRs, and discussions
44
*/
55

6-
import type { RepositoryIndexer } from '@lytics/dev-agent-core';
76
import type { Agent, AgentContext, Message } from '../types';
87
import { GitHubIndexer } from './indexer';
98
import type {
@@ -15,8 +14,10 @@ import type {
1514

1615
export interface GitHubAgentConfig {
1716
repositoryPath: string;
18-
codeIndexer: RepositoryIndexer;
19-
storagePath?: string;
17+
vectorStorePath: string; // Path to LanceDB storage for GitHub data
18+
statePath?: string; // Path to state file (default: .dev-agent/github-state.json)
19+
autoUpdate?: boolean; // Enable auto-updates (default: true)
20+
staleThreshold?: number; // Stale threshold in ms (default: 15 minutes)
2021
}
2122

2223
export class GitHubAgent implements Agent {
@@ -35,7 +36,17 @@ export class GitHubAgent implements Agent {
3536
this.context = context;
3637
this.name = context.agentName;
3738

38-
this.indexer = new GitHubIndexer(this.config.codeIndexer, this.config.repositoryPath);
39+
this.indexer = new GitHubIndexer(
40+
{
41+
vectorStorePath: this.config.vectorStorePath,
42+
statePath: this.config.statePath,
43+
autoUpdate: this.config.autoUpdate,
44+
staleThreshold: this.config.staleThreshold,
45+
},
46+
this.config.repositoryPath
47+
);
48+
49+
await this.indexer.initialize();
3950

4051
context.logger.info('GitHub agent initialized', {
4152
capabilities: this.capabilities,
@@ -69,10 +80,18 @@ export class GitHubAgent implements Agent {
6980
result = await this.handleSearch(request.query || '', request.searchOptions);
7081
break;
7182
case 'context':
72-
result = await this.handleGetContext(request.issueNumber!);
83+
if (typeof request.issueNumber !== 'number') {
84+
result = { action: 'context', error: 'issueNumber is required' };
85+
} else {
86+
result = await this.handleGetContext(request.issueNumber);
87+
}
7388
break;
7489
case 'related':
75-
result = await this.handleFindRelated(request.issueNumber!);
90+
if (typeof request.issueNumber !== 'number') {
91+
result = { action: 'related', error: 'issueNumber is required' };
92+
} else {
93+
result = await this.handleFindRelated(request.issueNumber);
94+
}
7695
break;
7796
default:
7897
result = {
@@ -114,7 +133,8 @@ export class GitHubAgent implements Agent {
114133
}
115134

116135
private async handleIndex(options?: GitHubIndexOptions): Promise<GitHubContextResult> {
117-
const stats = await this.indexer!.index(options);
136+
if (!this.indexer) throw new Error('Indexer not initialized');
137+
const stats = await this.indexer.index(options);
118138
return {
119139
action: 'index',
120140
stats,
@@ -125,23 +145,26 @@ export class GitHubAgent implements Agent {
125145
query: string,
126146
options?: { limit?: number }
127147
): Promise<GitHubContextResult> {
128-
const results = await this.indexer!.search(query, options);
148+
if (!this.indexer) throw new Error('Indexer not initialized');
149+
const results = await this.indexer.search(query, options);
129150
return {
130151
action: 'search',
131152
results,
132153
};
133154
}
134155

135156
private async handleGetContext(issueNumber: number): Promise<GitHubContextResult> {
136-
const context = await this.indexer!.getContext(issueNumber);
157+
if (!this.indexer) throw new Error('Indexer not initialized');
158+
const context = await this.indexer.getContext(issueNumber);
137159
return {
138160
action: 'context',
139161
context: context || undefined,
140162
};
141163
}
142164

143165
private async handleFindRelated(issueNumber: number): Promise<GitHubContextResult> {
144-
const related = await this.indexer!.findRelated(issueNumber);
166+
if (!this.indexer) throw new Error('Indexer not initialized');
167+
const related = await this.indexer.findRelated(issueNumber);
145168
return {
146169
action: 'related',
147170
related,

0 commit comments

Comments
 (0)