npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@schift-io/sdk

v0.13.0

Published

Workspace SDK for embeddings, search, RAG chat, and workflows

Downloads

1,453

Readme

@schift-io/sdk

TypeScript SDK for bucket upload, vector search, chat, and workflows.

Install

npm install @schift-io/sdk

Quick Start

import { WorkspaceClient } from "@schift-io/sdk";

const client = new WorkspaceClient({
  apiKey: "sch_your_api_key",
  baseUrl: process.env.SCHIFT_API_URL!,
});

// Create or reuse a bucket, upload a document, then search it.
// All bucket methods accept a name or ID — no need to track UUIDs.
await client.createBucket({ name: "company-docs" });
const file = new File([await readFile("manual.pdf")], "manual.pdf", {
  type: "application/pdf",
});
await client.db.upload("company-docs", { files: [file] });

const jobs = await client.listJobs({ bucket: "company-docs", limit: 5 });
const search = await client.bucketSearch("company-docs", {
  query: "refund policy",
  topK: 5,
});

console.log(jobs[0]?.status ?? "queued");
console.log(search.context, search.citations);

Use search({ bucket: ... }) for an answer-ready context block with citations, retrieve(bucketId, { query }) for raw scored chunks, POST /v1/chat for bucket-backed RAG chat with sources, and POST /v1/chat/completions for OpenAI-compatible LLM routing without bucket context.

API Key

Create or copy your API key from your workspace dashboard.

Configure the hosted API origin with baseUrl, SCHIFT_API_URL, or SCHIFT_BASE_URL. The SDK does not assume a production origin when none is configured.

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});

Local Auth Readiness

/v1/auth/me validates a user JWT, not a sch_* API key. Smoke tests should use fixed accounts from .env/.env.local; do not generate timestamped signup emails against production.

import { WorkspaceClient } from "@schift-io/sdk";

const auth = WorkspaceClient.auth({ baseUrl: "http://127.0.0.1:8011" });
await auth.health();
const login = await auth.login({
  email: process.env.SCHIFT_SMOKE_SYSTEM_EMAIL!,
  password: process.env.SCHIFT_SMOKE_SYSTEM_PASSWORD!,
});
const me = await auth.me(login.token!);
console.log(me.user.email, me.orgs[0]?.name);

With a local API already running:

SCHIFT_API_URL=http://127.0.0.1:8011 \
[email protected] \
SCHIFT_SMOKE_SYSTEM_PASSWORD=... \
npm run smoke:auth

Features

Embeddings

// Single text
const resp = await client.embed({
  text: "Search query",
  model: "openai/text-embedding-3-small", // optional
  dimensions: 1024,                       // optional
});
// resp: { embedding: number[], model: string, dimensions: number, usage: { tokens: number } }

// Batch (up to 2048 texts)
const batch = await client.embedBatch({
  texts: ["doc 1", "doc 2", "doc 3"],
  model: "gemini/text-embedding-004",
});
// batch: { embeddings: number[][], model: string, dimensions: number, usage: { tokens, count } }

Search (answer-ready)

const response = await client.search({
  query: "How does projection work?",
  bucket: "my-docs",
  topK: 10,
});
// response: { status, operational_status, bucket_id, query,
//             context: string, citations: [{ index, title?, page?, ... }],
//             warnings: [{ code, message, severity }] }

Retrieve (raw chunks)

const retrieved = await client.retrieve("my-docs-bucket-id", {
  query: "refund policy",
  topK: 8,
  options: { rerank: { enabled: true } },
});
// retrieved.results: Array<{ chunk_id, text, score, document_id?, metadata?, ... }>

Web Search

// Hosted web search
const results = await client.webSearch("latest AI regulations 2026", 5);
results.forEach((r) => {
  console.log(r.title, r.url);
});
// BYOK provider for direct web search
import { WebSearch } from "@schift-io/sdk";

const webSearch = new WebSearch({
  provider: "tavily",
  providerApiKey: process.env.TAVILY_API_KEY!,
  maxResults: 5,
});

const fresh = await webSearch.search("Schift framework launch updates");

Tool calling helpers created from client.tools include workspace_web_search when web search is enabled, so OpenAI/Claude/Vercel AI SDK integrations can call live web search without extra wiring.

PII Masking

Mask Korean PII before sending text to an LLM, vector store, or workflow step. Use types when you want the masking contract to be visible and selectable.

