npm package: @syncagent/js
Description: SyncAgent JavaScript SDK — AI database agent for any app
Latest version: 0.3.3

# @syncagent/js

Core JavaScript/TypeScript SDK for [SyncAgent](https://syncagent.dev) — add an AI database agent to any app.

## Install

```bash
npm install @syncagent/js
```

## Quick Start

```typescript
import { SyncAgentClient } from "@syncagent/js";

const agent = new SyncAgentClient({
  apiKey: "sa_your_api_key",
  connectionString: process.env.DATABASE_URL,
  // baseUrl: "http://localhost:3100", // dev only — defaults to https://syncagent.dev
});

const result = await agent.chat([
  { role: "user", content: "How many users do we have?" }
]);
console.log(result.text);
```

## Configuration

```typescript
new SyncAgentClient(config: SyncAgentConfig)
```

| Option             | Type                             | Required | Description                                                    |
| ------------------ | -------------------------------- | -------- | -------------------------------------------------------------- |
| `apiKey`           | `string`                         | ✅       | Your SyncAgent API key (`sa_...`)                              |
| `connectionString` | `string`                         | ✅       | Your database URL — sent at runtime, never stored              |
| `tools`            | `Record<string, ToolDefinition>` | —        | Custom tools the agent can call client-side                    |
| `baseUrl`          | `string`                         | —        | Override API URL. Defaults to `https://syncagent.dev`          |
| `filter`           | `Record<string, any>`            | —        | Mandatory query filter — scopes ALL operations to matching records. Use for multi-tenant SaaS. |
| `operations`       | `("read"\|"create"\|"update"\|"delete")[]` | — | Restrict which operations are allowed for this session. Must be a subset of the project's configured operations. |
| `toolsOnly`        | `boolean`                        | —        | When `true`, disables all built-in DB tools — the agent only uses your custom `tools`. `connectionString` becomes optional. |
| `autoDetectPage`   | `boolean`                        | `true`   | Auto-detect current page from `window.location` and include in context. Set `false` to disable. |
| `systemInstruction`| `string`                         | —        | Custom instructions for the agent — personality, tone, rules. Prepended to system prompt. |
| `confirmWrites`    | `boolean`                        | `false`  | When `true`, agent asks for explicit user confirmation before any create/update/delete. |
| `language`         | `string`                         | —        | Language the agent responds in. E.g. `"French"`, `"Spanish"`, `"Japanese"`. |
| `maxResults`       | `number`                         | `50`     | Default max records per query. Set lower for mobile, higher for dashboards. |
| `sensitiveFields`  | `string[]`                       | `["password","token","secret"]` | Fields the agent masks as "••••••••" in responses. |
| `onBeforeToolCall` | `(name, args) => boolean`        | —        | Called before each client tool executes. Return `false` to block. |
| `onAfterToolCall`  | `(name, args, result) => void`   | —        | Called after each client tool executes. Use for logging/analytics. |

## `client.chat(messages, options?)`

```typescript
const result = await agent.chat(messages, options);
```

**`messages`** — `Message[]`

```typescript
{ role: "user" | "assistant"; content: string }
```

**`options`** — `ChatOptions`

| Option       | Type                                             | Description                                    |
| ------------ | ------------------------------------------------ | ---------------------------------------------- |
| `onToken`    | `(token: string) => void`                        | Called for each streamed text chunk            |
| `onComplete` | `(text: string) => void`                         | Called with the full response text             |
| `onError`    | `(error: Error) => void`                         | Called on error                                |
| `onStatus`   | `(step: string, label: string) => void`          | Called with live status updates while working  |
| `onData`     | `(data: ToolData) => void`                       | Called when a DB tool returns structured data  |
| `onToolCall` | `(name: string, args: any, result: any) => void` | Called when a custom tool executes             |
| `signal`     | `AbortSignal`                                    | Cancel the request                             |
| `context`    | `Record<string, any>`                            | Extra context injected into every message      |

**Returns** `Promise<ChatResult>` → `{ text: string }`

### Status steps

`onStatus` fires with these `step` values:
- `"connecting"` — connecting to the database
- `"schema"` — discovering schema
- `"thinking"` — AI is reasoning
- `"querying"` — executing a DB tool
- `"done"` — complete

## `client.getSchema()`

```typescript
const schema = await agent.getSchema();
// CollectionSchema[]
```

## Context injection

The SDK **automatically detects** the current page from `window.location` on every message. The agent knows what page the user is on, what record they're viewing, and any relevant query params — with zero developer code.

```
URL: /dashboard/orders/ord_123?tab=details

Auto-detected context:
  currentPage: "orders"
  currentPath: "/dashboard/orders/ord_123"
  currentRecordId: "ord_123"
  param_tab: "details"
```

The user can now say "show me this order" and the agent knows to query `ord_123`.

You can also pass **additional context** that merges on top of the auto-detected values:

```typescript
await agent.chat(messages, {
  context: {
    userId: "user_123",
    userRole: "admin",
    orgName: "Acme Corp",
  }
});
```

To disable auto-detection:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  autoDetectPage: false, // only use manually passed context
});
```

Context is sent as a dedicated field in the request body and injected into the AI system prompt — not appended to the user's message text.

## `onData` callback

React to structured query results in your own UI:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
});

await agent.chat(messages, {
  onData: (data) => {
    // data.tool — "query_documents" | "aggregate_documents"
    // data.collection — e.g. "orders"
    // data.data — array of result rows
    // data.count — number of results
    console.log(`Got ${data.count} rows from ${data.collection}`);
    setTableData(data.data); // update your own table component
  }
});
```

