Skip to content

Repository files navigation

Seclai JavaScript SDK

The official JavaScript/TypeScript SDK for the Seclai API. Provides full typed coverage of all API endpoints, file uploads, SSE streaming, polling helpers, and automatic pagination.

Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and any runtime with a fetch implementation.

Install

npm install @seclai/sdk

Quick start

import { Seclai } from "@seclai/sdk";

const client = new Seclai({ apiKey: process.env.SECLAI_API_KEY });

// List all sources
const sources = await client.listSources();

// Run an agent and stream the result
const result = await client.runStreamingAgentAndWait(
  "agent_id",
  { input: "Summarize the latest uploads", metadata: {} },
  { timeoutMs: 120_000 },
);
console.log(result);

Configuration

Option Environment variable Default
apiKey SECLAI_API_KEY
accessToken
profile SECLAI_PROFILE "default"
configDir SECLAI_CONFIG_DIR ~/.seclai
autoRefresh true
accountId
baseUrl SECLAI_API_URL https://api.seclai.com
apiKeyHeader x-api-key
defaultHeaders {}
fetch globalThis.fetch

Authentication

Credentials are resolved via a chain (first match wins):

  1. Explicit apiKey option
  2. Explicit accessToken option (string or () => string | Promise<string>)
  3. SECLAI_API_KEY environment variable
  4. SSO — cached tokens from ~/.seclai/sso/cache/ (requires a prior seclai auth login)
// API key
const client = new Seclai({ apiKey: "sk-..." });
// Static bearer token
const client = new Seclai({ accessToken: "eyJhbGciOi..." });
// Dynamic bearer token provider (called per request)
const client = new Seclai({
  accessToken: async () => fetchTokenFromVault(),
});
// SSO profile (uses cached tokens, auto-refreshes)
const client = new Seclai({ profile: "my-profile" });
// Environment variable (no options needed)
// export SECLAI_API_KEY="sk-..."
const client = new Seclai();

SSO authentication

SSO is the default fallback when no explicit credentials are provided. The SDK includes built-in production SSO defaults, so seclai configure sso is not required. You only need to log in once to populate the token cache:

npx @seclai/cli auth login    # authenticate via browser — no prior setup needed

To customize SSO settings (e.g. for a staging environment), use seclai configure sso or set environment variables:

Variable Description Default
SECLAI_SSO_DOMAIN Cognito domain auth.seclai.com
SECLAI_SSO_CLIENT_ID Cognito app client ID 4bgf8v9qmc5puivbaqon9n5lmr
SECLAI_SSO_REGION AWS region us-west-2

API documentation

Online API documentation (latest):

https://seclai.github.io/seclai-javascript/latest/

Release history is in CHANGELOG.md.

API versioning

The API dates its backward-incompatible changes. Nothing changes for you until you opt in, either per client or by pinning the account:

const client = new Seclai({
  apiKey: "...",
  apiVersion: SeclaiApiVersion.V2026_07_27, // sent as the Seclai-Version header
});

const state = await client.getApiVersion();      // what this request resolved to
await client.updateApiVersion(SeclaiApiVersion.V2026_07_27); // pin the whole account

Leave apiVersion unset and the header is omitted, so the account's pinned baseline applies and responses keep their current shapes. Upgrading this package alone never changes the wire contract.

Known versions are on SeclaiApiVersion (V2026_07_01, V2026_07_27, plus Default and Latest), imported from @seclai/sdk. A version this release was not built against throws at construction: a newer version can reshape responses, and this client would decode them incorrectly rather than reject them. Upgrade the package to adopt a new version, or set allowUnknownApiVersion if you have to move first and accept that risk.

The guard only covers the header. An account pinned server-side can still be newer than this release — getApiVersion() reports the effective_version the request resolved to, and comparing it against SeclaiApiVersion.Latest is how you detect the gap.