const piiTypes = [
  "resident_id",
  "alien_registration",
  "passport",
  "driver_license",
  "address",
  "phone",
  "bank_account",
];

const result = await client.redactPii({
  text: "주민등록번호 850205-1234567, 연락처 010-1234-5678",
  returnMode: "both",
  types: piiTypes,
});

const masked = await client.mask("계좌 123-45-678901", {
  types: ["bank_account"],
});

The default token format returns tokens such as [PII_PHONE_1] and a reverse_map so the caller can restore the original values after the workflow step. Keep that map only in your temporary restore path; Schift does not persist or gateway-cache it for pii_type_index requests. Set tokenFormat: "label_index" only when you need legacy tokens such as [PHONE_1].

Send only masked into the LLM, agent, vector store, or workflow step. Never include reverse_map in the AI payload; use it only after the AI result returns if your app needs to restore values.

const restored = await client.restorePii({
  text: "고객 연락처 [PII_PHONE_1]로 안내하세요.",
  reverseMap: result.reverse_map!,
});

restorePii() is a stateless API call. It requires an API key and does not persist or cache the map.

BYOK (Bring Your Own LLM Key)

Register your own OpenAI / Google / Anthropic key so /v1/chat and /v1/chat/completions call the provider directly instead of consuming the hosted platform shared LLM quota. Supported providers: "openai", "google", "anthropic".

// Register a key
await client.providers.set("google", {
  api_key: process.env.GOOGLE_API_KEY!,
  // endpoint_url: "https://custom-proxy.example.com",  // optional
});

// Check whether a provider is configured (api_key is never returned)
const status = await client.providers.get("openai");
// { provider: "openai", configured: true | false, endpoint_url: string | null }

Rotation: a stored BYOK record shadows any server-side env var or secret for that provider. To rotate, call set() again with the new key — changing env vars alone has no effect on orgs with a BYOK record.

Agent SDK Compatibility

The SDK sits underneath the agent framework. The integration point is always the same:

  1. let the agent call a workspace search tool
  2. run retrieval against workspace buckets
  3. return grounded chunks back to the model

That means you can keep your preferred agent SDK and swap only the retrieval layer.

Workflow v2 SDK Adapters

Run complete Workflow v2 graphs through the local SDK runtime. The YAML stays a contract in the user's process; it is not sent to the hosted platform unless you explicitly save/publish it through the workflow API:

import { WorkspaceClient } from "@schift-io/sdk";

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = client.workflow({ yaml });

const run = await wf.run({
  query: "계약서 리스크 봐줘",
});

// For local nodes that need hosted APIs, pass the client explicitly:
await wf.run({ inputs: { query: "계약서 리스크 봐줘" }, client });

// Webhook/metadata side effects stay caller-owned through middleware.
await wf.run({
  inputs: { webhooks: { "teacher-request": { caseId: "case_1" } } },
  middleware: {
    receiveWebhook: (event) => event.payload,
    writeMetadata: async (entry) => {
      await localDb.metadata.put(entry.namespace, entry.key, entry.value);
      return { stored: true };
    },
    deliverWebhook: async (event) => {
      await appServer.deliver(event.webhook, event.payload);
      return { delivered: true };
    },
    requestHttp: (request) => appServer.fetch(request),
    readSecret: (request) => localSecrets.get(request.secret),
    requestApproval: (request) => approvals.enqueue(request),
    requestForm: (request) => forms.enqueue(request),
    wait: (request) => scheduler.defer(request),
    runSubworkflow: (request) => workflowRegistry.run(request.workflowRef, request.subworkflowInputs),
  },
});

for await (const event of wf.stream({ query: "계약서 리스크 봐줘" })) {
  if (event.type === "block.completed") console.log(event.blockId);
  if (event.type === "workflow.completed") console.log(event.run.outputs);
}

Framework adapters are intentionally narrower. asVercelAI() projects one selected Workflow v2 llm_generate block into Vercel AI SDK call options. It does not execute upstream retrieval, transforms, conditions, or the full graph. Use the local runtime with workflow.run() / workflow.stream() for graph execution. Use client.workflowsV2.run(workflowId, options) only for intentionally hosted, published Workflow v2 definitions:

const hosted = await client.workflowsV2.run("wf_v2_123", {
  inputs: { invoice: "T-001" },
  mode: "live",
  approvals: { review_issue: true },
  mockBindings: { tax_provider: { status: "mocked" } },
});
import { generateText } from "ai";
import { WorkspaceClient } from "@schift-io/sdk";
import { asVercelAI } from "@schift-io/workflow-vercel-ai";

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = client.workflow({ yaml });

