Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions packages/cli/src/commands/gh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* CLI commands for indexing and searching GitHub data
*/

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

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

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

// Create GitHub indexer
const ghIndexer = new GitHubIndexer(codeIndexer);
await ghIndexer.initialize();

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

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

spinner.text = 'Initializing...';

// Initialize indexers
const codeIndexer = new RepositoryIndexer(config);
await codeIndexer.initialize();
const ghIndexer = new GitHubIndexer(codeIndexer);
// Initialize GitHub indexer
const ghIndexer = new GitHubIndexer({
vectorStorePath: `${config.vectorStorePath}-github`,
statePath: '.dev-agent/github-state.json',
autoUpdate: true,
staleThreshold: 15 * 60 * 1000,
});
await ghIndexer.initialize();

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

spinner.text = 'Initializing...';

const codeIndexer = new RepositoryIndexer(config);
await codeIndexer.initialize();
const ghIndexer = new GitHubIndexer(codeIndexer);
const ghIndexer = new GitHubIndexer({
vectorStorePath: `${config.vectorStorePath}-github`,
statePath: '.dev-agent/github-state.json',
autoUpdate: true,
staleThreshold: 15 * 60 * 1000,
});
await ghIndexer.initialize();

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

const codeIndexer = new RepositoryIndexer(config);
await codeIndexer.initialize();
const ghIndexer = new GitHubIndexer(codeIndexer);
const ghIndexer = new GitHubIndexer({
vectorStorePath: `${config.vectorStorePath}-github`,
statePath: '.dev-agent/github-state.json',
autoUpdate: true,
staleThreshold: 15 * 60 * 1000,
});
await ghIndexer.initialize();

const stats = ghIndexer.getStats();

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CoreService, type CoreConfig } from '@lytics/dev-agent-core';
import { type CoreConfig, CoreService } from '@lytics/dev-agent-core';

