@squidcloud/cli 1.0.485 → 1.0.487

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
@@ -32200,6 +32200,7 @@ exports.AI_PROVIDER_TYPES = [
32200
32200
  'voyage',
32201
32201
  'mistral',
32202
32202
  'textract',
32203
+ 'azure_document_intelligence',
32203
32204
  'vertex',
32204
32205
  'external', // This is a technicality, referring to user-defined providers.
32205
32206
  ];
@@ -32225,12 +32226,17 @@ exports.oL = ['gemini-3.1-pro', 'gemini-3.6-flash', 'gemini-3.5-flash-lite'];
32225
32226
  * Public Grok chat model names (active models only).
32226
32227
  * @category AI
32227
32228
  */
32228
- exports.Kq = ['grok-4.3', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning'];
32229
+ exports.Kq = ['grok-4.5', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning'];
32229
32230
  /**
32230
32231
  * Public Anthropic chat model names (active models only).
32231
32232
  * @category AI
32232
32233
  */
32233
- exports.xV = ['claude-haiku-4-5-20251001', 'claude-opus-5', 'claude-sonnet-5'];
32234
+ exports.xV = [
32235
+ 'claude-fable-5',
32236
+ 'claude-haiku-4-5-20251001',
32237
+ 'claude-opus-5',
32238
+ 'claude-sonnet-5',
32239
+ ];
32234
32240
  /**
32235
32241
  * The supported AI model names.
32236
32242
  * @category AI
@@ -32737,7 +32743,7 @@ function getConsoleAppRegionByStage(stage) {
32737
32743
  var __webpack_unused_export__;
32738
32744
 
32739
32745
  __webpack_unused_export__ = ({ value: true });
32740
- __webpack_unused_export__ = __webpack_unused_export__ = exports.MILLIS_PER_DAY = __webpack_unused_export__ = __webpack_unused_export__ = exports.MILLIS_PER_SECOND = exports.LW = exports.FN = exports.xz = exports.NG = exports.fA = void 0;
32746
+ __webpack_unused_export__ = __webpack_unused_export__ = exports.MILLIS_PER_DAY = __webpack_unused_export__ = exports.MILLIS_PER_MINUTE = exports.MILLIS_PER_SECOND = exports.LW = exports.FN = exports.xz = exports.NG = exports.fA = void 0;
32741
32747
  /** @internal */
32742
32748
  exports.fA = 60;
32743
32749
  /** @internal */
@@ -32751,7 +32757,7 @@ exports.LW = 30 * exports.xz;
32751
32757
  /** @internal */
32752
32758
  exports.MILLIS_PER_SECOND = 1000;
32753
32759
  /** @internal */
32754
- __webpack_unused_export__ = exports.fA * exports.MILLIS_PER_SECOND;
32760
+ exports.MILLIS_PER_MINUTE = exports.fA * exports.MILLIS_PER_SECOND;
32755
32761
  /** @internal */
32756
32762
  __webpack_unused_export__ = exports.NG * exports.MILLIS_PER_SECOND;
32757
32763
  /** @internal */
@@ -33049,7 +33055,6 @@ exports.sQ = [
33049
33055
  'mutation',
33050
33056
  'native-query',
33051
33057
  'query',
33052
- 'queue',
33053
33058
  // Note: every `/ws/*` WebSocket path is served by the TypeScript core (port 8000), never Kotlin.
33054
33059
  ];
33055
33060
  /**
@@ -34367,6 +34372,726 @@ async function initWebpack() {
34367
34372
  }
34368
34373
 
34369
34374
 
34375
+ /***/ },
34376
+
34377
+ /***/ 2713
34378
+ (__unused_webpack_module, exports, __webpack_require__) {
34379
+
34380
+ "use strict";
34381
+
34382
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
34383
+ if (k2 === undefined) k2 = k;
34384
+ var desc = Object.getOwnPropertyDescriptor(m, k);
34385
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
34386
+ desc = { enumerable: true, get: function() { return m[k]; } };
34387
+ }
34388
+ Object.defineProperty(o, k2, desc);
34389
+ }) : (function(o, m, k, k2) {
34390
+ if (k2 === undefined) k2 = k;
34391
+ o[k2] = m[k];
34392
+ }));
34393
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34394
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
34395
+ }) : function(o, v) {
34396
+ o["default"] = v;
34397
+ });
34398
+ var __importStar = (this && this.__importStar) || (function () {
34399
+ var ownKeys = function(o) {
34400
+ ownKeys = Object.getOwnPropertyNames || function (o) {
34401
+ var ar = [];
34402
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34403
+ return ar;
34404
+ };
34405
+ return ownKeys(o);
34406
+ };
34407
+ return function (mod) {
34408
+ if (mod && mod.__esModule) return mod;
34409
+ var result = {};
34410
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34411
+ __setModuleDefault(result, mod);
34412
+ return result;
34413
+ };
34414
+ })();
34415
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
34416
+ exports.setupKbUploadCommand = setupKbUploadCommand;
34417
+ const assertic_1 = __webpack_require__(3205);
34418
+ const dotenv = __importStar(__webpack_require__(9650));
34419
+ const fs_1 = __webpack_require__(9896);
34420
+ const fs = __importStar(__webpack_require__(1943));
34421
+ const path = __importStar(__webpack_require__(6928));
34422
+ const stream_1 = __webpack_require__(2203);
34423
+ const communication_types_1 = __webpack_require__(3443);
34424
+ const time_units_1 = __webpack_require__(1929);
34425
+ const http_1 = __webpack_require__(866);
34426
+ const process_utils_1 = __webpack_require__(8251);
34427
+ /** CLI-side pacing default: files staged per bulk-ingestion job (not a server-enforced cap). */
34428
+ const DEFAULT_BATCH_SIZE = 200;
34429
+ /** Upper bound accepted for `--batchSize`; keeps a single job's blast radius reasonable. */
34430
+ const MAX_BATCH_SIZE = 1000;
34431
+ /**
34432
+ * Byte budget for one batch's files on disk. Mirrors the server's `BULK_INGESTION_MAX_STAGED_BYTES`
34433
+ * (256 MiB, ai-knowledge-base-management.service.ts) with headroom, since that ceiling applies to
34434
+ * EXTRACTED content and extraction can produce more text than the source file holds. Deliberately a
34435
+ * separate constant rather than an import: the CLI does not depend on core.
34436
+ */
34437
+ const MAX_BATCH_BYTES = 192 * 1024 * 1024;
34438
+ /** Server-enforced cap on `bulk/createUploadUrls`'s `files` array (`BULK_INGESTION_MAX_UPLOAD_URLS_PER_CALL`). */
34439
+ const MAX_UPLOAD_URLS_PER_CALL = 500;
34440
+ /** Number of concurrent presigned-URL PUTs. */
34441
+ const PUT_CONCURRENCY = 8;
34442
+ /**
34443
+ * Files whose upload URLs are minted together. Kept a small multiple of {@link PUT_CONCURRENCY} so a wave's
34444
+ * URLs are always used well inside their 15-minute lifetime, while still amortizing the mint round-trip.
34445
+ */
34446
+ const UPLOAD_WAVE_SIZE = 40;
34447
+ /**
34448
+ * How long a SIGINT waits for an in-flight `upsertContexts` to yield its job id before cancelling without it.
34449
+ * Bounded because that call has no request timeout of its own: an unbounded wait would leave the first Ctrl-C
34450
+ * hanging behind a wedged server, and a job whose id never arrives is better left to the 7-day deadline than
34451
+ * a CLI that will not quit.
34452
+ */
34453
+ const SIGINT_STAGING_WAIT_MILLIS = 10 * time_units_1.MILLIS_PER_SECOND;
34454
+ /** Default per-job wait budget before falling back to "still running server-side" and moving on. */
34455
+ const DEFAULT_TIMEOUT_MINUTES = 120;
34456
+ /** Interval between `bulk/getJob` polls while waiting for a job to reach a terminal state. */
34457
+ const POLL_INTERVAL_MILLIS = 10 * time_units_1.MILLIS_PER_SECOND;
34458
+ /** Extensions ingested by default when `--extensions` is not provided. */
34459
+ const DEFAULT_EXTENSIONS = ['pdf', 'docx', 'txt', 'md', 'html', 'csv', 'xlsx', 'xls', 'xlsm', 'xlsb', 'pptx'];
34460
+ /** Job states that end a bulk-ingestion job's lifecycle (`BulkIngestionJobState` in core). */
34461
+ const TERMINAL_JOB_STATES = new Set(['completed', 'failed', 'cancelled']);
34462
+ const DEFAULT_MIME_TYPE = 'application/octet-stream';
34463
+ /** Placeholder status when a job's real status could never be retrieved before the timeout. */
34464
+ const UNKNOWN_JOB_STATUS = {
34465
+ state: 'unknown',
34466
+ counts: { files: 0, finalized: 0, failed: 0, requestsPending: 0, requestsSubmitted: 0 },
34467
+ providerBatchIds: [],
34468
+ files: [],
34469
+ };
34470
+ /**
34471
+ * Fixed line width the progress line pads to, so a shorter update fully overwrites a longer one. 140 because
34472
+ * at Pritzker scale a real line (`batch N/M job <uuid>: X/Y finalized, Z failed, state=...`) exceeds 100 chars.
34473
+ */
34474
+ const PROGRESS_LINE_WIDTH = 140;
34475
+ /** Small extension → MIME map so the presigned PUT always carries an explicit Content-Type. */
34476
+ const EXTENSION_MIME_TYPES = {
34477
+ pdf: 'application/pdf',
34478
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
34479
+ doc: 'application/msword',
34480
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
34481
+ ppt: 'application/vnd.ms-powerpoint',
34482
+ txt: 'text/plain',
34483
+ md: 'text/markdown',
34484
+ html: 'text/html',
34485
+ htm: 'text/html',
34486
+ csv: 'text/csv',
34487
+ tsv: 'text/tab-separated-values',
34488
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
34489
+ xls: 'application/vnd.ms-excel',
34490
+ xlsm: 'application/vnd.ms-excel.sheet.macroEnabled.12',
34491
+ xlsb: 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
34492
+ eml: 'message/rfc822',
34493
+ msg: 'application/vnd.ms-outlook',
34494
+ json: 'application/json',
34495
+ };
34496
+ /** Registers the `kb-upload` command: bulk-ingests a local directory tree into a knowledge base. */
34497
+ function setupKbUploadCommand(yargs) {
34498
+ yargs.command('kb-upload', 'Bulk-ingests a local directory tree into a knowledge base via the direct-to-storage bulk-ingestion API', yargs => {
34499
+ yargs.option('dir', {
34500
+ type: 'string',
34501
+ demandOption: true,
34502
+ describe: 'The local directory to walk recursively for files to ingest',
34503
+ });
34504
+ yargs.option('knowledgeBase', {
34505
+ type: 'string',
34506
+ demandOption: true,
34507
+ describe: 'The id of the knowledge base to ingest into',
34508
+ });
34509
+ yargs.option('appId', {
34510
+ type: 'string',
34511
+ describe: 'The application ID (Can be retrieved from the Squid Console). Falls back to SQUID_APP_ID',
34512
+ });
34513
+ yargs.option('apiKey', {
34514
+ type: 'string',
34515
+ describe: 'The application API key (Can be retrieved from the Squid Console). Falls back to SQUID_API_KEY',
34516
+ });
34517
+ yargs.option('internalApiKey', {
34518
+ type: 'string',
34519
+ describe: 'Internal API key for local/on-prem use instead of --apiKey. Falls back to SQUID_INTERNAL_API_KEY',
34520
+ });
34521
+ yargs.option('region', {
34522
+ type: 'string',
34523
+ describe: 'The Squid region the application lives in. Falls back to SQUID_REGION',
34524
+ });
34525
+ yargs.option('environmentId', {
34526
+ type: 'string',
34527
+ describe: "The environment to ingest into ('dev' or 'prod'). Falls back to SQUID_ENVIRONMENT_ID",
34528
+ });
34529
+ yargs.option('batchSize', {
34530
+ type: 'number',
34531
+ default: DEFAULT_BATCH_SIZE,
34532
+ describe: `Files staged per bulk-ingestion job (max ${MAX_BATCH_SIZE})`,
34533
+ });
34534
+ yargs.option('extensions', {
34535
+ type: 'string',
34536
+ default: DEFAULT_EXTENSIONS.join(','),
34537
+ describe: 'Comma-separated allow-list of file extensions to ingest',
34538
+ });
34539
+ yargs.option('dryRun', {
34540
+ type: 'boolean',
34541
+ default: false,
34542
+ describe: 'List the files that would be uploaded and exit without contacting the server',
34543
+ });
34544
+ yargs.option('timeoutMinutes', {
34545
+ type: 'number',
34546
+ default: DEFAULT_TIMEOUT_MINUTES,
34547
+ describe: 'Minutes to wait for each job to finish before moving on and reporting it as still running',
34548
+ });
34549
+ }, async (argv) => {
34550
+ // Mirrors what `deploy`/`undeploy` do in main.ts: an initialized project keeps its appId, region,
34551
+ // environment and developer id in a local `.env`, and nothing else loads it for this command — so
34552
+ // without this every normal project fails on "Missing application ID" until the user exports the
34553
+ // variables by hand.
34554
+ dotenv.config({ path: path.resolve('./', '.env') });
34555
+ await kbUpload(argv);
34556
+ });
34557
+ }
34558
+ /** Drives the full walk → stage → wait → summarize flow and sets `process.exitCode` accordingly. */
34559
+ async function kbUpload(argv) {
34560
+ const extensions = parseExtensions(argv.extensions);
34561
+ const batchSize = validateBatchSize(argv.batchSize);
34562
+ const files = await collectFiles(argv.dir, extensions);
34563
+ if (argv.dryRun) {
34564
+ await printDryRun(argv.dir, files, batchSize);
34565
+ return;
34566
+ }
34567
+ if (files.length === 0) {
34568
+ console.log(`No files with extensions [${Array.from(extensions).join(', ')}] found under ${argv.dir}`);
34569
+ return;
34570
+ }
34571
+ const appId = getDataPlaneAppId(argv);
34572
+ const region = getRegion(argv);
34573
+ const { apiKey, internalApiKey } = getAuth(argv);
34574
+ const timeoutMillis = argv.timeoutMinutes * time_units_1.MILLIS_PER_MINUTE;
34575
+ const client = new BulkIngestionClient(region, appId, apiKey, internalApiKey);
34576
+ const batches = await groupFilesIntoBatches(files, batchSize);
34577
+ console.log(`Found ${files.length} file(s) under ${argv.dir}; staging in ${batches.length} batch(es) of up to ${batchSize}.`);
34578
+ const activeJobIds = new Set();
34579
+ // The job id of a batch whose `upsertContexts` call is CURRENTLY in flight. That call runs extraction
34580
+ // inline server-side for a whole batch, so it is a long window in which a job exists on the server but has
34581
+ // no id here yet — and a SIGINT landing in it would otherwise snapshot an empty `activeJobIds`, exit, and
34582
+ // leave that job running and billing. The handler waits on this before cancelling.
34583
+ let pendingStaging;
34584
+ let interrupted = false;
34585
+ const onSigint = () => {
34586
+ if (interrupted) {
34587
+ process.exit(130);
34588
+ }
34589
+ interrupted = true;
34590
+ process.stdout.write('\n');
34591
+ console.log('Received interrupt; cancelling in-flight bulk-ingestion job(s)... (press Ctrl-C again to exit immediately)');
34592
+ // Resolve any in-flight staging FIRST so its job id can be cancelled too; a staging that itself fails
34593
+ // created no job, so an undefined result simply adds nothing. Never rejects — a staging error is already
34594
+ // reported by runBatch — so cancellation always runs.
34595
+ //
34596
+ // Raced against a deadline: `upsertContexts` runs extraction inline server-side for a whole batch and
34597
+ // carries no request timeout, so waiting on it unconditionally would hang the FIRST Ctrl-C behind a slow
34598
+ // or wedged server. On timeout we fall back to cancelling what we already know about, which is strictly
34599
+ // better than not exiting.
34600
+ const stagingOrTimeout = Promise.race([
34601
+ // Belt and braces: the published promise is already non-rejecting (see onStagingStarted).
34602
+ Promise.resolve(pendingStaging).catch(() => undefined),
34603
+ new Promise(resolve => setTimeout(() => resolve(undefined), SIGINT_STAGING_WAIT_MILLIS)),
34604
+ ]);
34605
+ void stagingOrTimeout
34606
+ .then(stagingJobId => cancelActiveJobs(client, stagingJobId ? new Set([...activeJobIds, stagingJobId]) : activeJobIds))
34607
+ .finally(() => process.exit(130));
34608
+ };
34609
+ process.on('SIGINT', onSigint);
34610
+ const results = [];
34611
+ for (let batchIndex = 0; batchIndex < batches.length && !interrupted; batchIndex++) {
34612
+ // runBatch handles the expected failure modes internally (upload failures, and a staging/job-creation
34613
+ // failure that happens after uploads already succeeded) and always returns a BatchResult for those. This
34614
+ // catch is a last resort for something genuinely unexpected happening before any of that is known (e.g.
34615
+ // createUploadUrls itself throwing), so one batch's crash doesn't abort the whole run.
34616
+ try {
34617
+ const result = await runBatch(client, argv.knowledgeBase, batches[batchIndex], batchIndex, batches.length, timeoutMillis, activeJobIds, () => interrupted, staging => {
34618
+ pendingStaging = staging;
34619
+ });
34620
+ results.push(result);
34621
+ // Cleared once the batch is done: its job id is in `activeJobIds` now (or the job was never created),
34622
+ // so a later SIGINT must not re-resolve this settled promise and issue a pointless cancel.
34623
+ pendingStaging = undefined;
34624
+ }
34625
+ catch (error) {
34626
+ const message = (0, assertic_1.getMessageFromError)(error);
34627
+ console.error(`batch ${batchIndex + 1}/${batches.length}: unexpected error before staging: ${message}`);
34628
+ results.push({
34629
+ batchIndex,
34630
+ batchCount: batches.length,
34631
+ uploadFailures: [],
34632
+ duplicates: [],
34633
+ timedOut: false,
34634
+ batchError: message,
34635
+ });
34636
+ }
34637
+ }
34638
+ process.off('SIGINT', onSigint);
34639
+ printSummary(files.length, results);
34640
+ process.exitCode = interrupted || results.some(hasBatchFailed) ? 1 : 0;
34641
+ }
34642
+ /** Stages one batch (upload + upsertContexts) and waits for its job to finish or time out. */
34643
+ async function runBatch(client, knowledgeBaseId, filePaths, batchIndex, batchCount, timeoutMillis, activeJobIds, isInterrupted,
34644
+ /** Publishes this batch's in-flight `upsertContexts` promise so a SIGINT can cancel the job it creates. */
34645
+ onStagingStarted) {
34646
+ const batchLabel = `batch ${batchIndex + 1}/${batchCount}`;
34647
+ // Minted wave by wave, immediately before the PUTs that use them, rather than all at once up front. The
34648
+ // server gives a presigned URL BULK_INGESTION_UPLOAD_URL_EXPIRATION_SECONDS (15 min) of life, and a large
34649
+ // batch on a slow link uploads at only PUT_CONCURRENCY at a time — so URLs minted for the tail of the
34650
+ // batch could expire before their turn arrived, and the single retry would reuse the same dead URL.
34651
+ const uploadResults = [];
34652
+ for (const wave of chunk(filePaths, UPLOAD_WAVE_SIZE)) {
34653
+ // SIGINT is observed between waves, not only after the whole batch has uploaded.
34654
+ if (isInterrupted())
34655
+ break;
34656
+ try {
34657
+ const waveUrls = await createUploadUrlsForBatch(client, wave.map(filePath => path.basename(filePath)));
34658
+ (0, assertic_1.assertTruthy)(waveUrls.length === wave.length, `${batchLabel}: createUploadUrls returned ${waveUrls.length} entries for ${wave.length} files`);
34659
+ uploadResults.push(...(await runWithConcurrency(wave, PUT_CONCURRENCY, async (filePath, index) => uploadFile(filePath, waveUrls[index], waveUrls[index].stagedObjectKey))));
34660
+ }
34661
+ catch (error) {
34662
+ // A mint failure on a LATER wave must not discard the earlier waves' successful uploads: those objects
34663
+ // are already staged, and throwing here would leave them orphaned until the 24h expiry with no job
34664
+ // referencing them. Record this wave's files as upload failures and stage whatever did land — the
34665
+ // summary machinery below already reports per-file failures honestly.
34666
+ const message = (0, assertic_1.getMessageFromError)(error);
34667
+ console.warn(`${batchLabel}: failed to mint or upload a wave of ${wave.length} file(s): ${message}`);
34668
+ uploadResults.push(...wave.map(filePath => ({ fileName: path.basename(filePath), error: message })));
34669
+ }
34670
+ }
34671
+ const uploadFailures = uploadResults.filter(result => !isUploaded(result));
34672
+ for (const failure of uploadFailures) {
34673
+ console.warn(`${batchLabel}: failed to upload ${failure.fileName}: ${failure.error}`);
34674
+ }
34675
+ // BulkIngestionContext.metadata (ENG-2527 Phase-7) is available here for a future path-rule feature —
34676
+ // e.g. tagging each staged file with its source folder/glob-derived metadata — but this command has no
34677
+ // such flag yet, so no metadata is stamped today.
34678
+ const stagedContexts = uploadResults
34679
+ .filter(isUploaded)
34680
+ .map(result => ({ type: 'file', stagedObjectKey: result.stagedObjectKey }));
34681
+ if (stagedContexts.length === 0) {
34682
+ console.warn(`${batchLabel}: every file failed to upload; no job was created.`);
34683
+ return { batchIndex, batchCount, uploadFailures, duplicates: [], timedOut: false };
34684
+ }
34685
+ // Re-checked immediately before staging, not only between waves: the wave loop's `break` lands right here,
34686
+ // so without this Ctrl-C would SKIP the remaining uploads and then promptly create a job anyway. Worse, its
34687
+ // id reaches `activeJobIds` only after `cancelActiveJobs` has already snapshotted the set, so the job would
34688
+ // survive the CLI and keep billing. Uploaded objects with no job simply expire on their own 24h clock.
34689
+ if (isInterrupted()) {
34690
+ console.warn(`${batchLabel}: interrupted before job creation; ${stagedContexts.length} uploaded object(s) remain ` +
34691
+ `staged (auto-expire in 24h) and no job was created.`);
34692
+ return {
34693
+ batchIndex,
34694
+ batchCount,
34695
+ uploadFailures,
34696
+ duplicates: [],
34697
+ timedOut: false,
34698
+ stagedCount: stagedContexts.length,
34699
+ };
34700
+ }
34701
+ // The uploads above already succeeded, so a failure from here on is NOT an upload failure: the objects are
34702
+ // already sitting in staging. Catch this call specifically so that outcome is reported honestly instead of
34703
+ // being conflated with "no job created (all uploads failed)".
34704
+ let jobId;
34705
+ let duplicates;
34706
+ try {
34707
+ const staging = client.upsertContexts(knowledgeBaseId, stagedContexts);
34708
+ // The published promise must never REJECT. It is a derived branch of `staging`, and the only rejection
34709
+ // handler on it lives inside the SIGINT path — which usually never runs, so a staging failure would
34710
+ // otherwise surface as an unhandled rejection and take the whole CLI down AFTER it had already reported
34711
+ // the failure honestly. `undefined` is exactly what the handler treats as "no job to cancel".
34712
+ onStagingStarted(staging.then(response => response.jobId).catch(() => undefined));
34713
+ const response = await staging;
34714
+ jobId = response.jobId;
34715
+ // Named by the title the server rejected them under, which for this command is always the file name.
34716
+ duplicates = (response.duplicates ?? []).map(duplicate => duplicate.name);
34717
+ }
34718
+ catch (error) {
34719
+ const message = (0, assertic_1.getMessageFromError)(error);
34720
+ console.error(`${batchLabel}: staged ${stagedContexts.length} upload(s) but job creation failed: ${message}; ` +
34721
+ `${stagedContexts.length} uploaded object(s) remain staged (auto-expire in 24h).`);
34722
+ return {
34723
+ batchIndex,
34724
+ batchCount,
34725
+ uploadFailures,
34726
+ duplicates: [],
34727
+ timedOut: false,
34728
+ stagingError: message,
34729
+ stagedCount: stagedContexts.length,
34730
+ };
34731
+ }
34732
+ if (duplicates.length > 0) {
34733
+ console.log(`${batchLabel}: ${duplicates.length} file(s) skipped as content this knowledge base already holds.`);
34734
+ }
34735
+ activeJobIds.add(jobId);
34736
+ const { finalStatus, timedOut } = await waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, isInterrupted);
34737
+ activeJobIds.delete(jobId);
34738
+ return { batchIndex, batchCount, jobId, uploadFailures, duplicates, finalStatus, timedOut };
34739
+ }
34740
+ /** Mints presigned upload URLs for a batch, chunking requests to the server's per-call cap. */
34741
+ async function createUploadUrlsForBatch(client, fileNames) {
34742
+ const uploads = [];
34743
+ for (const group of chunk(fileNames, MAX_UPLOAD_URLS_PER_CALL)) {
34744
+ const response = await client.createUploadUrls(group);
34745
+ uploads.push(...response.uploads);
34746
+ }
34747
+ return uploads;
34748
+ }
34749
+ /**
34750
+ * PUTs one file's bytes to its presigned URL, retrying once on a network error or non-2xx response.
34751
+ * `upload.requiredHeaders` carries whatever the storage backend mandates beyond `Content-Type` (Azure Blob
34752
+ * rejects a PUT without `x-ms-blob-type`), so it must be sent as given rather than assumed empty.
34753
+ */
34754
+ async function uploadFile(filePath, upload, stagedObjectKey) {
34755
+ const fileName = path.basename(filePath);
34756
+ const mimeType = EXTENSION_MIME_TYPES[extensionOf(fileName)] || DEFAULT_MIME_TYPE;
34757
+ // Sized once, up front: a presigned PUT needs an explicit Content-Length because a stream body is not
34758
+ // measurable, and knowing it also lets the retry below reopen the file rather than hold it in memory.
34759
+ const { size } = await fs.stat(filePath);
34760
+ let lastError = 'Unknown upload error';
34761
+ for (let attempt = 0; attempt < 2; attempt++) {
34762
+ try {
34763
+ // Streamed from disk, NOT buffered: PUT_CONCURRENCY workers each reading a whole file into memory
34764
+ // allocates their combined size, and the direct-upload contract admits files far larger than this
34765
+ // process should ever hold. A fresh stream per attempt — a consumed one cannot be replayed.
34766
+ const response = await fetch(upload.uploadUrl, {
34767
+ method: 'PUT',
34768
+ headers: {
34769
+ 'Content-Type': mimeType,
34770
+ 'Content-Length': String(size),
34771
+ ...(upload.requiredHeaders ?? {}),
34772
+ },
34773
+ body: stream_1.Readable.toWeb((0, fs_1.createReadStream)(filePath)),
34774
+ // Node requires this for a streaming request body; without it fetch rejects the stream outright.
34775
+ duplex: 'half',
34776
+ });
34777
+ if (response.ok) {
34778
+ return { fileName, stagedObjectKey };
34779
+ }
34780
+ lastError = `HTTP ${response.status}: ${await response.text()}`;
34781
+ }
34782
+ catch (error) {
34783
+ lastError = (0, assertic_1.getMessageFromError)(error);
34784
+ }
34785
+ }
34786
+ return { fileName, error: lastError };
34787
+ }
34788
+ /**
34789
+ * Polls `bulk/getJob` until the job reaches a terminal state or the timeout elapses. A run can wait up to
34790
+ * `--timeoutMinutes` (default two hours), so a single transient poll error must not crash the whole CLI —
34791
+ * it's logged and retried on the next tick instead.
34792
+ */
34793
+ async function waitForJobCompletion(client, jobId, batchLabel, timeoutMillis, isInterrupted) {
34794
+ const deadline = Date.now() + timeoutMillis;
34795
+ let status;
34796
+ let isFirstPoll = true;
34797
+ while (!isInterrupted() && Date.now() < deadline && (!status || !TERMINAL_JOB_STATES.has(status.state))) {
34798
+ if (!isFirstPoll) {
34799
+ await sleep(POLL_INTERVAL_MILLIS);
34800
+ }
34801
+ isFirstPoll = false;
34802
+ try {
34803
+ status = await client.getJob(jobId);
34804
+ printProgress(batchLabel, jobId, status);
34805
+ }
34806
+ catch (error) {
34807
+ console.warn(`\n${batchLabel}: transient error polling job ${jobId}, retrying: ${(0, assertic_1.getMessageFromError)(error)}`);
34808
+ }
34809
+ }
34810
+ process.stdout.write('\n');
34811
+ if (!status) {
34812
+ console.warn(`${batchLabel}: could not retrieve job ${jobId}'s status before the timeout.`);
34813
+ return { finalStatus: UNKNOWN_JOB_STATUS, timedOut: true };
34814
+ }
34815
+ const timedOut = !TERMINAL_JOB_STATES.has(status.state) && !isInterrupted();
34816
+ if (timedOut) {
34817
+ console.warn(`${batchLabel}: job ${jobId} did not finish within the timeout; it may still be running server-side.`);
34818
+ }
34819
+ return { finalStatus: status, timedOut };
34820
+ }
34821
+ /** Best-effort cancellation of every job still active when the process is interrupted. */
34822
+ async function cancelActiveJobs(client, activeJobIds) {
34823
+ await Promise.allSettled(Array.from(activeJobIds).map(async (jobId) => {
34824
+ try {
34825
+ await client.cancelJob(jobId);
34826
+ console.log(`Cancelled job ${jobId}.`);
34827
+ }
34828
+ catch (error) {
34829
+ console.warn(`Failed to cancel job ${jobId}: ${(0, assertic_1.getMessageFromError)(error)}`);
34830
+ }
34831
+ }));
34832
+ }
34833
+ function printProgress(batchLabel, jobId, status) {
34834
+ const { counts, state } = status;
34835
+ const line = `${batchLabel} job ${jobId}: ${counts.finalized}/${counts.files} finalized, ${counts.failed} failed, state=${state}`;
34836
+ process.stdout.write(`\r${line}${' '.repeat(Math.max(0, PROGRESS_LINE_WIDTH - line.length))}`);
34837
+ }
34838
+ /** Narrows a `FileUploadResult` to one that uploaded successfully (has a `stagedObjectKey`, no `error`). */
34839
+ function isUploaded(result) {
34840
+ return result.stagedObjectKey !== undefined;
34841
+ }
34842
+ async function printDryRun(dir, files, batchSize) {
34843
+ const preview = files.slice(0, 20);
34844
+ for (const filePath of preview) {
34845
+ console.log(path.relative(dir, filePath));
34846
+ }
34847
+ if (files.length > preview.length) {
34848
+ console.log(`... and ${files.length - preview.length} more`);
34849
+ }
34850
+ // Uses the real grouping rather than a count division: batches are bounded by bytes as well, so a
34851
+ // division would under-report the batch count for exactly the large-corpus runs this flag is used to plan.
34852
+ const batchCount = (await groupFilesIntoBatches(files, batchSize)).length;
34853
+ console.log(`\n${files.length} file(s) would be uploaded in ${batchCount} batch(es) of up to ${batchSize} file(s) ` +
34854
+ `and ${Math.floor(MAX_BATCH_BYTES / (1024 * 1024))} MiB. Dry run: no files were uploaded.`);
34855
+ }
34856
+ function printSummary(totalFiles, results) {
34857
+ 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);
34859
+ const totalUploadFailed = results.reduce((sum, result) => sum + result.uploadFailures.length, 0);
34860
+ const totalDuplicates = results.reduce((sum, result) => sum + result.duplicates.length, 0);
34861
+ const totalStagedButFailed = results.reduce((sum, result) => sum + (result.stagingError ? (result.stagedCount ?? 0) : 0), 0);
34862
+ console.log('\n=== Bulk KB upload summary ===');
34863
+ console.log(`Files scanned: ${totalFiles}`);
34864
+ console.log(`Finalized (server): ${totalFinalized}`);
34865
+ console.log(`Failed (server-side): ${totalServerFailed}`);
34866
+ console.log(`Failed to upload: ${totalUploadFailed}`);
34867
+ if (totalDuplicates > 0) {
34868
+ // Between "scanned" and "finalized" so the two reconcile: a duplicate is neither finalized nor failed,
34869
+ // and reading the summary without this line the difference looks like files that silently disappeared.
34870
+ console.log(`Skipped as duplicate: ${totalDuplicates}`);
34871
+ }
34872
+ if (totalStagedButFailed > 0) {
34873
+ console.log(`Staged but job failed: ${totalStagedButFailed} (uploaded objects remain; auto-expire in 24h)`);
34874
+ }
34875
+ for (const result of results) {
34876
+ const label = `batch ${result.batchIndex + 1}/${result.batchCount}`;
34877
+ if (result.stagingError) {
34878
+ console.log(` ${label}: staged ${result.stagedCount ?? 0} upload(s) but job creation failed: ${result.stagingError}; ` +
34879
+ `uploaded object(s) remain (auto-expire in 24h)`);
34880
+ continue;
34881
+ }
34882
+ if (result.batchError) {
34883
+ console.log(` ${label}: unexpected error before staging: ${result.batchError}`);
34884
+ continue;
34885
+ }
34886
+ if (!result.jobId) {
34887
+ console.log(` ${label}: no job created (all uploads failed)`);
34888
+ continue;
34889
+ }
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}]`);
34894
+ // Per-file failure reasons, straight from the job-status response (BulkIngestionFileStatus).
34895
+ const failedFiles = result.finalStatus?.files.filter(file => file.status === 'failed') ?? [];
34896
+ for (const failedFile of failedFiles) {
34897
+ console.log(` - ${failedFile.title}: ${failedFile.errorMessage ?? 'unknown error'}`);
34898
+ }
34899
+ // Named individually for the same reason failures are: "20 skipped" does not tell an operator whether
34900
+ // the right 20 were skipped, and these files never reach the job, so nothing else will ever name them.
34901
+ for (const duplicate of result.duplicates) {
34902
+ console.log(` - ${duplicate}: skipped, already in this knowledge base`);
34903
+ }
34904
+ }
34905
+ }
34906
+ function hasBatchFailed(result) {
34907
+ if (result.uploadFailures.length > 0 || result.timedOut)
34908
+ return true;
34909
+ if (!result.finalStatus)
34910
+ return true;
34911
+ return result.finalStatus.state !== 'completed' || result.finalStatus.counts.failed > 0;
34912
+ }
34913
+ /** Recursively walks `dir`, skipping symlinks and dotfiles/dot-directories, filtered to `extensions`. */
34914
+ async function collectFiles(dir, extensions) {
34915
+ const stat = await fs.stat(dir).catch(() => undefined);
34916
+ if (!stat || !stat.isDirectory()) {
34917
+ (0, process_utils_1.exitWithError)(`--dir must point to an existing directory: ${dir}`);
34918
+ }
34919
+ const files = [];
34920
+ await walk(dir, files, extensions);
34921
+ files.sort();
34922
+ return files;
34923
+ }
34924
+ async function walk(dir, files, extensions) {
34925
+ const entries = await fs.readdir(dir, { withFileTypes: true });
34926
+ for (const entry of entries) {
34927
+ if (entry.name.startsWith('.') || entry.isSymbolicLink()) {
34928
+ continue;
34929
+ }
34930
+ const fullPath = path.join(dir, entry.name);
34931
+ if (entry.isDirectory()) {
34932
+ await walk(fullPath, files, extensions);
34933
+ }
34934
+ else if (entry.isFile() && extensions.has(extensionOf(entry.name))) {
34935
+ files.push(fullPath);
34936
+ }
34937
+ }
34938
+ }
34939
+ function extensionOf(fileName) {
34940
+ return path.extname(fileName).slice(1).toLowerCase();
34941
+ }
34942
+ function parseExtensions(raw) {
34943
+ const extensions = raw
34944
+ .split(',')
34945
+ .map(extension => extension.trim().replace(/^\./, '').toLowerCase())
34946
+ .filter(extension => extension.length > 0);
34947
+ if (extensions.length === 0) {
34948
+ (0, process_utils_1.exitWithError)('--extensions must list at least one extension');
34949
+ }
34950
+ return new Set(extensions);
34951
+ }
34952
+ function validateBatchSize(batchSize) {
34953
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > MAX_BATCH_SIZE) {
34954
+ (0, process_utils_1.exitWithError)(`--batchSize must be an integer between 1 and ${MAX_BATCH_SIZE}`);
34955
+ }
34956
+ return batchSize;
34957
+ }
34958
+ /**
34959
+ * The app id the DATA PLANE is addressed by: base id plus environment plus developer id, exactly as
34960
+ * `deploy` (appIdWithEnvironmentId) and `sample` (appIdWithEnvironmentIdAndDevId) build it.
34961
+ *
34962
+ * The base id alone is not a synonym for it. `appIdWithEnvironmentId` renders `prod` as the bare id, so
34963
+ * uploading with an unsuffixed id targets PRODUCTION; and omitting the developer id targets the shared
34964
+ * dev environment instead of the developer's own sandbox — which the API key does not catch, because key
34965
+ * lookup strips the developer id (`omitSquidDevId`), so both authenticate with the same key and the
34966
+ * upload silently lands in the wrong place.
34967
+ */
34968
+ function getDataPlaneAppId(argv) {
34969
+ const appId = argv.appId || process.env['SQUID_APP_ID'];
34970
+ if (!appId) {
34971
+ (0, process_utils_1.exitWithError)('Missing application ID: pass --appId or set SQUID_APP_ID');
34972
+ }
34973
+ const environmentId = (argv.environmentId || process.env['SQUID_ENVIRONMENT_ID']);
34974
+ const developerId = process.env['SQUID_DEVELOPER_ID'];
34975
+ return (0, communication_types_1.appIdWithEnvironmentIdAndDevId)(appId, environmentId, developerId);
34976
+ }
34977
+ function getRegion(argv) {
34978
+ const region = argv.region || process.env['SQUID_REGION'];
34979
+ if (!region) {
34980
+ (0, process_utils_1.exitWithError)('Missing Squid region: pass --region or set SQUID_REGION');
34981
+ }
34982
+ return region;
34983
+ }
34984
+ function getAuth(argv) {
34985
+ const apiKey = argv.apiKey || process.env['SQUID_API_KEY'];
34986
+ const internalApiKey = argv.internalApiKey || process.env['SQUID_INTERNAL_API_KEY'];
34987
+ if (!apiKey && !internalApiKey) {
34988
+ (0, process_utils_1.exitWithError)('Missing credentials: pass --apiKey (or SQUID_API_KEY) or --internalApiKey (or SQUID_INTERNAL_API_KEY)');
34989
+ }
34990
+ return { apiKey, internalApiKey };
34991
+ }
34992
+ function chunk(items, size) {
34993
+ const chunks = [];
34994
+ for (let i = 0; i < items.length; i += size) {
34995
+ chunks.push(items.slice(i, i + size));
34996
+ }
34997
+ return chunks;
34998
+ }
34999
+ /**
35000
+ * Groups files into batches bounded by BOTH count and total bytes.
35001
+ *
35002
+ * Count alone is not a bound on what the server will accept: it caps a job's staged CONTENT at
35003
+ * BULK_INGESTION_MAX_STAGED_BYTES, so a default 200-file batch of individually legal 2 MiB documents adds
35004
+ * up to roughly 400 MiB and the whole job is rejected with BULK_INGESTION_TOO_LARGE — every file in it,
35005
+ * including the 199 that were fine.
35006
+ *
35007
+ * On-disk size is a proxy for extracted content, not an equality: plain text is about 1:1, while a PDF or
35008
+ * a workbook can extract to more text than it occupies. {@link MAX_BATCH_BYTES} is set well under the
35009
+ * server ceiling to absorb that, and a file bigger than the budget still gets its own batch — splitting a
35010
+ * single document is not something this tool can do.
35011
+ */
35012
+ async function groupFilesIntoBatches(files, batchSize) {
35013
+ const batches = [];
35014
+ let current = [];
35015
+ let currentBytes = 0;
35016
+ for (const filePath of files) {
35017
+ // A file that cannot be stat'ed is counted as weightless rather than skipped: it still belongs in a
35018
+ // batch, and the upload itself is what should report its failure.
35019
+ const size = await fs
35020
+ .stat(filePath)
35021
+ .then(stat => stat.size)
35022
+ .catch(() => 0);
35023
+ if (current.length > 0 && (current.length >= batchSize || currentBytes + size > MAX_BATCH_BYTES)) {
35024
+ batches.push(current);
35025
+ current = [];
35026
+ currentBytes = 0;
35027
+ }
35028
+ current.push(filePath);
35029
+ currentBytes += size;
35030
+ }
35031
+ if (current.length > 0)
35032
+ batches.push(current);
35033
+ return batches;
35034
+ }
35035
+ /** Runs `worker` over `items` with at most `concurrency` in flight at once, preserving result order. */
35036
+ async function runWithConcurrency(items, concurrency, worker) {
35037
+ const results = new Array(items.length);
35038
+ let nextIndex = 0;
35039
+ const runNext = async () => {
35040
+ while (nextIndex < items.length) {
35041
+ const currentIndex = nextIndex;
35042
+ nextIndex += 1;
35043
+ results[currentIndex] = await worker(items[currentIndex], currentIndex);
35044
+ }
35045
+ };
35046
+ const workerCount = Math.min(concurrency, items.length);
35047
+ await Promise.all(Array.from({ length: workerCount }, runNext));
35048
+ return results;
35049
+ }
35050
+ function sleep(millis) {
35051
+ return new Promise(resolve => setTimeout(resolve, millis));
35052
+ }
35053
+ /** Thin raw-fetch client for the Phase-5 bulk-ingestion HTTP surface (no SDK dependency — see report). */
35054
+ class BulkIngestionClient {
35055
+ constructor(region, appId, apiKey, internalApiKey) {
35056
+ this.region = region;
35057
+ this.appId = appId;
35058
+ this.apiKey = apiKey;
35059
+ this.internalApiKey = internalApiKey;
35060
+ this.lp = 'BulkIngestionClient';
35061
+ }
35062
+ async createUploadUrls(fileNames) {
35063
+ return this.post('ai/knowledge-base/bulk/createUploadUrls', { files: fileNames.map(fileName => ({ fileName })) });
35064
+ }
35065
+ async upsertContexts(knowledgeBaseId, contexts) {
35066
+ return this.post('ai/knowledge-base/bulk/upsertContexts', { knowledgeBaseId, contexts });
35067
+ }
35068
+ async getJob(jobId) {
35069
+ return this.post('ai/knowledge-base/bulk/getJob', { jobId });
35070
+ }
35071
+ async cancelJob(jobId) {
35072
+ await this.post('ai/knowledge-base/bulk/cancelJob', { jobId });
35073
+ }
35074
+ async post(urlPath, body) {
35075
+ const lp = `${this.lp}.post:`;
35076
+ const url = (0, http_1.getApplicationUrl)(this.region, this.appId, urlPath);
35077
+ console.debug(`${lp} POST ${url}`);
35078
+ const headers = { 'Content-Type': 'application/json' };
35079
+ if (this.apiKey) {
35080
+ headers['Authorization'] = `ApiKey ${this.apiKey}`;
35081
+ }
35082
+ if (this.internalApiKey) {
35083
+ headers['x-squid-secret'] = this.internalApiKey;
35084
+ }
35085
+ const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
35086
+ const text = await response.text();
35087
+ if (!response.ok) {
35088
+ throw new Error(`${urlPath} failed: ${response.status} ${text}`);
35089
+ }
35090
+ return (text ? JSON.parse(text) : undefined);
35091
+ }
35092
+ }
35093
+
35094
+
34370
35095
  /***/ },
34371
35096
 
34372
35097
  /***/ 4291
@@ -34422,6 +35147,7 @@ const build_1 = __webpack_require__(8584);
34422
35147
  const deploy_1 = __webpack_require__(8705);
34423
35148
  const init_env_1 = __webpack_require__(1838);
34424
35149
  const init_webpack_1 = __webpack_require__(1134);
35150
+ const kb_upload_1 = __webpack_require__(2713);
34425
35151
  const sample_1 = __webpack_require__(4328);
34426
35152
  const start_1 = __webpack_require__(496);
34427
35153
  const undeploy_1 = __webpack_require__(7097);
@@ -34449,6 +35175,7 @@ function run() {
34449
35175
  setupInitEnvCommand(yargs_1.default);
34450
35176
  setupInitSampleCommand(yargs_1.default);
34451
35177
  setupInitWebpackCommand(yargs_1.default);
35178
+ (0, kb_upload_1.setupKbUploadCommand)(yargs_1.default);
34452
35179
  setupStartCommand(yargs_1.default);
34453
35180
  setupUndeployCommand(yargs_1.default);
34454
35181
  setupUpdateSkillsCommand(yargs_1.default);
@@ -40002,7 +40729,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"name":"seek-bzip","version":"1.0.6",
40002
40729
  (module) {
40003
40730
 
40004
40731
  "use strict";
40005
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.485","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.485","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","@types/node":"^20.19.9","terminal-link":"^3.0.0"}}');
40732
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@squidcloud/cli","version":"1.0.487","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.487","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"}}');
40006
40733
 
40007
40734
  /***/ }
40008
40735
 
@@ -23,7 +23,7 @@ Squid is a backend-as-a-service platform that provides:
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, getUrlContent, createShortUrl, jobs, getJob, awaitJob, observability, metrics, reportMetric, queryMetrics, notifications, publishNotification, observeNotifications
25
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
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, spreadsheet understanding, querySpreadsheetsWithAi, searchMode, keyword search, xlsx, csv
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(), 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, 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
@@ -436,6 +436,19 @@ const searchResults = await kb.search({
436
436
  limit: 10
437
437
  });
438
438
 
439
+ // Keyword (lexical, embedding-free) search — best for exact tokens: IDs, error codes, SKUs, file names.
440
+ // searchMode: 'vector' | 'hybrid' | 'keyword' | 'graph'. Default: 'hybrid' ('graph' on GraphRAG-enabled
441
+ // KBs; setting 'graph' on other KBs throws). Keyword mechanics differ by the KB's vectorDbType:
442
+ // mongoAtlas = ranked BM25, term-optional (partial matches still rank); postgres = unranked boolean
443
+ // filter where EVERY whitespace-separated term must appear as a literal substring — a multi-word
444
+ // prompt returns zero chunks when any one term is absent, so pass few, specific tokens.
445
+ // On postgres 'hybrid' degrades to dense-only (no lexical channel; hybridWeights is ignored off
446
+ // Mongo), so exact-token retrieval there REQUIRES explicitly setting searchMode: 'keyword'.
447
+ const keywordResults = await kb.search({
448
+ prompt: 'ERR_0000_4F2A',
449
+ searchMode: 'keyword'
450
+ });
451
+
439
452
  // Get context
440
453
  const context = await kb.getContext('doc-123');
441
454
 
@@ -451,6 +464,14 @@ await kb.deleteContext('doc-123');
451
464
  await kb.deleteContexts(['doc-1', 'doc-2']);
452
465
  ```