const result = await generateText(
  await asVercelAI(wf, {
    mode: "generateText",
    entry: "answer",
  }),
);

Structured output schemas can live in the Workflow v2 YAML block config as response_schema / output_schema / schema. Vercel adapters pass that schema through as schema for generateObject/streamObject, while Google Gen AI adapters map it to config.responseMimeType = "application/json" and config.responseSchema.

import { GoogleGenAI } from "@google/genai";
import { WorkspaceClient } from "@schift-io/sdk";
import { asGoogleGenAI } from "@schift-io/workflow-google-genai";

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});
const wf = client.workflow({ yaml });

const response = await ai.models.generateContent(
  await asGoogleGenAI(wf, { entry: "answer" }),
);
import { asLangGraph } from "@schift-io/workflow-langgraph";

const graph = await asLangGraph(wf, {
  state: { messages: [] },
});

const result = await graph.invoke({
  messages: [{ role: "user", content: "계약서 리스크 봐줘" }],
});

Use asDify() when you need a Dify-compatible workflow export graph from a canonical Workflow v2 artifact:

import { asDify } from "@schift-io/workflow-dify";

const difyExport = await asDify(wf);

The adapter packages declare framework SDKs as peer dependencies and do not add Vercel AI SDK, Google Gen AI, LangGraph, or Dify-specific projection code to the core @schift-io/sdk bundle.

Google Gen AI SDK

import { GoogleGenAI } from "@google/genai";
import { WorkspaceClient } from "@schift-io/sdk";

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});

const firstTurn = await ai.models.generateContent({
  model: "gemini-2.5-flash",
  contents: "What changed in the latest billing policy?",
  config: {
    tools: client.tools.googleGenAI(),
  },
});

const functionCall = firstTurn.functionCalls?.[0];
if (functionCall) {
  const functionResponsePart = await client.tools.googleFunctionResponse({
    name: functionCall.name,
    args: functionCall.args ?? {},
  });

  const secondTurn = await ai.models.generateContent({
    model: "gemini-2.5-flash",
    contents: [
      { role: "user", parts: [{ text: "What changed in the latest billing policy?" }] },
      { role: "model", parts: [{ functionCall }] },
      { role: "user", parts: [functionResponsePart] },
    ],
  });

  console.log(secondTurn.text);
}

Vercel AI SDK

import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { WorkspaceClient } from "@schift-io/sdk";

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});

const result = await generateText({
  model: openai("gpt-4o-mini"),
  prompt: "What changed in the latest billing policy?",
  tools: client.tools.vercelAI(),
  maxSteps: 5,
});

console.log(result.text);

Mastra

If you are using Mastra, wrap client.search() in a Mastra tool and keep the rest of the agent stack unchanged.

import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
import { WorkspaceClient } from "@schift-io/sdk";

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});

const workspaceSearchTool = createTool({
  id: "workspace-search",
  description: "Retrieve context from a workspace bucket.",
  inputSchema: z.object({
    bucket: z.string(),
    query: z.string(),
    topK: z.number().int().min(1).max(10).default(5),
  }),
  execute: async ({ context }) => ({
    search: await client.search({
      bucket: context.bucket,
      query: context.query,
      topK: context.topK,
    }),
  }),
});

const agent = new Agent({
  name: "docs-agent",
  instructions: "Use workspace-search before answering document questions.",
  model: openai("gpt-4o-mini"),
  tools: { workspaceSearchTool },
});

Examples:

Buckets

// List all buckets
const buckets = await client.listBuckets();

await client.db.upload("company-docs", {
  files: [file],
});

const collections = await client.listBucketCollections("company-docs");

// Legacy collection aliases remain available for older integrations
const col = await client.getCollection("bucket-id");

await client.deleteCollection("bucket-id");

Bucket search is permission-scoped: the server searches only the child collections the caller can access and merges the ranked results.

Document Metadata And Connectors

Uploading a document or syncing it in through a connector (Notion, Gmail, Obsidian, …) attaches metadata you can filter on at search time.

Reserved keys — populated structurally (connector API field mapping or Markdown frontmatter parsing, never LLM-inferred). Availability depends on what the source provides.