What 2026-07-27 changes. Undeclared query parameters become a 422 instead of being ignored, and list endpoints move to the canonical {data, pagination} envelope. The affected methods read both shapes, so they keep working either way — but the metadata moves:

Method Before From 2026-07-27
listEvaluationCriteriaPage() bare array data + pagination
listRunEvaluationResults() bare array data + pagination
listAlertConfigs() configs + total data + pagination
listModelAlerts() alerts + total data + pagination

Prefer pagination over the flat total/page/limit properties, and read the last two with res.data ?? res.configs / res.data ?? res.alerts. The legacy keys will be deprecated and then removed once the canonical envelope is the default.

Resources

Identity

Discover who the credential belongs to and which organizations it can act for. Each organization's account_id is what you pass as the client's accountId option (sent as the X-Account-Id header) to target that org context.

const me = await client.getMe();
console.log(me.account_id);     // the user's personal account
for (const org of me.organizations) {
  console.log(org.name, org.account_id);
}

// Act as an organization
const orgClient = new Seclai({ accountId: me.organizations[0].account_id });

Agents

// CRUD
const agents = await client.listAgents({ page: 1, limit: 20 });
const agent = await client.createAgent({ name: "My Agent", description: "..." });
const fetched = await client.getAgent("agent_id");
const updated = await client.updateAgent("agent_id", { name: "Renamed" });
await client.deleteAgent("agent_id");

// Pause / resume — a disabled agent stops firing from every trigger path
const callers = await client.getAgentCallers("agent_id"); // live agents calling this one
await client.disableAgent("agent_id"); // 409 if any caller above is still live
await client.enableAgent("agent_id");

// Definition (step workflow)
const def = await client.getAgentDefinition("agent_id");
await client.updateAgentDefinition("agent_id", { steps: [...], change_id: def.change_id });

// Export / import an agent
const exported = await client.exportAgent("agent_id");

// Validate the payload first to surface unresolved entity refs in this account
const preview = await client.previewImportAgent({ agent_definition: exported });
const unresolved = (preview.unresolved_refs ?? []) as Array<{ ref_id: string }>;
const entity_remap = Object.fromEntries(
  unresolved.map((ref) => [ref.ref_id, /* picked target uuid */ ""]),
);

// Commit — `entity_remap` substitutes workflow refs before save
const imported = await client.createAgent({
  name: "Imported",
  trigger_type: "dynamic_input",
  agent_definition: exported,
  entity_remap,
});
// `imported.import_warnings` lists any items that couldn't be applied.

Agent runs

// Start a run
const run = await client.runAgent("agent_id", { input: "Hello" });

// List & search runs
const runs = await client.listAgentRuns("agent_id", { status: "completed" });
const search = await client.searchAgentRuns({ agent_id: "...", status: ["completed"] });

// Fetch run details (optionally with step outputs)
const detail = await client.getAgentRun("run_id", { includeStepOutputs: true });

// Cancel or delete
await client.cancelAgentRun("run_id");
await client.deleteAgentRun("run_id");

Streaming

The SDK provides two streaming patterns over the SSE /runs/stream endpoint:

Block until done — returns the final done payload or throws on timeout:

const result = await client.runStreamingAgentAndWait(
  "agent_id",
  { input: "Hello", metadata: {} },
  { timeoutMs: 60_000 },
);

Async iterator — yields every SSE event as { event, data }:

for await (const event of client.runStreamingAgent(
  "agent_id",
  { input: "Hello" },
  { timeoutMs: 120_000 },
)) {
  console.log(event.event, event.data);
  if (event.event === "done") break;
}

Polling

For environments where SSE is not practical, poll for a completed run:

const result = await client.runAgentAndPoll(
  "agent_id",
  { input: "Hello" },
  { pollIntervalMs: 2_000, timeoutMs: 120_000 },
);

Agent input uploads