## Custom Tools

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  tools: {
    sendEmail: {
      description: "Send an email to a user",
      inputSchema: {
        to:      { type: "string", description: "Recipient email" },
        subject: { type: "string", description: "Subject line" },
        body:    { type: "string", description: "Email body" },
      },
      execute: async ({ to, subject, body }) => {
        await mailer.send({ to, subject, text: body });
        return { sent: true };
      },
    },
  },
});
```

## Multi-tenant SaaS — scoping queries per organization

When building a SaaS app, multiple organizations share the same database. Pass `filter` to scope every agent operation to the current user's organization. The agent cannot query, insert, update, or delete outside this scope — it's enforced server-side.

```typescript
import { SyncAgentClient } from "@syncagent/js";

// Scope to the current user's organization
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  filter: { organizationId: currentUser.orgId },
});

// Every query is now automatically scoped:
// "Show all orders" → db.orders.find({ organizationId: "org_123" })
// "Count users"     → db.users.countDocuments({ organizationId: "org_123" })
// "Add a product"   → db.products.insertOne({ ...doc, organizationId: "org_123" })
```

**Without `filter`** — agent queries everything (suitable for single-tenant apps or admin tools).

**With `filter`** — every read, write, update, and delete is strictly scoped. The agent is told about the scope in its system prompt and cannot override it.

Common patterns:

```typescript
// By organization ID
filter: { organizationId: org.id }

// By tenant slug
filter: { tenant: "acme-corp" }

// By user ID (personal data)
filter: { userId: currentUser.id }

// Multiple fields
filter: { orgId: org.id, deleted: false }

// SQL databases (same syntax)
filter: { tenant_id: tenant.id }
```

## Multi-turn conversations

```typescript
const history: Message[] = [];

history.push({ role: "user", content: "Show top 5 customers" });
const r1 = await agent.chat(history);
history.push({ role: "assistant", content: r1.text });