| Key | Meaning | Always set? | |---|---|---| | source_kind | Kind of source (notion_page, email, markdown_note, upload, …) | yes | | source_created_at | Original creation time (ISO 8601) | if source provides it | | source_modified_at | Original last-modified time (ISO 8601) | if source provides it | | source_author | Author | if source provides it | | source_uri | Deep link back to the original source | if source provides it | | source_filename | File name | uploads always; connectors if provided | | source_tags | Tags/labels from the source (JSON-encoded array string) | if source provides it | | ingested_by | Who/what ingested it (upload or connector:<provider>) | yes | | ingested_at | When Schift received the document | yes | | cclg_source | Internal lineage identifier | yes |

Avoid naming custom metadata keys the same as these — the system value takes precedence.

For Markdown uploads (.md/.markdown), pass parseFrontmatter: true to promote YAML frontmatter (created/date/author/tags) into the reserved keys above:

await client.db.upload("company-docs", {
  files: [file],
  parseFrontmatter: true,
});

Custom metadata is always explicit — Schift does not infer business metadata from document content:

await client.db.upload("company-docs", {
  files: [file],
  metadata: { department: "finance", priority: "high" },
});

Custom metadata is set per document and inherited by every chunk derived from it. Values must be strings, numbers, or booleans (coerced to strings server-side).

Filtering — reserved and custom keys are filtered the same way, with flat, top-level keys (not a nested { metadata: {...} } object):

const resp = await client.search({
  query: "revenue guidance",
  bucket: "company-docs",
  filters: {
    source_kind: "email",
    department: "finance",
  },
});

There is no contains operator today — use like ({ like: "%urgent%" }) for substring matches, including against JSON-encoded fields like source_tags.

Workflows

Build and run RAG pipelines as composable DAGs.

One-block RAG (recommended)

BlockType.RAG is a single composite block that does retrieve + prompt + LLM inline. No graph wiring required.

import { WorkspaceClient, BlockType } from "@schift-io/sdk";

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});

const wf = await client.workflows.create({ name: "fire-code-qa" });
await client.workflows.addBlock(wf.id, {
  type: BlockType.RAG,
  config: { collection: "fire-code" }, // bucket name is the only required field
});

const run = await client.workflows.run(wf.id, {
  query: "소방 설비는 얼마나 자주 점검해야 하나요?",
});
// run.outputs → { answer, sources, text, data, usage, results }

Advanced config (all optional, sensible defaults): top_k, mode, filter, rerank, model, temperature, max_tokens, thinking_budget, system_prompt, template, response_schema, include_sources.

Runtime overrides (passed via workflows.run() inputs): bucket, filter, tags, top_k, response_schema.

Direct RAG endpoint (skip workflow entirely)

When you don't need a persisted workflow record, hit /v1/rag/run directly — shares the same code path, less overhead, structured output supported.

That shared runtime is intentionally modular inside the server while staying fused on the serving path: retrieval resolves the bucket and fetches hits, context-build formats the prompt evidence, generation renders/calls the LLM, and evidence-format returns sources/results to the caller. Keep custom prompt templates on workflow RAG config or /v1/rag/run; public /v1/chat owns its server-built RAG prompt and rejects client-supplied system_prompt. The TypeScript SDK mirrors that boundary: client.chat({ systemPrompt: ... }) and client.chatStream({ systemPrompt: ... }) fail locally before a request is sent.

const apiBaseUrl = process.env.SCHIFT_API_URL ?? "https://api.example.com";

const resp = await fetch(`${apiBaseUrl}/v1/rag/run`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SCHIFT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    query: "소방 점검 주기?",
    bucket: "fire-code",
    response_schema: {
      type: "object",
      properties: { months: { type: "integer" }, cite: { type: "string" } },
    },
  }),
});
const { answer, data, sources } = await resp.json();

Legacy multi-block pipelines

For custom pipelines (multi-source merge, conditional routing, tool use) you can still build the graph from primitive blocks (retriever, llm, prompt_template, etc.).

// Create from template or blank
const wf = await client.workflows.create({ name: "My RAG Pipeline" });

// Run with inputs (multiple values supported)
const result = await client.workflows.run(wf.id, {
  query: "maternity leave policy",
  language: "ko",
});
console.log(result.outputs);

CRUD

const wf = await client.workflows.create({ name: "Pipeline" });
const all = await client.workflows.list();
const one = await client.workflows.get(wf.id);
const updated = await client.workflows.update(wf.id, { name: "Renamed" });
await client.workflows.delete(wf.id);

Blocks & Edges