// Discover which files (if any) the agent expects before staging uploads
const refs = await client.getAgentAttachmentReferences("agent_id");
if (refs.requires_uploads) {
  // refs.agent lists the exact_names / indexes_max / patterns a run batch must satisfy
}

const upload = await client.uploadAgentInput("agent_id", {
  file: new Uint8Array([...]),
  fileName: "input.pdf",
});
const status = await client.getAgentInputUploadStatus("agent_id", upload.upload_id);

Agent run attachments

// Download a file emitted by a step in an agent run. The attachment_id is the
// URL-safe-base64 storage_key surfaced in run output manifests / webhooks.
const resp = await client.downloadAgentRunAttachment("run_id", "attachment_id");
const blob = await resp.blob(); // raw Response — stream or save the bytes

Agent email triggers

Configure the inbound address and handling rules on an agent's EMAIL_RECEIVED trigger. Omitted fields are left unchanged; null (or "" for alias) clears them.

const config = await client.setEmailTriggerConfig("agent_id", "trigger_id", {
  alias: "support",
  allowed_senders: ["example.com", "ops@partner.com"], // empty/null accepts any sender
  ignore_auto_generated: true,  // drop auto-replies/bulk mail to prevent loops
  require_sender_auth: true,    // require SPF or DMARC even on an open inbox
  queue_on_quota: false,        // park over-rate mail instead of failing it
});
console.log(config.email_addresses); // ["support.<accountID>@agent.seclai.com", ...]

Agent email governance

// Recipients who opted out of this account's agent emails
const optOuts = await client.listAgentEmailOptOuts({ agentId: "agent_id", limit: 50 });
await client.removeAgentEmailOptOut("optout_id"); // opt them back in

// Blocked inbound senders (owner/admin only)
const blocked = await client.listBlockedEmailSenders({ limit: 50, offset: 0 });
await client.blockEmailSender({ sender_email: "spam.example.com", match_type: "domain" });
await client.unblockEmailSender("blocked_id");

// Auto-block on a governance BLOCK: "disabled" | "input" | "input_and_output"
await client.setAutoBlockMode({ mode: "input_and_output" });

// Inbound emails discarded before running an agent
const rejections = await client.listInboundEmailRejections({ agentId: "agent_id" });

// Account-wide overload circuit breaker
const status = await client.getInboundEmailStatus(); // { paused, queued_backlog }
await client.cancelQueuedEmailRuns();  // fail all QUEUED (over-quota parked) runs
await client.resumeInboundEmail();     // one-shot override; re-arms if still overloaded

Agent AI assistant

const steps = await client.generateAgentSteps("agent_id", { user_input: "Build a RAG pipeline" });
const config = await client.generateStepConfig("agent_id", { step_type: "llm", user_input: "..." });

// Conversation history
const history = await client.getAgentAiConversationHistory("agent_id");
await client.markAgentAiSuggestion("agent_id", "conversation_id", { accepted: true });

Agent evaluations

const criteria = await client.listEvaluationCriteria("agent_id", { page: 1, limit: 50 });
// page/limit only take effect with apiVersion "2026-07-27" or later; the legacy
// response is unpaginated. listEvaluationCriteriaPage() returns the same items
// plus a `pagination` object when opted in.
const created = await client.createEvaluationCriteria("agent_id", { name: "Accuracy", ... });
const detail = await client.getEvaluationCriteria("criteria_id");
await client.updateEvaluationCriteria("criteria_id", { ... });
await client.deleteEvaluationCriteria("criteria_id");

// Test a draft
await client.testDraftEvaluation("agent_id", { criteria: { ... }, run_id: "..." });

// Results by criteria
const results = await client.listEvaluationResults("criteria_id");
const summary = await client.getEvaluationCriteriaSummary("criteria_id");
await client.createEvaluationResult("criteria_id", { ... });

// Results by run
const runResults = await client.listRunEvaluationResults("agent_id", "run_id");