history.push({ role: "user", content: "Now email all of them" });
const r2 = await agent.chat(history);
```

## Abort / cancel

```typescript
const controller = new AbortController();
agent.chat(messages, { signal: controller.signal });
controller.abort(); // cancel at any time
```

## TypeScript types

```typescript
import type {
  SyncAgentConfig, Message, ChatOptions, ChatResult,
  CollectionSchema, SchemaField, ToolDefinition, ToolParameter, ToolData,
} from "@syncagent/js";
```

## License

MIT

## Per-user operation restrictions

Use `operations` to give different users different levels of access within the same project. The client can only restrict further — it can never grant more than what the project dashboard allows.

```typescript
// Read-only for regular users
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  filter: { organizationId: currentUser.orgId },
  operations: ["read"],
});

// Full access for admins
const adminAgent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  filter: { organizationId: currentUser.orgId },
  operations: ["read", "create", "update", "delete"],
});
```

## Tools-only mode

When you want the agent to **only** use your custom tools — with no database access at all — set `toolsOnly: true`. This is useful when you want to build an AI assistant powered by your own APIs, webhooks, or business logic.

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  toolsOnly: true, // no connectionString needed
  tools: {
    searchProducts: {
      description: "Search products by name or category",
      inputSchema: {
        query: { type: "string", description: "Search query" },
      },
      execute: async ({ query }) => {
        const res = await fetch(`/api/products?q=${query}`);
        return res.json();
      },
    },
    createOrder: {
      description: "Create a new order for a customer",
      inputSchema: {
        productId: { type: "string", description: "Product ID" },
        quantity:  { type: "number", description: "Quantity" },
      },
      execute: async ({ productId, quantity }) => {
        const res = await fetch("/api/orders", {
          method: "POST",
          body: JSON.stringify({ productId, quantity }),
        });
        return res.json();
      },
    },
  },
});

// The agent will ONLY call searchProducts and createOrder.
// No database queries, no schema discovery, no DB connection.
```

You can also combine `toolsOnly` with a `connectionString` if you want — the DB connection is simply skipped when `toolsOnly` is enabled.

## System Instructions

Customize the agent's personality, tone, domain knowledge, or rules:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  systemInstruction: "You are a friendly sales assistant for Acme Corp. Always suggest upsells when showing order data. Never mention competitor names.",
});
```

Instructions are prepended to the system prompt and take priority over default behavior.

## Write Confirmation

Require explicit user confirmation before any create, update, or delete:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  confirmWrites: true,
});

// User: "Add a new customer named John"
// Agent: "I'd like to create a record in customers: name=John. Should I proceed?"
// User: "yes"
// Agent: "✅ Created customer John"
```

## Response Language

Make the agent respond in any language:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  language: "French",
});

// User: "Show me all orders"
// Agent: "Voici toutes les commandes..."
```

Field names and code stay in English so tools work correctly.

## Max Results

Control the default query limit:

```typescript
// Mobile app
new SyncAgentClient({ ..., maxResults: 10 });

// Admin dashboard
new SyncAgentClient({ ..., maxResults: 100 });
```

## Sensitive Field Masking

Specify which fields the agent should mask in responses:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  sensitiveFields: ["ssn", "creditCard", "salary", "bankAccount"],
});

// Agent shows: ssn: "••••••••", salary: "••••••••"
```

Default: `["password", "token", "secret"]`.

## Middleware Hooks

Intercept tool calls for logging, audit trails, or dynamic blocking:

```typescript
const agent = new SyncAgentClient({
  apiKey: "sa_your_key",
  connectionString: process.env.DATABASE_URL,
  tools: { /* your tools */ },

  // Block specific operations dynamically
  onBeforeToolCall: (toolName, args) => {
    console.log(`[Audit] ${toolName}`, args);
    if (toolName === "deleteUser" && !currentUser.isAdmin) return false;
    return true;
  },

  // Log every result
  onAfterToolCall: (toolName, args, result) => {
    analytics.track("tool_call", { tool: toolName, success: result.success });
  },
});
```

When `onBeforeToolCall` returns `false`, the tool is blocked and the AI receives an error message explaining the operation was denied.