// Add blocks
const retriever = await client.workflows.addBlock(wf.id, {
  type: "retriever",
  title: "Search Docs",
  config: { collection: "my-docs", top_k: 5, rerank: true },
});

const llm = await client.workflows.addBlock(wf.id, {
  type: "llm",
  config: {
    model: "openai/gpt-4o-mini", // or "anthropic/claude-sonnet-4-20250514", "gemini-2.5-flash"
    temperature: 0.7,
  },
});

// Connect blocks
await client.workflows.addEdge(wf.id, {
  source: retriever.id,
  target: llm.id,
});

// Remove
await client.workflows.removeBlock(wf.id, retriever.id);
await client.workflows.removeEdge(wf.id, edgeId);

WorkflowBuilder (Fluent API)

Build a graph locally, then send to the API in one call:

import { WorkflowBuilder } from "@schift-io/sdk";

const request = new WorkflowBuilder("My RAG Pipeline")
  .description("Retrieval-augmented generation")
  .addBlock("start", { type: "start" })
  .addBlock("retriever", {
    type: "retriever",
    config: { collection: "my-docs", top_k: 5 },
  })
  .addBlock("prompt", {
    type: "prompt_template",
    config: { template: "Context:\n{{results}}\n\nQ: {{query}}" },
  })
  .addBlock("llm", {
    type: "llm",
    config: { model: "openai/gpt-4o-mini" },
  })
  .addBlock("end", { type: "end" })
  .connect("start", "retriever")
  .connect("retriever", "prompt")
  .connect("prompt", "llm")
  .connect("llm", "end")
  .build();

const wf = await client.workflows.create(request);

YAML Import / Export

// Export
const yaml = await client.workflows.exportYaml(wf.id);

// Import from YAML string
const imported = await client.workflows.importYaml(yamlString);

Validation & Meta

// Validate graph
const { valid, errors } = await client.workflows.validate(wf.id);

// List available block types
const blockTypes = await client.workflows.getBlockTypes();

// List available templates
const templates = await client.workflows.getTemplates();

Block Types

| Category | Types | |----------|-------| | Control | start, end, conditional, loop | | Retrieval | retriever, reranker | | LLM | llm, prompt_template, answer | | Data | document_loader, chunker, embedder, text_processor | | Web | web_search | | Integration | api_call, webhook, code_executor | | Storage | vector_store, cache |

Configuration

const client = new WorkspaceClient({
  apiKey: "sch_...",                      // required
  baseUrl: process.env.SCHIFT_API_URL,    // required unless SCHIFT_API_URL/SCHIFT_BASE_URL is set
  timeout: 60_000,                        // default, in milliseconds
});

Error Handling

import {
  WorkspaceClient,
  AuthError,
  QuotaError,
  PlatformError,
} from "@schift-io/sdk";

const client = new WorkspaceClient({
  apiKey: process.env.SCHIFT_API_KEY!,
  baseUrl: process.env.SCHIFT_API_URL!,
});

try {
  await client.embed({ text: "test" });
} catch (err) {
  if (err instanceof AuthError) {
    // 401: Invalid or expired API key
  } else if (err instanceof QuotaError) {
    // 402: Insufficient credits
  } else if (err instanceof PlatformError) {
    // Other API errors (403, 422, 429, 500, 502)
    console.error(err.message, err.status);
  }
}

Supported Models

| Model | Provider | Dimensions | |-------|----------|------------| | openai/text-embedding-3-small | OpenAI | 1536 | | openai/text-embedding-3-large | OpenAI | 3072 | | gemini/text-embedding-004 | Google | 768 | | voyage/voyage-3-large | Voyage | 1024 | | schift-embed-1-small | Schift | 1024 |

All models output to a canonical 1024-dimensional space via Schift's projection layer.

Releases

Published from the schift-io/schift monorepo to npm via .github/workflows/sdk-publish-ts.yml. Cut a release with:

# 1. Bump version
$EDITOR clients/sdk/ts/package.json   # version: "0.X.Y"
git add clients/sdk/ts/package.json && git commit -m "chore(sdk-ts): bump 0.X.Y"
git push origin main

# 2. Create the release (tag pattern: npm-v*)
gh release create npm-v0.X.Y \
  --title "@schift-io/sdk v0.X.Y" \
  --notes "..."

The workflow verifies the tag matches package.json, runs tsc, npm test, npm run build, then npm publish --access public.

The public mirror at schift-io/schift-ts is sync-only (no workflows there). All publish automation lives in the monorepo.

License

MIT