@squidcloud/cli 1.0.486 → 1.0.488

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -40729,7 +40729,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"name":"seek-bzip","version":"1.0.6",
40729
40729
  (module) {
40730
40730
 
40731
40731
  "use strict";
40732
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.486","description":"The Squid CLI","main":"dist/index.js","scripts":{"start":"node dist/index.js","start-ts":"ts-node -r tsconfig-paths/register src/index.ts","prebuild":"rimraf dist","build":"webpack --mode=production","build:dev":"webpack --mode=development","lint":"eslint","link":"npm run build && chmod 755 dist/index.js && npm link","watch":"webpack --watch","deploy":"npm run build && npm pack --silent | xargs -I {} mv {} package.tgz && npm install -g package.tgz && rm -rf package.tgz","publish:public":"npm run build && npm publish --access public"},"files":["dist/**/*"],"bin":{"squid":"dist/index.js"},"keywords":[],"author":"","license":"ISC","engines":{"node":">=18.0.0"},"dependencies":{"@squidcloud/local-backend":"^1.0.486","adm-zip":"^0.5.16","copy-webpack-plugin":"^14.0.0","decompress":"^4.2.1","logpipes":"^1.11.0","nodemon":"^3.1.9","terser-webpack-plugin":"^5.5.0","ts-loader":"^9.5.1","ts-node":"^10.9.2","tsconfig-paths":"^4.2.0","tsconfig-paths-webpack-plugin":"^4.1.0","webpack":"^5.106.2","zip-webpack-plugin":"^4.0.1"},"devDependencies":{"@types/adm-zip":"^0.5.7","@types/decompress":"^4.2.7","terminal-link":"^3.0.0"}}');
40732
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.488","description":"The Squid CLI","main":"dist/index.js","scripts":{"start":"node dist/index.js","start-ts":"ts-node -r tsconfig-paths/register src/index.ts","prebuild":"rimraf dist","build":"webpack --mode=production","build:dev":"webpack --mode=development","lint":"eslint","link":"npm run build && chmod 755 dist/index.js && npm link","watch":"webpack --watch","deploy":"npm run build && npm pack --silent | xargs -I {} mv {} package.tgz && npm install -g package.tgz && rm -rf package.tgz","publish:public":"npm run build && npm publish --access public"},"files":["dist/**/*"],"bin":{"squid":"dist/index.js"},"keywords":[],"author":"","license":"ISC","engines":{"node":">=18.0.0"},"dependencies":{"@squidcloud/local-backend":"^1.0.488","adm-zip":"^0.5.16","copy-webpack-plugin":"^14.0.0","decompress":"^4.2.1","logpipes":"^1.11.0","nodemon":"^3.1.9","terser-webpack-plugin":"^5.5.0","ts-loader":"^9.5.1","ts-node":"^10.9.2","tsconfig-paths":"^4.2.0","tsconfig-paths-webpack-plugin":"^4.1.0","webpack":"^5.106.2","zip-webpack-plugin":"^4.0.1"},"devDependencies":{"@types/adm-zip":"^0.5.7","@types/decompress":"^4.2.7","terminal-link":"^3.0.0"}}');
40733
40733
 
40734
40734
  /***/ }
40735
40735
 
@@ -21,15 +21,15 @@ Squid is a backend-as-a-service platform that provides:
21
21
 
22
22
  ## Feature-Specific Guidance
23
23
 
24
- - **[client.md](reference/client.md)** → client SDK, initialization, setup, auth, login, tokens, OAuth, appId, region, environmentId, apiKey, authProvider, getToken, setAuthProvider, Squid client, frontend, collection, executeFunction, executeFunctionWithHeaders, getWebhookUrl, externalAuth, saveAuthCode, getAccessToken, storage, uploadFile, downloadUrl, queues, produce, consume, distributed locks, acquireLock, withLock, web, aiSearch, getUrlContent, createShortUrl, jobs, getJob, awaitJob, observability, metrics, reportMetric, queryMetrics, notifications, publishNotification, observeNotifications
25
- - **[console.md](reference/console.md)** → Squid Console, web UI, organizations, applications, AI Studio, knowledge bases, integrations, monitoring, logs, API keys, secrets, testing, debugging, profile settings, management API keys
26
- - **[ai.md](reference/ai.md)** → AI agents, chat, ask, askWithAnnotations, askAsync, askWithVoiceResponse, transcribeAndChat, transcribeAndAsk, knowledge bases, RAG, embeddings, image generation, audio, transcription, text-to-speech, TTS, connectedAgents, connectedIntegrations, connectedKnowledgeBases, @aiFunction, @secureAiAgent, @secureAiQuery, memory, memoryOptions, voiceOptions, OpenAI, Anthropic, Gemini, Grok, DALL-E, Whisper, MCP, @mcpServer, @mcpTool, executeAiQuery, executeAiApiCall, extraction, createPdf, upsert agent, listAgents
24
+ - **[client.md](reference/client.md)** → client SDK, initialization, setup, auth, login, tokens, OAuth, appId, region, environmentId, apiKey, authProvider, getToken, setAuthProvider, Squid client, frontend, collection, executeFunction, executeFunctionWithHeaders, getWebhookUrl, externalAuth, saveAuthCode, getAccessToken, storage, uploadFile, downloadUrl, queues, produce, consume, distributed locks, acquireLock, withLock, web, aiSearch, allowedDomains, getUrlContent, createShortUrl, jobs, getJob, awaitJob, startJob, completeJob, failJob, events, squid.events(), emit, TriggerEvent, LangGraph, squid.langGraph(), invoke, invokeAsync, resume, getState, deleteThread, observability, metrics, reportMetric, queryMetrics, setMetricAnnotations, notifications, publishNotification, observeNotifications
25
+ - **[console.md](reference/console.md)** → Squid Console, web UI, organizations, applications, AI Studio, knowledge bases, knowledge graph toggle, integrations, monitoring, logs, API keys, secrets, testing, debugging, profile settings, management API keys
26
+ - **[ai.md](reference/ai.md)** → AI agents, chat, ask, askWithAnnotations, askAsync, askWithVoiceResponse, transcribeAndChat, transcribeAndAsk, knowledge bases, RAG, embeddings, image generation, audio, transcription, text-to-speech, TTS, connectedAgents, connectedIntegrations, connectedKnowledgeBases, @aiFunction, @secureAiAgent, @secureAiQuery, memory, memoryOptions, metricAnnotations, voiceOptions, OpenAI, Anthropic, Gemini, Grok, DALL-E, Whisper, MCP, @mcpServer, @mcpTool, agent as MCP server, updateMcpServer, generateMcpDescriptions, agent API key, executeAiQuery, executeAiApiCall, extraction, createPdf, upsert agent, listAgents, listChatModels, listFunctions, spreadsheet understanding, querySpreadsheetsWithAi, searchMode, keyword search, xlsx, csv, grep, literal scan, knowledge graph, GraphRAG, graphRag, graph search, queryGraph, getGraphStatus, rebuildGraph, exploreGraph, searchWithGraphContext, graphOptions, graphFilter, underConcept, entityTypes, queryKnowledgeGraph, bulk ingestion, bulkUpsertContexts, createBulkUploadUrls, observeBulkIngestionJob, listContextsPage, generateMetadataFieldDescriptions
27
27
  - **[chat-widget.md](reference/chat-widget.md)** → AI chat widget, @squidcloud/react-chat-widget, squid-chat-widget, squid-chat-widget-with-fab-button, embeddable chat, custom API webhook, AI query mode, chain-of-thought, status updates, session management, agentContext, memoryOptions, squid-ai-custom-api-url, squid-ai-agent-chat-options, FAB button, theming, CSS variables, localization, RTL, menu items slots, suggested prompts, voice transcription, error formatting, authentication, squid-auth-provider, onChange events
