TypeScript
The @grepvine/sdk package covers the whole API with one client, GrepvineClient, whose methods return promises. Indexing and searching show each call in every client. This page covers what is specific to TypeScript.
Install.
Section titled “Install.”npm install @grepvine/sdkThe package is ESM only, ships type declarations, and runs on Node 22.18 or later and in modern browsers. Import everything from the package root. Code in a browser page exposes any API key it holds to whoever loads the page, so call Grepvine from a server unless each user brings their own key.
Create a client.
Section titled “Create a client.”import { GrepvineClient } from "@grepvine/sdk";
const client = new GrepvineClient({ apiKey: process.env.GREPVINE_API_KEY! });
const index = await client.indexes.get({ organization: "acme", index: "support" });console.log(index.document_count);Every option is optional:
| Option | Default | Meaning |
|---|---|---|
apiKey |
None | Sent as a bearer token. Without one, only device login and account sign-in work. |
endpoint |
https://api.grepvine.ai |
The service URL, with or without /v1. |
timeout |
60_000 |
Milliseconds per attempt of a request. |
retry |
new RetryPolicy() |
How failed requests are retried. See retries. |
fetch |
globalThis.fetch |
The fetch requests go through, such as a fake one in tests. |
- Pass configuration in. The package reads no environment variables or configuration files.
- Share one client. Any number of concurrent calls can use the same client, and it holds nothing that needs closing.
- Pass methods around. Methods keep their client when detached, so
paginate(client.search.query, ...)andconst query = client.search.querywork.
Make calls.
Section titled “Make calls.”The client’s resources follow the API: organizations (with organizations.members), indexes, documents, search, apiKeys, account and deviceLogin. Each method takes one object holding all of the operation’s parameters, whether they belong in the path, the query or the body:
const page = await client.search.query({ organization: "acme", index: "support", pattern: "connection reset by peer", mode: "literal", case_sensitive: false, amount: 100,});for (const hit of page.results) console.log(hit.key, hit.line_number);- API names. Parameters and responses use the API’s field names, such as
lines_afteranddocument_count. The helpers the SDK adds,putMany,waitUntilReadyanddeviceLogin.authorize, take camelCase options such asidempotencyKeyandifNoneMatch. - Headers. A parameter sent as a header keeps the header’s name:
Rangeondocuments.get, and"Idempotency-Key"and"If-Match"on writes. - Types. Models and per-operation types are exported under their API names, such as
QueryResult,MutationResult,IndexandListIndexesData. - Absent fields are
undefined. Writehit.lines_after ?? [].
Page through results.
Section titled “Page through results.”Each list method and search.query returns one page. paginate(method, parameters) fetches pages as the loop asks for them and yields every result:
import { paginate } from "@grepvine/sdk";
for await (const document of paginate(client.documents.list, { organization: "acme", index: "support", prefix: "tickets/2026/10/",})) { console.log(document.key, document.content_bytes);}To page by hand, pass page: page.pagination.next_page_id with the same parameters, including amount, until page.pagination.has_next is false. A page token lasts 15 minutes. An expired or mismatched token throws InvalidPageError.
Cancel a call.
Section titled “Cancel a call.”Every method takes a second argument of request options, and paginate takes them as its third. Pass signal to cancel a call:
const index = await client.indexes.get( { organization: "acme", index: "support" }, { signal: AbortSignal.timeout(5_000) },);timeout limits each attempt, and the client retries after a timed-out attempt. AbortSignal.timeout limits the whole call, retries and 429 pauses included. A cancelled call throws the signal’s reason unchanged, such as a DOMException named TimeoutError or AbortError, so it isn’t a GrepvineError.
Handle errors.
Section titled “Handle errors.”Every error the SDK throws is a GrepvineError. An error answer from the service throws an APIError subclass with statusCode, code, message, requestId and retryAfter. code is the API’s error code, such as invalid_filter, and String(error) includes the code and the request ID.
| Error | Status | Thrown when |
|---|---|---|
InvalidRequestError |
400 | The input was rejected, such as a pattern or filter that doesn’t parse. Its subclass InvalidPageError is for page tokens. |
AuthenticationError |
401 | The API key or session is missing, wrong or revoked. |
PermissionDeniedError |
403 | The key’s role doesn’t allow the action. |
NotFoundError |
404 | The organization, index or document doesn’t exist. |
ConflictError |
409 | The request conflicts with the current state, such as creating an index that exists. |
PreconditionFailedError |
412 | A condition failed on documents.put or documents.delete. putMany and the batch methods report it per document instead. |
PayloadTooLargeError |
413 | A document or batch is over its size limit. |
RangeNotSatisfiableError |
416 | A byte range doesn’t overlap the document. |
RateLimitedError |
429 | The client waited out Retry-After for longer than its retry policy allows. |
ServerError |
5xx | The service failed on every attempt. |
Three errors don’t come from a response. TransportError means the request couldn’t be delivered, or its last attempt timed out. ConfigurationError means the endpoint isn’t an http or https URL. PartialBatchError comes from putMany and is described in indexing.
This creates an index only if it doesn’t exist yet:
import { ConflictError } from "@grepvine/sdk";
try { await client.indexes.create({ organization: "acme", name: "support" });} catch (error) { if (!(error instanceof ConflictError)) throw error;}await client.indexes.waitUntilReady({ organization: "acme", index: "support" });Retries.
Section titled “Retries.”The client retries on its own:
429pauses every request of the client for theRetry-Afterinterval, varied by up to 25% either way, at most 60 seconds at a time. Once one request has been paused for 600 seconds in total, it throwsRateLimitedError.5xxand connection failures are retried for up to 3 attempts in total, after 100 and then 200 milliseconds.- Document writes carry an
Idempotency-Keythat stays the same across attempts, so a retried write is applied once. Pass your own as the"Idempotency-Key"parameter.
An agent answering a person shouldn’t wait ten minutes for a search. Shorten the policy for interactive use:
import { GrepvineClient, RetryPolicy } from "@grepvine/sdk";
const client = new GrepvineClient({ apiKey: process.env.GREPVINE_API_KEY!, timeout: 10_000, retry: new RetryPolicy({ maxAttempts: 5, maxThrottledWait: 30_000 }),});RetryPolicy also takes backoff, defaultRetryAfter, maxRetryAfter and retryAfterJitter. Its durations are in milliseconds.
Common patterns.
Section titled “Common patterns.”Give an agent a search tool.
Section titled “Give an agent a search tool.”The function below is the body of a search tool. It takes a pattern and an optional path prefix, caps the number of matches, and returns grep-style text with two lines of context. A rejected pattern or filter returns the service’s message, such as filter byte 9: expected a value, so the model can correct its query on the next turn. Pass the agent’s signal through, so a cancelled turn stops paging. Register the function with your agent framework as a tool.
import { InvalidRequestError, paginate, type QueryResult } from "@grepvine/sdk";
function formatHit(hit: QueryResult): string { const context = (lines: QueryResult["lines_before"]) => (lines ?? []).map((line) => `${hit.key}-${line.line_number}- ${line.text}`); return [ ...context(hit.lines_before), `${hit.key}:${hit.line_number}: ${hit.text}`, ...context(hit.lines_after), ].join("\n");}
export async function searchCode( { pattern, pathPrefix = "", maxResults = 50 }: { pattern: string; pathPrefix?: string; maxResults?: number }, signal?: AbortSignal,): Promise<string> { const filter = pathPrefix ? `path STARTSWITH '${pathPrefix.replaceAll("'", "''")}'` : undefined; const found: string[] = []; try { for await (const hit of paginate( client.search.query, { organization: "acme", index: "monorepo", pattern, filter, lines_before: 2, lines_after: 2 }, { signal }, )) { found.push(formatHit(hit)); if (found.length === maxResults) break; } } catch (error) { if (error instanceof InvalidRequestError) return `The search was rejected: ${error.message}`; throw error; } return found.join("\n--\n") || "No matches.";}Doubling single quotes keeps a prefix such as docs/o'brien/ from breaking the filter. Best practices explains the cap.
Write batches in parallel.
Section titled “Write batches in parallel.”putMany sends its requests one after another, and it goes through every document before it sends the first one. For a large import, split the documents into slices and let a few workers take slices from a shared iterator:
import type { DocumentInput, MutationResult } from "@grepvine/sdk";
const documents: DocumentInput[] = await loadTickets(); // your loader: { key, content, metadata }[]const slices = Array.from({ length: Math.ceil(documents.length / 2048) }, (_, n) => documents.slice(n * 2048, (n + 1) * 2048),);
const results: MutationResult[][] = [];const queue = slices.entries();await Promise.all( Array.from({ length: 8 }, async () => { for (const [n, part] of queue) { results[n] = await client.documents.putMany({ organization: "acme", index: "support", documents: part, idempotencyKey: `support-import-2026-10-14-${n}`, }); } }),);A suffix per slice keeps each idempotency key stable when you rerun the import. results.flat() lists one result per document in input order. Check each result’s error as in indexing. For a single directory, gv upload does the same with 16 batches in flight.
Search for several patterns at once.
Section titled “Search for several patterns at once.”Independent searches run concurrently on one client. This counts possible leaked credentials of three kinds:
const patterns = [ String.raw`AKIA[0-9A-Z]{16}`, String.raw`-----BEGIN [A-Z ]*PRIVATE KEY-----`, String.raw`xox[bp]-[0-9A-Za-z-]{10,}`,];const counts = await Promise.all( patterns.map(async (pattern) => { const hits = paginate(client.search.query, { organization: "acme", index: "monorepo", pattern }); return (await Array.fromAsync(hits)).length; }),);patterns.forEach((pattern, i) => console.log(String(counts[i]).padStart(5), pattern));A 429 on one search pauses the others too, so they slow down together.
Read the matched text.
Section titled “Read the matched text.”Offsets in a result count bytes, and JavaScript strings index by UTF-16 code unit. Encode text before you slice it, or a match after a non-ASCII character comes out shifted:
const encoder = new TextEncoder();const decoder = new TextDecoder();
function matchedText(hit: QueryResult): string[] { const bytes = encoder.encode(hit.text); return hit.matches.map((match) => { const start = match.offset - hit.text_start_offset; return decoder.decode(bytes.subarray(start, start + match.length)); });}documents.get returns a Blob holding the whole response. Pass Range to read part of a document, as searching shows.
Test without the service.
Section titled “Test without the service.”Pass a fake fetch to answer requests in-process. It receives a Request:
import { test } from "node:test";import assert from "node:assert/strict";import { GrepvineClient } from "@grepvine/sdk";
test("search without matches", async () => { const client = new GrepvineClient({ apiKey: "gvk_test", fetch: async (input) => { const request = input as Request; assert.equal(new URL(request.url).pathname, "/v1/organizations/acme/indexes/monorepo/query"); return Response.json({ results: [], pagination: { next_page_id: "", has_next: false } }); }, }); const page = await client.search.query({ organization: "acme", index: "monorepo", pattern: "OrderNotFound" }); assert.deepEqual(page.results, []);});Error answers from the fake throw the same errors as real ones. A 429 or 5xx answer is retried, so a test of either should pass retry: new RetryPolicy({ maxAttempts: 1, maxThrottledWait: 0 }) to fail at once.
API reference.
Section titled “API reference.”The full reference is generated from the package: grepvine-ai.github.io/grepvine-typescript-sdk.