// Non-manual evaluation summary
const nonManual = await client.getNonManualEvaluationSummary("agent_id");

// Compatible runs for a criteria
const compatible = await client.listCompatibleRuns("criteria_id");

Knowledge bases

const kbs = await client.listKnowledgeBases();
const kb = await client.createKnowledgeBase({ name: "Docs KB" });
const fetched = await client.getKnowledgeBase("kb_id");
await client.updateKnowledgeBase("kb_id", { name: "Renamed KB" });
await client.deleteKnowledgeBase("kb_id");

Memory banks

const banks = await client.listMemoryBanks();
const bank = await client.createMemoryBank({ name: "Chat Memory", type: "conversation" });
const fetched = await client.getMemoryBank("mb_id");
await client.updateMemoryBank("mb_id", { name: "Renamed" });
await client.deleteMemoryBank("mb_id");

// Stats & compaction
const stats = await client.getMemoryBankStats("mb_id");
await client.compactMemoryBank("mb_id");

// Test compaction
const test = await client.testMemoryBankCompaction("mb_id", { ... });
const standalone = await client.testCompactionPromptStandalone({ ... });

// Templates & agents
const templates = await client.listMemoryBankTemplates();
const agents = await client.getAgentsUsingMemoryBank("mb_id");

// AI assistant
const suggestion = await client.generateMemoryBankConfig({ user_input: "..." });
const history = await client.getMemoryBankAiLastConversation();
await client.acceptMemoryBankAiSuggestion("conv_id", { ... });

// Source management
await client.deleteMemoryBankSource("mb_id");

Sources

const sources = await client.listSources({ page: 1, limit: 20, order: "asc" });
const source = await client.createSource({ name: "My Source", ... });
const fetched = await client.getSource("source_id");
await client.updateSource("source_id", { name: "Renamed" });
await client.deleteSource("source_id");

File uploads

Upload a file to a source (max 200 MiB). The SDK infers MIME type from the file extension when mimeType is not provided.

import { readFile } from "node:fs/promises";

await client.uploadFileToSource("source_id", {
  file: await readFile("document.pdf"),
  fileName: "document.pdf",
  title: "Q4 Report",
  metadata: { department: "finance" },
});

Upload inline text:

await client.uploadInlineTextToSource("source_id", {
  text: "Hello, world!",
  title: "Greeting",
});

Replace a content version with a new file:

await client.uploadFileToContent("content_version_id", {
  file: await readFile("updated.pdf"),
  fileName: "updated.pdf",
  mimeType: "application/pdf",
});

Source exports

const exports = await client.listSourceExports("source_id");
const exp = await client.createSourceExport("source_id", { format: "json" });
const status = await client.getSourceExport("source_id", exp.id);
const estimate = await client.estimateSourceExport("source_id", {});
const response = await client.downloadSourceExport("source_id", exp.id);
await client.deleteSourceExport("source_id", exp.id);
await client.cancelSourceExport("source_id", exp.id);

Source embedding migrations

const migration = await client.getSourceEmbeddingMigration("source_id");
await client.startSourceEmbeddingMigration("source_id", { target_model: "..." });
await client.cancelSourceEmbeddingMigration("source_id");

Content

const detail = await client.getContentDetail("content_id", { start: 0, end: 1000 });
const embeddings = await client.listContentEmbeddings("content_id");
await client.deleteContent("content_id");

// Replace content with inline text
await client.replaceContentWithInlineText("content_id", { text: "Updated text", title: "Updated" });

// Upload a replacement file
await client.uploadFileToContent("content_id", {
  file: await readFile("updated.pdf"),
  fileName: "updated.pdf",
});

Solutions

const solutions = await client.listSolutions();
const sol = await client.createSolution({ name: "My Solution" });
const fetched = await client.getSolution("solution_id");
await client.updateSolution("solution_id", { name: "Renamed" });
await client.deleteSolution("solution_id");