28
- - **[databases.md](reference/databases.md)** → collections, documents, queries, subscriptions, snapshots, insert, update, delete, CRUD, real-time, dereference, pagination, transactions, query operators, eq, neq, gt, gte, lt, lte, like, in, nin, arrayIncludesSome, arrayIncludesAll, sortBy, limit, join queries, OR queries, @trigger, native queries, SQL, MongoDB, Elasticsearch, incrementInPath, decrementInPath, watch changes, doc(), projectFields, field projection, __docId__, __id
29
- - **[backend.md](reference/backend.md)** → SquidService, @executable, @webhook, @trigger, TriggerRequest, @scheduler, @limits, rate limiting, quotas, decorators, backend functions, WebhookRequest, CronExpression, cron, file handling, SquidFile, getUserAuth, isAuthenticated, assertIsAuthenticated, createWebhookResponse, this.squid, this.secrets, @clientConnectionStateHandler, @onQueueMessage, QueueMessageRequest, queue message handler, server-side queue consumer, CLI, squid init, squid start, squid deploy, squid build, project structure, multiple services, service architecture, squidInject, cross-service communication
30
- - **[security.md](reference/security.md)** → security rules, @secureDatabase, @secureCollection, @secureTopic, @secureStorage, @secureApi, @secureNativeQuery, @secureAiQuery, @secureAiAgent, @secureDistributedLock, @secureGraphQL, QueryContext, MutationContext, isSubqueryOf, affectsPath, permissions, authorization, row-level security, role-based access
31
- - **[admin.md](reference/admin.md)** → ManagementClient, management API keys, organizations, applications, programmatic management, CI/CD, automation, integrations admin, secrets admin, upsertIntegration, discoverDataConnectionSchema, testDataConnection, createOrganization, createApplication
32
- - **[api.md](reference/api.md)** → API, REST API, HTTP endpoints, API reference, Agent API, AI Audio API, AI Image API, KnowledgeBase API, Matchmaking API, Web Utilities API, Database API, Extraction API
28
+ - **[databases.md](reference/databases.md)** → collections, documents, queries, subscriptions, snapshots, insert, update, delete, CRUD, real-time, dereference, pagination, transactions, query operators, eq, neq, gt, gte, lt, lte, like, in, nin, arrayIncludesSome, arrayIncludesAll, sortBy, limit, join queries, OR queries, @trigger, native queries, SQL, MongoDB, Elasticsearch, incrementInPath, decrementInPath, watch changes, doc(), docId, docIds, query.delete(), delete by query, projectFields, field projection, __docId__, __id
29
+ - **[backend.md](reference/backend.md)** → SquidService, @executable, @webhook, @trigger, TriggerRequest, @scheduler, @limits, rate limiting, quotas, decorators, backend functions, WebhookRequest, CronExpression, cron, file handling, SquidFile, getUserAuth, isAuthenticated, assertIsAuthenticated, createWebhookResponse, this.squid, getSquid, getPassiveSquid, this.secrets, @clientConnectionStateHandler, @onQueueMessage, QueueMessageRequest, queue message handler, server-side queue consumer, @eventHandler, event handler, workspace, CLI, squid init, squid start, squid deploy, squid build, squid kb-upload, printBundle, project structure, multiple services, service architecture, squidInject, cross-service communication
30
+ - **[security.md](reference/security.md)** → security rules, @secureDatabase, @secureCollection, @publicCollection, public read, @secureTopic, @secureStorage, @secureApi, @secureNativeQuery, @secureAiQuery, @secureAiAgent, @secureLangGraph, @secureDistributedLock, @secureGraphQL, QueryContext, MutationContext, SecureLangGraphContext, isSubqueryOf, affectsPath, permissions, authorization, row-level security, role-based access
31
+ - **[admin.md](reference/admin.md)** → ManagementClient, management API keys, organizations, applications, programmatic management, CI/CD, automation, integrations admin, secrets admin, connectors admin, squid.admin().connectors(), attach connector, appConnectors, upsertIntegration, discoverDataConnectionSchema, testDataConnection, createOrganization, createApplication
32
+ - **[api.md](reference/api.md)** → API, REST API, HTTP endpoints, API reference, Agent API, AiSettings API, AiFunction API, AI Audio API, AI Image API, KnowledgeBase API, Matchmaking API, Web Utilities API, Database API, Extraction API, apiKeyAuth, agentApiKeyAuth, x-squid-agent-api-key, memoryOptions, memoryId, memoryMode
33
33
  - **[openai.md](reference/openai.md)** → OpenAI, code interpreter, verbosity, reasoning models, o1, o3, gpt-5, DALL-E, Whisper, TTS, voice options, structured output, file upload
34
34
  - **[connectors.md](reference/connectors.md)** → connectors, integrations, IntegrationType, postgres, mongo, auth0, s3, kafka, salesforce, essentials connector, built-in connector, @squidcloud/slack-client, @squidcloud/jira-client, @squidcloud/github-client, @squidcloud/salesforce-client, @squidcloud/freshdesk-client, @squidcloud/linear-client, @squidcloud/mail-client, @squidcloud/google-calendar-client, @squidcloud/google-drive-client, @squidcloud/teams-client, @squidcloud/sharepoint-client, @squidcloud/servicenow_csm-client, @squidcloud/jira-jsm-client, SquidSlackClient, SquidJiraClient, SquidGitHubClient, sendMail, sendMessage, searchIssues, createIssue, connector SDK
35
35
  - **Integrations** → See `squid-integrations` skill for full catalog, connector AI functions, SDK methods, and configuration
@@ -5,6 +5,7 @@ This document covers programmatic management of Squid organizations, application
5
5
  ## Contents
6
6
  - ManagementClient
7
7
  - Admin Integrations
8
+ - Admin Connectors
8
9
  - Admin Secrets
9
10
 
10
11
  ## ManagementClient