export interface CliConfig {
coreConfig: CoreConfig;
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export interface ContextProviderOptions {
}

export class ContextProvider {

constructor(_options: ContextProviderOptions) {
// Placeholder constructor
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ export interface GitHubOptions {
}

export class GitHubIntegration {

constructor(_options: GitHubOptions) {
// Placeholder constructor
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,58 @@
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { RepositoryIndexer } from '@lytics/dev-agent-core';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { GitHubAgentConfig } from '../github/agent';
import { GitHubAgent } from '../github/agent';
import type { GitHubContextRequest, GitHubContextResult } from '../github/types';
import type { GitHubContextRequest, GitHubContextResult, GitHubDocument } from '../github/types';
import { SubagentCoordinator } from './coordinator';

// Mock GitHub utilities to avoid actual gh CLI calls
vi.mock('../github/utils/index', () => ({
fetchAllDocuments: vi.fn(() => [
{
type: 'issue',
number: 1,
title: 'Test Issue',
body: 'Test body',
state: 'open',
author: 'testuser',
labels: [],
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
url: 'https://github.com/test/repo/issues/1',
relatedIssues: [],
relatedPRs: [],
linkedFiles: [],
mentions: [],
},
]),
enrichDocument: vi.fn((doc: GitHubDocument) => doc),
getCurrentRepository: vi.fn(() => 'lytics/dev-agent'),
calculateRelevance: vi.fn(() => 0.8),
matchesQuery: vi.fn(() => true),
}));

describe('Coordinator → GitHub Integration', () => {
let coordinator: SubagentCoordinator;
let github: GitHubAgent;
let tempDir: string;
let codeIndexer: RepositoryIndexer;

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

// Initialize code indexer
codeIndexer = new RepositoryIndexer({
repositoryPath: process.cwd(),
vectorStorePath: join(tempDir, '.vectors'),
});
await codeIndexer.initialize();

// Create coordinator
coordinator = new SubagentCoordinator({
logLevel: 'error', // Reduce noise in tests
});

// Create GitHub agent
// Create GitHub agent with vector storage config
const config: GitHubAgentConfig = {
repositoryPath: process.cwd(),
codeIndexer,
storagePath: join(tempDir, '.github-index'),
vectorStorePath: join(tempDir, '.github-vectors'),
statePath: join(tempDir, 'github-state.json'),
autoUpdate: false, // Disable for tests
};
github = new GitHubAgent(config);

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

afterEach(async () => {
await coordinator.stop();
await codeIndexer.close();
await rm(tempDir, { recursive: true, force: true });
});

Expand All @@ -67,7 +84,7 @@ describe('Coordinator → GitHub Integration', () => {
it('should prevent duplicate registration', async () => {
const duplicate = new GitHubAgent({
repositoryPath: process.cwd(),
codeIndexer,
vectorStorePath: join(tempDir, '.github-vectors-dup'),
});
await expect(coordinator.registerAgent(duplicate)).rejects.toThrow('already registered');
});
Expand Down Expand Up @@ -101,6 +118,17 @@ describe('Coordinator → GitHub Integration', () => {
});

it('should route search request to GitHub agent', async () => {
// Index first (required for search)
await coordinator.sendMessage({
type: 'request',
sender: 'test',
recipient: 'github',
payload: {
action: 'index',
indexOptions: {},
} as GitHubContextRequest,
});

const response = await coordinator.sendMessage({
type: 'request',
sender: 'test',
Expand Down
43 changes: 33 additions & 10 deletions packages/subagents/src/github/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Provides rich context from GitHub issues, PRs, and discussions
*/

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

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

export class GitHubAgent implements Agent {
Expand All @@ -35,7 +36,17 @@ export class GitHubAgent implements Agent {
this.context = context;
this.name = context.agentName;

this.indexer = new GitHubIndexer(this.config.codeIndexer, this.config.repositoryPath);
this.indexer = new GitHubIndexer(
{
vectorStorePath: this.config.vectorStorePath,
statePath: this.config.statePath,
autoUpdate: this.config.autoUpdate,
staleThreshold: this.config.staleThreshold,
},
this.config.repositoryPath
);

await this.indexer.initialize();

context.logger.info('GitHub agent initialized', {
capabilities: this.capabilities,
Expand Down Expand Up @@ -69,10 +80,18 @@ export class GitHubAgent implements Agent {
result = await this.handleSearch(request.query || '', request.searchOptions);
break;
case 'context':
result = await this.handleGetContext(request.issueNumber!);
if (typeof request.issueNumber !== 'number') {
result = { action: 'context', error: 'issueNumber is required' };
} else {
result = await this.handleGetContext(request.issueNumber);
}
break;
case 'related':
result = await this.handleFindRelated(request.issueNumber!);
if (typeof request.issueNumber !== 'number') {
result = { action: 'related', error: 'issueNumber is required' };
} else {
result = await this.handleFindRelated(request.issueNumber);
}
break;
default:
result = {
Expand Down Expand Up @@ -114,7 +133,8 @@ export class GitHubAgent implements Agent {
}

private async handleIndex(options?: GitHubIndexOptions): Promise<GitHubContextResult> {
const stats = await this.indexer!.index(options);
if (!this.indexer) throw new Error('Indexer not initialized');
const stats = await this.indexer.index(options);
return {
action: 'index',
stats,
Expand All @@ -125,23 +145,26 @@ export class GitHubAgent implements Agent {
query: string,
options?: { limit?: number }
): Promise<GitHubContextResult> {
const results = await this.indexer!.search(query, options);
if (!this.indexer) throw new Error('Indexer not initialized');
const results = await this.indexer.search(query, options);
return {
action: 'search',
results,
};
}

private async handleGetContext(issueNumber: number): Promise<GitHubContextResult> {
const context = await this.indexer!.getContext(issueNumber);
if (!this.indexer) throw new Error('Indexer not initialized');
const context = await this.indexer.getContext(issueNumber);
return {
action: 'context',
context: context || undefined,
};
}

private async handleFindRelated(issueNumber: number): Promise<GitHubContextResult> {
const related = await this.indexer!.findRelated(issueNumber);
if (!this.indexer) throw new Error('Indexer not initialized');
const related = await this.indexer.findRelated(issueNumber);
return {
action: 'related',
related,
Expand Down
Loading