@squidcloud/cli 1.0.487 → 1.0.489

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.
@@ -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.6-terra"
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,8 +76,8 @@ 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) |
78
- | **Image Generation** | `squid.ai().image()` | Generate images with DALL-E | [ai.md](ai.md) |
79
+ | **Knowledge Bases** | `squid.ai().knowledgeBase()` | RAG with semantic, keyword, and knowledge-graph search plus reranking | [ai.md](ai.md) |
80
+ | **Image Generation** | `squid.ai().image()` | Generate images with `gpt-image-*`, Stable Diffusion, or Flux | [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) |
81
83
  | **Observability** | `squid.observability` | Report and query custom metrics | [Observability section](#observability--metrics) |
@@ -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
@@ -151,7 +156,7 @@ const agent = squid.ai().agent('my-agent');
151
156
  await agent.upsert({
152
157
  description: 'Customer support agent',
153
158
  options: {
154
- model: 'gpt-4o',
159
+ model: 'gpt-5.6-terra',
155
160
  instructions: 'You are a helpful support agent...'
156
161
  }
157
162
  });
@@ -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:
@@ -7,7 +7,7 @@ OpenAI-specific features in Squid. For general AI usage, see [ai.md](ai.md).
7
7
  ```typescript
8
8
  import {
9
9
  OPENAI_CHAT_MODEL_NAMES, // Chat models
10
- OPENAI_IMAGE_MODEL_NAMES, // DALL-E
10
+ OPENAI_IMAGE_MODEL_NAMES, // gpt-image-*
11
11
  OPENAI_AUDIO_TRANSCRIPTION_MODEL_NAMES, // Whisper
12
12
  OPENAI_AUDIO_CREATE_SPEECH_MODEL_NAMES, // TTS
13
13
  OPENAI_EMBEDDINGS_MODEL_NAMES, // Embeddings
@@ -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.6-terra',
16
16
  instructions: 'You are a support agent that can search tickets and send messages.'
17
17
  }
18
18
  });
@@ -164,12 +164,13 @@ function TaskList({ userId }: { userId: string }) {
164
164
  const tasksCollection = useCollection<Task>('tasks');
165
165
  const query = tasksCollection.query().where('userId', '==', userId);
166
166
 
167
- const { loading, data, error } = useQuery(query, {
167
+ const { loading, data, error, enabled } = useQuery(query, {
168
168
  enabled: !!userId, // Only run when userId is set
169
169
  subscribe: true, // Real-time updates (default)
170
170
  initialData: [], // Initial data before first load
171
171
  }, [userId]); // Re-subscribe when userId changes
172
172
 
173
+ if (!enabled) return <div>No user selected</div>; // Check before `loading`
173
174
  if (loading) return <div>Loading...</div>;
174
175
  if (error) return <div>Error: {error.message}</div>;
175
176
 
@@ -201,9 +202,20 @@ interface QueryType<T> {
201
202
  loading: boolean;
202
203
  data: Array<T>;
203
204
  error: any;
205
+ enabled: boolean; // Mirrors options.enabled
204
206
  }
205
207
  ```
206
208
 
209
+ **`loading` stays `true` while the query is disabled.** A disabled query never subscribes, so it
210
+ never resolves — gate on `enabled` first, or `if (loading)` renders a spinner forever:
211
+
212
+ ```typescript
213
+ const { loading, data, error, enabled } = useQuery(query, { enabled: !!userId }, [userId]);
214
+
215
+ if (!enabled) return <SelectUserPrompt />; // Not "loading" - just not running
216
+ if (loading) return <Spinner />;
217
+ ```
218
+
207
219
  **Single Snapshot vs Real-time:**
208
220
  ```typescript
209
221
  // Real-time updates (subscribe: true) - default
@@ -940,7 +952,7 @@ function AdvancedFeatures() {
940
952
  const agent = squid.ai().agent('new-agent');
941
953
  await agent.upsert({
942
954
  description: 'My new agent',
943
- options: { model: 'gpt-4o' },
955
+ options: { model: 'gpt-5.6-terra' },
944
956
  });
945
957
  };
946
958
 
@@ -1054,13 +1066,18 @@ const { data } = useQuery(
1054
1066
  ### 2. Use `enabled` for Conditional Fetching
1055
1067
 
1056
1068
  ```typescript
1057
- // GOOD: Prevent unnecessary queries
1058
- const { data } = useQuery(query, { enabled: !!userId }, [userId]);
1069
+ // GOOD: Prevent unnecessary queries, and distinguish "disabled" from "loading"
1070
+ const { data, loading, enabled } = useQuery(query, { enabled: !!userId }, [userId]);
1071
+ if (!enabled) return <EmptyState />; // `loading` is still true here
1072
+ if (loading) return <Spinner />;
1059
1073
 
1060
1074
  // BAD: Query runs even when userId is null
1061
1075
  const { data } = useQuery(query, {}, [userId]);
1062
1076
  ```
1063
1077
 
1078
+ Only `useQuery` returns `enabled`. For the other hooks, track the same condition yourself rather
1079
+ than relying on `loading` to go false while they are disabled.
1080
+
1064
1081
  ### 3. Provide Type Parameters
1065
1082
 
1066
1083
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squidcloud/cli",
3
- "version": "1.0.487",
3
+ "version": "1.0.489",
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.487",
31
+ "@squidcloud/local-backend": "^1.0.489",
32
32
  "adm-zip": "^0.5.16",
33
33
  "copy-webpack-plugin": "^14.0.0",
34
34
  "decompress": "^4.2.1",