@squidcloud/cli 1.0.488 → 1.0.490

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
@@ -32174,7 +32174,8 @@ module.exports = function toBuffer(data, encoding) {
32174
32174
  var __webpack_unused_export__;
32175
32175
 
32176
32176
  __webpack_unused_export__ = ({ value: true });
32177
- __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.h3 = exports.VF = __webpack_unused_export__ = exports.F3 = exports.eS = exports.Eh = exports.dH = exports.mz = exports.Pq = exports.lZ = exports.I1 = exports.xV = exports.Kq = exports.oL = exports.hp = exports.AI_PROVIDER_TYPES = __webpack_unused_export__ = void 0;
32177
+ __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.h3 = exports.VF = __webpack_unused_export__ = exports.F3 = exports.eS = exports.Eh = exports.dH = exports.mz = exports.Pq = exports.lZ = exports.I1 = exports.xV = exports.Kq = exports.oL = exports.hp = exports.MD = exports.AI_PROVIDER_TYPES = __webpack_unused_export__ = void 0;
32178
+ __webpack_unused_export__ = isFileCapableProviderType;
32178
32179
  __webpack_unused_export__ = isVendorAiChatModelName;
32179
32180
  __webpack_unused_export__ = isAiEmbeddingsModelName;
32180
32181
  __webpack_unused_export__ = isIntegrationEmbeddingModelSpec;
@@ -32182,7 +32183,7 @@ __webpack_unused_export__ = isIntegrationModelSpec;
32182
32183
  /**
32183
32184
  * @category AI
32184
32185
  */
32185
- __webpack_unused_export__ = ['cohere', 'none'];
32186
+ __webpack_unused_export__ = ['cohere', 'voyage', 'none'];
32186
32187
  /** List of available AI provider types. See AiProviderType. */
32187
32188
  exports.AI_PROVIDER_TYPES = [
32188
32189
  'anthropic',
@@ -32204,35 +32205,47 @@ exports.AI_PROVIDER_TYPES = [
32204
32205
  'vertex',
32205
32206
  'external', // This is a technicality, referring to user-defined providers.
32206
32207
  ];
32208
+ /**
32209
+ * The AI providers that support file upload operations, and so are the only ones that resolve a
32210
+ * pre-uploaded file id. Declared as the list, with the type derived from it, so the two cannot drift:
32211
+ * a provider added to one is added to both.
32212
+ */
32213
+ exports.MD = [
32214
+ 'openai',
32215
+ 'gemini',
32216
+ 'anthropic',
32217
+ 'grok',
32218
+ ];
32219
+ /**
32220
+ * True when the provider resolves pre-uploaded file ids. Every other provider — the integration-backed
32221
+ * and user-defined ones, which report no vendor identity of their own — ignores `fileIds` outright, so
32222
+ * for them the ids are inert rather than wrong.
32223
+ */
32224
+ function isFileCapableProviderType(type) {
32225
+ return exports.MD.includes(type);
32226
+ }
32207
32227
  /**
32208
32228
  * Public OpenAI chat model names (active models only).
32209
32229
  * @category AI
32210
32230
  */
32211
- exports.hp = [
32212
- 'gpt-5.4-mini',
32213
- 'gpt-5.4-nano',
32214
- 'gpt-5.5',
32215
- 'gpt-5.5-pro',
32216
- 'gpt-5.6-sol',
32217
- 'gpt-5.6-terra',
32218
- 'gpt-5.6-luna',
32219
- ];
32231
+ exports.hp = ['gpt-5.5-pro', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'];
32220
32232
  /**
32221
32233
  * Public Gemini chat model names (active models only).
32222
32234
  * @category AI
32223
32235
  */
32224
- exports.oL = ['gemini-3.1-pro', 'gemini-3.6-flash', 'gemini-3.5-flash-lite'];
32236
+ exports.oL = ['gemini-3.1-pro', 'gemini-3.7-flash', 'gemini-3.5-flash-lite'];
32225
32237
  /**
32226
32238
  * Public Grok chat model names (active models only).
32227
32239
  * @category AI
32228
32240
  */
32229
- exports.Kq = ['grok-4.5', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning'];
32241
+ exports.Kq = ['grok-4.6', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning'];
32230
32242
  /**
32231
32243
  * Public Anthropic chat model names (active models only).
32232
32244
  * @category AI
32233
32245
  */
32234
32246
  exports.xV = [
32235
32247
  'claude-fable-5',
32248
+ 'claude-fable-5-1',
32236
32249
  'claude-haiku-4-5-20251001',
32237
32250
  'claude-opus-5',
32238
32251
  'claude-sonnet-5',
@@ -32258,7 +32271,7 @@ exports.lZ = ['text-embedding-3-small'];
32258
32271
  /**
32259
32272
  * @category AI
32260
32273
  */
32261
- exports.Pq = ['voyage-3-large'];
32274
+ exports.Pq = ['voyage-3-large', 'voyage-4-large', 'voyage-4'];
32262
32275
  /**
32263
32276
  * @category AI
32264
32277
  */
@@ -32380,6 +32393,7 @@ exports.CONNECTOR_IDS = [
32380
32393
  'cotomi',
32381
32394
  'essentials',
32382
32395
  'freshdesk',
32396
+ 'generic_site_ingester',
32383
32397
  'github',
32384
32398
  'google_calendar',
32385
32399
  'google_drive',
@@ -32410,6 +32424,7 @@ var __webpack_unused_export__;
32410
32424
 
32411
32425
  __webpack_unused_export__ = ({ value: true });
32412
32426
  exports.EL = exports.y4 = exports.q7 = exports.lO = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = __webpack_unused_export__ = exports.INTEGRATION_TYPES = __webpack_unused_export__ = void 0;
32427
+ __webpack_unused_export__ = isIntegrationDisabled;
32413
32428
  __webpack_unused_export__ = isBuiltInIntegrationId;
32414
32429
  /** @internal */
32415
32430
  __webpack_unused_export__ = 'ai_agents';
@@ -32447,11 +32462,13 @@ exports.INTEGRATION_TYPES = [
32447
32462
  'descope',
32448
32463
  'documentdb',
32449
32464
  'azure-entra-external-id',
32465
+ 'azure-entra-id',
32450
32466
  'dynamodb',
32451
32467
  'elasticsearch',
32452
32468
  'firebase_auth',
32453
32469
  'firestore',
32454
32470
  'gcs',
32471
+ 'generic_site_ingester',
32455
32472
  'github',
32456
32473
  'google_calendar',
32457
32474
  'google_docs',
@@ -32532,6 +32549,7 @@ __webpack_unused_export__ = [
32532
32549
  'descope',
32533
32550
  'firebase_auth',
32534
32551
  'azure-entra-external-id',
32552
+ 'azure-entra-id',
32535
32553
  ];
32536
32554
  /**
32537
32555
  * Auth integration types that can OAuth-protect an MCP server (see `McpOAuthOptions` and
@@ -32558,6 +32576,10 @@ exports.q7 = 'built_in_queue';
32558
32576
  * @category
32559
32577
  */
32560
32578
  exports.y4 = 'built_in_storage';
32579
+ /** Returns true when the integration is explicitly disabled. An absent or null `enabled` means enabled. */
32580
+ function isIntegrationDisabled(integration) {
32581
+ return integration.enabled === false;
32582
+ }
32561
32583
  /** Integration IDs used for built-in integrations by Squid. */
32562
32584
  exports.EL = [
32563
32585
  exports.lO,
@@ -33049,13 +33071,15 @@ exports.sQ = void 0;
33049
33071
  __webpack_unused_export__ = isKotlinPath;
33050
33072
  __webpack_unused_export__ = getEnvironmentPrefix;
33051
33073
  exports.getApplicationUrl = getApplicationUrl;
33074
+ __webpack_unused_export__ = getQueryKotlinServiceUrl;
33052
33075
  exports.sQ = [
33053
33076
  'application',
33054
- 'auth',
33055
33077
  'mutation',
33056
- 'native-query',
33057
33078
  'query',
33058
33079
  // Note: every `/ws/*` WebSocket path is served by the TypeScript core (port 8000), never Kotlin.
33080
+ // Note: `native-query` is absent on purpose — the TypeScript core answers it and forwards the
33081
+ // integration types it has no implementation for to Kotlin itself, via
33082
+ // {@link getQueryKotlinServiceUrl}.
33059
33083
  ];
33060
33084
  /**
33061
33085
  * Returns true if the given path is served by Kotlin core in local dev (port 8001). All `/ws/*`
@@ -33101,6 +33125,26 @@ function getApplicationUrl(environmentPrefix, appId, path, appIdPlaceholder, env
33101
33125
  const cleanPath = path.replace(/^\/+/, '');
33102
33126
  return cleanPath ? `${origin}/${cleanPath}` : origin;
33103
33127
  }
33128
+ /**
33129
+ * Returns the URL of [path] on the `core-query-kotlin` Service, addressed from inside the cluster.
33130
+ *
33131
+ * Naming the Service rather than building the public URL with {@link getApplicationUrl} keeps a
33132
+ * purely intra-cluster call off the external load balancer, which is what the Kotlin core's own
33133
+ * `getApplicationUrlInternal` already does for the hop in the other direction; and it makes the call
33134
+ * independent of where ingress sends [path], so moving that route cannot turn a forward into a
33135
+ * request back to this core.
33136
+ *
33137
+ * The Service is shared and not per-app, so the application is identified by the
33138
+ * `SquidHeaders.APP_ID` header rather than by the hostname. Local development has no cluster DNS and
33139
+ * keeps the per-app host on Kotlin's port.
33140
+ */
33141
+ function getQueryKotlinServiceUrl(environmentPrefix, appId, path) {
33142
+ if (isLocal(environmentPrefix)) {
33143
+ return getApplicationUrl(environmentPrefix, appId, path, undefined, undefined, true);
33144
+ }
33145
+ const cleanPath = path.replace(/^\/+/, '');
33146
+ return `http://core-query-kotlin.default.svc.cluster.local:8001/${cleanPath}`;
33147
+ }
33104
33148
  function isLocal(regionPrefix) {
33105
33149
  return /^local/.test(regionPrefix);
33106
33150
  }
@@ -34455,6 +34499,20 @@ const SIGINT_STAGING_WAIT_MILLIS = 10 * time_units_1.MILLIS_PER_SECOND;
34455
34499
  const DEFAULT_TIMEOUT_MINUTES = 120;
34456
34500
  /** Interval between `bulk/getJob` polls while waiting for a job to reach a terminal state. */
34457
34501
  const POLL_INTERVAL_MILLIS = 10 * time_units_1.MILLIS_PER_SECOND;
34502
+ /**
34503
+ * How old the last successful poll may be for the timeout classification to trust it. A status older
34504
+ * than this (every recent poll failed) says nothing about the job NOW, so the timeout falls back to the
34505
+ * conservative failure exit instead of waving a stale "still draining" snapshot through.
34506
+ */
34507
+ const STALE_STATUS_TOLERANCE_MILLIS = 3 * POLL_INTERVAL_MILLIS;
34508
+ /**
34509
+ * How recently a poll must have observed an ACTIVE provider batch for a timeout to defer when NO work
34510
+ * is queued: the no-pending grace for the tick-long gap between one batch's reconciliation and the
34511
+ * next stage's submission. With `requestsPending > 0` behind an observed batch the deferral holds
34512
+ * regardless of this window (queued work is stalled-but-alive under cluster load); a job past this
34513
+ * window with nothing pending is wedged and takes the failure exit.
34514
+ */
34515
+ const ACTIVE_BATCH_RECENCY_MILLIS = 5 * time_units_1.MILLIS_PER_MINUTE;
34458
34516
  /** Extensions ingested by default when `--extensions` is not provided. */
34459
34517
  const DEFAULT_EXTENSIONS = ['pdf', 'docx', 'txt', 'md', 'html', 'csv', 'xlsx', 'xls', 'xlsm', 'xlsb', 'pptx'];
34460
34518
  /** Job states that end a bulk-ingestion job's lifecycle (`BulkIngestionJobState` in core). */
@@ -34465,6 +34523,7 @@ const UNKNOWN_JOB_STATUS = {
34465
34523
  state: 'unknown',
34466
34524
  counts: { files: 0, finalized: 0, failed: 0, requestsPending: 0, requestsSubmitted: 0 },
34467
34525
  providerBatchIds: [],
34526
+ activeProviderBatchCount: 0,
34468
34527
  files: [],
34469
34528
  };
34470
34529
  /**
@@ -34733,9 +34792,41 @@ onStagingStarted) {
34733
34792
  console.log(`${batchLabel}: ${duplicates.length} file(s) skipped as content this knowledge base already holds.`);
34734
34793
  }
34735
34794
  activeJobIds.add(jobId);
34736
- const { finalStatus, timedOut } = await waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, isInterrupted);
34795
+ const { finalStatus, timedOut, statusStale, lastActiveBatchAtMillis } = await waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, isInterrupted);
34737
34796
  activeJobIds.delete(jobId);
34738
- return { batchIndex, batchCount, jobId, uploadFailures, duplicates, finalStatus, timedOut };
34797
+ // A duplicate found by the deferred extraction pass (a presigned object's bytes are first hashed
34798
+ // there) reports as a failed file with `errorCode: 'DUPLICATE_CONTENT'` — the same skip a
34799
+ // staging-time rejection is, so fold it into the duplicates the summary reconciles by name.
34800
+ const deferredDuplicates = (finalStatus?.files ?? [])
34801
+ .filter(file => file.errorCode === 'DUPLICATE_CONTENT')
34802
+ .map(file => file.title);
34803
+ if (deferredDuplicates.length > 0) {
34804
+ console.log(`${batchLabel}: ${deferredDuplicates.length} file(s) skipped as content this knowledge base already holds.`);
34805
+ }
34806
+ return {
34807
+ batchIndex,
34808
+ batchCount,
34809
+ jobId,
34810
+ uploadFailures,
34811
+ duplicates: [...duplicates, ...deferredDuplicates],
34812
+ finalStatus,
34813
+ timedOut,
34814
+ // Only a FRESH status may waive the timeout, and only on OBSERVED batch activity: an active batch
34815
+ // on the final poll; one seen within the recency window; or one seen at ANY point of this wait
34816
+ // while queued work remains (`requestsPending > 0`) — inter-stage stalls scale with cluster load
34817
+ // (the submit pass visits ~20 running jobs per 15s tick, and an app at its in-flight batch cap
34818
+ // waits for a sibling to drain), so pending-work-behind-an-observed-batch is stalled-but-alive,
34819
+ // not wedged. A job with batches long drained and NOTHING pending takes the failure exit. The
34820
+ // job's `transport` is deliberately NOT consulted: it is 'batch' from staging for every large
34821
+ // job, batches or none, so it cannot say whether anything is draining NOW.
34822
+ deferredInFlight: timedOut &&
34823
+ !statusStale &&
34824
+ finalStatus.state === 'running' &&
34825
+ ((finalStatus.activeProviderBatchCount ?? 0) > 0 ||
34826
+ (lastActiveBatchAtMillis > 0 &&
34827
+ (Date.now() - lastActiveBatchAtMillis < ACTIVE_BATCH_RECENCY_MILLIS ||
34828
+ finalStatus.counts.requestsPending > 0))),
34829
+ };
34739
34830
  }
34740
34831
  /** Mints presigned upload URLs for a batch, chunking requests to the server's per-call cap. */
34741
34832
  async function createUploadUrlsForBatch(client, fileNames) {
@@ -34793,7 +34884,10 @@ async function uploadFile(filePath, upload, stagedObjectKey) {
34793
34884
  async function waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, isInterrupted) {
34794
34885
  const deadline = Date.now() + timeoutMillis;
34795
34886
  let status;
34887
+ let statusAtMillis = 0;
34888
+ let lastActiveBatchAtMillis = 0;
34796
34889
  let isFirstPoll = true;
34890
+ let deferralAnnounced = false;
34797
34891
  while (!isInterrupted() && Date.now() < deadline && (!status || !TERMINAL_JOB_STATES.has(status.state))) {
34798
34892
  if (!isFirstPoll) {
34799
34893
  await sleep(POLL_INTERVAL_MILLIS);
@@ -34801,6 +34895,17 @@ async function waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, is
34801
34895
  isFirstPoll = false;
34802
34896
  try {
34803
34897
  status = await client.getJob(jobId);
34898
+ statusAtMillis = Date.now();
34899
+ if ((status.activeProviderBatchCount ?? 0) > 0) {
34900
+ lastActiveBatchAtMillis = statusAtMillis;
34901
+ if (!deferralAnnounced) {
34902
+ deferralAnnounced = true;
34903
+ console.log(`\n${batchLabel}: job ${jobId} submitted a provider batch — half-price processing whose tail can ` +
34904
+ `reach 24 hours, possibly past --timeoutMinutes. A timed-out wait leaves the job running ` +
34905
+ `without failing the run; the job finishes server-side, and re-runs are idempotent ` +
34906
+ `(already-staged content skips as duplicate).`);
34907
+ }
34908
+ }
34804
34909
  printProgress(batchLabel, jobId, status);
34805
34910
  }
34806
34911
  catch (error) {
@@ -34810,13 +34915,18 @@ async function waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, is
34810
34915
  process.stdout.write('\n');
34811
34916
  if (!status) {
34812
34917
  console.warn(`${batchLabel}: could not retrieve job ${jobId}'s status before the timeout.`);
34813
- return { finalStatus: UNKNOWN_JOB_STATUS, timedOut: true };
34918
+ return { finalStatus: UNKNOWN_JOB_STATUS, timedOut: true, statusStale: true, lastActiveBatchAtMillis: 0 };
34814
34919
  }
34815
34920
  const timedOut = !TERMINAL_JOB_STATES.has(status.state) && !isInterrupted();
34816
34921
  if (timedOut) {
34817
34922
  console.warn(`${batchLabel}: job ${jobId} did not finish within the timeout; it may still be running server-side.`);
34818
34923
  }
34819
- return { finalStatus: status, timedOut };
34924
+ return {
34925
+ finalStatus: status,
34926
+ timedOut,
34927
+ statusStale: Date.now() - statusAtMillis > STALE_STATUS_TOLERANCE_MILLIS,
34928
+ lastActiveBatchAtMillis,
34929
+ };
34820
34930
  }
34821
34931
  /** Best-effort cancellation of every job still active when the process is interrupted. */
34822
34932
  async function cancelActiveJobs(client, activeJobIds) {
@@ -34832,7 +34942,13 @@ async function cancelActiveJobs(client, activeJobIds) {
34832
34942
  }
34833
34943
  function printProgress(batchLabel, jobId, status) {
34834
34944
  const { counts, state } = status;
34835
- const line = `${batchLabel} job ${jobId}: ${counts.finalized}/${counts.files} finalized, ${counts.failed} failed, state=${state}`;
34945
+ // Deferred duplicates sit inside counts.failed but are skips to the caller (see printSummary) — shown
34946
+ // raw, the live line says "1 failed" on a job the summary then reports as 0 failed, 1 skipped.
34947
+ const duplicateSkips = status.files.filter(file => file.errorCode === 'DUPLICATE_CONTENT').length;
34948
+ const failed = Math.max(0, counts.failed - duplicateSkips);
34949
+ const skippedPart = duplicateSkips > 0 ? `, ${duplicateSkips} skipped` : '';
34950
+ const line = `${batchLabel} job ${jobId}: ${counts.finalized}/${counts.files} finalized, ` +
34951
+ `${failed} failed${skippedPart}, state=${state}`;
34836
34952
  process.stdout.write(`\r${line}${' '.repeat(Math.max(0, PROGRESS_LINE_WIDTH - line.length))}`);
34837
34953
  }
34838
34954
  /** Narrows a `FileUploadResult` to one that uploaded successfully (has a `stagedObjectKey`, no `error`). */
@@ -34855,7 +34971,13 @@ async function printDryRun(dir, files, batchSize) {
34855
34971
  }
34856
34972
  function printSummary(totalFiles, results) {
34857
34973
  const totalFinalized = results.reduce((sum, result) => sum + (result.finalStatus?.counts.finalized ?? 0), 0);
34858
- const totalServerFailed = results.reduce((sum, result) => sum + (result.finalStatus?.counts.failed ?? 0), 0);
34974
+ // Deferred duplicates sit inside counts.failed but are reported on the "Skipped as duplicate" line
34975
+ // (runBatch folds them into `duplicates`), so subtract them here or the two lines double-count.
34976
+ const totalServerFailed = results.reduce((sum, result) => {
34977
+ const failed = result.finalStatus?.counts.failed ?? 0;
34978
+ const duplicateFailures = (result.finalStatus?.files ?? []).filter(file => file.errorCode === 'DUPLICATE_CONTENT').length;
34979
+ return sum + Math.max(0, failed - duplicateFailures);
34980
+ }, 0);
34859
34981
  const totalUploadFailed = results.reduce((sum, result) => sum + result.uploadFailures.length, 0);
34860
34982
  const totalDuplicates = results.reduce((sum, result) => sum + result.duplicates.length, 0);
34861
34983
  const totalStagedButFailed = results.reduce((sum, result) => sum + (result.stagingError ? (result.stagedCount ?? 0) : 0), 0);
@@ -34872,6 +34994,17 @@ function printSummary(totalFiles, results) {
34872
34994
  if (totalStagedButFailed > 0) {
34873
34995
  console.log(`Staged but job failed: ${totalStagedButFailed} (uploaded objects remain; auto-expire in 24h)`);
34874
34996
  }
34997
+ const totalStillDeferred = results.reduce((sum, result) => result.deferredInFlight
34998
+ ? sum +
34999
+ Math.max(0, (result.finalStatus?.counts.files ?? 0) -
35000
+ (result.finalStatus?.counts.finalized ?? 0) -
35001
+ (result.finalStatus?.counts.failed ?? 0))
35002
+ : sum, 0);
35003
+ if (totalStillDeferred > 0) {
35004
+ // Reconciles the summary the same way the duplicates line does: these files are neither finalized
35005
+ // nor failed, and without this line they read as silently missing from a run that exits 0.
35006
+ console.log(`Still processing: ${totalStillDeferred} (queued or draining server-side; the job finishes there)`);
35007
+ }
34875
35008
  for (const result of results) {
34876
35009
  const label = `batch ${result.batchIndex + 1}/${result.batchCount}`;
34877
35010
  if (result.stagingError) {
@@ -34887,12 +35020,20 @@ function printSummary(totalFiles, results) {
34887
35020
  console.log(` ${label}: no job created (all uploads failed)`);
34888
35021
  continue;
34889
35022
  }
34890
- const state = result.timedOut
34891
- ? `${result.finalStatus?.state ?? 'unknown'} (timed out waiting)`
34892
- : result.finalStatus?.state;
34893
- console.log(` ${label}: job ${result.jobId} — ${result.finalStatus?.counts.finalized ?? 0}/${result.finalStatus?.counts.files ?? 0} finalized, ${result.finalStatus?.counts.failed ?? 0} failed [${state}]`);
35023
+ const state = result.deferredInFlight
35024
+ ? 'running work still queued or draining server-side (batch tail can reach 24h); re-runs are idempotent'
35025
+ : result.timedOut
35026
+ ? `${result.finalStatus?.state ?? 'unknown'} (timed out waiting)`
35027
+ : result.finalStatus?.state;
35028
+ // Deferred duplicates are excluded from BOTH the failed count and the per-file failure lines: they
35029
+ // are already named on the "skipped" lines below, and a clean idempotent re-run must not read as
35030
+ // "0/1 finalized, 1 failed" while exiting 0 — the CLI tells one story about a skip.
35031
+ const deferredDuplicateCount = result.finalStatus?.files.filter(file => file.errorCode === 'DUPLICATE_CONTENT').length ?? 0;
35032
+ const failedCount = Math.max(0, (result.finalStatus?.counts.failed ?? 0) - deferredDuplicateCount);
35033
+ console.log(` ${label}: job ${result.jobId} — ${result.finalStatus?.counts.finalized ?? 0}/${result.finalStatus?.counts.files ?? 0} finalized, ${failedCount} failed [${state}]`);
34894
35034
  // Per-file failure reasons, straight from the job-status response (BulkIngestionFileStatus).
34895
- const failedFiles = result.finalStatus?.files.filter(file => file.status === 'failed') ?? [];
35035
+ const failedFiles = result.finalStatus?.files.filter(file => file.status === 'failed' && file.errorCode !== 'DUPLICATE_CONTENT') ??
35036
+ [];
34896
35037
  for (const failedFile of failedFiles) {
34897
35038
  console.log(` - ${failedFile.title}: ${failedFile.errorMessage ?? 'unknown error'}`);
34898
35039
  }
@@ -34904,11 +35045,21 @@ function printSummary(totalFiles, results) {
34904
35045
  }
34905
35046
  }
34906
35047
  function hasBatchFailed(result) {
34907
- if (result.uploadFailures.length > 0 || result.timedOut)
35048
+ if (result.uploadFailures.length > 0)
34908
35049
  return true;
34909
35050
  if (!result.finalStatus)
34910
35051
  return true;
34911
- return result.finalStatus.state !== 'completed' || result.finalStatus.counts.failed > 0;
35052
+ // Deferred duplicates are skips, not failures: an idempotent re-run over an already-ingested
35053
+ // directory must exit 0, exactly as it does when staging rejects the duplicates synchronously.
35054
+ const duplicateFailures = result.finalStatus.files.filter(file => file.errorCode === 'DUPLICATE_CONTENT').length;
35055
+ const nonDuplicateFailed = result.finalStatus.counts.failed - duplicateFailures;
35056
+ // A wait-budget expiry on a still-draining provider batch is not a failure BY ITSELF: the pending
35057
+ // files will finish server-side, and exiting 1 would report success-in-progress as an error to every
35058
+ // unattended corpus load. Files the job has ALREADY failed still fail the run — a deferral must never
35059
+ // hide them behind exit 0. Every other timeout shape fails the run outright.
35060
+ if (result.timedOut)
35061
+ return result.deferredInFlight ? nonDuplicateFailed > 0 : true;
35062
+ return result.finalStatus.state !== 'completed' || nonDuplicateFailed > 0;
34912
35063
  }
34913
35064
  /** Recursively walks `dir`, skipping symlinks and dotfiles/dot-directories, filtered to `extensions`. */
34914
35065
  async function collectFiles(dir, extensions) {
@@ -35504,6 +35655,9 @@ const process_utils_1 = __webpack_require__(8251);
35504
35655
  const report_utils_1 = __webpack_require__(3066);
35505
35656
  const version_check_1 = __webpack_require__(4827);
35506
35657
  const ORG = 'squid-cloud-samples';
35658
+ // Audit and fund are extra registry round-trips that tell the user nothing about a project the
35659
+ // CLI just generated, and the audit request alone can take minutes.
35660
+ const NPM_INSTALL_COMMAND = 'npm install --no-audit --no-fund';
35507
35661
  async function initSample(consoleRegion, isOnPremConsole, dirPath, appId, apiKey, environmentId, squidDeveloperId, region, templateName, verbose = false) {
35508
35662
  (0, global_utils_1.enableDebugLogs)(verbose || (0, global_utils_1.isDebugEnabled)());
35509
35663
  (0, logpipes_1.installConsoleOverrides)(enable_debug_decorator_utils_1.debugLogFilterPipe);
@@ -35552,18 +35706,18 @@ async function initSample(consoleRegion, isOnPremConsole, dirPath, appId, apiKey
35552
35706
  let monorepo = false;
35553
35707
  try {
35554
35708
  await promises_1.default.access(path_1.default.resolve(dirPath, 'package.json'));
35555
- await (0, shell_runner_1.runInShell)('npm install', verbose, path_1.default.resolve(dirPath));
35709
+ await (0, shell_runner_1.runInShell)(NPM_INSTALL_COMMAND, verbose, path_1.default.resolve(dirPath));
35556
35710
  }
35557
35711
  catch { }
35558
35712
  try {
35559
35713
  await promises_1.default.access(path_1.default.resolve(dirPath, 'backend'));
35560
- await (0, shell_runner_1.runInShell)('npm install', verbose, path_1.default.resolve(dirPath, 'backend'));
35714
+ await (0, shell_runner_1.runInShell)(NPM_INSTALL_COMMAND, verbose, path_1.default.resolve(dirPath, 'backend'));
35561
35715
  monorepo = true;
35562
35716
  }
35563
35717
  catch { }
35564
35718
  try {
35565
35719
  await promises_1.default.access(path_1.default.resolve(dirPath, 'frontend'));
35566
- await (0, shell_runner_1.runInShell)('npm install', verbose, path_1.default.resolve(dirPath, 'frontend'));
35720
+ await (0, shell_runner_1.runInShell)(NPM_INSTALL_COMMAND, verbose, path_1.default.resolve(dirPath, 'frontend'));
35567
35721
  }
35568
35722
  catch { }
35569
35723
  const hasEnvars = appId && region && apiKey && environmentId && squidDeveloperId;
@@ -40729,7 +40883,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"name":"seek-bzip","version":"1.0.6",
40729
40883
  (module) {
40730
40884
 
40731
40885
  "use strict";
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"}}');
40886
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.490","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.490","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
40887
 
40734
40888
  /***/ }
40735
40889
 
@@ -22,8 +22,8 @@ Squid is a backend-as-a-service platform that provides:
22
22
  ## Feature-Specific Guidance
23
23
 
24
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
25
+ - **[console.md](reference/console.md)** → Squid Console, web UI, organizations, applications, AI Studio, knowledge bases, knowledge graph toggle, extraction method dropdown, 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, extraction method, preferredExtractionMethod, legacy_with_llm, mistral_ocr, amazon_textract, azure_document_intelligence, page screenshot, image description, extractImages
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
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
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
@@ -59,10 +59,10 @@ import {
59
59
 
60
60
  | Provider | `*_CHAT_MODEL_NAMES` |
61
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` |
62
+ | OpenAI | `gpt-5.5-pro`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` |
63
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` |
64
+ | Gemini | `gemini-3.1-pro`, `gemini-3.7-flash`, `gemini-3.5-flash-lite` |
65
+ | Grok | `grok-4.6`, `grok-4-1-fast-reasoning`, `grok-4-1-fast-non-reasoning` |
66
66
 
67
67
  This list moves. **Don't hardcode a model name from this table into user code** — call
68
68
  `squid.ai().listChatModels()`, which also returns the app's custom integration models
@@ -72,7 +72,7 @@ This list moves. **Don't hardcode a model name from this table into user code**
72
72
  **Model Categories:**
73
73
  - **Chat Models**: Used for AI Agents, AI Query, etc. (OpenAI, Anthropic, Gemini, Grok)
74
74
  - **Embedding Models**: Used for Knowledge Bases (OpenAI, Voyage)
75
- - **Image Generation Models**: Used for image creation (DALL-E, Stable Diffusion, Flux)
75
+ - **Image Generation Models**: Used for image creation (OpenAI `gpt-image-*`, Stable Diffusion, Flux)
76
76
  - **Audio Models**: Transcription (Whisper, GPT-4o) and Text-to-Speech (TTS-1, GPT-4o-mini-tts)
77
77
 
78
78
  ## AI Agents
@@ -115,7 +115,7 @@ await myAgent.upsert({
115
115
  isPublic: false, // Whether the agent is publicly accessible
116
116
  auditLog: true, // Enable audit logging for compliance
117
117
  options: {
118
- model: 'gpt-5.4-mini', // or 'claude-sonnet-5', 'gemini-3.6-flash' - see Supported Models
118
+ model: 'gpt-5.6-terra', // or 'claude-sonnet-5', 'gemini-3.7-flash' - see Supported Models
119
119
  instructions: 'You are a helpful customer support assistant. Be concise and professional.',
120
120
  temperature: 0.7
121
121
  }
@@ -128,7 +128,7 @@ console.log(agentInfo.id, agentInfo.description, agentInfo.options.model);
128
128
  // Update specific properties
129
129
  await myAgent.updateInstructions('You are a technical support specialist.');
130
130
  await myAgent.updateModel('claude-sonnet-5');
131
- await myAgent.updateGuardrails(['no-harmful-content']);
131
+ await myAgent.updateGuardrails({ disableProfanity: true, disablePii: true });
132
132
 
133
133
  // Delete an agent
134
134
  await myAgent.delete();
@@ -386,14 +386,18 @@ const chatObs = agent.chat('What is your return policy?', {
386
386
  connectedKnowledgeBases: [{ knowledgeBaseId: 'kb1', description: 'When to use this KB' }],
387
387
 
388
388
  // Model & generation
389
- model: 'gpt-5.4-mini', // Override agent's default model
389
+ model: 'gpt-5.6-terra', // Override agent's default model
390
390
  temperature: 0.7,
391
391
  maxTokens: 4000,
392
392
  maxOutputTokens: 2000,
393
393
  instructions: 'Additional instructions for this request only',
394
394
  responseFormat: 'json_object', // or 'text'
395
395
  verbosity: 'medium', // 'low' | 'medium' | 'high' (OpenAI only)
396
- reasoningEffort: 'high', // 'minimal' | 'low' | 'medium' | 'high' (for reasoning models)
396
+ reasoningEffort: 'high', // 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra'
397
+ // (for reasoning models). On an `openai_compatible` integration the value
398
+ // is sent to the endpoint as written, so a level outside this list reaches
399
+ // it too — OpenAI's Chat Completions API, for one, serves function tools on
400
+ // a gpt-5.6 model only with reasoning off outright (`'none'`).
397
401
 
398
402
  // Context & RAG
399
403
  disableContext: false,
@@ -404,7 +408,13 @@ const chatObs = agent.chat('What is your return policy?', {
404
408
  },
405
409
 
406
410
  // Guardrails & quotas
407
- guardrails: ['no-harmful-content', 'no-pii'],
411
+ guardrails: {
412
+ disableProfanity: true,
413
+ offTopicAnswers: true,
414
+ professionalTone: true,
415
+ disablePii: true,
416
+ custom: 'Never reveal internal case numbers'
417
+ },
408
418
  quotas: {
409
419
  maxAiCallStackSize: 5
410
420
  },
@@ -433,7 +443,7 @@ const chatObs = agent.chat('What is your return policy?', {
433
443
  useCodeInterpreter: 'llm', // 'none' | 'llm' (OpenAI/Gemini only)
434
444
  executionPlanOptions: {
435
445
  enabled: true,
436
- model: 'gpt-5.4-mini',
446
+ model: 'gpt-5.6-terra',
437
447
  reasoningEffort: 'high',
438
448
  allowClarificationQuestions: false
439
449
  },
@@ -530,6 +540,12 @@ await kb.upsertContexts([
530
540
  { contextId: 'doc-2', text: '...' }
531
541
  ], optionalFiles);
532
542
 
543
+ // File contexts can pick how the document is extracted — see "Document Extraction Methods" below
544
+ await kb.upsertContexts(
545
+ [{ contextId: 'report-1', type: 'file', preferredExtractionMethod: 'legacy_with_llm' }],
546
+ [reportPdfFile]
547
+ );
548
+
533
549
  // Search with prompt
534
550
  const results = await kb.searchContextsWithPrompt({
535
551
  prompt: 'How do I reset password?',
@@ -784,6 +800,14 @@ await kb.bulkUpsertContexts(uploads.map(u => ({ type: 'file', stagedObjectKey: u
784
800
  The CLI wraps this whole flow: `squid kb-upload --dir ./docs --knowledgeBase my-kb` (see
785
801
  [backend.md](backend.md#cli-commands)).
786
802
 
803
+ ### Document Extraction Methods
804
+
805
+ File contexts (and `squid.extraction()` calls) accept `preferredExtractionMethod: 'mistral_ocr' | 'legacy' | 'legacy_with_llm' | 'amazon_textract' | 'azure_document_intelligence'`. Default: the deployment's `defaultDocExtractionMethod` (usually `mistral_ocr`). The OCR providers (`mistral_ocr`, `amazon_textract`, `azure_document_intelligence`) are available only where the deployment configures them; `legacy` and `legacy_with_llm` run in-process and are always available. The same option applies to `upsertContexts`, `bulkUpsertContexts`, and the extraction client alike.
806
+
807
+ - `legacy` — reads the PDF's text layer (pdfjs) and extracts embedded images; no external service, but a scanned page yields nothing (there is no text layer to read). Console label: **Basic**.
808
+ - `legacy_with_llm` — the same text extraction, plus a screenshot of each page described by a vision model as a *complement* to the stored page text: figures, charts, layout, handwriting, and scanned regions the text layer cannot carry, without restating the page's prose. Notably, vector-drawn figures/graphs are not embedded images, so `legacy` cannot see them at all; the page screenshot (like provider OCR) is what catches them. Each description lands as its own chunk beside the page's text chunk, with page provenance; a page whose screenshot adds nothing beyond its text stores no extra chunk. Costs one vision call per page, so ingestion of long documents is slower and pricier than `legacy`. Console label: **Basic + page understanding**.
809
+ - `extractImages` (default `true` for file contexts) gates image handling for every method, including the `legacy_with_llm` page screenshots; `imageMinSizePixels` (default 100) filters small embedded images; `imageExtractionModel` overrides the model used for image/screenshot descriptions.
810
+
787
811
  ### Spreadsheet Understanding
788
812
 
789
813
  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.
@@ -818,10 +842,10 @@ REST API: [AI Image API](https://docs.getsquid.ai/reference-docs/api/#tag/AI-Ima
818
842
  ### Image Generation
819
843
 
820
844
  ```typescript
821
- // Generate image
845
+ // Generate image - `modelName` is required
822
846
  const imageUrl = await squid.ai().image().generate(
823
847
  'A futuristic city',
824
- { size: '1024x1024', quality: 'hd' }
848
+ { modelName: 'gpt-image-1', size: '1024x1024', quality: 'high' }
825
849
  );
826
850
 
827
851
  // Remove background
@@ -894,7 +918,7 @@ const settings = await aiClient.getApplicationAiSettings();
894
918
 
895
919
  // Set application AI settings
896
920
  await aiClient.setApplicationAiSettings({
897
- defaultModel: 'gpt-5.4-mini',
921
+ defaultModel: 'gpt-5.6-terra',
898
922
  // ... other settings
899
923
  });
900
924
 
@@ -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.4-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='{
@@ -77,7 +77,7 @@ Every Squid application includes the **Essentials Connector** - a built-in integ
77
77
  | **Web Utilities** | `squid.web()` | AI-powered web search, URL content extraction, short URLs | [Web section](#web) |
78
78
  | **AI Agents** | `squid.ai().agent()` | Chat with built-in or custom AI agents | [ai.md](ai.md) |
79
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 DALL-E | [ai.md](ai.md) |
80
+ | **Image Generation** | `squid.ai().image()` | Generate images with `gpt-image-*`, Stable Diffusion, or Flux | [ai.md](ai.md) |
81
81
  | **Audio** | `squid.ai().audio()` | Transcription and text-to-speech | [ai.md](ai.md) |
82
82
  | **PDF/Extraction** | `squid.extraction()` | Create PDFs, extract data from documents | [Extraction section](#extraction) |
83
83
  | **Observability** | `squid.observability` | Report and query custom metrics | [Observability section](#observability--metrics) |
@@ -389,7 +389,12 @@ const extractedData = await extraction.extractDataFromDocumentFile(
389
389
  { name: 'total', type: 'number', description: 'Total amount' },
390
390
  { name: 'date', type: 'date', description: 'Invoice date' }
391
391
  ]
392
- }
392
+ },
393
+ // 'mistral_ocr' | 'legacy' | 'legacy_with_llm' | 'amazon_textract' | 'azure_document_intelligence'.
394
+ // Default: the deployment's configured method (usually 'mistral_ocr'). Semantics: see the
395
+ // "Document Extraction Methods" section in ai.md.
396
+ preferredExtractionMethod: 'legacy_with_llm',
397
+ extractImages: true // default true; also gates legacy_with_llm's page screenshots
393
398
  }
394
399
  );
395
400
 
@@ -73,6 +73,7 @@ The AI Studio provides visual tools for building and testing AI agents:
73
73
 
74
74
  ### Managing Content
75
75
  - Upload documents (PDF, text, markdown, etc.)
76
+ - Pick the **Extraction Method** in the add-knowledge dialog: **Basic** (`legacy`, in-process text extraction) and **Basic + page understanding** (`legacy_with_llm`, adds an LLM description of each page's screenshot) are always offered; OCR providers (**Mistral OCR**, **Amazon Textract**, **Azure AI Document Intelligence**) appear when the deployment configures them, with the deployment default marked **(Default)**
76
77
  - View and edit contexts
77
78
  - Search and preview indexed content
78
79
  - Monitor indexing status
@@ -156,7 +157,7 @@ const agent = squid.ai().agent('my-agent');
156
157
  await agent.upsert({
157
158
  description: 'Customer support agent',
158
159
  options: {
159
- model: 'gpt-4o',
160
+ model: 'gpt-5.6-terra',
160
161
  instructions: 'You are a helpful support agent...'
161
162
  }
162
163
  });
@@ -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
@@ -194,9 +194,11 @@ const content = await squid.web().getUrlContent('https://example.com');
194
194
  // Short URLs
195
195
  const shortUrl = await squid.web().createShortUrl('https://long-url.com/path');
196
196
 
197
- // Data Extraction from documents
198
- const data = await squid.extraction().extractDataFromDocumentUrl(url, schema);
199
- const data = await squid.extraction().extractDataFromDocumentFile(file, schema);
197
+ // Data Extraction from documents. Optional preferredExtractionMethod:
198
+ // 'mistral_ocr' | 'legacy' | 'legacy_with_llm' | 'amazon_textract' | 'azure_document_intelligence'
199
+ // (see the squid-development skill's ai.md "Document Extraction Methods")
200
+ const data = await squid.extraction().extractDataFromDocumentUrl(url);
201
+ const data = await squid.extraction().extractDataFromDocumentFile(file, { preferredExtractionMethod: 'legacy_with_llm' });
200
202
 
201
203
  // PDF Creation
202
204
  const pdf = await squid.extraction().createPdf(htmlContent);
@@ -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.4-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.488",
3
+ "version": "1.0.490",
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.488",
31
+ "@squidcloud/local-backend": "^1.0.490",
32
32
  "adm-zip": "^0.5.16",
33
33
  "copy-webpack-plugin": "^14.0.0",
34
34
  "decompress": "^4.2.1",