// Link / unlink resources
await client.linkAgentsToSolution("solution_id", { ids: ["agent_id"] });
await client.unlinkAgentsFromSolution("solution_id", { ids: ["agent_id"] });
await client.linkKnowledgeBasesToSolution("solution_id", { ids: ["kb_id"] });
await client.unlinkKnowledgeBasesFromSolution("solution_id", { ids: ["kb_id"] });
await client.linkSourceConnectionsToSolution("solution_id", { ids: ["source_id"] });
await client.unlinkSourceConnectionsFromSolution("solution_id", { ids: ["source_id"] });

// AI assistant
const plan = await client.generateSolutionAiPlan("solution_id", { user_input: "Set up a RAG pipeline" });
await client.acceptSolutionAiPlan("solution_id", "conversation_id", {});
await client.declineSolutionAiPlan("solution_id", "conversation_id");

// AI-generated knowledge base / source within the solution
await client.generateSolutionAiKnowledgeBase("solution_id", { user_input: "..." });
await client.generateSolutionAiSource("solution_id", { user_input: "..." });

// Conversations
const convs = await client.listSolutionConversations("solution_id");
await client.addSolutionConversationTurn("solution_id", { user_input: "..." });
await client.markSolutionConversationTurn("solution_id", "conversation_id", { ... });

Governance AI

const plan = await client.generateGovernanceAiPlan({ user_input: "Add a toxicity policy" });
const convs = await client.listGovernanceAiConversations();
await client.acceptGovernanceAiPlan("conversation_id");
await client.declineGovernanceAiPlan("conversation_id");

Alerts

const alerts = await client.listAlerts({ status: "active" });
const alert = await client.getAlert("alert_id");
await client.changeAlertStatus("alert_id", { status: "resolved" });
await client.addAlertComment("alert_id", { text: "Investigating" });

// Subscriptions
await client.subscribeToAlert("alert_id");
await client.unsubscribeFromAlert("alert_id");

// Alert configs
const configs = await client.listAlertConfigs();
await client.createAlertConfig({ ... });
await client.getAlertConfig("config_id");
await client.updateAlertConfig("config_id", { ... });
await client.deleteAlertConfig("config_id");

// Organization preferences
const prefs = await client.listOrganizationAlertPreferences();
await client.updateOrganizationAlertPreference("org_id", "alert_type", { ... });

Models

// List models, optionally filtered by capability
const providers = await client.listModels({ supportsToolUse: true });
const imageModels = await client.listModels({ supportsOutputMedia: "image" });
const pdfModels = await client.listModels({ supportsInputMedia: "pdf" });
const model = await client.getModel("model_id");

// Media-generation quality tiers (fast/balanced/thorough) and what each resolves to
const tiers = await client.getGenerationTiers();

const alerts = await client.listModelAlerts();
await client.markModelAlertRead("alert_id");
await client.markAllModelAlertsRead();
const unread = await client.getUnreadModelAlertCount();
const recs = await client.getModelRecommendations("model_id");

// Model playground experiments
const experiment = await client.createExperiment({ model_ids: ["model_id"], prompt: "..." });
const experiments = await client.listExperiments();
const detail = await client.getExperiment("experiment_id");
await client.cancelExperiment("experiment_id");
await client.deleteExperiment("experiment_id"); // soft-delete, preserves audit history

Search

const results = await client.search({ query: "quarterly report" });
const filtered = await client.search({ query: "my agent", entityType: "agent", limit: 5 });

Documentation search

Search the Seclai docs. Results are global (not account-scoped) and each carries a doc_slug plus an optional anchor for building a https://seclai.com/docs/<doc_slug>[#<anchor>] link.

// Fast title/summary match, no AI cost
const hits = await client.searchDocs({ query: "email triggers" });

// Semantic body match — adds a `highlight` with the best matching sentence
const semantic = await client.searchDocs({
  query: "how do I stop auto-reply loops",
  mode: "semantic",
  limit: 5,
});