@@ -204,6 +205,35 @@ const openApiSchema = await integrations.discoverOpenApiSchema('my-api', {
204
205
  const fileSchema = await integrations.discoverOpenApiSchemaFromFile('my-api');
205
206
  ```
206
207
 
208
+ ### Admin - Connectors
209
+
210
+ Attach and detach **app-level (integration-less) connectors**. Unlike integrations, these carry no
211
+ per-connector configuration record — attaching one adds it to the application's `appConnectors` list
212
+ and (re)deploys its bundle. Any configuration the connector needs comes in out of band, e.g. via
213
+ application secrets.
214
+
215
+ ```typescript
216
+ const connectors = squid.admin().connectors();
217
+
218
+ // List the connectors explicitly attached to the app
219
+ const attached = await connectors.list(); // Array<ConnectorId>
220
+
221
+ // Attach (idempotent - attaching an already-attached connector is a no-op)
222
+ await connectors.connect('slack');
223
+
224
+ // Detach
225
+ await connectors.disconnect('slack');
226
+ ```
227
+
228
+ `ConnectorId` is one of: `bitbucket`, `confluence`, `cotomi`, `essentials`, `freshdesk`, `github`,
229
+ `google_calendar`, `google_drive`, `hubspot`, `sharepoint`, `jira`, `jira_jsm`, `linear`, `mail`,
230
+ `onedrive`, `outlook`, `salesforce`, `servicenow_csm`, `slack`, `teams`, `ticketmind_companion`,
231
+ `zendesk`.
232
+
233
+ **Connectors vs integrations:** use `squid.admin().integrations()` when the connection needs
234
+ configuration (credentials, a connection string, an OAuth app). Use `connectors()` for connectors the
235
+ app just needs turned on.
236
+
207
237
  ### Admin - Secrets
208
238
 
209
239
  ```typescript
@@ -54,6 +54,21 @@ import {
54
54
  } from '@squidcloud/client';
55
55
  ```
56
56
 
57
+ **Active chat models** (the constants above hold active models only; deprecated ones carry a
58
+ `replacedBy` and are excluded unless asked for):
59
+
60
+ | Provider | `*_CHAT_MODEL_NAMES` |
61
+ |---|---|
62
+ | OpenAI | `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` |
63
+ | Anthropic | `claude-fable-5`, `claude-haiku-4-5-20251001`, `claude-opus-5`, `claude-sonnet-5` |
64
+ | Gemini | `gemini-3.1-pro`, `gemini-3.6-flash`, `gemini-3.5-flash-lite` |
65
+ | Grok | `grok-4.5`, `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning` |
66
+
67
+ This list moves. **Don't hardcode a model name from this table into user code** — call
68
+ `squid.ai().listChatModels()`, which also returns the app's custom integration models
69
+ (`openai_compatible`, Bedrock, Vertex) and flags deprecated ones. See
70
+ [Application Settings](#application-settings).
71
+
57
72
  **Model Categories:**
58
73
  - **Chat Models**: Used for AI Agents, AI Query, etc. (OpenAI, Anthropic, Gemini, Grok)
59
74
  - **Embedding Models**: Used for Knowledge Bases (OpenAI, Voyage)
@@ -76,6 +91,7 @@ An AI Agent can:
76
91
  - Collaborate with other AI agents
77
92
  - Process voice input and generate voice output
78
93
  - Accept files as part of chat requests
94
+ - Refuse prompts that contain PII before they reach the model (see Rejecting PII in prompts)
79
95
 
80
96
  ### Creating and Managing Agents
81
97
 
@@ -99,7 +115,7 @@ await myAgent.upsert({
99
115
  isPublic: false, // Whether the agent is publicly accessible
100
116
  auditLog: true, // Enable audit logging for compliance
101
117
  options: {
102
- model: 'gpt-5-mini', // or 'claude-sonnet-4-5-20250929', 'gemini-3-flash'
118
+ model: 'gpt-5.4-mini', // or 'claude-sonnet-5', 'gemini-3.6-flash' - see Supported Models
103
119
  instructions: 'You are a helpful customer support assistant. Be concise and professional.',
104
120
  temperature: 0.7
105
121
  }
@@ -111,7 +127,7 @@ console.log(agentInfo.id, agentInfo.description, agentInfo.options.model);
111
127
 
112
128
  // Update specific properties
113
129
  await myAgent.updateInstructions('You are a technical support specialist.');
114
- await myAgent.updateModel('claude-sonnet-4-5-20250929');
130
+ await myAgent.updateModel('claude-sonnet-5');
115
131
  await myAgent.updateGuardrails(['no-harmful-content']);
116
132
 
117
133
  // Delete an agent
@@ -140,8 +156,85 @@ await myAgent.updateConnectedAgents([
140
156
  // Update or delete custom guardrails
141
157
  await myAgent.updateCustomGuardrails('Never reveal sensitive information');
142
158
  await myAgent.deleteCustomGuardrail();
159
+
160
+ // Refuse prompts that contain PII (see "Rejecting PII in prompts" below)
161
+ await myAgent.updatePii({ onDetect: 'reject' });
162
+
163
+ // Expose the agent itself as an MCP server (see "Agent as an MCP server" below)
164
+ await myAgent.updateMcpServer({ enabled: true, requireApiKey: true });
165
+ ```
166
+
167
+ ### Agent as an MCP Server
168
+
169
+ Any agent can be published as an MCP server at `/mcp/<agentId>` with **no backend code** — the server
170
+ exposes a single `ask` tool that calls the agent. This is the inverse of `@mcpServer`, which exposes
171
+ *your* backend tools to agents. Transport is the official MCP **Streamable HTTP** transport.
172
+ `updateMcpServer` requires an API key.
173
+
174
+ ```typescript
175
+ // AI-generate the server + tool descriptions from the agent's instructions and connected resources.
176
+ // Returns them WITHOUT persisting - save them with updateMcpServer.
177
+ const generated = await myAgent.generateMcpDescriptions();
178
+
179
+ await myAgent.updateMcpServer({
180
+ enabled: true,
181
+ description: generated.description, // served as `instructions` in the MCP initialize result
182
+ toolDescription: generated.toolDescription, // description of the `ask` tool in the tools manifest
183
+ requireApiKey: true, // require the agent's API key as a bearer token
184
+ oauthIntegrationId: 'auth0' // or OAuth-protect it with an auth integration
185
+ });
186
+ ```
187
+
188
+ **`AiAgentMcpServerConfig`:**
189
+ - `enabled` — whether the agent is exposed as an MCP server.
190
+ - `description` — defaults to the agent description when omitted.
191
+ - `toolDescription` — defaults to a generic ask-the-agent description.
192
+ - `requireApiKey` — when true, requests must present the agent's API key as
193
+ `Authorization: Bearer <agent API key>` or the `x-squid-agent-api-key` header.
194
+ - `oauthIntegrationId` — OAuth-protects the endpoint with an auth integration's bearer tokens.
195
+ - Combining `oauthIntegrationId` and `requireApiKey` accepts **either** credential.
196
+ - **With neither set, the MCP endpoint is public.**
197
+
198
+ ### Rejecting PII in prompts
199
+
200
+ `pii` refuses an incoming prompt that carries personal information **before it reaches the
201
+ agent's model**. The prompt is not answered, no quota is consumed, and nothing is written to
202
+ chat memory. The caller gets an error whose message starts with `PII_DETECTED_IN_PROMPT`.
203
+
204
+ Do not confuse it with `guardrails.disablePii`, which is the opposite direction — that one asks
205
+ the agent's own model not to *emit* PII in its answer, and is best-effort prompt text. Use `pii`
206
+ when the requirement is that the data must not reach the model at all.
207
+
208
+ ```typescript
209
+ await squid.ai().agent('support-agent').updatePii({
210
+ onDetect: 'reject', // 'off' (default) disables screening
211
+ entities: ['email', 'ssn'], // omit for every kind
212
+ customRules: ['internal case numbers like CASE-12345'],
213
+ allowList: ['support@squid.cloud'], // literal values that never count as PII
214
+ classifierModel: 'gpt-5.6-luna', // default; small and fast
215
+ });
143
216
  ```
144
217
 
218
+ | Field | Meaning |
219
+ |---|---|
220
+ | `onDetect` | `'reject'` refuses the prompt; `'off'` (default) disables screening |
221
+ | `entities` | Kinds to screen for: `email`, `phoneNumber`, `creditCard`, `ssn`, `iban`, `passport`. Omitted means all |
222
+ | `customRules` | App-specific PII in plain language — anything a model can recognize from a description |
223
+ | `allowList` | Exact values that must never be flagged |
224
+ | `classifierModel` | Model doing the screening. Defaults to `gpt-5.6-luna` |
225
+
226
+ Things to know before enabling it:
227
+
228
+ - **Every prompt costs one extra model call.** Screening runs ahead of the agent's own model, so
229
+ it adds latency and tokens to each turn.
230
+ - **It is prompt-side only.** Tool results and knowledge-base chunks can still bring PII into the
231
+ model. Don't describe it to a user as an end-to-end guarantee.
232
+ - **It is fail-closed.** If the classifier is unreachable the turn is rejected with
233
+ `PII_SCREENING_UNAVAILABLE` rather than admitted unscreened.
234
+ - **It cannot be overridden per request.** Passing `pii` in `ask`/`chat` options is ignored; the
235
+ stored agent's value always wins, so a caller cannot opt out of its own screening.
236
+ - **Rejections are audited with the prompt redacted** — matched kinds are recorded, never values.
237
+
145
238
  ### Agent Lifecycle
146
239
 
147
240
  1. **Creation** - Use `upsert()` to create a new agent with an ID
@@ -165,6 +258,8 @@ Agents become powerful when connected to resources:
165
258
  - **Backend functions** (`functions`) - Custom logic via `@aiFunction` decorators
166
259
  - **Integrations** (`connectedIntegrations`) - Database/API queries
167
260
  - **Knowledge bases** (`connectedKnowledgeBases`) - RAG (Retrieval Augmented Generation)
261
+ - **Source repositories** (a `github`/`bitbucket` integration with code analysis enabled) - see
262
+ [Source-Code Analysis](#source-code-analysis)
168
263
 
169
264
  #### Connected Agents
170
265
 
@@ -258,6 +353,8 @@ const response = await agent.ask('What is our return policy?', {
258
353
  });
259
354
  ```
260
355
 
356
+ Knowledge bases with a knowledge graph give the agent graph retrieval and a `queryKnowledgeGraph` tool automatically — see [Knowledge Graph (GraphRAG)](#knowledge-graph-graphrag).
357
+
261
358
  ### Agent Chat Methods
262
359
 
263
360
  ```typescript
@@ -268,13 +365,20 @@ const agent = squid.ai().agent('my-agent');
268
365
  // - NO connected resources: streams token-by-token
269
366
  // - HAS connected resources: emits ONCE with complete response
270
367
  const chatObs = agent.chat('What is your return policy?', {
271
- // Memory management
368
+ // Memory management. `memoryOptions` is the only way to name a conversation and control
369
+ // history: `memoryId` names it, `memoryMode` controls whether history is read and written.
272
370
  memoryOptions: {
273
371
  memoryMode: 'read-write', // 'none' | 'read-only' | 'read-write'
274
372
  memoryId: 'user-123', // Unique per user/session
275
373
  expirationMinutes: 1440 // 24 hours
276
374
  },
277
375
 
376
+ // Usage tracking: reported as `annotation.<key>` tags on Squid AI usage metrics, so token
377
+ // usage can be filtered/grouped by them. Inherited by nested connected-agent calls.
378
+ // Limits: max 10 entries, keys <= 64 chars, values <= 256 chars; excess is dropped/truncated.
379
+ // Set app-wide defaults with squid.setMetricAnnotations({...}); per-call values merge on top.
380
+ metricAnnotations: { feature: 'support-bot', requestSource: 'mobile' },
381
+
278
382
  // Connected resources (can also be set on agent.upsert)
279
383
  connectedAgents: [{ agentId: 'specialist-agent', description: 'Handles X' }],
280
384
  functions: ['function1', 'function2'],
@@ -282,7 +386,7 @@ const chatObs = agent.chat('What is your return policy?', {
282
386
  connectedKnowledgeBases: [{ knowledgeBaseId: 'kb1', description: 'When to use this KB' }],
283
387
 
284
388
  // Model & generation
285
- model: 'gpt-5-mini', // Override agent's default model
389
+ model: 'gpt-5.4-mini', // Override agent's default model
286
390
  temperature: 0.7,
287
391
  maxTokens: 4000,
288
392
  maxOutputTokens: 2000,
@@ -305,6 +409,15 @@ const chatObs = agent.chat('What is your return policy?', {
305
409
  maxAiCallStackSize: 5
306
410
  },
307
411
 
412
+ // Refuse incoming prompts that carry PII (stored on the agent; ignored if passed per request)
413
+ pii: {
414
+ onDetect: 'reject',
415
+ entities: ['email', 'phoneNumber', 'creditCard', 'ssn', 'iban', 'passport'],
416
+ customRules: ['internal case numbers like CASE-12345'],
417
+ allowList: ['support@squid.cloud'],
418
+ classifierModel: 'gpt-5.6-luna'
419
+ },
420
+
308
421
  // Files & voice
309
422
  fileUrls: [
310
423
  { id: 'file1', type: 'image', purpose: 'context', url: 'https://...', description: 'Product image' }
@@ -320,7 +433,7 @@ const chatObs = agent.chat('What is your return policy?', {
320
433
  useCodeInterpreter: 'llm', // 'none' | 'llm' (OpenAI/Gemini only)
321
434
  executionPlanOptions: {
322
435
  enabled: true,
323
- model: 'gpt-5-mini',
436
+ model: 'gpt-5.4-mini',
324
437
  reasoningEffort: 'high',
325
438
  allowClarificationQuestions: false
326
439
  },
@@ -436,6 +549,19 @@ const searchResults = await kb.search({
436
549
  limit: 10
437
550
  });
438
551
 
552
+ // Keyword (lexical, embedding-free) search — best for exact tokens: IDs, error codes, SKUs, file names.
553
+ // searchMode: 'vector' | 'hybrid' | 'keyword' | 'graph'. Default: 'hybrid' ('graph' on GraphRAG-enabled
554
+ // KBs; setting 'graph' on other KBs throws). Keyword mechanics differ by the KB's vectorDbType:
555
+ // mongoAtlas = ranked BM25, term-optional (partial matches still rank); postgres = unranked boolean
556
+ // filter where EVERY whitespace-separated term must appear as a literal substring — a multi-word
557
+ // prompt returns zero chunks when any one term is absent, so pass few, specific tokens.
558
+ // On postgres 'hybrid' degrades to dense-only (no lexical channel; hybridWeights is ignored off
559
+ // Mongo), so exact-token retrieval there REQUIRES explicitly setting searchMode: 'keyword'.
560
+ const keywordResults = await kb.search({
561
+ prompt: 'ERR_0000_4F2A',
562
+ searchMode: 'keyword'
563
+ });
564
+
439
565
  // Get context
440
566
  const context = await kb.getContext('doc-123');
441
567
 
@@ -443,14 +569,229 @@ const context = await kb.getContext('doc-123');
443
569
  const allContexts = await kb.listContexts(1000); // truncateTextAfter
444
570
  const contextIds = await kb.listContextIds();
445
571
 
572
+ // List a PAGE of contexts (prefer this over listContexts for large KBs)
573
+ const page = await kb.listContextsPage({
574
+ offset: 0,
575
+ limit: 50,
576
+ truncateTextAfter: 500,
577
+ search: 'invoice' // case-insensitive substring match on id/title only
578
+ });
579
+
446
580
  // Download context
447
581
  const download = await kb.downloadContext('doc-123');
448
582
 
449
583
  // Delete contexts
450
584
  await kb.deleteContext('doc-123');
451
585
  await kb.deleteContexts(['doc-1', 'doc-2']);
586
+
587
+ // AI-generate descriptions for the KB's metadata fields (helps metadata filtering quality).
588
+ // Returns { fields } WITHOUT persisting - save them yourself via upsertKnowledgeBase.
589
+ // By default only fields with an empty description are generated.
590
+ const { fields } = await kb.generateMetadataFieldDescriptions({
591
+ fieldNames: ['category'], // optional; omitted => all declared fields
592
+ overwriteExisting: false // true regenerates fields that already have a description
593
+ });
594
+ ```
595
+
596
+ `upsertContexts` accepts an options object with an `onUploaded` callback, fired once the payload has
597
+ been uploaded and accepted — server-side extraction and embedding continue after it, and the promise
598
+ still resolves only when ingestion completes:
599
+
600
+ ```typescript
601
+ await kb.upsertContexts(contexts, files, { onUploaded: () => setStatus('Uploaded, indexing…') });
602
+ ```
603
+
604
+ ### Metadata Filters
605
+
606
+ A MongoDB-style grammar shared across KB context filtering, metrics tag filtering, and matchmaking
607
+ (the types live in `@squidcloud/client`). Used by `kb.search`, `kb.grep`, and the chat option
608
+ `contextMetadataFilterForKnowledgeBase`.
609
+
610
+ ```typescript
611
+ // A bare scalar is shorthand for $eq
612
+ { category: 'user-guide' }
613
+
614
+ // Field operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists
615
+ { version: { $gte: 2 }, status: { $in: ['published', 'review'] }, archivedAt: { $exists: false } }
616
+
617
+ // Boolean combinators
618
+ { $or: [{ category: 'faq' }, { $and: [{ category: 'guide' }, { version: { $gte: 2 } }] }] }
619
+ ```
620
+
621
+ **`$underPath` (knowledge bases only)** — segment-safe subtree match on a hierarchical string field
622
+ such as `folderPath`:
623
+
624
+ ```typescript
625
+ { folderPath: { $underPath: 'reports/2023' } }
626
+ ```
627
+
628
+ Matches `reports/2023` and `reports/2023/q1/deck`, but never the lexical sibling
629
+ `reports/2023 drafts` — the `/` separator supplies the segment boundary, so subtree scoping cannot
630
+ leak into a sibling that merely shares a prefix. Leading/trailing `/` in the operand are stripped, an
631
+ empty operand matches every document that *has* the field, and matching is **case-sensitive**.
632
+
633
+ The canonical `folderPath` form: POSIX `/` separators, no leading/trailing slash, `''` (not
634
+ `undefined`) for the upload root, case preserved, NFC-normalized. `folderPath` is an ordinary
635
+ metadata key, not a reserved one.
636
+
637
+ `$underPath` is deliberately absent from the shared filter type that metrics and matchmaking consume
638
+ — they reject unknown operators at runtime.
639
+
640
+ ### Literal Scan (grep)
641
+
642
+ `grep()` scans the KB's **raw extracted text** (pre-chunking, so spreadsheet rows appear verbatim)
643
+ for a literal string and returns every matching line with its file and locator. Use it when you need
644
+ characters, not meaning: IDs, error codes, SKUs, exact phrases.
645
+
646
+ ```typescript
647
+ const hits = await kb.grep('ERR_0000_4F2A', {
648
+ metadataFilter: { category: 'runbooks' }, // same grammar as chunk search, incl. $and/$or/$underPath
649
+ maxMatches: 100
650
+ });
651
+ ```
652
+
653
+ - The pattern is **always literal** — every regex metacharacter is escaped — and may span lines.
654
+ - Matching is case-insensitive **for ASCII letters only**: `acme` finds `ACME`, but `café` does not
655
+ find `CAFÉ`. Search the exact casing for non-ASCII text.
656
+ - `metadataFilter` selects contexts **before** any text is matched.
657
+
658
+ ### Knowledge Graph (GraphRAG)
659
+
660
+ A graph-enabled knowledge base extracts an entity/relationship graph at ingest and layers concepts on top (document nodes, themes clustered from the entity graph, facet trees from context metadata). Retrieval can then follow links across documents — answering questions whose supporting facts are split over several files — and agents can navigate corpus structure instead of only quoting passages.
661
+
662
+ Requirements: `vectorDbType: 'mongoAtlas'` (immutable after creation — a `postgres` KB can never gain a graph). `graphRag` itself is mutable: enabling it on an existing Atlas KB auto-backfills the graph; disabling only makes graph search unavailable and RETAINS the extracted graph data, so re-enabling is cheap (a structural build folds in never-indexed contexts, reusing prior extraction). To actually reclaim the graph data, call `rebuildGraph()` while `graphRag.enabled` is false — that is the explicit cleanup path; otherwise only deleting the KB drops it. Extraction runs an LLM over every chunk at ingest — budget before enabling on a large corpus, and track spend via `getGraphStatus()`.
663
+
664
+ ```typescript
665
+ // Enable at creation, or later by upserting with the current config spread — upserting
666
+ // graphRag replaces the whole object, so omitted fields are lost.
667
+ await kb.upsertKnowledgeBase({
668
+ description: 'Company filings',
669
+ vectorDbType: 'mongoAtlas', // Required for the graph.
670
+ graphRag: {
671
+ enabled: true,
672
+ // Optional extraction hint; proper nouns (orgs, people, places) extract far more
673
+ // reliably than generic product categories.
674
+ entityTypes: ['ORGANIZATION', 'PRODUCT', 'LOCATION'],
675
+ // Other fields: extractionModel, autoBuildDebounceMs (quiet window before automatic
676
+ // concept builds, default 5 min), concepts: { facets: 'auto' | [fields], pathFacets:
677
+ // [{ field: 'folderPath', type: 'path' }] } (folder tree from a path metadata field).
678
+ },
679
+ });
680
+
681
+ // Graph search: seeds entities from the prompt via vector search, walks relationships,
682
+ // maps reached entities back to chunks, fuses with hybrid search. 'graph' is the DEFAULT
683
+ // searchMode on a graph-enabled KB, so it can be omitted. Passing it explicitly on a KB
684
+ // without GraphRAG throws GRAPH_SEARCH_NOT_ENABLED — no silent fallback to hybrid.
685
+ const graphChunks = await kb.search({
686
+ prompt: 'Which drugs does Aldous Corporation sell?',
687
+ searchMode: 'graph',
688
+ graphOptions: { seedLimit: 8, maxHops: 2 }, // Defaults shown; caps 25 and 3.
689
+ });
690
+
691
+ // Like search() but returns the full response, including the traversed subgraph
692
+ // (entities + relationships) — for debugging retrieval or explaining results.
693
+ const response = await kb.searchWithGraphContext({
694
+ prompt: 'Which drugs does Aldous Corporation sell?',
695
+ searchMode: 'graph',
696
+ graphOptions: { includeGraphContext: true },
697
+ });
698
+
699
+ // Scope ANY search mode to the documents under one theme or facet; composes with
700
+ // contextMetadataFilter. Value: a concept name (resolved server-side) or a facet nodeId.
701
+ // Unresolvable concepts fail with CONCEPT_NOT_FOUND plus the nearest matching names.
702
+ await kb.search({ prompt: 'What was the purchase price?', graphFilter: { underConcept: 'Acquisitions' } });
703
+
704
+ // Structure queries — one round trip each. Ops: 'overview' (themes/facets/counts map),
705
+ // 'resolve' | 'describe' | 'subtree' | 'docsUnder' | 'neighborhood' (take ref),
706
+ // 'conceptsOf' (takes contextId), 'pathBetween' (ref + refB -> relationship chain, or the
707
+ // concepts both sit under when no chain exists), 'globalSummary' (takes query).
708
+ const overview = await kb.queryGraph({ op: 'overview' });
709
+ const path = await kb.queryGraph({ op: 'pathBetween', ref: 'Aldous Corporation', refB: 'Zalofen' });
710
+
711
+ // Build state, coverage and cost: contextsIndexed/contextsTotal, entityCount,
712
+ // relationshipCount, topics/facets, structureStale/staleDocCount, buildJob,
713
+ // nextAutoBuildAt, ingestUsage/lastRebuildUsage (LLM tokens + estimated USD).
714
+ const status = await kb.getGraphStatus();
715
+
716
+ // Force a concept-layer build now instead of waiting for the debounced automatic build.
717
+ // Default mode 'structural' reuses extracted entities (cheap). mode: 'full' wipes the
718
+ // graph and re-extracts every chunk with the LLM — cost on the order of the initial
719
+ // ingest; only needed after changing entityTypes or extractionModel.
720
+ await kb.rebuildGraph();
721
+
722
+ // Bounded entity-graph slice for visualization (highest-degree entities and their
723
+ // relationships). nodeLimit default 200, max 1000; pass topicId to restrict to one theme.
724
+ const subgraph = await kb.exploreGraph({ nodeLimit: 100 });
452
725
  ```
453
726
 
727
+ Gotchas:
728
+ - Entity extraction is triggered by ingestion but runs asynchronously off the request path: `upsertContexts()` resolving does NOT mean the graph is ready — poll `getGraphStatus()` until `contextsIndexed === contextsTotal` before relying on graph retrieval for just-ingested documents. Poll with a timeout rather than an unbounded `while` loop: that equality can legitimately never hold, because contexts past the graph chunk cap (next bullet) are never stamped, and a pod restart mid-extraction leaves the remaining contexts unstamped until the debounced auto-build sweep re-extracts them. Treat a shortfall that stops moving as one of those cases, not as work still in flight — `graphIndexingSkippedContextIds` from the upsert identifies the cap case. No rebuild is needed for extraction; what lags further is the concept layer (themes/facets read by `queryGraph`), rebuilt after a quiet window (default 5 min, `autoBuildDebounceMs`) or a manual `rebuildGraph()`. Only one rebuild runs per KB at a time (`JOB_ALREADY_EXISTS`).
729
+ - Per-KB graph chunk cap (a deployment-level limit whose value can change — detect it via the signals, don't design around a fixed number): contexts beyond it stay fully vector/keyword-searchable but are not graph-indexed. Upsert results flag them — the batch `upsertContexts()` returns `graphIndexingSkippedContextIds`, only the singular `upsertContext()` sets the `graphIndexingSkipped` boolean — and `getGraphStatus()` shows `contextsIndexed < contextsTotal`.
730
+ - Only facet nodeIds and document contextIds are stable handles. Theme and entity ids churn on every rebuild — resolve names each time instead of persisting ids.
731
+ - Agents connected to a graph-enabled KB pick it up automatically: `'graph'` becomes the search tool's default mode, the tool description carries a compact graph overview, and the agent gets a `queryKnowledgeGraph` tool backed by the ops above (agent status broadcast: `'Querying Knowledge Base Graph'`). Exception: an active app/per-KB metadata visibility filter suppresses the graph navigation surface (overview block, `underConcept`, the `queryKnowledgeGraph` tool) so out-of-scope concepts/entities cannot leak; graph search mode stays available, filter-scoped.
732
+
733
+ ### Bulk Ingestion
734
+
735
+ A durable, asynchronous lane for high-volume ingestion that runs contexts through provider batch APIs
736
+ instead of inline. Unlike `upsertContexts`, `bulkUpsertContexts` returns as soon as the request is
737
+ **staged** and never awaits completion.
738
+
739
+ ```typescript
740
+ // 1. Stage contexts (files passed positionally for `type: 'file'` contexts)
741
+ const { jobId, contextIds, duplicates } = await kb.bulkUpsertContexts(contexts, files);
742
+ // contextIds is index-aligned with `contexts`. A context rejected as a content duplicate - the KB
743
+ // already holds identical content, or an earlier context in this same call did - still occupies its
744
+ // slot, so cross-reference `duplicates` by contextId to see what was actually staged.
745
+
746
+ // 2a. Watch it
747
+ kb.observeBulkIngestionJob(jobId).subscribe(s => console.log(s.state, s.counts, s.files));
748
+
749
+ // 2b. …or poll it
750
+ const status = await kb.getBulkIngestionJob(jobId);
751
+ // { state, counts, providerBatchIds, files }
752
+
753
+ // Cancel: already-finalized contexts are kept; the job goes to `cancelled` once in-flight work drains
754
+ await kb.cancelBulkIngestionJob(jobId);
755
+ ```
756
+
757
+ **Large file sets — upload straight to storage.** The multipart path caps at **50 files, 100 MB per
758
+ file, and 256 MB of request body** (the body is buffered in memory server-side), so a single 150 MB
759
+ PDF is rejected there no matter how few files accompany it. Past any of the three, mint presigned
760
+ URLs and reference `stagedObjectKey` instead: core never holds the bytes and none of the multipart
761
+ caps apply. Two staging limits still bound every call whatever path the files arrived by: **10,000
762
+ contexts** and **256 MB of extracted content** — extracted, so base64 image payloads (~1.33x the raw
763
+ image bytes) count and a body that passed the multipart check can still be rejected during staging.
764
+ Split large corpora across jobs, which is what the CLI's `--batchSize` of 200 does:
765
+
766
+ ```typescript
767
+ const { uploads } = await kb.createBulkUploadUrls(fileNames); // max 500 names per call
768
+ for (const u of uploads) {
769
+ // requiredHeaders is empty on S3, but Azure Blob rejects the PUT with 400 MissingRequiredHeader without it
770
+ await fetch(u.uploadUrl, { method: 'PUT', body: bytes, headers: u.requiredHeaders });
771
+ }
772
+ await kb.bulkUpsertContexts(uploads.map(u => ({ type: 'file', stagedObjectKey: u.stagedObjectKey, /* ... */ })));
773
+ ```
774
+
775
+ **`observeBulkIngestionJob` semantics:**
776
+ - Completes on a terminal state (`completed`, `failed`, `cancelled`). A terminal `failed`/`cancelled`
777
+ arrives as a **normal emitted value** — the observable only errors on transport failure.
778
+ - Cold: each subscription registers its own server-side push. Share it (rxjs `share()`) for multiple
779
+ consumers.
780
+ - Re-subscribes automatically if the client connection id changes mid-subscription.
781
+ - If the app or KB is deleted mid-flight the job row is purged without a terminal push, so the
782
+ observable never completes — bound it (rxjs `timeout()`) when deletion is possible.
783
+
784
+ The CLI wraps this whole flow: `squid kb-upload --dir ./docs --knowledgeBase my-kb` (see
785
+ [backend.md](backend.md#cli-commands)).
786
+
787
+ ### Spreadsheet Understanding
788
+
789
+ Spreadsheet files (`.csv`, `.tsv`, `.xlsx`, `.xlsm`, `.xls`, `.xlsb`) uploaded as file context take a dedicated ingestion lane: instead of chunking raw cell text, Squid extracts the workbook structure (sheets, headers, hidden sheets; charts and pivot tables where the format exposes them — absent for CSV/TSV and `.xls`, pivots also absent for `.xlsb`) and embeds a generated whole-workbook summary, so search results describe what a workbook contains.
790
+
791
+ When a connected knowledge base holds spreadsheet contexts, the agent automatically gets a `querySpreadsheetsWithAi` tool that runs Python in a sandbox against the actual uploaded files — for exact values (counts, sums, lookups, filtering, sorting), joins/comparisons across several workbooks in one call, and structure/provenance questions (which sheets feed live calculations, formulas vs hardcoded cells; formula/dependency inspection is fullest for `.xlsx`/`.xlsm`, partial for `.xls`, values-only for `.xlsb`, absent for CSV/TSV). No configuration needed, but the lane and the tool depend on the retained original file: with `discardOriginalFile: true` the spreadsheet ingests as plain extracted text and gets no query tool.
792
+
793
+ Spreadsheets attached to a chat via `fileUrls` similarly get a per-file Python query tool. Detection is by file extension from `fileName` (or the URL path when `fileName` is absent; query strings are stripped, so a signed URL ending in `.xlsx?sig=...` is detected) — set `fileName` only when the URL path itself lacks the extension, e.g. opaque download routes.
794
+
454
795
  ## AI Files
455
796
 
456
797
  Manage files stored with AI providers (OpenAI, etc.) for use with agents.
@@ -553,7 +894,7 @@ const settings = await aiClient.getApplicationAiSettings();
553
894
 
554
895
  // Set application AI settings
555
896
  await aiClient.setApplicationAiSettings({
556
- defaultModel: 'gpt-5-mini',
897
+ defaultModel: 'gpt-5.4-mini',
557
898
  // ... other settings
558
899
  });
559
900
 
@@ -562,8 +903,20 @@ await aiClient.setAiProviderApiKeySecret(
562
903
  'openai', // providerType
563
904
  'OPENAI_API_KEY' // secret key name
564
905
  );
906
+
907
+ // List the chat models available to this application (Squid vendor models + custom integration models)
908
+ const models = await aiClient.listChatModels();
909
+ // Deprecated models (those carrying `replacedBy`) are excluded by default:
910
+ const withDeprecated = await aiClient.listChatModels({ includeDeprecated: true });
911
+
912
+ // List the AI functions the application exposes
913
+ const functions = await aiClient.listFunctions();
565
914
  ```
566
915
 
916
+ **Prefer `listChatModels()` over hardcoding a model name** — it reflects what the app can actually
917
+ use, including custom `openai_compatible`/Bedrock/Vertex integration models, and marks deprecated
918
+ models via `replacedBy`.
919
+
567
920
  ## Backend Decorators
568
921
 
569
922
  ### @aiFunction
@@ -619,6 +972,36 @@ async bookHotel(args: BookingArgs): Promise<string> {
619
972
 
620
973
  For `@secureAiAgent` and `@secureAiQuery`, see [security.md](security.md#ai-security).
621
974
 
975
+ ## Source-Code Analysis
976
+
977
+ An agent can answer questions about real repositories. Enable code analysis on a `github` or
978
+ `bitbucket` integration; the agent then gets an `analyzeCode` tool that clones the selected
979
+ repositories into an isolated workspace and runs a CLI coding agent over them.
980
+
981
+ ```typescript
982
+ // 1. Discover the repositories an integration can reach (admin client - requires an API key)
983
+ const integrations = squid.admin().integrations();
984
+ const { repositories } = await integrations.discoverSourceRepositories('my-github');
985
+ // repositories: { provider, providerRepositoryId, fullName, cloneUrl, webUrl, defaultBranch }[]
986
+
987
+ // Or, before the integration is saved:
988
+ const draft = await integrations.discoverSourceRepositoriesFromDraft(integrationInfo);
989
+
990
+ // 2. Select repositories on the integration and configure the agent's analysis options
991
+ // (AiAgentSourceCodeIntegrationOptions):
992
+ // codeAnalysisEnabled, analyzerModel, analyzerModelId, analyzerReasoningEffort, analyzerInstructions
993
+ ```
994
+
995
+ **Constants** (exported from both `@squidcloud/client` and `@squidcloud/backend`):
996
+ - `SOURCE_CODE_ANALYZE_FUNCTION_ID` = `'analyzeCode'` — the AI function id
997
+ - `SOURCE_CODE_ANALYZER_MODELS` = `['claude-code', 'codex']` — the CLI providers
998
+ - `SOURCE_CONTROL_PROVIDERS` / `SOURCE_CODE_INTEGRATION_TYPES` = `['github', 'bitbucket']`
999
+ - `SOURCE_CODE_MAX_REPOSITORIES` = 20
1000
+ - `SOURCE_CODE_MAX_SUPPLEMENTAL_INSTRUCTIONS_LENGTH` = 20,000 characters per instruction field
1001
+
1002
+ Selecting repositories and enabling analysis is usually done in the Console — the SDK path exists for
1003
+ automation.
1004
+
622
1005
  ## MCP (Model Context Protocol)
623
1006
 
624
1007
  Squid supports MCP for extending agent capabilities.
@@ -766,7 +1149,7 @@ configureAiFunctions(request: AiFunctionsConfiguratorRequest): AiFunctionsConfig
766
1149
  ## Best Practices
767
1150
 
768
1151
  1. **NEVER invoke LLMs directly** - Do NOT import `openai`, `@anthropic-ai/sdk`, or call LLM APIs directly. ALWAYS use Squid AI Agents via `squid.ai().agent()`. Squid agents provide built-in security, memory, streaming, function calling, guardrails, and model management. If you need an LLM call, create/use an AI agent.
769
- 2. **Use memoryOptions for AI conversations** - Not deprecated chatId
1152
+ 2. **Use memoryOptions for AI conversations** - `memoryOptions.memoryId` names the conversation, `memoryOptions.memoryMode` controls history
770
1153
  3. **Agent IDs are permanent** - Plan naming carefully, cannot be changed after creation
771
1154
  4. **Memory is enabled by default** - Set `memoryMode: 'none'` to disable conversation history
772
1155
  5. **Streaming behavior differs with connected resources** - No resources: streams token-by-token; Has resources: emits once with complete response
@@ -13,6 +13,15 @@ All API endpoints require an API key. Include it in the request headers:
13
13
  Authorization: Bearer YOUR_API_KEY
14
14
  ```
15
15
 
16
+ Two security schemes exist:
17
+
18
+ | Scheme | Header | Scope |
19
+ |---|---|---|
20
+ | `apiKeyAuth` | `x-app-api-key` | The application API key. Accepted everywhere. |
21
+ | `agentApiKeyAuth` | `x-squid-agent-api-key` | An **agent-scoped** key, accepted by the agent `ask` endpoints and by an agent's MCP endpoint when `requireApiKey` is set. |
22
+
23
+ Get an agent key with `squid.ai().agent(id).getApiKey()` / `regenerateApiKey()`.
24
+
16
25
  ## API Categories
17
26
 
18
27
  ### [Agent API](https://docs.getsquid.ai/reference-docs/api/#tag/Agent)
@@ -26,12 +35,27 @@ Manage AI agents. SDK: [ai.md](ai.md)
26
35
  - `POST /agent/updateGuardrails` - Configure guardrails
27
36
  - `POST /agent/updateCustomGuardrails` - Set custom guardrails
28
37
  - `POST /agent/deleteCustomGuardrails` - Remove custom guardrails
38
+ - `POST /agent/setAgentOptionInPath` - Set any agent option by path; how the SDK's `updatePii()` writes the `pii` policy (no dedicated PII endpoint)
29
39
  - `POST /agent/ask` - Send prompt, get response
30
40
  - `POST /agent/askWithAnnotations` - Get response with annotations
41
+ - `GET /agent/listAgents` - List the application's agents
31
42
  - `GET /agent/revisions/{agentId}` - List revisions
32
43
  - `POST /agent/restoreRevision` - Restore revision
33
44
  - `POST /agent/deleteRevision` - Delete revision
34
45
 
46
+ The `ask` / `askWithAnnotations` endpoints accept `agentApiKeyAuth` in addition to `apiKeyAuth`.
47
+
48
+ Exposing an agent as an MCP server (`updateMcpServer()`) and generating its MCP descriptions
49
+ (`generateMcpDescriptions()`) are SDK-only — they are not part of the published REST surface.
50
+
51
+ ### AiFunction API
52
+ List the AI functions the application exposes. SDK: [ai.md](ai.md)
53
+ - `GET /ai/function/listFunctions` - List AI functions
54
+
55
+ ### AiSettings API
56
+ Application-level AI settings. SDK: [ai.md](ai.md)
57
+ - `GET /ai/settings/listChatModels?includeDeprecated=false` - Chat models available to the app (Squid vendor models + custom integration models)
58
+
35
59
  ### [AI Audio API](https://docs.getsquid.ai/reference-docs/api/#tag/AI-Audio)
36
60
  Transcribe audio, text-to-speech. SDK: [ai.md](ai.md)
37
61
  - `POST /audio/transcribe` - Transcribe audio to text
@@ -51,7 +75,17 @@ Manage knowledge bases for RAG. SDK: [ai.md](ai.md)
51
75
  - `POST /knowledge-base/deleteContexts` - Delete contexts
52
76
  - `GET /knowledge-base/getContext/{knowledgeBaseId}/{contextId}` - Get context
53
77
  - `GET /knowledge-base/listContexts/{knowledgeBaseId}` - List contexts
54
- - `POST /knowledge-base/search` - Semantic search
78
+ - `GET /knowledge-base/listContextsPage/{knowledgeBaseId}` - List a page of contexts (offset/limit/search)
79
+ - `GET /knowledge-base/listKnowledgeBases` - List the application's knowledge bases
80
+ - `POST /knowledge-base/search` - Search (vector/hybrid/keyword/graph via `searchMode`; graph fields in the response when requested)
81
+ - `POST /knowledge-base/queryGraph` - Query graph structure (overview, resolve, describe, subtree, docsUnder, conceptsOf, neighborhood, pathBetween, globalSummary)
82
+ - `GET /knowledge-base/getGraphStatus/{knowledgeBaseId}` - Graph build status, coverage, and LLM cost
83
+ - `POST /knowledge-base/rebuildGraph` - Enqueue a graph rebuild (`structural` or `full`)
84
+ - `POST /knowledge-base/exploreGraph` - Bounded entity-graph slice for visualization
85
+
86
+ Literal scan (`kb.grep()`) and bulk ingestion (`kb.bulkUpsertContexts()` and friends) are SDK-only —
87
+ they are not part of the published REST surface. For bulk ingestion from a machine, use the
88
+ `squid kb-upload` CLI command.
55
89
 
56
90
  ### [Matchmaking API](https://docs.getsquid.ai/reference-docs/api/#tag/Matchmaking) *(deprecated)*
57
91
  Use `knowledgeBase().searchContextsWith*()` instead.
@@ -85,3 +119,14 @@ Create PDFs, extract data from documents. SDK: [client.md](client.md)
85
119
  | Knowledge Bases | `squid.ai().knowledgeBase()` | KnowledgeBase API |
86
120
  | Web Utilities | `squid.web()` | Web Utilities API |
87
121
  | Extraction | `squid.extraction()` | Extraction Utilities API |
122
+ | Chat models / AI functions | `squid.ai().listChatModels()` / `listFunctions()` | AiSettings API / AiFunction API |
123
+ | Events | `squid.events().emit()` | SDK only |
124
+ | LangGraph | `squid.langGraph(id)` | SDK only |
125
+ | KB literal scan, bulk ingestion | `kb.grep()`, `kb.bulkUpsertContexts()` | SDK only (bulk also via `squid kb-upload`) |
126
+
127
+ ## Chat Options: Naming a Conversation
128
+
129
+ The agent endpoints take conversation identity and history control under `memoryOptions`:
130
+ `memoryOptions.memoryId` names the conversation, and `memoryOptions.memoryMode`
131
+ (`'none' | 'read-only' | 'read-write'`) controls whether history is read and written. An unrecognized
132
+ option key is rejected with a **400** naming the option that carries it.
@@ -18,6 +18,7 @@ Docs: https://docs.getsquid.ai/reference-docs/backend/
18
18
  - Schedulers (@scheduler)
19
19
  - Rate Limiting (@limits)
20
20
  - Queue Message Handlers (@onQueueMessage)
21
+ - Events (@eventHandler)
21
22
  - Client Connection State (@clientConnectionStateHandler)
22
23
  - Cross-Service Communication (squidInject)
23
24
  - Using Squid Client in Backend
@@ -37,8 +38,10 @@ squid init backend --appId YOUR_APP_ID --apiKey YOUR_API_KEY --environmentId dev
37
38
 
38
39
  **`squid start`** - Runs backend locally with hot-reload. Connects to Squid Cloud via reverse proxy.
39
40
  ```bash
40
- cd backend && squid start
41
+ cd backend && squid start [--printBundle]
41
42
  ```
43
+ `--printBundle` makes the local dev server log the full application bundle data on startup instead of
44
+ just the webhooks — useful when checking what the build actually registered.
42
45
 
43
46
  **`squid deploy`** - Builds and deploys to Squid Cloud.
44
47
  ```bash
@@ -50,6 +53,20 @@ squid deploy [--apiKey KEY] [--environmentId prod] [--skipBuild]
50
53
  squid build [--dev] [--skip-version-check]
51
54
  ```
52
55
 
56
+ **`squid kb-upload`** - Bulk-ingests a local directory into a knowledge base using the durable bulk
57
+ ingestion pipeline (presigned uploads + provider batch APIs). Walks `--dir` recursively, stages files
58
+ in batches, and polls each job to completion.
59
+ ```bash
60
+ squid kb-upload --dir ./docs --knowledgeBase my-kb
61
+ ```
62
+ Options: `--dir` (required), `--knowledgeBase` (required), `--appId`/`--apiKey`/`--region`/`--environmentId`
63
+ (fall back to `SQUID_APP_ID`, `SQUID_API_KEY`, `SQUID_REGION`, `SQUID_ENVIRONMENT_ID`),
64
+ `--batchSize` (files per job, default 200, max 1000), `--extensions` (comma-separated allow-list;
65
+ defaults to pdf, docx, txt, md, html, csv, xlsx, xls, xlsm, xlsb, pptx), `--dryRun` (list files and
66
+ exit without contacting the server), `--timeoutMinutes` (per-job wait budget, default 120 — on
67
+ timeout the CLI reports the job as still running server-side and moves on).
68
+ Ctrl-C cancels the in-flight job. See [ai.md](ai.md#bulk-ingestion) for the SDK equivalent.
69
+
53
70
  **Extended logging** - Add to `.env`:
54
71
  ```env
55
72
  SQUID_LOG_TYPES=QUERY,MUTATION,AI,API,ERROR
@@ -500,6 +517,33 @@ export class MyService extends SquidService {
500
517
  - To secure the topic, use `@secureTopic` — see [security.md](security.md)
501
518
  - The handler receives each message individually (not batched)
502
519
 
520
+ ## Events (@eventHandler)
521
+
522
+ `@eventHandler(type)` subscribes a backend function to events emitted with
523
+ `squid.events().emit(...)` (see [client.md](client.md#events)). Unlike `@onQueueMessage`, **every**
524
+ handler registered for the type receives the event, so one event can fan out to several services.
525
+
526
+ ```typescript
527
+ import { SquidService, eventHandler } from '@squidcloud/backend';
528
+ import { TriggerEvent } from '@squidcloud/client';
529
+
530
+ export class OrderEventsService extends SquidService {
531
+ @eventHandler<{ orderId: string; total: number }>('order.created')
532
+ async onOrderCreated(event: TriggerEvent<{ orderId: string; total: number }>): Promise<void> {
533
+ console.log(event.id); // unique event id
534
+ console.log(event.type); // 'order.created'
535
+ console.log(event.payload.orderId);
536
+ }
537
+ }
538
+ ```
539
+
540
+ **Notes:**
541
+ - Delivery is at-least-once with **no ordering guarantee** — handlers must be idempotent and
542
+ order-independent.
543
+ - Emitting requires API key authentication; a user token is rejected.
544
+ - Multiple subscribers per type are supported (that is the point — use `@onQueueMessage` when you
545
+ want queue semantics instead).
546
+
503
547
  ## Client Connection State (@clientConnectionStateHandler)
504
548
 
505
549
  Handle client connection and disconnection events.
@@ -620,6 +664,18 @@ export class MyService extends SquidService {
620
664
  }
621
665
  ```
622
666
 
667
+ **`getSquid()` / `getPassiveSquid()`** — `getSquid()` returns the same instance backing `this.squid`.
668
+ `getPassiveSquid()` returns a dedicated **passive-mode** instance (HTTP-only, no WebSocket), kept
669
+ separate from the main one. Use it for one-off HTTP calls that should not open or keep a socket.
670
+
671
+ ```typescript
672
+ import { getSquid, getPassiveSquid } from '@squidcloud/backend';
673
+ ```
674
+
675
+ **Experimental:** `this.workspace` (a `WorkspaceClient`) gives a backend-only scratch directory
676
+ synced across pod replicas via the shared workspace server — distinct from the tenant's shared
677
+ storage.
678
+
623
679
  ## File Handling
624
680
 
625
681
  ```typescript
@@ -763,7 +763,7 @@ declare namespace JSX {
763
763
  squid-ai-instructions="Be concise and professional."
764
764
  squid-ai-functions="getOrderStatus,lookupAccount"
765
765
  squid-ai-connected-agents='[{"agentId":"billing-agent","description":"Handles billing questions"}]'
766
- squid-ai-override-model="gpt-5-mini"
766
+ squid-ai-override-model="gpt-5.4-mini"
767
767
  squid-ai-temperature="0.7"
768
768
  squid-ai-max-tokens="2000"
769
769
  squid-ai-agent-chat-options='{
@@ -18,6 +18,8 @@ Docs: https://docs.getsquid.ai/reference-docs/typescript-client/
18
18
  - Web
19
19
  - Extraction
20
20
  - Jobs
21
+ - Events
22
+ - LangGraph
21
23
  - Observability & Metrics
22
24
  - Custom Notifications
23
25
 
@@ -74,7 +76,7 @@ Every Squid application includes the **Essentials Connector** - a built-in integ
74
76
  |---------|--------|-------------|------|
75
77
  | **Web Utilities** | `squid.web()` | AI-powered web search, URL content extraction, short URLs | [Web section](#web) |
76
78
  | **AI Agents** | `squid.ai().agent()` | Chat with built-in or custom AI agents | [ai.md](ai.md) |
77
- | **Knowledge Bases** | `squid.ai().knowledgeBase()` | RAG with semantic search and reranking | [ai.md](ai.md) |
79
+ | **Knowledge Bases** | `squid.ai().knowledgeBase()` | RAG with semantic, keyword, and knowledge-graph search plus reranking | [ai.md](ai.md) |
78
80
  | **Image Generation** | `squid.ai().image()` | Generate images with DALL-E | [ai.md](ai.md) |
79
81
  | **Audio** | `squid.ai().audio()` | Transcription and text-to-speech | [ai.md](ai.md) |
80
82
  | **PDF/Extraction** | `squid.extraction()` | Create PDFs, extract data from documents | [Extraction section](#extraction) |
@@ -324,9 +326,9 @@ const result = await squid.withLock('payment-processing', async (lock) => {
324
326
  - `maxHoldTimeMillis` (number, default: no limit) - Max time in ms the lock can be held before automatic release. If not set, the lock is held until explicitly released or the connection is lost.
325
327
 
326
328
  **DistributedLock methods:**
327
- - `release()` - Release the lock
329
+ - `release()` - Release the lock. The lock is considered released immediately; the message to the server is sent asynchronously.
328
330
  - `isReleased()` - Check if already released
329
- - `observeRelease()` - Observable that emits when lock is released (including unexpected release due to connection loss)
331
+ - `observeRelease()` - Observable that emits when lock is released (including unexpected release due to connection loss). It emits as soon as the lock is considered released **locally**, without waiting for the server to confirm, and it replays — a subscriber that attaches after the release still learns about it.
330
332
  - `resourceId` - The mutex name
331
333
  - `lockId` - Unique lock instance ID
332
334
 
@@ -344,6 +346,12 @@ const web = squid.web();
344
346
  // AI-powered web search
345
347
  const results = await web.aiSearch('latest AI developments');
346
348
 
349
+ // aiSearch(query, abortSignal?, allowedDomains?)
350
+ // - abortSignal cancels the in-flight HTTP request, bounding a single call.
351
+ // - allowedDomains restricts the search (subdomains included); every cited URL comes from that list.
352
+ const controller = new AbortController();
353
+ const scoped = await web.aiSearch('pricing changes', controller.signal, ['getsquid.ai', 'docs.getsquid.ai']);
354
+
347
355
  // Get URL content (as markdown)
348
356
  const content = await web.getUrlContent('https://example.com/article');
349
357
 
@@ -416,6 +424,69 @@ if (job?.status === 'completed') {
416
424
  const result = await jobClient.awaitJob<Result>('job-123');
417
425
  ```
418
426
 
427
+ ### Driving a job from your own code
428
+
429
+ A job can also be created and resolved by the caller. Use this when a long operation runs outside
430
+ Squid and clients should still `awaitJob` on it. These three methods require an API key.
431
+
432
+ ```typescript
433
+ const jobId = crypto.randomUUID();
434
+
435
+ await jobClient.startJob(jobId); // mark the job as running
436
+ await jobClient.completeJob(jobId, { rows: 42 }); // resolve waiters with a result
437
+ await jobClient.failJob(jobId, 'Upstream timed out'); // reject waiters with an error
438
+ ```
439
+
440
+ ## Events
441
+
442
+ Emit a generic event that every backend function declared with `@eventHandler` for the same type
443
+ receives. Delivery is durable and at-least-once with **no ordering guarantee**, so handlers must be
444
+ idempotent and order-independent. `emit()` resolves once the event is enqueued, not once handlers ran.
445
+
446
+ Requires API key authentication — calls authenticated with a user token are rejected.
447
+
448
+ ```typescript
449
+ import { generateUUID } from '@squidcloud/client';
450
+
451
+ await squid.events().emit({
452
+ id: generateUUID(), // unique event id
453
+ type: 'order.created', // selects the @eventHandler subscribers
454
+ payload: { orderId: 'o-1', total: 99.5 },
455
+ });
456
+ ```
457
+
458
+ Handle it in the backend — see [backend.md](backend.md#events-eventhandler).
459
+
460
+ ## LangGraph
461
+
462
+ Invoke Python LangGraph graphs defined in a Python backend. Get a reference with
463
+ `squid.langGraph(graphId)`.
464
+
465
+ ```typescript
466
+ const graph = squid.langGraph('support-graph');
467
+
468
+ // Run and wait for the result (new thread when threadId is omitted)
469
+ const result = await graph.invoke({ input: { question: 'Where is my order?' } });
470
+ // result: { ok, threadId, status: 'completed' | 'interrupted', state, next }
471
+
472
+ // Run without holding a promise — returns a jobId to await later
473
+ const jobId = await graph.invokeAsync({ threadId: 'thread-1', input: { question: '...' } });
474
+ const later = await squid.job().awaitJob(jobId);
475
+
476
+ // Resume a graph paused on interrupt()
477
+ const resumed = await graph.resume('thread-1', { approved: true });
478
+ const resumeJobId = await graph.resumeAsync('thread-1', { approved: true });
479
+
480
+ // Inspect and clean up thread state
481
+ const state = await graph.getState('thread-1'); // { ok, threadId, state, next }
482
+ await graph.deleteThread('thread-1'); // idempotent; next run starts fresh
483
+ ```
484
+
485
+ Graph execution caps at the tenant IPC timeout (currently 4 minutes) for every variant — the `Async`
486
+ methods only free the caller from holding the promise.
487
+
488
+ Secure invocations with the backend `@secureLangGraph` decorator — see [security.md](security.md#securelanggraph).
489
+
419
490
  ## Observability & Metrics
420
491
 
421
492
  Report and query custom metrics for monitoring application performance.
@@ -48,6 +48,7 @@ The AI Studio provides visual tools for building and testing AI agents:
48
48
  ### Agent Configuration
49
49
  - Create and configure AI agents visually
50
50
  - Set agent instructions, models, and guardrails
51
+ - Refuse prompts containing PII under Guardrails → Prompt Privacy (toggle, a checkbox per kind, and custom rules)
51
52
  - Configure agent memory and conversation settings
52
53
  - Connect agents to functions, knowledge bases, and integrations
53
54
 
@@ -76,6 +77,10 @@ The AI Studio provides visual tools for building and testing AI agents:
76
77
  - Search and preview indexed content
77
78
  - Monitor indexing status
78
79
 
80
+ ### Knowledge Graph
81
+ - Enable via the **Knowledge Graph** toggle when creating a knowledge base with **Vector Store** set to `mongoAtlas` (disabled for other stores — the store is immutable after creation), or later by editing an Atlas knowledge base
82
+ - Knowledge base page shows a **Knowledge Graph** card: last build time, documents changed since it, next automatic build ETA, and a **Build now** button (runs a structural build)
83
+
79
84
  ## Integrations Setup
80
85
 
81
86
  ### Database Connections
@@ -8,6 +8,8 @@ Squid provides database functionality similar to Firestore but more powerful, wi
8
8
  - CRUD Operations
9
9
  - Real-time Subscriptions
10
10
  - Query Operators
11
+ - Querying by Document ID
12
+ - Deleting by Query
11
13
  - OR Queries
12
14
  - Join Queries
13
15
  - Dereference
@@ -231,6 +233,52 @@ const sorted = await users.query()
231
233
 
232
234
  **Note:** `offset()` does NOT exist - use `paginate()` for pagination.
233
235
 
236
+ ## Querying by Document ID
237
+
238
+ Filter on the document id with the `docId()` / `docIds()` shortcuts, or with `__docId__` (the
239
+ `DOC_ID_FIELD` constant) in `eq`/`neq`/`in`/`nin`/`where`. This works for both single- and
240
+ composite-key collections: pass the primitive for a single-field key, or the key object for a
241
+ composite one.
242
+
243
+ ```typescript
244
+ import { DOC_ID_FIELD } from '@squidcloud/client';
245
+
246
+ // Single-key collection
247
+ const one = await users.query().docId('user-1').snapshot();
248
+ const some = await users.query().docIds(['user-1', 'user-2']).snapshot();
249
+
250
+ // Composite-key collection - pass the key object
251
+ const line = await orderLines.query().docId({ orderId: 'o-1', lineNo: 3 }).snapshot();
252
+
253
+ // Equivalent explicit forms
254
+ await users.query().eq(DOC_ID_FIELD, 'user-1').snapshot();
255
+ await users.query().in(DOC_ID_FIELD, ['user-1', 'user-2']).snapshot();
256
+ await users.query().where(DOC_ID_FIELD, '==', 'user-1').snapshot();
257
+ await users.query().where(DOC_ID_FIELD, 'in', ['user-1', 'user-2']).snapshot();
258
+ ```
259
+
260
+ `docId()` is `eq(DOC_ID_FIELD, ...)` and `docIds()` is `in(DOC_ID_FIELD, ...)` — use them instead of
261
+ fetching a page and filtering client-side.
262
+
263
+ ## Deleting by Query
264
+
265
+ `delete()` on a query deletes every document the query selects and returns how many were selected.
266
+
267
+ ```typescript
268
+ const { deletedCount } = await users.query()
269
+ .eq('status', 'inactive')
270
+ .lt('lastLoginAt', cutoff)
271
+ .delete();
272
+ ```
273
+
274
+ `sortBy`, `limit`, and `limitBy` are rejected — `delete()` throws if the query carries any of them.
275
+ A query whose `in` condition is an empty array selects nothing, so it deletes nothing and returns
276
+ `{ deletedCount: 0 }`. An empty `nin` behaves the opposite way: it contributes no condition at all,
277
+ so it does not restrict the delete — `users.query().nin('id', []).delete()` deletes everything the
278
+ remaining conditions select, the whole collection when there are none. Narrow an empty exclusion list
279
+ in your own code before calling `delete()`. Deletes go through the collection's `delete` security
280
+ rules, same as `doc.delete()`.
281
+
234
282
  ## OR Queries
235
283
 
236
284
  Combine multiple queries with OR logic:
@@ -10,7 +10,7 @@ Squid provides a comprehensive security model using backend decorators. Every op
10
10
  - Storage Security
11
11
  - API Security
12
12
  - Native Query Security
13
- - AI Security
13
+ - AI Security (including @secureLangGraph)
14
14
  - Distributed Lock Security
15
15
  - GraphQL Security
16
16
  - Authentication Patterns
@@ -132,6 +132,34 @@ context.affectsPath(path) // Check if specific field path was modified
132
132
 
133
133
  Both types are exported from `@squidcloud/backend`.
134
134
 
135
+ ### @publicCollection
136
+
137
+ A **class** decorator that declares reads of a collection as intentionally public. Public reads
138
+ bypass security-rule evaluation entirely: Core serves them without calling backend code, so any
139
+ client can read the collection, **including unauthenticated ones**. Use it only for data that is
140
+ safe to expose to everyone; prefer a `@secureCollection` read rule when access depends on the caller.
141
+
142
+ ```typescript
143
+ import { SquidService, publicCollection, secureCollection, MutationContext } from '@squidcloud/backend';
144
+
145
+ // Anyone can read `articles`, but only authenticated users can write to it.
146
+ @publicCollection('articles', 'read')
147
+ export class ArticleService extends SquidService {
148
+ @secureCollection('articles', 'write')
149
+ async secureArticlesWrite(context: MutationContext): Promise<boolean> {
150
+ return this.isAuthenticated();
151
+ }
152
+ }
153
+ ```
154
+
155
+ - Signature: `publicCollection(collectionName, 'read', integrationId?)`. `integrationId` defaults to
156
+ the built-in database.
157
+ - Only `read` is supported. Writes always go through their security rules, so a public-read
158
+ collection can still restrict `insert`/`update`/`delete`/`write`.
159
+ - **Secure wins on conflict:** if a `@secureCollection` or `@secureDatabase` `read` (or `all`) rule
160
+ also applies to the same collection, that rule takes precedence at runtime and the public
161
+ declaration is ignored.
162
+
135
163
  ## Queue Security
136
164
 
137
165
  ### @secureTopic
@@ -270,6 +298,51 @@ allowAllAgents(): boolean {
270
298
  }
271
299
  ```
272
300
 
301
+ ### @secureLangGraph
302
+
303
+ Secures LangGraph invocations made with `squid.langGraph(graphId)` (see
304
+ [client.md](client.md#langgraph)).
305
+
306
+ ```typescript
307
+ import { SquidService, secureLangGraph, SecureLangGraphContext } from '@squidcloud/backend';
308
+
309
+ // Secure a specific graph
310
+ @secureLangGraph('support-graph')
311
+ allowSupportGraph(context: SecureLangGraphContext): boolean {
312
+ // context.operation: 'invoke' | 'resume' | 'getState' | 'deleteThread'
313
+ const userId = this.getUserAuth()?.userId;
314
+ if (!userId) return false;
315
+ // This app mints thread ids as `${userId}:${conversation}` and passes one on every call, so
316
+ // ownership is derived from threadId. Check it on every operation, not just deleteThread:
317
+ // threadId is a client-supplied field everywhere and threads carry no owner of their own, so
318
+ // this rule is all that stops one authenticated user from reading, extending, or deleting
319
+ // another's thread. An invoke that omits threadId is refused too: the runtime would mint a bare
320
+ // UUID, and no later call on that thread could satisfy this rule.
321
+ // Never derive ownership from context.input — it is populated on invoke only, so the comparison
322
+ // would be undefined === undefined, which passes for exactly the callers it should refuse.
323
+ return !!context.threadId && context.threadId.startsWith(`${userId}:`);
324
+ }
325
+
326
+ // Secure all graphs
327
+ @secureLangGraph()
328
+ allowAllGraphs(context: SecureLangGraphContext): boolean {
329
+ return this.isAuthenticated();
330
+ }
331
+ ```
332
+
333
+ `SecureLangGraphContext` carries `operation`, `graphId`, `threadId`, `input`, and `resumePayload`.
334
+ Only the fields the triggering request itself carries are populated: `input` on `invoke` only,
335
+ `resumePayload` on `resume` only, and `threadId` on everything except an `invoke` that starts a new
336
+ thread. A rule that reads a field its operation does not carry sees `undefined`, so write ownership
337
+ checks that cannot pass on `undefined` — otherwise an unauthenticated caller, whose `getUserAuth()`
338
+ is also `undefined`, is the one the rule lets through.
339
+
340
+ A thread has no owner at the storage layer and `threadId` reaches the rule straight from the client
341
+ body on every operation, so the `@secureLangGraph` rule is the only per-thread access control there
342
+ is. The `allowAllGraphs` shape above authenticates the caller but does not scope them to their own
343
+ threads — enough for a single-tenant graph, not for one where users must not see each other's
344
+ conversations.
345
+
273
346
  ## Distributed Lock Security
274
347
 
275
348
  ### @secureDistributedLock
@@ -12,7 +12,7 @@ Each Squid SaaS connector provides built-in AI functions that AI agents can call
12
12
  await squid.ai().agent('my-agent').upsert({
13
13
  description: 'Support agent',
14
14
  options: {
15
- model: 'gpt-5-mini',
15
+ model: 'gpt-5.4-mini',
16
16
  instructions: 'You are a support agent that can search tickets and send messages.'
17
17
  }
18
18
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squidcloud/cli",
3
- "version": "1.0.486",
3
+ "version": "1.0.488",
4
4
  "description": "The Squid CLI",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@squidcloud/local-backend": "^1.0.486",
31
+ "@squidcloud/local-backend": "^1.0.488",
32
32
  "adm-zip": "^0.5.16",
33
33
  "copy-webpack-plugin": "^14.0.0",
34
34
  "decompress": "^4.2.1",