diff --git a/examples/typescript-sdk/context/.gitignore b/examples/typescript-sdk/context/.gitignore new file mode 100644 index 0000000..152a08d --- /dev/null +++ b/examples/typescript-sdk/context/.gitignore @@ -0,0 +1,52 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# State files (example outputs) +/tmp/direct-context-state.json diff --git a/examples/typescript-sdk/context/README.md b/examples/typescript-sdk/context/README.md new file mode 100644 index 0000000..14c9031 --- /dev/null +++ b/examples/typescript-sdk/context/README.md @@ -0,0 +1,149 @@ +# Context Examples + +Examples demonstrating the Auggie SDK's context modes and AI-powered code analysis. + +## Prerequisites + +1. **Node.js 18+** - Required to run the examples +2. **Auggie CLI** - Required for FileSystem Context examples + ```bash + npm install -g @augmentcode/auggie + ``` +3. **Authentication** - Required for all examples + ```bash + auggie login + ``` + This creates a session file at `~/.augment/session.json` with your API token. + + Alternatively, you can set environment variables: + ```bash + export AUGMENT_API_TOKEN=your_token_here + export AUGMENT_API_URL=https://staging-shard-0.api.augmentcode.com/ + ``` + +## Setup + +Install dependencies: + +```bash +cd examples/typescript-sdk/context +npm install +``` + +## Examples + +### [Direct Context](./direct-context/) +API-based indexing with semantic search and AI Q&A. + +**Run it:** +```bash +npm run direct-context +``` + +### [FileSystem Context](./filesystem-context/) +Local directory search via MCP protocol. + +**Prerequisites:** +- Auggie CLI must be installed and in your PATH +- Authentication via `auggie login` or `AUGMENT_API_TOKEN` environment variable +- A `.gitignore` or `.augmentignore` file in the workspace directory to exclude `node_modules/` and other large directories + +**Important:** The FileSystem Context indexes all files in the workspace directory. To avoid timeouts when indexing large directories (like `node_modules/`), make sure you have a `.gitignore` or `.augmentignore` file that excludes them. The auggie CLI respects both `.gitignore` and `.augmentignore` patterns during indexing. + +**Run it:** +```bash +npm run filesystem-context +``` + +### [File Search Server](./file-search-server/) +REST API for semantic file search with AI summarization. + +**Prerequisites:** Auggie CLI must be installed and in your PATH. + +**Run it:** +```bash +npm run file-search-server [workspace-directory] +``` + +Then query the API: +```bash +curl "http://localhost:3000/search?q=typescript" +``` + +### [Prompt Enhancer Server](./prompt-enhancer-server/) +HTTP server that enhances prompts with codebase context. + +**Prerequisites:** Auggie CLI must be installed and in your PATH. + +**Run it:** +```bash +npm run prompt-enhancer-server [workspace-directory] +``` + +Then enhance prompts: +```bash +curl -X POST http://localhost:3001/enhance \ + -H "Content-Type: application/json" \ + -d '{"prompt": "fix the login bug"}' +``` + +### [GitHub Action Indexer](./github-action-indexer/) +Index GitHub repositories for semantic search. + +This example has its own package.json and dependencies. + +**Setup:** +```bash +npm run github-indexer:install +``` + +**Run it:** +```bash +npm run github-indexer:index +npm run github-indexer:search +``` + +See [github-action-indexer/README.md](./github-action-indexer/README.md) for more details. + +## Running Examples Directly with tsx + +You can also run examples directly without installing dependencies: + +```bash +npx tsx direct-context/index.ts +npx tsx filesystem-context/index.ts +npx tsx file-search-server/index.ts . +npx tsx prompt-enhancer-server/index.ts . +``` + +Note: This will download dependencies on each run. For better performance, use `npm install` first. + +## Troubleshooting + +### MCP Timeout in FileSystem Context + +**Problem:** The FileSystem Context example times out during indexing. + +**Cause:** The workspace directory contains too many files (e.g., `node_modules/` with 45,000+ files). + +**Solution:** Create a `.gitignore` or `.augmentignore` file in the workspace directory to exclude large directories: + +```bash +# .gitignore or .augmentignore +node_modules/ +dist/ +*.log +.DS_Store +``` + +The auggie CLI respects both `.gitignore` and `.augmentignore` patterns and will skip excluded files during indexing. + +### Authentication Errors + +**Problem:** `Error: API key is required for searchAndAsk()` + +**Cause:** The SDK cannot find your authentication credentials. + +**Solution:** Run `auggie login` to authenticate, or set the `AUGMENT_API_TOKEN` and `AUGMENT_API_URL` environment variables. + + diff --git a/examples/typescript-sdk/context/direct-context/README.md b/examples/typescript-sdk/context/direct-context/README.md new file mode 100644 index 0000000..643f641 --- /dev/null +++ b/examples/typescript-sdk/context/direct-context/README.md @@ -0,0 +1,29 @@ +# Direct Context Example + +API-based indexing with semantic search, AI Q&A, and state persistence. + +## Usage + +```bash +# Authenticate +auggie login + +# Run the example +npx tsx examples/context/direct-context/index.ts +``` + +## What It Does + +- Creates a Direct Context instance +- Adds sample files to the index +- Performs semantic searches +- Uses `searchAndAsk()` for AI-powered Q&A +- Generates documentation +- Exports/imports context state + +## Key Features + +- **`search()`**: Semantic search returning formatted code snippets +- **`searchAndAsk()`**: One-step AI Q&A about indexed code +- **State persistence**: Export/import index for reuse + diff --git a/examples/typescript-sdk/context/direct-context/index.ts b/examples/typescript-sdk/context/direct-context/index.ts new file mode 100644 index 0000000..ddf541d --- /dev/null +++ b/examples/typescript-sdk/context/direct-context/index.ts @@ -0,0 +1,423 @@ +/** + * Sample: Direct Context - API-based indexing with import/export state + * + * This sample demonstrates: + * - Creating a Direct Context instance + * - Adding files to the index + * - Searching the indexed files + * - Using Generation API to ask questions about indexed code + * - Generating documentation from indexed code + * - Exporting state to a file + * - Importing state from a file + */ + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { DirectContext } from "@augmentcode/auggie-sdk"; + +async function main() { + console.log("=== Direct Context Sample ===\n"); + + // Create a Direct Context instance + // Authentication is automatic via: + // 1. AUGMENT_API_TOKEN / AUGMENT_API_URL env vars, or + // 2. ~/.augment/session.json (created by `auggie login`) + console.log("Creating Direct Context..."); + const context = await DirectContext.create({ debug: true }); + + // Add some sample files to the index + console.log("\nAdding files to index..."); + // Using realistic, detailed code examples for better search results + const files = [ + { + path: "src/utils/string-helpers.ts", + contents: `/** + * String utility functions for text processing and formatting + */ + +/** + * Format a number with thousands separators + * @param num Number to format + * @param locale Locale for formatting (default: 'en-US') + * @returns Formatted number string + */ +export function formatNumber(num: number, locale: string = 'en-US'): string { + return num.toLocaleString(locale); +} + +/** + * Check if a number is even + * @param num Number to check + * @returns True if number is even, false otherwise + */ +export function isEven(num: number): boolean { + return num % 2 === 0; +} + +/** + * Check if a number is odd + * @param num Number to check + * @returns True if number is odd, false otherwise + */ +export function isOdd(num: number): boolean { + return num % 2 !== 0; +} + +/** + * Clamp a value between min and max bounds + * @param value Value to clamp + * @param min Minimum allowed value + * @param max Maximum allowed value + * @returns Clamped value + */ +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +/** + * Capitalize the first letter of a string + * @param str String to capitalize + * @returns String with first letter capitalized + */ +export function capitalize(str: string): string { + if (!str) return str; + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +} + +/** + * Convert string to title case + * @param str String to convert + * @returns String in title case + */ +export function toTitleCase(str: string): string { + return str.split(' ').map(word => capitalize(word)).join(' '); +} + +/** + * Truncate string to specified length with ellipsis + * @param str String to truncate + * @param maxLength Maximum length + * @returns Truncated string + */ +export function truncate(str: string, maxLength: number): string { + if (str.length <= maxLength) return str; + return str.slice(0, maxLength - 3) + '...'; +}`, + }, + { + path: "src/data/user-service.ts", + contents: `/** + * User service for managing user data and authentication + */ + +export interface User { + id: string; + email: string; + name: string; + role: 'admin' | 'user' | 'guest'; + createdAt: Date; + lastLoginAt?: Date; +} + +export interface CreateUserRequest { + email: string; + name: string; + password: string; + role?: 'user' | 'guest'; +} + +/** + * Service class for user management operations + */ +export class UserService { + private users: Map = new Map(); + + /** + * Create a new user account + * @param request User creation request + * @returns Created user (without password) + */ + async createUser(request: CreateUserRequest): Promise { + const id = this.generateUserId(); + const user: User = { + id, + email: request.email, + name: request.name, + role: request.role || 'user', + createdAt: new Date(), + }; + + this.users.set(id, user); + return user; + } + + /** + * Find user by ID + * @param id User ID + * @returns User if found, undefined otherwise + */ + async findUserById(id: string): Promise { + return this.users.get(id); + } + + /** + * Find user by email address + * @param email Email address + * @returns User if found, undefined otherwise + */ + async findUserByEmail(email: string): Promise { + for (const user of this.users.values()) { + if (user.email === email) { + return user; + } + } + return undefined; + } + + /** + * Update user's last login timestamp + * @param id User ID + */ + async updateLastLogin(id: string): Promise { + const user = this.users.get(id); + if (user) { + user.lastLoginAt = new Date(); + } + } + + /** + * Get all users with optional role filter + * @param role Optional role filter + * @returns Array of users + */ + async getUsers(role?: User['role']): Promise { + const allUsers = Array.from(this.users.values()); + if (role) { + return allUsers.filter(user => user.role === role); + } + return allUsers; + } + + /** + * Delete user by ID + * @param id User ID + * @returns True if user was deleted, false if not found + */ + async deleteUser(id: string): Promise { + return this.users.delete(id); + } + + private generateUserId(): string { + return 'user_' + Math.random().toString(36).substr(2, 9); + } +}`, + }, + { + path: "src/api/http-client.ts", + contents: `/** + * HTTP client for making API requests with error handling and retries + */ + +export interface RequestConfig { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + url: string; + headers?: Record; + body?: any; + timeout?: number; +} + +export interface ApiResponse { + data: T; + status: number; + headers: Record; +} + +/** + * HTTP client class for making API requests + */ +export class HttpClient { + private baseURL: string; + private defaultHeaders: Record; + + constructor(baseURL: string, defaultHeaders: Record = {}) { + this.baseURL = baseURL.replace(/\\/$/, ''); + this.defaultHeaders = defaultHeaders; + } + + /** + * Make a GET request + * @param url Request URL + * @param headers Optional headers + * @returns Promise with response data + */ + async get(url: string, headers?: Record): Promise> { + return this.request({ + method: 'GET', + url, + headers, + }); + } + + /** + * Make a POST request + * @param url Request URL + * @param body Request body + * @param headers Optional headers + * @returns Promise with response data + */ + async post(url: string, body?: any, headers?: Record): Promise> { + return this.request({ + method: 'POST', + url, + body, + headers, + }); + } + + /** + * Make a PUT request + * @param url Request URL + * @param body Request body + * @param headers Optional headers + * @returns Promise with response data + */ + async put(url: string, body?: any, headers?: Record): Promise> { + return this.request({ + method: 'PUT', + url, + body, + headers, + }); + } + + /** + * Make a DELETE request + * @param url Request URL + * @param headers Optional headers + * @returns Promise with response data + */ + async delete(url: string, headers?: Record): Promise> { + return this.request({ + method: 'DELETE', + url, + headers, + }); + } + + /** + * Make a generic HTTP request + * @param config Request configuration + * @returns Promise with response data + */ + private async request(config: RequestConfig): Promise> { + const url = config.url.startsWith('http') ? config.url : \`\${this.baseURL}\${config.url}\`; + + const headers = { + 'Content-Type': 'application/json', + ...this.defaultHeaders, + ...config.headers, + }; + + const response = await fetch(url, { + method: config.method, + headers, + body: config.body ? JSON.stringify(config.body) : undefined, + }); + + if (!response.ok) { + throw new Error(\`HTTP \${response.status}: \${response.statusText}\`); + } + + const data = await response.json(); + + return { + data, + status: response.status, + headers: Object.fromEntries(response.headers.entries()), + }; + } +}`, + }, + ]; + + const result = await context.addToIndex(files); + console.log("\nIndexing result:"); + console.log(" Newly uploaded:", result.newlyUploaded); + console.log(" Already uploaded:", result.alreadyUploaded); + + // Search the codebase - returns formatted string ready for LLM use or display + // Using queries that work well with our realistic content + console.log("\n--- Search 1: Find string utility functions ---"); + const results1 = await context.search("string utility functions for text formatting"); + console.log("Search results:"); + console.log(results1); + + console.log("\n--- Search 2: Find user management service ---"); + const results2 = await context.search("user management service with CRUD operations"); + console.log("Search results:"); + console.log(results2); + + console.log("\n--- Search 3: Find HTTP client for API requests ---"); + const httpResults = await context.search("HTTP client for making API requests"); + console.log("Search results:"); + console.log(httpResults); + + // Use searchAndAsk to ask questions about the indexed code + console.log("\n--- searchAndAsk Example 1: Ask questions about the code ---"); + const question = "How does the UserService class handle user creation and validation?"; + console.log(`Question: ${question}`); + + const answer = await context.searchAndAsk( + "user creation and validation in UserService", + question + ); + + console.log(`\nAnswer: ${answer}`); + + // Use searchAndAsk to generate documentation + console.log("\n--- searchAndAsk Example 2: Generate documentation ---"); + const documentation = await context.searchAndAsk( + "string utility functions", + "Generate API documentation in markdown format for the string utility functions" + ); + + console.log("\nGenerated Documentation:"); + console.log(documentation); + + // Use searchAndAsk to explain code patterns + console.log("\n--- searchAndAsk Example 3: Explain code patterns ---"); + const explanation = await context.searchAndAsk( + "utility functions", + "Explain what these utility functions do and when they would be useful" + ); + + console.log(`\nExplanation: ${explanation}`); + + // Export state to a file + const stateFile = join("/tmp", "direct-context-state.json"); + console.log(`\nExporting state to ${stateFile}...`); + await context.exportToFile(stateFile); + console.log("State exported successfully"); + + // Show the exported state + const exportedState = JSON.parse(readFileSync(stateFile, "utf-8")); + console.log("\nExported state:"); + console.log(JSON.stringify(exportedState, null, 2)); + + // Import state in a new context + console.log("\n--- Testing state import ---"); + const context2 = await DirectContext.importFromFile(stateFile, { debug: false }); + console.log("State imported successfully"); + + // Verify we can still search + const results3 = await context2.search("string utility functions"); + console.log("\nSearch after importing state:"); + console.log(results3); + + console.log("\n=== Sample Complete ==="); +} + +main().catch((error) => { + console.error("Error:", error); + process.exit(1); +}); diff --git a/examples/typescript-sdk/context/file-search-server/README.md b/examples/typescript-sdk/context/file-search-server/README.md new file mode 100644 index 0000000..4285aab --- /dev/null +++ b/examples/typescript-sdk/context/file-search-server/README.md @@ -0,0 +1,31 @@ +# File Search Server Example + +REST API for semantic file search with AI-powered summarization. + +## Prerequisites + +Install the `auggie` CLI and authenticate: +```bash +npm install -g @augmentcode/auggie +auggie login +``` + +## Usage + +```bash +npx tsx examples/context/file-search-server/index.ts . +``` + +## API Endpoints + +### Search Files +```bash +curl "http://localhost:3000/search?q=typescript" +curl "http://localhost:3000/search?q=authentication+logic" +``` + +### Health Check +```bash +curl "http://localhost:3000/health" +``` + diff --git a/examples/typescript-sdk/context/file-search-server/index.ts b/examples/typescript-sdk/context/file-search-server/index.ts new file mode 100644 index 0000000..7399b7b --- /dev/null +++ b/examples/typescript-sdk/context/file-search-server/index.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * File Search Server Sample + * + * A simple HTTP server that provides AI-powered file search using FileSystem Context. + * Search results are processed with Haiku 4.5 to summarize only the relevant results. + * + * Usage: + * npm run build + * node dist/examples/context/file-search-server/index.js [workspace-directory] + * + * Or with tsx: + * npx tsx examples/context/file-search-server/index.ts [workspace-directory] + * + * Endpoints: + * GET /search?q= - Search for files and get AI-summarized results + * GET /health - Health check + */ + +import { createServer } from "node:http"; +import { FileSystemContext } from "@augmentcode/auggie-sdk"; +import { handleSearch } from "./search-handler"; + +const PORT = 3000; +const workspaceDir = process.argv[2] || process.cwd(); + +console.log("=== File Search Server ===\n"); +console.log(`Workspace directory: ${workspaceDir}`); +console.log(`Starting server on port ${PORT}...\n`); + +// Create FileSystem Context +let context: FileSystemContext | null = null; + +async function initializeContext() { + console.log("Initializing FileSystem Context..."); + context = await FileSystemContext.create({ + directory: workspaceDir, + debug: false, + }); + console.log("FileSystem Context initialized\n"); +} + +// HTTP request handler +const server = createServer(async (req, res) => { + // Set CORS headers + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + const url = new URL(req.url || "/", `http://localhost:${PORT}`); + + if (url.pathname === "/search" && req.method === "GET") { + const query = url.searchParams.get("q"); + + if (!query) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Missing query parameter 'q'" })); + return; + } + + if (!context) { + res.writeHead(503, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Context not initialized yet" })); + return; + } + + try { + console.log(`[${new Date().toISOString()}] Search request: "${query}"`); + + const result = await handleSearch(query, context); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(result, null, 2)); + } catch (error) { + console.error("Search error:", error); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }) + ); + } + } else if (url.pathname === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + status: "ok", + workspace: workspaceDir, + contextReady: context !== null, + }) + ); + } else { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); + } +}); + +// Initialize and start server +initializeContext() + .then(() => { + server.listen(PORT, () => { + console.log(`✅ Server running at http://localhost:${PORT}/`); + console.log("\nExample requests:"); + console.log(" # Search with AI-summarized results"); + console.log(` curl "http://localhost:${PORT}/search?q=typescript"`); + console.log( + ` curl "http://localhost:${PORT}/search?q=authentication+logic"` + ); + console.log("\n # Health check"); + console.log(` curl "http://localhost:${PORT}/health"`); + console.log("\nPress Ctrl+C to stop\n"); + }); + }) + .catch((error) => { + console.error("Failed to initialize:", error); + process.exit(1); + }); + +// Cleanup on exit +process.on("SIGINT", async () => { + console.log("\n\nShutting down..."); + if (context) { + await context.close(); + } + server.close(); + process.exit(0); +}); diff --git a/examples/typescript-sdk/context/file-search-server/search-handler.ts b/examples/typescript-sdk/context/file-search-server/search-handler.ts new file mode 100644 index 0000000..7f38212 --- /dev/null +++ b/examples/typescript-sdk/context/file-search-server/search-handler.ts @@ -0,0 +1,38 @@ +import type { FileSystemContext } from "@augmentcode/auggie-sdk"; + +export type SearchResponse = { + query: string; + summary: string; + formattedResults: string; +}; + +/** + * Handle search request + */ +export async function handleSearch( + query: string, + context: FileSystemContext +): Promise { + // Search for relevant code - returns formatted string ready for LLM use + const formattedResults = await context.search(query); + + if (!formattedResults || formattedResults.trim().length === 0) { + return { + query, + summary: "No relevant results found.", + formattedResults: "", + }; + } + + // Use searchAndAsk to summarize the relevant results + const summary = await context.searchAndAsk( + query, + `Provide a concise summary of the relevant results for the query "${query}". Focus only on the most relevant information.` + ); + + return { + query, + summary, + formattedResults, + }; +} diff --git a/examples/typescript-sdk/context/filesystem-context/README.md b/examples/typescript-sdk/context/filesystem-context/README.md new file mode 100644 index 0000000..8f41146 --- /dev/null +++ b/examples/typescript-sdk/context/filesystem-context/README.md @@ -0,0 +1,36 @@ +# FileSystem Context Example + +Local directory search via MCP protocol with AI-powered Q&A and code review. + +## Prerequisites + +Install the `auggie` CLI: +```bash +auggie --version +``` + +## Usage + +```bash +# Authenticate (for AI features) +auggie login + +# Run the example +npx tsx examples/context/filesystem-context/index.ts +``` + +## What It Does + +- Spawns `auggie --mcp` process for file system operations +- Searches local directories without explicit indexing +- Uses `searchAndAsk()` for AI Q&A about the workspace +- Performs AI-powered code review +- Explains code patterns + +## Key Features + +- **`search()`**: Semantic search over local files +- **`searchAndAsk()`**: One-step AI Q&A about workspace +- **MCP Protocol**: Standardized context access +- **Auto-indexing**: Files indexed on-the-fly + diff --git a/examples/typescript-sdk/context/filesystem-context/index.ts b/examples/typescript-sdk/context/filesystem-context/index.ts new file mode 100644 index 0000000..acf0687 --- /dev/null +++ b/examples/typescript-sdk/context/filesystem-context/index.ts @@ -0,0 +1,101 @@ +/** + * Sample: FileSystem Context - Local directory retrieval via MCP + * + * This sample demonstrates: + * - Creating a FileSystem Context instance + * - Searching a local directory using MCP protocol + * - Getting formatted search results + * - Interactive Q&A about the workspace using AI + * - Code review suggestions using AI + * - Properly closing the MCP connection + */ + +import { FileSystemContext } from "@augmentcode/auggie-sdk"; + +async function main() { + console.log("=== FileSystem Context Sample ===\n"); + + // Use the current SDK directory as the workspace + const workspaceDir = process.cwd(); + console.log(`Workspace directory: ${workspaceDir}`); + + // Create a FileSystem Context instance + // Authentication is handled automatically by the auggie CLI via: + // 1. AUGMENT_API_TOKEN / AUGMENT_API_URL env vars, or + // 2. ~/.augment/session.json (created by `auggie login`) + console.log("\nCreating FileSystem Context (spawning auggie --mcp)..."); + const context = await FileSystemContext.create({ + directory: workspaceDir, + auggiePath: "auggie", // or specify full path to auggie binary + debug: true, + }); + + try { + // Search 1: Find TypeScript SDK implementation + console.log("\n--- Search 1: TypeScript SDK implementation ---"); + const results1 = await context.search("TypeScript SDK implementation"); + console.log("Search results:"); + console.log(results1.substring(0, 500)); // Show first 500 chars + if (results1.length > 500) { + console.log(`... (${results1.length - 500} more characters)`); + } + + // Search 2: Find context modes + console.log("\n--- Search 2: Context modes implementation ---"); + const results2 = await context.search("context modes implementation"); + console.log("Search results:"); + console.log(results2.substring(0, 500)); // Show first 500 chars + if (results2.length > 500) { + console.log(`... (${results2.length - 500} more characters)`); + } + + // searchAndAsk Example 1: Ask questions about the workspace + console.log("\n--- searchAndAsk Example 1: Ask about context modes ---"); + const question1 = "What context modes are available in this SDK?"; + console.log(`Question: ${question1}`); + const answer1 = await context.searchAndAsk("context modes", question1); + console.log(`\nAnswer: ${answer1}`); + + // searchAndAsk Example 2: Ask about implementation + console.log("\n--- searchAndAsk Example 2: Ask about generation API ---"); + const question2 = "How is the generation API implemented?"; + console.log(`Question: ${question2}`); + const answer2 = await context.searchAndAsk( + "generation API implementation", + question2 + ); + console.log(`\nAnswer: ${answer2}`); + + // searchAndAsk Example 3: Code review + console.log("\n--- searchAndAsk Example 3: Code review ---"); + const reviewFile = "src/index.ts"; + console.log(`Reviewing: ${reviewFile}`); + const review = await context.searchAndAsk( + `file:${reviewFile}`, + "Review this code for potential issues, bugs, and improvements. Provide specific, actionable feedback." + ); + console.log(`\nReview:\n${review}`); + + // searchAndAsk Example 4: Explain patterns + console.log("\n--- searchAndAsk Example 4: Explain code patterns ---"); + const pattern = "error handling"; + console.log(`Pattern: ${pattern}`); + const patternExplanation = await context.searchAndAsk( + pattern, + `Explain this code pattern: "${pattern}". What does it do, why is it used, and what are the best practices?` + ); + console.log(`\nExplanation:\n${patternExplanation}`); + } finally { + // Always close the MCP connection + console.log("\nClosing MCP connection..."); + await context.close(); + console.log("MCP connection closed"); + } + + console.log("\n=== Sample Complete ==="); +} + +main().catch((error) => { + console.error("Error:", error); + process.exit(1); +}); diff --git a/examples/typescript-sdk/context/github-action-indexer/.github/workflows/index.yml b/examples/typescript-sdk/context/github-action-indexer/.github/workflows/index.yml new file mode 100644 index 0000000..40e755e --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/.github/workflows/index.yml @@ -0,0 +1,79 @@ +name: Index Repository + +on: + push: + branches: + - main + - develop + - 'feature/**' # Index feature branches + - 'release/**' # Index release branches + workflow_dispatch: + inputs: + branch: + description: 'Branch to index (leave empty for current branch)' + required: false + type: string + force_full_reindex: + description: 'Force full re-index' + required: false + type: boolean + default: false + +jobs: + index: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for comparison + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Restore index state + uses: actions/cache@v4 + with: + path: .augment-index-state + # Use branch-specific cache key + key: augment-index-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + augment-index-${{ github.ref_name }}- + + - name: Index repository + id: index + run: npm run index + env: + AUGMENT_API_TOKEN: ${{ secrets.AUGMENT_API_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + STORAGE_TYPE: file + # Branch-specific state path (automatically determined from GITHUB_REF) + # STATE_PATH is optional - defaults to .augment-index-state/{branch}/state.json + MAX_COMMITS: 100 + MAX_FILES: 500 + + - name: Print results + if: always() + run: | + echo "Success: ${{ steps.index.outputs.success }}" + echo "Type: ${{ steps.index.outputs.type }}" + echo "Files Indexed: ${{ steps.index.outputs.files_indexed }}" + echo "Files Deleted: ${{ steps.index.outputs.files_deleted }}" + echo "Checkpoint ID: ${{ steps.index.outputs.checkpoint_id }}" + echo "Commit SHA: ${{ steps.index.outputs.commit_sha }}" + + - name: Upload state artifact + if: success() + uses: actions/upload-artifact@v4 + with: + name: index-state + path: .augment-index-state/ + retention-days: 30 + diff --git a/examples/typescript-sdk/context/github-action-indexer/.gitignore b/examples/typescript-sdk/context/github-action-indexer/.gitignore new file mode 100644 index 0000000..c7f37bb --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Index state +.augment-index-state/ + +# Environment variables +.env +.env.local + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + diff --git a/examples/typescript-sdk/context/github-action-indexer/README.md b/examples/typescript-sdk/context/github-action-indexer/README.md new file mode 100644 index 0000000..9e0b284 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/README.md @@ -0,0 +1,245 @@ +# GitHub Action Repository Indexer + +A TypeScript example showing how to index a GitHub repository using the Augment SDK Direct Mode with incremental updates. + +## Overview + +This example demonstrates: +- **Incremental indexing** using GitHub's Compare API +- **State persistence** for efficient updates +- **Automatic fallback** to full re-index when needed +- **Local development** and **GitHub Actions** support + +## Try It Locally First + +The quickest way to see how this works is to try it locally: + +```bash +# Clone the repository +git clone https://github.com/augmentcode/auggie.git +cd auggie/examples/typescript-sdk/context/github-action-indexer + +# Install dependencies +npm install + +# Set required environment variables +export AUGMENT_API_TOKEN="your-token" +export AUGMENT_API_URL="https://your-tenant.api.augmentcode.com/" +export GITHUB_TOKEN="your-github-token" +export GITHUB_REPOSITORY="owner/repo" +export GITHUB_SHA="$(git rev-parse HEAD)" + +# Index the repository +npm run index + +# Search the indexed repository +npm run search "authentication functions" +npm run search "error handling" +``` + +The index state is saved to `.augment-index-state/{branch}/state.json` by default (where `{branch}` is the current branch name), so subsequent runs will perform incremental updates. + +## Deploy to Production (GitHub Actions) + +Once you've tested locally, you can deploy the indexer to run automatically on every push using GitHub Actions. + +### 1. Install the Indexer + +Use the installation script to set up everything automatically: + +```bash +# Install in current directory +cd /path/to/your/repository +npx @augment-samples/github-action-indexer install + +# Or specify the repository path +npx @augment-samples/github-action-indexer install /path/to/your/repository +``` + +The installation script will: +- ✅ Create the GitHub workflow file (`.github/workflows/augment-index.yml`) +- ✅ Copy all necessary source files to your repository +- ✅ Set up TypeScript configuration with optimized defaults +- ✅ Update your `.gitignore` +- ✅ No questions asked - uses sensible defaults that work for most projects + +### 2. Configure Repository Secrets + +Add these secrets to your repository (Settings → Secrets and variables → Actions): + +| Secret Name | Description | Required | +|-------------|-------------|----------| +| `AUGMENT_API_TOKEN` | Your Augment API token (can be a JSON object with `accessToken` and `tenantURL` fields, or a plain token string) | Yes | + +**Note:** +- If using a plain token string, you must also set `AUGMENT_API_URL` as a secret or environment variable +- If your token is a JSON object from `~/.augment/session.json` (with `accessToken` and `tenantURL`), the URL will be extracted automatically +- `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_SHA` are automatically provided by GitHub Actions + +### 3. Push and Run + +Push your changes to trigger the first indexing run. The workflow will: +- Index your repository automatically on every push +- Use incremental updates for efficiency +- Cache the index state between runs + +## How It Works + +1. **Load previous state** from storage (if exists) +2. **Check if full re-index is needed**: + - First run (no previous state) + - Force push detected + - Too many commits or file changes + - Ignore files changed +3. **If full re-index**: Download tarball and index all files +4. **If incremental**: Use Compare API to index only changed files +5. **Save new state** to storage + +## Customizing the Workflow + +The installation script generates a workflow that indexes only the main branch by default. This works well for most projects. + +If you need to index multiple branches, you can edit the generated workflow file (`.github/workflows/augment-index.yml`) to add additional branches: + +```yaml +on: + push: + branches: + - main + - develop + - 'feature/**' +``` + +You can also adjust performance settings by editing the `MAX_COMMITS` and `MAX_FILES` values in the workflow file if needed. + +## Troubleshooting + +### Common Issues + +#### 1. Authentication Errors + +**Error:** `Authentication failed` or `Invalid API token` + +**Solutions:** +- Verify `AUGMENT_API_TOKEN` is set correctly in repository secrets +- Check that `AUGMENT_API_URL` matches your tenant URL +- Ensure the token has necessary permissions + +#### 2. Workflow Not Triggering + +**Error:** Workflow doesn't run on push + +**Solutions:** +- Check that the workflow file is in `.github/workflows/` +- Verify the branch name matches your push branch +- Ensure the workflow file has correct YAML syntax + +#### 3. Index State Issues + +**Error:** `Failed to load index state` or frequent full re-indexes + +**Solutions:** +- Check GitHub Actions cache limits (10GB per repository) +- Verify `.augment-index-state/` is in `.gitignore` +- Re-run the installation script to ensure optimal performance settings + +#### 4. Large Repository Performance + +**Error:** Workflow times out or runs slowly + +**Solutions:** +- Increase `timeout-minutes` in workflow (default: 360 minutes) +- Edit the workflow file to use larger performance settings (increase `MAX_COMMITS` and `MAX_FILES`) +- Consider indexing fewer branches or using manual dispatch + +### Debug Mode + +Enable debug logging by adding this to your workflow: + +```yaml +env: + ACTIONS_STEP_DEBUG: true + ACTIONS_RUNNER_DEBUG: true +``` + +### Local Testing + +Test the indexer locally before deploying: + +```bash +# Set up environment +export AUGMENT_API_TOKEN="your-token" +export AUGMENT_API_URL="https://your-tenant.api.augmentcode.com/" +export GITHUB_TOKEN="your-github-token" +export GITHUB_REPOSITORY="owner/repo" +export GITHUB_SHA="$(git rev-parse HEAD)" + +# Run indexing +npm run index + +# Test search +npm run search "your search query" +``` + +### Getting Help + +1. **Check workflow logs** in your repository's Actions tab +2. **Review the troubleshooting guide** above +3. **Test locally** using the environment variables +4. **Open an issue** in the Augment repository with: + - Your workflow file + - Error messages from the logs + - Repository size and structure details + +## Storage Backends + +The index state is stored as a JSON file on the file system by default (`.augment-index-state/{branch}/state.json`). In GitHub Actions, the state is persisted between runs using GitHub Actions cache for efficient incremental updates. + +The indexer can be adapted to use other storage backends like Redis, S3, or databases. The state save/load operations in `src/index-manager.ts` can be modified to work with any storage system that can persist JSON data. + +## Searching the Index + +After indexing, you can search the repository using the CLI tool: + +```bash +# Search for specific functionality +npm run search "authentication functions" + +# Search for error handling patterns +npm run search "error handling" + +# Search for specific implementations +npm run search "database queries" +``` + +The search tool will: +1. Load the index state from storage +2. Perform semantic search using the Augment SDK +3. Display matching code chunks with file paths and line numbers + +Example output: +``` +Searching for: "authentication functions" + +Loading index state... +Loaded index: 42 files indexed +Last indexed commit: abc123def456 +Branch: main + +Found 3 result(s): + +📄 src/auth/login.ts + Lines 15-28 + ──────────────────────────────────────────────────────────── + 15 │ export async function authenticateUser( + 16 │ username: string, + 17 │ password: string + 18 │ ): Promise { + 19 │ // Authentication logic... + 20 │ } +``` + +## License + +MIT + diff --git a/examples/typescript-sdk/context/github-action-indexer/install.js b/examples/typescript-sdk/context/github-action-indexer/install.js new file mode 100755 index 0000000..734a7eb --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/install.js @@ -0,0 +1,483 @@ +#!/usr/bin/env node + +/** + * GitHub Action Indexer Installation Script + * + * This script helps developers install the Augment GitHub Action Indexer + * into their repositories with minimal setup. + * + * Usage: + * npx @augment-samples/github-action-indexer install + * node install.js + */ + +import { promises as fs } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createInterface } from 'node:readline'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Colors for console output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', +}; + +function colorize(color, text) { + return `${colors[color]}${text}${colors.reset}`; +} + +function log(message, color = 'reset') { + console.log(colorize(color, message)); +} + +function logStep(step, message) { + log(`[${step}] ${message}`, 'cyan'); +} + +function logSuccess(message) { + log(`✅ ${message}`, 'green'); +} + +function logWarning(message) { + log(`⚠️ ${message}`, 'yellow'); +} + +function logError(message) { + log(`❌ ${message}`, 'red'); +} + +// Create readline interface for user input +const rl = createInterface({ + input: process.stdin, + output: process.stdout, +}); + +function question(prompt) { + return new Promise((resolve) => { + rl.question(prompt, resolve); + }); +} + +// Configuration options +const defaultConfig = { + template: 'basic', // 'basic' or 'multi-branch' + repositorySize: 'medium', // 'small', 'medium', 'large' + nodeVersion: '20', +}; + +// Performance settings based on repository size +const performanceSettings = { + small: { maxCommits: 50, maxFiles: 200 }, + medium: { maxCommits: 100, maxFiles: 500 }, + large: { maxCommits: 200, maxFiles: 1000 }, +}; + +// File templates +const packageJsonTemplate = { + "name": "augment-github-indexer", + "version": "1.0.0", + "description": "Augment GitHub Action repository indexer", + "type": "module", + "scripts": { + "index": "tsx src/index.ts", + "search": "tsx src/search.ts", + "dev": "tsx watch src/index.ts", + "build": "tsc", + "test": "vitest" + }, + "keywords": [ + "augment", + "github", + "indexing", + "sdk" + ], + "author": "Your Name", + "license": "MIT", + "dependencies": { + "@augmentcode/auggie-sdk": "^0.1.6", + "@octokit/rest": "^20.0.2", + "ignore": "^5.3.0", + "tar": "^6.2.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/tar": "^6.1.10", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + } +}; + +const tsconfigTemplate = { + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}; + +const gitignoreTemplate = `# Dependencies +node_modules/ + +# Build output +dist/ + +# Index state +.augment-index-state/ + +# Environment variables +.env +.env.local + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +`; + +function generateWorkflowTemplate(config) { + const settings = performanceSettings[config.repositorySize]; + + if (config.template === 'basic') { + return `name: Index Repository + +on: + push: + branches: + - main + +jobs: + index: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for comparison + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '${config.nodeVersion}' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Restore index state + uses: actions/cache@v4 + with: + path: .augment-index-state + key: augment-index-\${{ github.ref_name }}-\${{ github.sha }} + restore-keys: | + augment-index-\${{ github.ref_name }}- + + - name: Index repository + run: npm run index + env: + AUGMENT_API_TOKEN: \${{ secrets.AUGMENT_API_TOKEN }} + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + MAX_COMMITS: ${settings.maxCommits} + MAX_FILES: ${settings.maxFiles} +`; + } else { + // multi-branch template + return `name: Index Repository + +on: + push: + branches: + - main + - develop + - 'feature/**' + - 'release/**' + workflow_dispatch: + inputs: + force_full_reindex: + description: 'Force full re-index' + required: false + type: boolean + default: false + +jobs: + index: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for comparison + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '${config.nodeVersion}' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Restore index state + uses: actions/cache@v4 + with: + path: .augment-index-state + key: augment-index-\${{ github.ref_name }}-\${{ github.sha }} + restore-keys: | + augment-index-\${{ github.ref_name }}- + + - name: Index repository + id: index + run: npm run index + env: + AUGMENT_API_TOKEN: \${{ secrets.AUGMENT_API_TOKEN }} + GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} + MAX_COMMITS: ${settings.maxCommits} + MAX_FILES: ${settings.maxFiles} + + - name: Print results + if: always() + run: | + echo "Success: \${{ steps.index.outputs.success }}" + echo "Type: \${{ steps.index.outputs.type }}" + echo "Files Indexed: \${{ steps.index.outputs.files_indexed }}" + echo "Files Deleted: \${{ steps.index.outputs.files_deleted }}" + echo "Checkpoint ID: \${{ steps.index.outputs.checkpoint_id }}" + echo "Commit SHA: \${{ steps.index.outputs.commit_sha }}" +`; + } +} + +async function checkTargetDirectory(targetDir) { + try { + const stat = await fs.stat(targetDir); + if (!stat.isDirectory()) { + throw new Error(`${targetDir} is not a directory`); + } + return true; + } catch (error) { + if (error.code === 'ENOENT') { + return false; + } + throw error; + } +} + +async function copySourceFiles(sourceDir, targetDir) { + const srcDir = join(sourceDir, 'src'); + const targetSrcDir = join(targetDir, 'src'); + + // Create target src directory + await fs.mkdir(targetSrcDir, { recursive: true }); + + // Copy all TypeScript source files + const files = await fs.readdir(srcDir); + for (const file of files) { + if (file.endsWith('.ts')) { + const sourcePath = join(srcDir, file); + const targetPath = join(targetSrcDir, file); + await fs.copyFile(sourcePath, targetPath); + logSuccess(`Copied ${file}`); + } + } +} + +async function createConfigFiles(targetDir, config) { + // Create package.json + const packageJsonPath = join(targetDir, 'package.json'); + await fs.writeFile(packageJsonPath, JSON.stringify(packageJsonTemplate, null, 2)); + logSuccess('Created package.json'); + + // Create tsconfig.json + const tsconfigPath = join(targetDir, 'tsconfig.json'); + await fs.writeFile(tsconfigPath, JSON.stringify(tsconfigTemplate, null, 2)); + logSuccess('Created tsconfig.json'); + + // Create or update .gitignore + const gitignorePath = join(targetDir, '.gitignore'); + let existingGitignore = ''; + try { + existingGitignore = await fs.readFile(gitignorePath, 'utf-8'); + } catch (error) { + // File doesn't exist, that's fine + } + + if (!existingGitignore.includes('.augment-index-state/')) { + const updatedGitignore = existingGitignore + (existingGitignore ? '\n\n' : '') + + '# Augment indexer files\n.augment-index-state/\n'; + await fs.writeFile(gitignorePath, updatedGitignore); + logSuccess('Updated .gitignore'); + } else { + logWarning('.gitignore already contains Augment indexer entries'); + } +} + +async function createWorkflowFile(targetDir, config) { + const workflowDir = join(targetDir, '.github', 'workflows'); + await fs.mkdir(workflowDir, { recursive: true }); + + const workflowPath = join(workflowDir, 'augment-index.yml'); + const workflowContent = generateWorkflowTemplate(config); + + await fs.writeFile(workflowPath, workflowContent); + logSuccess('Created GitHub workflow file'); +} + +function getDefaultConfiguration() { + // Use opinionated defaults for zero-question setup + const config = { + template: 'basic', // Basic template (main branch only) works for 80% of projects + repositorySize: 'medium', // Medium settings work well for most repositories + nodeVersion: '20', + }; + + const settings = performanceSettings[config.repositorySize]; + log('\n' + colorize('bright', '🔧 Configuration')); + log(colorize('green', 'Using optimized defaults:')); + log(`• Template: Basic (indexes main branch only)`); + log(`• Performance: Medium repository settings (${settings.maxCommits} max commits, ${settings.maxFiles} max files)`); + log(`• Node.js version: ${config.nodeVersion}\n`); + + log(colorize('blue', 'Need different settings? You can customize the generated workflow file after installation.\n')); + + return config; +} + +async function displayNextSteps(targetDir) { + log('\n' + colorize('bright', '🎉 Installation Complete!')); + log('\nNext steps:\n'); + + log(colorize('yellow', '1. Install dependencies:')); + log(' cd ' + targetDir); + log(' npm install\n'); + + log(colorize('yellow', '2. Set up GitHub repository secrets:')); + log(' Go to your repository Settings > Secrets and variables > Actions'); + log(' Add the following secrets:'); + log(' • AUGMENT_API_TOKEN - Your Augment API token'); + log(' • AUGMENT_API_URL - Your tenant-specific Augment API URL\n'); + + log(colorize('yellow', '3. Test locally (optional):')); + log(' export AUGMENT_API_TOKEN="your-token"'); + log(' export AUGMENT_API_URL="https://your-tenant.api.augmentcode.com/"'); + log(' export GITHUB_TOKEN="your-github-token"'); + log(' export GITHUB_REPOSITORY="owner/repo"'); + log(' export GITHUB_SHA="$(git rev-parse HEAD)"'); + log(' npm run index\n'); + + log(colorize('yellow', '4. Push to trigger the workflow:')); + log(' git add .'); + log(' git commit -m "Add Augment GitHub Action Indexer"'); + log(' git push\n'); + + log(colorize('green', 'The indexer will automatically run on pushes to your configured branches!')); + log(colorize('blue', '\nFor more information, see the documentation at:')); + log('https://github.com/augmentcode/auggie/tree/main/examples/typescript-sdk/context/github-action-indexer\n'); +} + +async function main() { + try { + log(colorize('bright', '🚀 Augment GitHub Action Indexer Installation')); + log('This script will set up the Augment GitHub Action Indexer in your repository.\n'); + + // Get target directory + const args = process.argv.slice(2); + let targetDir = args[0] || process.cwd(); + targetDir = resolve(targetDir); + + log(colorize('bright', '📁 Target Directory')); + if (args[0]) { + log(`Installing to specified directory: ${colorize('cyan', targetDir)}`); + } else { + log(`Installing to current directory: ${colorize('cyan', targetDir)}`); + log(colorize('blue', 'Tip: You can specify a different directory: npx @augment-samples/github-action-indexer install /path/to/repo')); + } + log(''); + + // Check if target directory exists + const dirExists = await checkTargetDirectory(targetDir); + if (!dirExists) { + const create = await question(`Directory ${targetDir} doesn't exist. Create it? (y/N): `); + if (!create.toLowerCase().startsWith('y')) { + log('Installation cancelled.'); + process.exit(0); + } + await fs.mkdir(targetDir, { recursive: true }); + logSuccess(`Created directory ${targetDir}`); + } + + // Check if this looks like a git repository + const gitDir = join(targetDir, '.git'); + const isGitRepo = await checkTargetDirectory(gitDir); + if (!isGitRepo) { + logWarning('This doesn\'t appear to be a Git repository. The GitHub Action will only work in a Git repository.'); + const continue_ = await question('Continue anyway? (y/N): '); + if (!continue_.toLowerCase().startsWith('y')) { + log('Installation cancelled.'); + process.exit(0); + } + } + + // Get configuration (no questions, use defaults) + const config = getDefaultConfiguration(); + + logStep('1', 'Copying source files...'); + await copySourceFiles(__dirname, targetDir); + + logStep('2', 'Creating configuration files...'); + await createConfigFiles(targetDir, config); + + logStep('3', 'Creating GitHub workflow...'); + await createWorkflowFile(targetDir, config); + + await displayNextSteps(targetDir); + + } catch (error) { + logError(`Installation failed: ${error.message}`); + process.exit(1); + } finally { + rl.close(); + } +} + +// Handle Ctrl+C gracefully +process.on('SIGINT', () => { + log('\n\nInstallation cancelled by user.'); + rl.close(); + process.exit(0); +}); + +main(); diff --git a/examples/typescript-sdk/context/github-action-indexer/package.json b/examples/typescript-sdk/context/github-action-indexer/package.json new file mode 100644 index 0000000..760dc62 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/package.json @@ -0,0 +1,38 @@ +{ + "name": "@augment-samples/github-action-indexer", + "version": "1.0.0", + "description": "GitHub Action repository indexer using Augment SDK Direct Mode", + "type": "module", + "scripts": { + "index": "tsx src/index.ts", + "search": "tsx src/search.ts", + "dev": "tsx watch src/index.ts", + "build": "tsc", + "test": "vitest", + "install-indexer": "node install.js" + }, + "bin": { + "augment-indexer-install": "./install.js" + }, + "keywords": [ + "augment", + "github", + "indexing", + "sdk" + ], + "author": "Augment Code", + "license": "MIT", + "dependencies": { + "@augmentcode/auggie-sdk": "^0.1.6", + "@octokit/rest": "^20.0.2", + "ignore": "^5.3.0", + "tar": "^6.2.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/tar": "^6.1.10", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + } +} diff --git a/examples/typescript-sdk/context/github-action-indexer/src/file-filter.ts b/examples/typescript-sdk/context/github-action-indexer/src/file-filter.ts new file mode 100644 index 0000000..ea5b36f --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/src/file-filter.ts @@ -0,0 +1,101 @@ +/** + * File filtering logic for GitHub repository indexing + */ + +/** + * Keyish pattern regex - matches files that likely contain secrets/keys + */ +const KEYISH_PATTERN = + /^(\.git|.*\.pem|.*\.key|.*\.pfx|.*\.p12|.*\.jks|.*\.keystore|.*\.pkcs12|.*\.crt|.*\.cer|id_rsa|id_ed25519|id_ecdsa|id_dsa)$/; + +/** + * Default max file size in bytes (1 MB) + */ +export const DEFAULT_MAX_FILE_SIZE = 1024 * 1024; // 1 MB + +/** + * Check if a path should always be ignored (security measure) + */ +export function alwaysIgnorePath(path: string): boolean { + return path.includes(".."); +} + +/** + * Check if a path matches the keyish pattern (secrets/keys) + */ +export function isKeyishPath(path: string): boolean { + // Extract filename from path + const filename = path.split("/").pop() || ""; + return KEYISH_PATTERN.test(filename); +} + +/** + * Check if file size is valid for upload + */ +export function isValidFileSize( + sizeBytes: number, + maxFileSize = DEFAULT_MAX_FILE_SIZE +): boolean { + return sizeBytes <= maxFileSize; +} + +/** + * Check if file content is valid UTF-8 (not binary) + */ +export function isValidUtf8(content: Buffer): boolean { + try { + // Try to decode as UTF-8 + const decoded = content.toString("utf-8"); + // Re-encode and compare to detect invalid UTF-8 + const reencoded = Buffer.from(decoded, "utf-8"); + return content.equals(reencoded); + } catch { + return false; + } +} + +/** + * Check if a file should be filtered out + * Returns { filtered: true, reason: string } if file should be skipped + * Returns { filtered: false } if file should be included + * + * Priority order: + * 1. Path validation (contains "..") + * 2. File size check + * 3. .augmentignore rules (checked by caller) + * 4. Keyish patterns + * 5. .gitignore rules (checked by caller) + * 6. UTF-8 validation + */ +export function shouldFilterFile(params: { + path: string; + content: Buffer; + maxFileSize?: number; +}): { filtered: boolean; reason?: string } { + const { path, content, maxFileSize } = params; + + // 1. Check for ".." in path (security) + if (alwaysIgnorePath(path)) { + return { filtered: true, reason: "path_contains_dotdot" }; + } + + // 2. Check file size + if (!isValidFileSize(content.length, maxFileSize)) { + return { + filtered: true, + reason: `file_too_large (${content.length} bytes)`, + }; + } + + // 3. Check keyish patterns (secrets/keys) + if (isKeyishPath(path)) { + return { filtered: true, reason: "keyish_pattern" }; + } + + // 4. Check UTF-8 validity (binary detection) + if (!isValidUtf8(content)) { + return { filtered: true, reason: "binary_file" }; + } + + return { filtered: false }; +} diff --git a/examples/typescript-sdk/context/github-action-indexer/src/github-client.ts b/examples/typescript-sdk/context/github-action-indexer/src/github-client.ts new file mode 100644 index 0000000..b4cb077 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/src/github-client.ts @@ -0,0 +1,342 @@ +/** + * GitHub API client for fetching repository data + */ + +import { Readable } from "node:stream"; +import { Octokit } from "@octokit/rest"; +import ignore from "ignore"; +import tar from "tar"; +import { shouldFilterFile } from "./file-filter.js"; +import type { FileChange } from "./types.js"; + +export class GitHubClient { + private readonly octokit: Octokit; + + constructor(token: string) { + this.octokit = new Octokit({ auth: token }); + } + + /** + * Resolve a ref (like "HEAD", "main", or a commit SHA) to a commit SHA + */ + async resolveRef(owner: string, repo: string, ref: string): Promise { + try { + const { data } = await this.octokit.repos.getCommit({ + owner, + repo, + ref, + }); + return data.sha; + } catch (error) { + throw new Error( + `Failed to resolve ref "${ref}" for ${owner}/${repo}: ${error}` + ); + } + } + + /** + * Download repository as tarball and extract files + */ + async downloadTarball( + owner: string, + repo: string, + ref: string + ): Promise> { + console.log(`Downloading tarball for ${owner}/${repo}@${ref}...`); + + // Get tarball URL + const { url } = await this.octokit.repos.downloadTarballArchive({ + owner, + repo, + ref, + }); + + // Download tarball + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download tarball: ${response.statusText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Extract files from tarball + const files = new Map(); + const { augmentignore, gitignore } = await this.loadIgnorePatterns( + owner, + repo, + ref + ); + + // Track filtering statistics + let totalFiles = 0; + let filteredFiles = 0; + const filterReasons = new Map(); + + // Create a readable stream from the buffer + const stream = Readable.from(buffer); + + // Use a promise to wait for tar extraction to complete + await new Promise((resolve, reject) => { + const parser = tar.list({ + onentry: (entry) => { + // Skip directories and symlinks + if (entry.type !== "File") { + return; + } + + totalFiles++; + + // Remove the root directory prefix (e.g., "owner-repo-sha/") + const pathParts = entry.path.split("/"); + pathParts.shift(); // Remove first component + const filePath = pathParts.join("/"); + + // Read file contents + const chunks: Buffer[] = []; + entry.on("data", (chunk) => chunks.push(chunk)); + entry.on("end", () => { + const contentBuffer = Buffer.concat(chunks); + + // Apply filtering in priority order: + // 1. .augmentignore + if (augmentignore.ignores(filePath)) { + filteredFiles++; + filterReasons.set( + "augmentignore", + (filterReasons.get("augmentignore") || 0) + 1 + ); + return; + } + + // 2. Path validation, file size, keyish patterns, UTF-8 validation + const filterResult = shouldFilterFile({ + path: filePath, + content: contentBuffer, + }); + + if (filterResult.filtered) { + filteredFiles++; + const reason = filterResult.reason || "unknown"; + filterReasons.set(reason, (filterReasons.get(reason) || 0) + 1); + return; + } + + // 3. .gitignore (checked last) + if (gitignore.ignores(filePath)) { + filteredFiles++; + filterReasons.set( + "gitignore", + (filterReasons.get("gitignore") || 0) + 1 + ); + return; + } + + // File passed all filters + const contents = contentBuffer.toString("utf-8"); + files.set(filePath, contents); + }); + }, + }); + + stream.pipe(parser); + parser.on("close", resolve); + stream.on("error", reject); + }); + + console.log(`Extracted ${files.size} files from tarball`); + console.log( + `Filtered ${filteredFiles} of ${totalFiles} files. Reasons:`, + Object.fromEntries(filterReasons) + ); + return files; + } + + /** + * Compare two commits and get file changes + */ + async compareCommits( + owner: string, + repo: string, + base: string, + head: string + ): Promise<{ + files: FileChange[]; + commits: number; + totalChanges: number; + }> { + console.log(`Comparing ${base}...${head}...`); + + const { data } = await this.octokit.repos.compareCommits({ + owner, + repo, + base, + head, + }); + + const files: FileChange[] = []; + + for (const file of data.files || []) { + const change: FileChange = { + path: file.filename, + status: this.mapGitHubStatus(file.status), + previousFilename: file.previous_filename, + }; + + // Download file contents for added/modified files + if (change.status === "added" || change.status === "modified") { + try { + const contents = await this.getFileContents( + owner, + repo, + file.filename, + head + ); + change.contents = contents; + } catch (error) { + console.warn(`Failed to download ${file.filename}: ${error}`); + } + } + + files.push(change); + } + + return { + files, + commits: data.commits.length, + totalChanges: data.files?.length || 0, + }; + } + + /** + * Get file contents at a specific ref + */ + async getFileContents( + owner: string, + repo: string, + path: string, + ref: string + ): Promise { + const { data } = await this.octokit.repos.getContent({ + owner, + repo, + path, + ref, + }); + + if (Array.isArray(data) || data.type !== "file") { + throw new Error(`${path} is not a file`); + } + + // Decode base64 content + return Buffer.from(data.content, "base64").toString("utf-8"); + } + + /** + * Load .gitignore and .augmentignore patterns separately + * Returns both filters to maintain proper priority order: + * .augmentignore → keyish → .gitignore + */ + private async loadIgnorePatterns( + owner: string, + repo: string, + ref: string + ): Promise<{ + augmentignore: ReturnType; + gitignore: ReturnType; + }> { + const augmentignore = ignore(); + const gitignore = ignore(); + + // Try to load .gitignore + try { + const gitignoreContent = await this.getFileContents( + owner, + repo, + ".gitignore", + ref + ); + gitignore.add(gitignoreContent); + } catch { + // .gitignore doesn't exist + } + + // Try to load .augmentignore + try { + const augmentignoreContent = await this.getFileContents( + owner, + repo, + ".augmentignore", + ref + ); + augmentignore.add(augmentignoreContent); + } catch { + // .augmentignore doesn't exist + } + + return { augmentignore, gitignore }; + } + + /** + * Map GitHub file status to our FileChange status + */ + private mapGitHubStatus(status: string): FileChange["status"] { + switch (status) { + case "added": + return "added"; + case "modified": + return "modified"; + case "removed": + return "removed"; + case "renamed": + return "renamed"; + default: + return "modified"; + } + } + + /** + * Check if ignore files changed between commits + */ + async ignoreFilesChanged( + owner: string, + repo: string, + base: string, + head: string + ): Promise { + const { data } = await this.octokit.repos.compareCommits({ + owner, + repo, + base, + head, + }); + + const ignoreFiles = [".gitignore", ".augmentignore"]; + return (data.files || []).some((file) => + ignoreFiles.includes(file.filename) + ); + } + + /** + * Check if the push was a force push + */ + async isForcePush( + owner: string, + repo: string, + base: string, + head: string + ): Promise { + try { + await this.octokit.repos.compareCommits({ + owner, + repo, + base, + head, + }); + return false; + } catch (_error) { + // If comparison fails, it's likely a force push + return true; + } + } +} diff --git a/examples/typescript-sdk/context/github-action-indexer/src/index-manager.ts b/examples/typescript-sdk/context/github-action-indexer/src/index-manager.ts new file mode 100644 index 0000000..7cb3ab2 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/src/index-manager.ts @@ -0,0 +1,417 @@ +/** + * Index Manager - Core indexing logic + */ + +import { promises as fs } from "node:fs"; +import { dirname } from "node:path"; +import { DirectContext } from "@augmentcode/auggie-sdk"; +import { GitHubClient } from "./github-client.js"; +import type { + FileChange, + IndexConfig, + IndexResult, + IndexState, +} from "./types.js"; + +const DEFAULT_MAX_COMMITS = 100; +const DEFAULT_MAX_FILES = 500; + +export class IndexManager { + private readonly github: GitHubClient; + private readonly context: DirectContext; + private readonly config: IndexConfig; + private readonly statePath: string; + + constructor(context: DirectContext, config: IndexConfig, statePath: string) { + this.context = context; + this.config = config; + this.statePath = statePath; + this.github = new GitHubClient(config.githubToken); + } + + /** + * Resolve the current commit ref to an actual commit SHA + * This handles cases where GITHUB_SHA might be "HEAD" or a branch name + */ + async resolveCommitSha(): Promise { + const resolvedSha = await this.github.resolveRef( + this.config.owner, + this.config.repo, + this.config.currentCommit + ); + this.config.currentCommit = resolvedSha; + } + + /** + * Load index state from file system + * + * EXTENDING TO OTHER STORAGE BACKENDS: + * Replace this method to load state from your preferred storage: + * - Redis: Use ioredis client to GET the state JSON + * - S3: Use AWS SDK to getObject from S3 bucket + * - Database: Query your database for the state record + * + * Example for Redis: + * const redis = new Redis(redisUrl); + * const data = await redis.get(stateKey); + * return data ? JSON.parse(data) : null; + * + * Example for S3: + * const s3 = new S3Client({ region }); + * const response = await s3.send(new GetObjectCommand({ Bucket, Key })); + * const data = await response.Body.transformToString(); + * return JSON.parse(data); + */ + private async loadState(): Promise { + try { + const data = await fs.readFile(this.statePath, "utf-8"); + return JSON.parse(data) as IndexState; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return null; // File doesn't exist + } + throw error; + } + } + + /** + * Save index state to file system + * + * EXTENDING TO OTHER STORAGE BACKENDS: + * Replace this method to save state to your preferred storage: + * - Redis: Use ioredis client to SET the state JSON + * - S3: Use AWS SDK to putObject to S3 bucket + * - Database: Insert or update the state record in your database + * + * Example for Redis: + * const redis = new Redis(redisUrl); + * await redis.set(stateKey, JSON.stringify(state)); + * + * Example for S3: + * const s3 = new S3Client({ region }); + * await s3.send(new PutObjectCommand({ + * Bucket, + * Key, + * Body: JSON.stringify(state), + * ContentType: 'application/json' + * })); + * + * Note: The state is just a JSON object (IndexState type) that can be + * serialized and stored anywhere. For distributed systems, consider using + * Redis or a database for shared state across multiple workers. + */ + private async saveState(state: IndexState): Promise { + // Ensure directory exists + await fs.mkdir(dirname(this.statePath), { recursive: true }); + + // Write state to file + await fs.writeFile(this.statePath, JSON.stringify(state, null, 2), "utf-8"); + } + + /** + * Main indexing entry point + */ + async index(): Promise { + console.log( + `Starting index for ${this.config.owner}/${this.config.repo}@${this.config.branch}` + ); + + try { + // Load previous state + const previousState = await this.loadState(); + + // If we have previous state, we'll need to create a new context with the imported state + // For now, we'll handle this in the incremental update logic + + // Determine if we need full re-index + const shouldFullReindex = await this.shouldFullReindex(previousState); + + if (shouldFullReindex.should) { + return await this.fullReindex(shouldFullReindex.reason); + } + + // Perform incremental update + // previousState is guaranteed to be non-null here because shouldFullReindex checks for it + if (!previousState) { + throw new Error("previousState should not be null at this point"); + } + return await this.incrementalUpdate(previousState); + } catch (error) { + console.error("Indexing failed:", error); + return { + success: false, + type: "full", + filesIndexed: 0, + filesDeleted: 0, + checkpointId: "", + commitSha: this.config.currentCommit, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Determine if full re-index is needed + */ + private async shouldFullReindex( + previousState: IndexState | null + ): Promise<{ should: boolean; reason?: string }> { + // No previous state - first run + if (!previousState) { + return { should: true, reason: "first_run" }; + } + + // Different repository + if ( + previousState.repository.owner !== this.config.owner || + previousState.repository.name !== this.config.repo + ) { + return { should: true, reason: "different_repository" }; + } + + // Same commit - no changes + if (previousState.lastCommitSha === this.config.currentCommit) { + console.log("No changes detected"); + return { should: false }; + } + + // Check for force push + const isForcePush = await this.github.isForcePush( + this.config.owner, + this.config.repo, + previousState.lastCommitSha, + this.config.currentCommit + ); + + if (isForcePush) { + return { should: true, reason: "force_push" }; + } + + // Get comparison + const comparison = await this.github.compareCommits( + this.config.owner, + this.config.repo, + previousState.lastCommitSha, + this.config.currentCommit + ); + + // Too many commits + const maxCommits = this.config.maxCommits || DEFAULT_MAX_COMMITS; + if (comparison.commits > maxCommits) { + return { + should: true, + reason: `too_many_commits (${comparison.commits} > ${maxCommits})`, + }; + } + + // Too many file changes + const maxFiles = this.config.maxFiles || DEFAULT_MAX_FILES; + if (comparison.totalChanges > maxFiles) { + return { + should: true, + reason: `too_many_files (${comparison.totalChanges} > ${maxFiles})`, + }; + } + + // Check if ignore files changed + const ignoreChanged = await this.github.ignoreFilesChanged( + this.config.owner, + this.config.repo, + previousState.lastCommitSha, + this.config.currentCommit + ); + + if (ignoreChanged) { + return { should: true, reason: "ignore_files_changed" }; + } + + return { should: false }; + } + + /** + * Perform full repository re-index + */ + private async fullReindex(reason?: string): Promise { + console.log(`Performing full re-index (reason: ${reason || "unknown"})`); + + // Download entire repository as tarball + const files = await this.github.downloadTarball( + this.config.owner, + this.config.repo, + this.config.currentCommit + ); + + // Add all files to index + const filesToIndex = Array.from(files.entries()).map( + ([path, contents]) => ({ + path, + contents, + }) + ); + + console.log(`Adding ${filesToIndex.length} files to index...`); + await this.context.addToIndex(filesToIndex); + + // Export DirectContext state + const contextState = this.context.export(); + + const newState: IndexState = { + contextState, + lastCommitSha: this.config.currentCommit, + repository: { + owner: this.config.owner, + name: this.config.repo, + }, + }; + + // Save state + await this.saveState(newState); + + return { + success: true, + type: "full", + filesIndexed: filesToIndex.length, + filesDeleted: 0, + checkpointId: contextState.checkpointId || "", + commitSha: this.config.currentCommit, + reindexReason: reason, + }; + } + + /** + * Process added or modified file + */ + private processAddedOrModifiedFile( + change: FileChange, + filesToAdd: Array<{ path: string; contents: string }> + ): void { + if (change.contents) { + filesToAdd.push({ path: change.path, contents: change.contents }); + } + } + + /** + * Process renamed file + */ + private processRenamedFile( + change: FileChange, + filesToAdd: Array<{ path: string; contents: string }>, + filesToDelete: string[] + ): void { + if (change.previousFilename) { + filesToDelete.push(change.previousFilename); + } + if (change.contents) { + filesToAdd.push({ path: change.path, contents: change.contents }); + } + } + + /** + * Process file changes and categorize them for indexing + */ + private processFileChanges(changes: FileChange[]): { + filesToAdd: Array<{ path: string; contents: string }>; + filesToDelete: string[]; + } { + const filesToAdd: Array<{ path: string; contents: string }> = []; + const filesToDelete: string[] = []; + + for (const change of changes) { + if (change.status === "added" || change.status === "modified") { + this.processAddedOrModifiedFile(change, filesToAdd); + } else if (change.status === "removed") { + filesToDelete.push(change.path); + } else if (change.status === "renamed") { + this.processRenamedFile(change, filesToAdd, filesToDelete); + } + } + + return { filesToAdd, filesToDelete }; + } + + /** + * Apply file changes to the index + */ + private async applyChangesToIndex( + filesToAdd: Array<{ path: string; contents: string }>, + filesToDelete: string[] + ): Promise { + if (filesToAdd.length > 0) { + await this.context.addToIndex(filesToAdd); + } + + if (filesToDelete.length > 0) { + await this.context.removeFromIndex(filesToDelete); + } + } + + /** + * Create new index state after update + */ + private createNewState(previousState: IndexState): IndexState { + const contextState = this.context.export(); + + return { + contextState, + lastCommitSha: this.config.currentCommit, + repository: previousState.repository, + }; + } + + /** + * Perform incremental update + */ + private async incrementalUpdate( + previousState: IndexState + ): Promise { + console.log("Performing incremental update..."); + + // Create a temporary file with the previous context state + const tempStateFile = `/tmp/github-indexer-incremental-${Date.now()}.json`; + await fs.writeFile(tempStateFile, JSON.stringify(previousState.contextState, null, 2)); + + try { + // Create a new context from the previous state + this.context = await DirectContext.importFromFile(tempStateFile, { + apiKey: this.config.apiKey, + apiUrl: this.config.apiUrl, + }); + } finally { + // Clean up temporary file + await fs.unlink(tempStateFile); + } + + // Get file changes + const comparison = await this.github.compareCommits( + this.config.owner, + this.config.repo, + previousState.lastCommitSha, + this.config.currentCommit + ); + + // Process changes + const { filesToAdd, filesToDelete } = this.processFileChanges( + comparison.files + ); + + console.log( + `Adding ${filesToAdd.length} files, deleting ${filesToDelete.length} files` + ); + + // Update index + await this.applyChangesToIndex(filesToAdd, filesToDelete); + + // Create and save new state + const newState = this.createNewState(previousState); + await this.saveState(newState); + + return { + success: true, + type: "incremental", + filesIndexed: filesToAdd.length, + filesDeleted: filesToDelete.length, + checkpointId: newState.contextState.checkpointId || "", + commitSha: this.config.currentCommit, + }; + } +} diff --git a/examples/typescript-sdk/context/github-action-indexer/src/index.ts b/examples/typescript-sdk/context/github-action-indexer/src/index.ts new file mode 100644 index 0000000..56823a0 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/src/index.ts @@ -0,0 +1,202 @@ +#!/usr/bin/env node +/** + * Main entry point for GitHub Action Indexer + */ + +import { DirectContext } from "@augmentcode/auggie-sdk"; +import { IndexManager } from "./index-manager.js"; +import type { IndexConfig } from "./types.js"; + +/** + * Parse API token from environment variable + * Handles both plain string tokens and JSON-formatted tokens + */ +function parseApiToken(): { apiToken: string; apiUrl: string } { + const apiTokenEnv = process.env.AUGMENT_API_TOKEN; + if (!apiTokenEnv) { + throw new Error("AUGMENT_API_TOKEN environment variable is required"); + } + + let apiToken: string; + let apiUrl: string | undefined = process.env.AUGMENT_API_URL; + + try { + const tokenObj = JSON.parse(apiTokenEnv) as { + accessToken?: string; + tenantURL?: string; + }; + if (tokenObj.accessToken) { + apiToken = tokenObj.accessToken; + // Use tenantURL from token if not overridden by env var + if (!apiUrl && tokenObj.tenantURL) { + apiUrl = tokenObj.tenantURL; + } + } else { + apiToken = apiTokenEnv; + } + } catch { + // Not JSON, use as-is + apiToken = apiTokenEnv; + } + + if (!apiUrl) { + throw new Error( + "AUGMENT_API_URL environment variable is required. Please set it to your tenant-specific URL (e.g., 'https://your-tenant.api.augmentcode.com') or include tenantURL in your API token JSON." + ); + } + + return { apiToken, apiUrl }; +} + +/** + * Parse repository information from environment variables + */ +function parseRepositoryInfo(): { + owner: string; + repo: string; + branch: string; + currentCommit: string; +} { + const repository = process.env.GITHUB_REPOSITORY || ""; + const [owner, repo] = repository.split("/"); + + if (!(owner && repo)) { + throw new Error('GITHUB_REPOSITORY must be in format "owner/repo"'); + } + + // Extract branch name from GitHub ref + const githubRef = process.env.GITHUB_REF || ""; + const githubRefName = process.env.GITHUB_REF_NAME || ""; + + let branch: string; + if (githubRef.startsWith("refs/heads/")) { + branch = githubRefName; + } else if (githubRef.startsWith("refs/tags/")) { + branch = `tag/${githubRefName}`; + } else if (githubRefName) { + branch = githubRefName; + } else { + branch = process.env.BRANCH || "main"; + } + + const currentCommit = process.env.GITHUB_SHA || ""; + if (!currentCommit) { + throw new Error("GITHUB_SHA environment variable is required"); + } + + return { owner, repo, branch, currentCommit }; +} + +/** + * Load configuration from environment variables + */ +function loadConfig(): IndexConfig { + const githubToken = process.env.GITHUB_TOKEN; + if (!githubToken) { + throw new Error("GITHUB_TOKEN environment variable is required"); + } + + const { apiToken, apiUrl } = parseApiToken(); + const { owner, repo, branch, currentCommit } = parseRepositoryInfo(); + + return { + apiToken, + apiUrl, + githubToken, + owner, + repo, + branch, + currentCommit, + maxCommits: process.env.MAX_COMMITS + ? Number.parseInt(process.env.MAX_COMMITS, 10) + : undefined, + maxFiles: process.env.MAX_FILES + ? Number.parseInt(process.env.MAX_FILES, 10) + : undefined, + }; +} + +/** + * Get the state file path for the current branch + */ +function getStatePath(branch: string): string { + const sanitizedBranch = branch.replace(/[^a-zA-Z0-9-_]/g, "-"); + return ( + process.env.STATE_PATH || + `.augment-index-state/${sanitizedBranch}/state.json` + ); +} + +/** + * Main function + */ +async function main() { + console.log("GitHub Action Indexer - Starting..."); + + try { + // Load configuration + const config = loadConfig(); + const statePath = getStatePath(config.branch); + + console.log(`Repository: ${config.owner}/${config.repo}`); + console.log(`Branch: ${config.branch}`); + console.log(`Commit ref: ${config.currentCommit}`); + console.log(`State path: ${statePath}`); + + // Create DirectContext + const context = await DirectContext.create({ + apiKey: config.apiToken, + apiUrl: config.apiUrl, + }); + + // Create index manager and resolve commit SHA + const manager = new IndexManager(context, config, statePath); + await manager.resolveCommitSha(); + + console.log(`Resolved commit SHA: ${config.currentCommit}`); + + // Perform indexing + const result = await manager.index(); + + // Print results + console.log("\n=== Indexing Results ==="); + console.log(`Success: ${result.success}`); + console.log(`Type: ${result.type}`); + console.log(`Files Indexed: ${result.filesIndexed}`); + console.log(`Files Deleted: ${result.filesDeleted}`); + console.log(`Checkpoint ID: ${result.checkpointId}`); + console.log(`Commit SHA: ${result.commitSha}`); + + if (result.reindexReason) { + console.log(`Re-index Reason: ${result.reindexReason}`); + } + + if (result.error) { + console.error(`Error: ${result.error}`); + process.exit(1); + } + + // Set GitHub Actions output + if (process.env.GITHUB_OUTPUT) { + const fs = await import("node:fs"); + const output = [ + `success=${result.success}`, + `type=${result.type}`, + `files_indexed=${result.filesIndexed}`, + `files_deleted=${result.filesDeleted}`, + `checkpoint_id=${result.checkpointId}`, + `commit_sha=${result.commitSha}`, + ].join("\n"); + + await fs.promises.appendFile(process.env.GITHUB_OUTPUT, `${output}\n`); + } + + console.log("\nIndexing completed successfully!"); + } catch (error) { + console.error("Fatal error:", error); + process.exit(1); + } +} + +// Run main function +main(); diff --git a/examples/typescript-sdk/context/github-action-indexer/src/search.ts b/examples/typescript-sdk/context/github-action-indexer/src/search.ts new file mode 100644 index 0000000..70c6c28 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/src/search.ts @@ -0,0 +1,169 @@ +#!/usr/bin/env node +/** + * CLI tool to search the indexed repository + * + * Usage: + * npm run search "your search query" + * tsx src/search.ts "your search query" + */ + +import { promises as fs } from "node:fs"; +import { DirectContext } from "@augmentcode/auggie-sdk"; +import type { IndexState } from "./types.js"; + +/** + * Get the state file path for the current branch + */ +function getStatePath(): string { + const branch = process.env.BRANCH || "main"; + const sanitizedBranch = branch.replace(/[^a-zA-Z0-9-_]/g, "-"); + return ( + process.env.STATE_PATH || + `.augment-index-state/${sanitizedBranch}/state.json` + ); +} + +/** + * Load index state from file system + */ +async function loadState(statePath: string): Promise { + try { + const data = await fs.readFile(statePath, "utf-8"); + return JSON.parse(data) as IndexState; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } + throw error; + } +} + +/** + * Main search function + */ +async function main(): Promise { + // Parse command line arguments + const args = process.argv.slice(2); + let query: string | undefined; + let maxOutputLength: number | undefined; + + // Parse arguments + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--max-chars") { + const maxCharsValue = args[i + 1]; + if (!maxCharsValue || Number.isNaN(Number(maxCharsValue))) { + console.error("Error: --max-chars requires a numeric value"); + console.error('Example: npm run search "query" --max-chars 5000'); + process.exit(1); + } + maxOutputLength = Number.parseInt(maxCharsValue, 10); + i++; // Skip the next argument since we consumed it + } else if (!query) { + query = arg; + } + } + + if (!query) { + console.error("Usage: npm run search [--max-chars ]"); + console.error('Example: npm run search "authentication functions"'); + console.error( + 'Example: npm run search "authentication functions" --max-chars 5000' + ); + process.exit(1); + } + + // Get API token + const apiTokenEnv = process.env.AUGMENT_API_TOKEN; + if (!apiTokenEnv) { + console.error("Error: AUGMENT_API_TOKEN environment variable is required"); + process.exit(1); + } + + // Parse API token - it can be either a JSON object or a plain string + let apiToken: string; + let apiUrl: string | undefined = process.env.AUGMENT_API_URL; + + try { + const tokenObj = JSON.parse(apiTokenEnv) as { + accessToken?: string; + tenantURL?: string; + }; + if (tokenObj.accessToken) { + apiToken = tokenObj.accessToken; + // Use tenantURL from token if not overridden by env var + if (!apiUrl && tokenObj.tenantURL) { + apiUrl = tokenObj.tenantURL; + } + } else { + apiToken = apiTokenEnv; + } + } catch { + // Not JSON, use as-is + apiToken = apiTokenEnv; + } + + if (!apiUrl) { + console.error( + "Error: AUGMENT_API_URL environment variable is required. Please set it to your tenant-specific URL (e.g., 'https://your-tenant.api.augmentcode.com') or include tenantURL in your API token JSON." + ); + process.exit(1); + } + + console.log(`Searching for: "${query}"`); + if (maxOutputLength !== undefined) { + console.log(`Limiting results to max ${maxOutputLength} characters\n`); + } else { + console.log(); + } + + try { + // Load the index state first + const statePath = getStatePath(); + console.log(`Loading index state from: ${statePath}`); + const state = await loadState(statePath); + + if (!state) { + console.error("Error: No index state found. Run indexing first."); + console.error(" npm run index"); + process.exit(1); + } + + // Create a temporary file with the context state for import + const tempStateFile = `/tmp/github-indexer-state-${Date.now()}.json`; + await fs.writeFile(tempStateFile, JSON.stringify(state.contextState, null, 2)); + + // Import state using DirectContext.importFromFile + const context = await DirectContext.importFromFile(tempStateFile, { apiKey: apiToken, apiUrl }); + + // Clean up temporary file + await fs.unlink(tempStateFile); + + const fileCount = state.contextState.blobs + ? state.contextState.blobs.length + : 0; + + console.log(`Loaded index: ${fileCount} files indexed`); + console.log( + `Repository: ${state.repository.owner}/${state.repository.name}` + ); + console.log(`Last indexed commit: ${state.lastCommitSha}\n`); + + // Perform search with optional character limit + // search returns a formatted string ready for display or LLM use + const results = await context.search(query, { maxOutputLength }); + + if (!results || results.trim().length === 0) { + console.log("No results found."); + return; + } + + console.log("Search results:\n"); + console.log(results); + } catch (error) { + console.error("Search failed:", error); + process.exit(1); + } +} + +main(); diff --git a/examples/typescript-sdk/context/github-action-indexer/src/types.ts b/examples/typescript-sdk/context/github-action-indexer/src/types.ts new file mode 100644 index 0000000..28c01c9 --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/src/types.ts @@ -0,0 +1,95 @@ +/** + * Types for the GitHub Action Indexer + */ + +import type { DirectContextState } from "@augmentcode/auggie-sdk"; + +export type IndexState = { + /** DirectContext state (checkpoint, blobs, etc.) */ + contextState: DirectContextState; + + /** Last indexed commit SHA (must be a full 40-character SHA, not a ref like "HEAD") */ + lastCommitSha: string; + + /** Repository information - used to verify we're indexing the same repository */ + repository: { + owner: string; + name: string; + }; +}; + +export type FileChange = { + /** File path */ + path: string; + + /** Change status: added, modified, removed, renamed */ + status: "added" | "modified" | "removed" | "renamed"; + + /** Previous filename (for renames) */ + previousFilename?: string; + + /** File contents (for added/modified files) */ + contents?: string; + + /** Blob name from previous index (for modified/removed files) */ + oldBlobName?: string; +}; + +export type IndexConfig = { + /** Augment API token */ + apiToken: string; + + /** + * Augment API URL + * Can be provided via AUGMENT_API_URL env var, or extracted from + * a JSON-formatted AUGMENT_API_TOKEN that includes tenantURL field + */ + apiUrl: string; + + /** GitHub token */ + githubToken: string; + + /** Repository owner */ + owner: string; + + /** Repository name */ + repo: string; + + /** Branch to index */ + branch: string; + + /** Current commit SHA */ + currentCommit: string; + + /** Maximum commits before full re-index */ + maxCommits?: number; + + /** Maximum file changes before full re-index */ + maxFiles?: number; +}; + +export type IndexResult = { + /** Whether indexing was successful */ + success: boolean; + + /** Type of indexing performed */ + type: "full" | "incremental" | "no-changes"; + + /** Number of files indexed */ + filesIndexed: number; + + /** Number of files deleted */ + filesDeleted: number; + + /** New checkpoint ID */ + checkpointId: string; + + /** Commit SHA that was indexed */ + commitSha: string; + + /** Error message if failed */ + error?: string; + + /** Reason for full re-index (if applicable) */ + reindexReason?: string; +}; diff --git a/examples/typescript-sdk/context/github-action-indexer/tsconfig.json b/examples/typescript-sdk/context/github-action-indexer/tsconfig.json new file mode 100644 index 0000000..3b952fc --- /dev/null +++ b/examples/typescript-sdk/context/github-action-indexer/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/typescript-sdk/context/package.json b/examples/typescript-sdk/context/package.json new file mode 100644 index 0000000..1bbffc8 --- /dev/null +++ b/examples/typescript-sdk/context/package.json @@ -0,0 +1,15 @@ +{ + "name": "@augment-examples/context", + "version": "1.0.0", + "type": "module", + "dependencies": { + "@augmentcode/auggie-sdk": "^0.1.9", + "tsx": "^4.19.2" + }, + "scripts": { + "direct-context": "tsx direct-context/index.ts", + "filesystem-context": "tsx filesystem-context/index.ts", + "file-search-server": "tsx file-search-server/index.ts", + "prompt-enhancer-server": "tsx prompt-enhancer-server/index.ts" + } +} diff --git a/examples/typescript-sdk/context/prompt-enhancer-server/README.md b/examples/typescript-sdk/context/prompt-enhancer-server/README.md new file mode 100644 index 0000000..d33015e --- /dev/null +++ b/examples/typescript-sdk/context/prompt-enhancer-server/README.md @@ -0,0 +1,41 @@ +# Prompt Enhancer Server Example + +HTTP server that enhances vague prompts using AI with codebase context. + +## Prerequisites + +Install the `auggie` CLI and authenticate: +```bash +npm install -g @augmentcode/auggie +auggie login +``` + +## Usage + +```bash +# Start server with workspace directory +npx tsx examples/context/prompt-enhancer-server/index.ts . +``` + +## API Endpoints + +### Enhance Prompt +```bash +curl -X POST http://localhost:3001/enhance \ + -H "Content-Type: application/json" \ + -d '{"prompt": "fix the bug"}' +``` + +Response: +```json +{ + "original": "fix the bug", + "enhanced": "Fix the bug in the authentication system. Specifically, investigate the login function..." +} +``` + +### Health Check +```bash +curl "http://localhost:3001/health" +``` + diff --git a/examples/typescript-sdk/context/prompt-enhancer-server/enhance-handler.ts b/examples/typescript-sdk/context/prompt-enhancer-server/enhance-handler.ts new file mode 100644 index 0000000..a6686bc --- /dev/null +++ b/examples/typescript-sdk/context/prompt-enhancer-server/enhance-handler.ts @@ -0,0 +1,49 @@ +import type { FileSystemContext } from "@augmentcode/auggie-sdk"; +import { parseEnhancedPrompt } from "./response-parser"; + +export type EnhanceResponse = { + original: string; + enhanced: string; +}; + +/** + * Handle prompt enhancement request using searchAndAsk + * + * Uses the FileSystemContext to search for relevant code and enhance the prompt + * with codebase context. + */ +export async function handleEnhance( + prompt: string, + context: FileSystemContext +): Promise { + console.log(`\n[${new Date().toISOString()}] Enhancing prompt: "${prompt}"`); + + // Build the enhancement instruction + const enhancementPrompt = + "Here is an instruction that I'd like to give you, but it needs to be improved. " + + "Rewrite and enhance this instruction to make it clearer, more specific, " + + "less ambiguous, and correct any mistakes. " + + "If there is code in triple backticks (```) consider whether it is a code sample and should remain unchanged. " + + "Reply with the following format:\n\n" + + "### BEGIN RESPONSE ###\n" + + "Here is an enhanced version of the original instruction that is more specific and clear:\n" + + "enhanced prompt goes here\n\n" + + "### END RESPONSE ###\n\n" + + "Here is my original instruction:\n\n" + + prompt; + + // Use searchAndAsk to get the enhancement with relevant codebase context + // The original prompt is used as the search query to find relevant code + const response = await context.searchAndAsk(prompt, enhancementPrompt); + + // Parse the enhanced prompt from the response + const enhanced = parseEnhancedPrompt(response); + if (!enhanced) { + throw new Error("Failed to parse enhanced prompt from response"); + } + + return { + original: prompt, + enhanced, + }; +} diff --git a/examples/typescript-sdk/context/prompt-enhancer-server/index.ts b/examples/typescript-sdk/context/prompt-enhancer-server/index.ts new file mode 100644 index 0000000..f33d6bb --- /dev/null +++ b/examples/typescript-sdk/context/prompt-enhancer-server/index.ts @@ -0,0 +1,142 @@ +#!/usr/bin/env node +/** + * Prompt Enhancer Server Sample + * + * An HTTP server that enhances user prompts using the Augment Generation API. + * This demonstrates how to use the actual prompt enhancer template from beachhead + * along with the generation API to intelligently improve user prompts. + * + * The prompt enhancer: + * 1. Takes a user's prompt + * 2. Uses the beachhead prompt template to create an enhancement request + * 3. Calls the generation API to enhance the prompt + * 4. Parses the enhanced prompt from the AI response + * 5. Returns the improved, more specific prompt + * + * Usage: + * npm run build + * node dist/samples/prompt-enhancer-server.js [workspace-directory] + * + * Or with tsx: + * npx tsx samples/prompt-enhancer-server/index.ts [workspace-directory] + * + * Then use curl to enhance prompts: + * curl -X POST http://localhost:3001/enhance \ + * -H "Content-Type: application/json" \ + * -d '{"prompt": "fix the bug"}' + */ + +import { createServer } from "node:http"; +import { FileSystemContext } from "@augmentcode/auggie-sdk"; +import { handleEnhance } from "./enhance-handler"; + +const PORT = 3001; +const workspaceDir = process.argv[2] || process.cwd(); + +console.log("=== Prompt Enhancer Server ===\n"); +console.log(`Workspace directory: ${workspaceDir}`); +console.log(`Starting server on port ${PORT}...\n`); + +// Create FileSystem Context +let context: FileSystemContext; + +async function initializeContext() { + console.log("Initializing FileSystem Context..."); + context = await FileSystemContext.create({ + directory: workspaceDir, + debug: false, + }); + console.log("FileSystem Context initialized\n"); +} + +// HTTP request handler +const server = createServer(async (req, res) => { + // Set CORS headers + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); + res.setHeader("Access-Control-Allow-Headers", "Content-Type"); + + if (req.method === "OPTIONS") { + res.writeHead(200); + res.end(); + return; + } + + const url = new URL(req.url || "/", `http://localhost:${PORT}`); + + if (url.pathname === "/enhance" && req.method === "POST") { + let body = ""; + req.on("data", (chunk) => { + body += chunk.toString(); + }); + + req.on("end", async () => { + try { + const data = JSON.parse(body); + const prompt = data.prompt; + + if (!prompt || typeof prompt !== "string") { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: "Missing or invalid 'prompt' field", + }) + ); + return; + } + + const result = await handleEnhance(prompt, context); + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(result, null, 2)); + } catch (error) { + console.error("Enhancement error:", error); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }) + ); + } + }); + } else if (url.pathname === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + status: "ok", + workspace: workspaceDir, + }) + ); + } else { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); + } +}); + +// Initialize and start server +initializeContext() + .then(() => { + server.listen(PORT, () => { + console.log(`✅ Server running at http://localhost:${PORT}/`); + console.log("\nExample requests:"); + console.log( + ` curl -X POST http://localhost:${PORT}/enhance -H "Content-Type: application/json" -d '{"prompt": "fix the bug"}'` + ); + console.log(` curl "http://localhost:${PORT}/health"`); + console.log("\nPress Ctrl+C to stop\n"); + }); + }) + .catch((error) => { + console.error("Failed to initialize:", error); + process.exit(1); + }); + +// Cleanup on exit +process.on("SIGINT", async () => { + console.log("\n\nShutting down..."); + if (context) { + await context.close(); + } + server.close(); + process.exit(0); +}); diff --git a/examples/typescript-sdk/context/prompt-enhancer-server/response-parser.ts b/examples/typescript-sdk/context/prompt-enhancer-server/response-parser.ts new file mode 100644 index 0000000..f274cdd --- /dev/null +++ b/examples/typescript-sdk/context/prompt-enhancer-server/response-parser.ts @@ -0,0 +1,17 @@ +// Regex for extracting enhanced prompt from AI response +const ENHANCED_PROMPT_REGEX = + /([\s\S]*?)<\/enhanced-prompt>/; + +/** + * Parse the enhanced prompt from the AI response + */ +export function parseEnhancedPrompt(response: string): string | null { + // Extract content between tags + const match = response.match(ENHANCED_PROMPT_REGEX); + + if (match?.[1]) { + return match[1].trim(); + } + + return null; +}