Email domains

Send and receive agent email on your own domain instead of the shared agent.seclai.com. These endpoints require a user-bound credential (an account-only API key is refused with 403) and, for mutations, an account owner/admin.

// Current domains, their DNS records, and what your plan allows
const { domains, can_add_custom, has_custom } = await client.listEmailDomains();

// Add a vanity subdomain (<slug>.seclai.com) or your own custom domain
const vanity = await client.addEmailDomain({ kind: "vanity", value: "acme" });
const custom = await client.addEmailDomain({
  kind: "custom",
  value: "agent.mycompany.com",
  delegated: true, // let Seclai manage a dedicated Route53 zone
});

// Publish custom.dns_records at your DNS provider, then check without waiting
// for the background sweep
const checked = await client.verifyEmailDomain(custom.id);

// Promote a verified domain, or fall back to the shared domain
await client.setPrimaryEmailDomain(custom.id);
await client.useSharedEmailDomain(); // keeps the domain configured & verified

// Confirm delivery end-to-end (always sends to the account owner only)
await client.sendEmailDomainTestEmail(custom.id);

// DMARC aggregate-report summary
const dmarc = await client.getDmarcSummary(custom.id, { days: 30, topSources: 10 });

// Removing a delegated domain returns a cleanup_note about the registrar NS record
const { cleanup_note } = await client.removeEmailDomain(custom.id);

Top-level AI assistant

// Generate plans for different resource types
const kb = await client.aiAssistantKnowledgeBase({ user_input: "Create a docs KB" });
const src = await client.aiAssistantSource({ user_input: "Add a web source" });
const sol = await client.aiAssistantSolution({ user_input: "Set up monitoring" });
const mb = await client.aiAssistantMemoryBank({ user_input: "Create a chat memory" });

// Accept or decline the generated plan
await client.acceptAiAssistantPlan("conversation_id", { confirm_deletions: true });
await client.declineAiAssistantPlan("conversation_id");

// Memory bank conversation history
const history = await client.getAiAssistantMemoryBankHistory();
await client.acceptAiMemoryBankSuggestion("conversation_id", { ... });

// Feedback
await client.submitAiFeedback({ ... });

Pagination helper

Automatically iterate through all pages:

for await (const source of client.paginate(
  (opts) => client.listSources(opts),
  { limit: 50 },
)) {
  console.log(source.name);
}

Error handling

All errors extend SeclaiError:

import {
  SeclaiError,
  SeclaiConfigurationError,
  SeclaiAPIStatusError,
  SeclaiAPIValidationError,
  SeclaiStreamingError,
} from "@seclai/sdk";

try {
  await client.getAgent("bad_id");
} catch (err) {
  if (err instanceof SeclaiAPIValidationError) {
    console.error("Validation:", err.validationError);
  } else if (err instanceof SeclaiAPIStatusError) {
    console.error(`HTTP ${err.statusCode}:`, err.responseText);
  } else if (err instanceof SeclaiStreamingError) {
    console.error("Stream failed for run:", err.runId);
  }
}

Cancellation (AbortSignal)

All low-level methods support an AbortSignal for request cancellation:

const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5_000);

const data = await client.request("GET", "/agents", {
  signal: controller.signal,
});

Low-level access

For endpoints not yet covered by a convenience method, use request or requestRaw:

// JSON request/response
const data = await client.request("POST", "/custom/endpoint", {
  json: { key: "value" },
  query: { filter: "active" },
});

// Raw Response (e.g. binary downloads)
const response = await client.requestRaw("GET", "/files/download/123");
const blob = await response.blob();

Development

Install dependencies

npm install

Type checking

npm run typecheck

Build

npm run build

This also regenerates src/openapi.ts from openapi/seclai.openapi.json.

Run tests

npm test

Generate docs

Generate HTML docs into build/docs/:

npm run docs