453
466
 
467
+ ### Spreadsheet Understanding
468
+
469
+ 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.
470
+
471
+ 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.
472
+
473
+ 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.
474
+
454
475
  ## AI Files
455
476
 
456
477
  Manage files stored with AI providers (OpenAI, etc.) for use with agents.
@@ -20,7 +20,8 @@ import {
20
20
 
21
21
  ### Verbosity Control
22
22
 
23
- Controls response length. OpenAI plain text responses only (ignored for other providers).
23
+ Controls response length. OpenAI only (ignored for other providers), and applies to every response
24
+ format — `text`, `json_object` and `json_schema` alike.
24
25
 
25
26
  ```typescript
26
27
  const response = await agent.ask('Explain quantum computing', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squidcloud/cli",
3
- "version": "1.0.485",
3
+ "version": "1.0.487",
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.485",
31
+ "@squidcloud/local-backend": "^1.0.487",
32
32
  "adm-zip": "^0.5.16",
33
33
  "copy-webpack-plugin": "^14.0.0",
34
34
  "decompress": "^4.2.1",
@@ -45,7 +45,6 @@
45
45
  "devDependencies": {
46
46
  "@types/adm-zip": "^0.5.7",
47
47
  "@types/decompress": "^4.2.7",
48
- "@types/node": "^20.19.9",
49
48
  "terminal-link": "^3.0.0"
50
49
  }
51
50
  }