Skip to main content
Experimental API - Context Connectors is experimental and subject to breaking changes.
Create custom storage backends to save and load DirectContext state from local filesystem, S3, or any storage system.

Local Filesystem Store

import { promises as fs } from "node:fs";
import { join } from "node:path";
import type { IndexStore, IndexState, IndexStateSearchOnly } from "@augmentcode/context-connectors";

class LocalStore implements IndexStore {
  constructor(private basePath: string = "./indexes") {}

  async save(key: string, fullState: IndexState, searchState: IndexStateSearchOnly): Promise<void> {
    const indexDir = join(this.basePath, key);
    await fs.mkdir(indexDir, { recursive: true });

    // Full state for incremental indexing
    await fs.writeFile(join(indexDir, "state.json"), JSON.stringify(fullState, null, 2));
    // Search-optimized state (smaller)
    await fs.writeFile(join(indexDir, "search.json"), JSON.stringify(searchState, null, 2));
  }

  async loadState(key: string): Promise<IndexState | null> {
    try {
      const data = await fs.readFile(join(this.basePath, key, "state.json"), "utf-8");
      return JSON.parse(data);
    } catch {
      return null;
    }
  }

  async loadSearch(key: string): Promise<IndexState | null> {
    try {
      const data = await fs.readFile(join(this.basePath, key, "search.json"), "utf-8");
      return JSON.parse(data);
    } catch {
      return null;
    }
  }

  async delete(key: string): Promise<void> {
    await fs.rm(join(this.basePath, key), { recursive: true, force: true });
  }

  async list(): Promise<string[]> {
    const entries = await fs.readdir(this.basePath, { withFileTypes: true });
    return entries.filter(e => e.isDirectory()).map(e => e.name);
  }
}

S3 Store

import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import type { IndexStore, IndexState, IndexStateSearchOnly } from "@augmentcode/context-connectors";

class S3Store implements IndexStore {
  private client: S3Client;

  constructor(
    private bucket: string,
    private prefix = "context-connectors/"
  ) {
    this.client = new S3Client({});
  }

  async save(key: string, fullState: IndexState, searchState: IndexStateSearchOnly): Promise<void> {
    await Promise.all([
      this.client.send(new PutObjectCommand({
        Bucket: this.bucket,
        Key: `${this.prefix}${key}/state.json`,
        Body: JSON.stringify(fullState, null, 2),
        ContentType: "application/json",
      })),
      this.client.send(new PutObjectCommand({
        Bucket: this.bucket,
        Key: `${this.prefix}${key}/search.json`,
        Body: JSON.stringify(searchState, null, 2),
        ContentType: "application/json",
      })),
    ]);
  }

  async loadState(key: string): Promise<IndexState | null> {
    try {
      const response = await this.client.send(new GetObjectCommand({
        Bucket: this.bucket,
        Key: `${this.prefix}${key}/state.json`,
      }));
      const body = await response.Body?.transformToString();
      return body ? JSON.parse(body) : null;
    } catch {
      return null;
    }
  }

  async loadSearch(key: string): Promise<IndexState | null> {
    try {
      const response = await this.client.send(new GetObjectCommand({
        Bucket: this.bucket,
        Key: `${this.prefix}${key}/search.json`,
      }));
      const body = await response.Body?.transformToString();
      return body ? JSON.parse(body) : null;
    } catch {
      return null;
    }
  }

  // ... delete() and list() implementations
}

// Usage
const store = new S3Store("my-bucket");
Built-in S3 Store: The @augmentcode/context-connectors package includes a built-in S3Store class. Use it directly:
import { S3Store } from "@augmentcode/context-connectors";
const store = new S3Store({ bucket: "my-bucket" });

Other Storage Examples

Redis:
import { createClient } from "redis";
import type { IndexStore, IndexState, IndexStateSearchOnly } from "@augmentcode/context-connectors";

class RedisStore implements IndexStore {
  constructor(private redis: ReturnType<typeof createClient>) {}

  async save(key: string, fullState: IndexState, searchState: IndexStateSearchOnly): Promise<void> {
    await Promise.all([
      this.redis.set(`context:${key}:state`, JSON.stringify(fullState)),
      this.redis.set(`context:${key}:search`, JSON.stringify(searchState)),
    ]);
  }

  async loadState(key: string): Promise<IndexState | null> {
    const data = await this.redis.get(`context:${key}:state`);
    return data ? JSON.parse(data) : null;
  }

  async loadSearch(key: string): Promise<IndexState | null> {
    const data = await this.redis.get(`context:${key}:search`);
    return data ? JSON.parse(data) : null;
  }
  // ... delete() and list() implementations
}

Index File Layout

Each index consists of two files:
FilePurposeSizeUsed By
state.jsonFull state with file pathsLargerIndexers (for incremental updates)
search.jsonSearch-optimized stateSmallerSearch clients
The search.json file excludes the blobs array (list of indexed file paths), making it much smaller for search-only use cases. Indexers need the full state.json to determine which files have changed for incremental updates.

Next Steps