@descix/cli 1.0.12 → 1.0.14

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/bin/descix.js CHANGED
@@ -2229,14 +2229,13 @@ corpusCommand
2229
2229
  });
2230
2230
 
2231
2231
  // ============ KB Doctor (M3, 2026-04-20) ============
2232
- // Compare local sync-state vs Pinecone vectorCount and scan recent sync
2233
- // logs for per-file 0-chunk warnings. Exits non-zero on drift > threshold.
2232
+ // Compare the live corpus files against the last sync's walk (orphans / missing) and scan recent
2233
+ // sync logs for per-file 0-chunk warnings. Exits 1 on orphans or missing files.
2234
2234
  kbCommand
2235
2235
  .command('doctor')
2236
- .description('Detect drift between local sync-state and live Pinecone vector count')
2236
+ .description('Check a KB against its last sync by FILE IDENTITY: live corpus files that were not walked (orphans) and walked files that are not live (missing)')
2237
2237
  .requiredOption('-a, --app <id>', 'App ID')
2238
2238
  .requiredOption('-k, --kb <name>', 'KB name')
2239
- .option('-t, --threshold <ratio>', 'Drift threshold as a fraction (default: 0.05)', parseFloat)
2240
2239
  .option('--live', 'Compute vectorCount from the TRUE live Pinecone scope (bypass the cached counter, which lies after an interrupted op)')
2241
2240
  .option('--reconcile', 'Compute the live count AND write it back to rag_vector_count so the cached read is truthful again (implies --live)')
2242
2241
  .action(async (options) => {
@@ -2537,6 +2536,11 @@ siteCommand
2537
2536
  return;
2538
2537
  }
2539
2538
 
2539
+ // 1b. Refuse to publish backend source, an agent's persona, credentials or workspace
2540
+ // state — BEFORE any token is requested, so a dry run refuses too (SiteDenyLint).
2541
+ const { assertSitePublishable } = await import('../lib/core/SiteDenyLint.js');
2542
+ assertSitePublishable(fileList.map((f) => f.path));
2543
+
2540
2544
  // 2. Request deploy token with file list
2541
2545
  const tokenResponse = await apiClient.invoke('get_site_deploy_token', {
2542
2546
  community_id: communityId,
@@ -156,6 +156,21 @@ async function chunkCorpusFile(fileEntry, context) {
156
156
  }).filter(Boolean);
157
157
  }
158
158
 
159
+
160
+ /**
161
+ * The walked files that produce NO chunks, for this sync's state. Returns null (UNKNOWN) when a
162
+ * file carried over unchanged comes from a state that never recorded the field: a partial list
163
+ * would make `kb doctor` report a file as lost from the store when it is merely empty.
164
+ */
165
+ export function zeroChunkBlobShas(previousState, localBlobShas, newOrChangedFiles, zeroThisRun) {
166
+ const changed = new Set(newOrChangedFiles.map((f) => f.blob_sha));
167
+ const carriedUnchanged = [...localBlobShas].filter((sha) => !changed.has(sha));
168
+ if (carriedUnchanged.length > 0 && !Array.isArray(previousState?.zero_chunk_blob_shas)) return null;
169
+ const previousZero = new Set(previousState?.zero_chunk_blob_shas || []);
170
+ const carried = carriedUnchanged.filter((sha) => previousZero.has(sha));
171
+ return [...new Set([...carried, ...zeroThisRun])].sort();
172
+ }
173
+
159
174
  /**
160
175
  * Read the previously-synced chunk total out of sync state, for REPORTING only.
161
176
  *
@@ -660,26 +675,41 @@ export async function runCorpusSync(apiClient, options) {
660
675
  const localBlobShas = new Set(files.map(f => f.blob_sha));
661
676
  const newOrChangedFiles = files.filter(f => !previousBlobShas.has(f.blob_sha));
662
677
 
663
- // Stale blob_shas: anything we synced previously that is no longer in the local
664
- // walk. This covers BOTH cases:
665
- // (a) File deleted from the source corpus entirely.
666
- // (b) File modified in place — same source_path, new blob_sha → old blob_sha
667
- // is no longer in localBlobShas.
668
- // Each stale blob_sha maps to a file_id of `corpus:{blob_sha}` in Pinecone metadata
669
- // and is purged via the deleteStaleChunksByFileId primitive (multi-tenancy filter).
670
- const deletedBlobShas = [...previousBlobShas].filter(sha => !localBlobShas.has(sha));
671
- const fileIdsToDelete = deletedBlobShas.map(sha => `corpus:${sha}`);
678
+ // STALE = what is LIVE in this KB's corpus scheme and absent from the walk. ONE
679
+ // derivation, read from the store itself — never from local history.
680
+ //
681
+ // It used to be "previously synced blob_shas minus the walk", which is only as good as
682
+ // the history. Measured 2026-09-18 (GODSWORLD-DEV, PROD): the first origin-keyed sync
683
+ // had NO history for its origin, so it purged nothing, and a superseded INDEX.md stayed
684
+ // retrievable beside its replacement indefinitely — no later incremental sync could
685
+ // ever name it. Lost state, a repointed origin and a hand edit all fail the same way.
686
+ // A `corpus:` file_id is written only by this manifest's sync, so a live corpus id the
687
+ // walk does not produce is stale by definition. Both cases are covered:
688
+ // (a) file deleted from the corpus; (b) file modified in place (new blob_sha).
689
+ // --rebuild purges the whole scope instead, so it computes no per-file list.
690
+ const localFileIds = new Set(files.map(f => `corpus:${f.blob_sha}`));
691
+ let fileIdsToDelete = [];
692
+ if (!options.rebuild) {
693
+ const remote = await listRemoteFileIds(apiClient, appId, kbName);
694
+ fileIdsToDelete = remote.file_ids
695
+ .filter(id => typeof id === 'string' && id.startsWith('corpus:') && !localFileIds.has(id))
696
+ .sort();
697
+ }
672
698
 
673
699
  const unchangedCount = files.length - newOrChangedFiles.length;
674
700
 
675
701
  if (options.verbose) {
676
702
  console.log(chalk.gray(` New/changed files: ${newOrChangedFiles.length}`));
677
703
  console.log(chalk.gray(` Unchanged files: ${unchangedCount}`));
678
- console.log(chalk.gray(` Stale blob SHAs (delete): ${deletedBlobShas.length}`));
704
+ console.log(chalk.gray(` Stale corpus file_ids live in the KB (delete): ${fileIdsToDelete.length}`));
679
705
  }
680
706
 
681
707
  // 3d. Chunk new/changed files
682
708
  const allChunks = [];
709
+ // Files this run chunked to NOTHING — recorded so `kb doctor` can tell "empty by design"
710
+ // from "lost from the store". A file that FAILED to chunk is not added: it is missing
711
+ // content, and the doctor should say so.
712
+ const zeroChunkShasThisRun = [];
683
713
  if (newOrChangedFiles.length > 0) {
684
714
  const chunkSpinner = ora(` Chunking ${newOrChangedFiles.length} file(s)...`).start();
685
715
 
@@ -689,6 +719,7 @@ export async function runCorpusSync(apiClient, options) {
689
719
  allChunks.push(...chunks);
690
720
 
691
721
  if (chunks.length === 0) {
722
+ zeroChunkShasThisRun.push(fileEntry.blob_sha);
692
723
  // M1 (2026-04-20): always surface 0-chunk files even without -v.
693
724
  // These are the silent failures that caused the 77% EGPT gap.
694
725
  // 'kb doctor' scans sync logs for this exact string.
@@ -938,6 +969,11 @@ export async function runCorpusSync(apiClient, options) {
938
969
  chunks_upserted: isReportedCount(upserted) ? upserted : null,
939
970
  chunks_deleted: isReportedCount(deleted) ? deleted : null,
940
971
  synced_blob_shas: allSyncedBlobShas,
972
+ // Walked files that produced no chunks. KNOWN only when every file's outcome is known:
973
+ // chunked this run, or carried from a previous state that recorded the field. A state
974
+ // written before this field existed leaves unchanged files unaccounted for, so the
975
+ // field is null (UNKNOWN) rather than a partial list the doctor would read as complete.
976
+ zero_chunk_blob_shas: zeroChunkBlobShas(previousState, localBlobShas, newOrChangedFiles, zeroChunkShasThisRun),
941
977
  // The retrieval canary's verdict — read this, not `total_chunks`, to know whether the
942
978
  // last sync's content was CONFIRMED searchable or only confirmed present.
943
979
  retrieval_canary: retrievalCanary
@@ -27,7 +27,7 @@ import { requireInteractive } from '../interactive.js';
27
27
  import { WorkspaceConfig } from '../workspace-config.js';
28
28
  import { hydrateKb, pushStaging, checkStagingFiles } from '../core/Hydrator.js';
29
29
  import { processKb } from '../core/Chunker.js';
30
- import { syncKb, getSyncStatus } from '../core/Syncer.js';
30
+ import { syncKb, getSyncStatus, listRemoteFileIds } from '../core/Syncer.js';
31
31
  import * as driveADC from '../google-storage-adc.js';
32
32
  // The one canonical KB-sync surface, from its owner. Never spell it as a literal here: these
33
33
  // commands' next steps are read by a developer as instructions, so they must name the live verb
@@ -252,32 +252,30 @@ export async function runKbPush(apiClient, options) {
252
252
 
253
253
  // ============ M3: `descix kb doctor` — drift detector (2026-04-20) ============
254
254
  /**
255
- * Compare local sync-state vs live Pinecone vectorCount, and scan the most
256
- * recent verbose sync log (if any) for per-file 0-chunk warnings. Reports
257
- * drift direction and exits non-zero when drift exceeds the threshold.
255
+ * Check a KB against its last corpus sync by FILE IDENTITY, and scan the most recent verbose
256
+ * sync log (if any) for per-file 0-chunk warnings.
258
257
  *
259
- * WHY THIS EXISTS
260
- * ---------------
261
- * `descix kb corpus sync` trusts local sync-state/<KB>.json for delta
262
- * detection. When the Pinecone index is reset (e.g. dev-env churn), the
263
- * local state still lists all blob SHAs as "synced", so the next sync
264
- * skips re-upload and orphan/loss goes undetected until a content
265
- * retrieval test fails. This command surfaces the drift directly.
258
+ * WHY IDENTITY, NOT COUNT
259
+ * -----------------------
260
+ * The first version compared Pinecone's vectorCount with the sync-state's total_chunks. But the
261
+ * sync writes total_chunks FROM the reconciled live count, so the comparison was the store
262
+ * against a copy of itself: drift 0 by construction, "HEALTHY" over a superseded document that
263
+ * was still live and retrievable (measured 2026-09-18, GODSWORLD-DEV, PROD). vectorCount also
264
+ * lags deletes. What matters is WHICH files are live, so that is what this checks.
266
265
  *
267
266
  * BEHAVIOUR
268
267
  * ---------
269
- * descix kb doctor -a <app> -k <kb>
268
+ * descix kb doctor -a <app> -k <kb> [--live | --reconcile]
270
269
  *
271
- * (a) Queries Pinecone via get_kb_rag_status for vectorCount.
272
- * (b) Reads {appRoot}/.descix/sync-state/{kb}.json for total_chunks.
273
- * (c) Reports diff and direction:
274
- * Pinecone < sync-state → LOST (chunks missing from Pinecone)
275
- * Pinecone > sync-state → ORPHANS (Pinecone has extra vectors)
276
- * |drift| / local ≤ threshold → HEALTHY
277
- * (d) Scans the most recent file matching logs/kb-sync-*.log for
278
- * '0-chunk' / 'skipped' warnings (these are the signal M1 wired
279
- * into corpus.js).
280
- * (e) Exits non-zero on drift above threshold (default: 5%).
270
+ * (a) get_kb_rag_status for vectorCount — reported as information only. --live / --reconcile
271
+ * still check the server's CACHED counter against the live store, a separate question.
272
+ * (b) Reads this origin's sync-state: the walked blob SHAs and the zero-chunk ones.
273
+ * (c) Lists the live corpus file_ids and reports:
274
+ * ORPHANS — live but not walked: retrievable stale content. Fails.
275
+ * MISSING — walked, has content, not live. Fails. UNVERIFIABLE (not failing) when the
276
+ * state predates the zero-chunk record, rather than guessing.
277
+ * (d) Scans the most recent logs/kb-sync-*.log for '0-chunk' / 'skipped' warnings.
278
+ * (e) Exits 1 on orphans or missing files, 2 when the state cannot be read.
281
279
  */
282
280
  export async function runKbDoctor(apiClient, options) {
283
281
  const { WorkspaceConfig } = await import('../workspace-config.js');
@@ -293,10 +291,6 @@ export async function runKbDoctor(apiClient, options) {
293
291
  const appRoot = appMeta.absolutePath;
294
292
  const communityId = appMeta.communityId || appMeta.community_id || null;
295
293
 
296
- // Drift threshold (fraction). CEO guidance 2026-04-20: ~5% is reasonable.
297
- const DRIFT_THRESHOLD = Number.isFinite(Number(options.threshold))
298
- ? Number(options.threshold) : 0.05;
299
-
300
294
  const scope = communityId ? `${communityId}/${appId}/${kbName}` : `${appId}/${kbName}`;
301
295
  console.log(chalk.cyan(`\n🩺 kb doctor — ${scope}\n`));
302
296
 
@@ -362,23 +356,33 @@ export async function runKbDoctor(apiClient, options) {
362
356
  console.log(chalk.gray(` Run 'descix kb corpus sync -a ${appId} -k ${kbName}' against this origin first.`));
363
357
  process.exit(2);
364
358
  }
365
- const localChunks = syncStateRaw.total_chunks;
366
- if (typeof localChunks !== 'number') {
367
- throw new Error(`sync-state has no total_chunks: ${statePathForOrigin}`);
359
+ // (c) IDENTITY, not count. Which corpus files are LIVE, against which files the last sync
360
+ // WALKED. The count comparison this replaced read the store against `total_chunks`, which the
361
+ // sync itself writes FROM the reconciled live count — so drift was zero by construction and
362
+ // the check could not fail (measured 2026-09-18, GODSWORLD-DEV: "HEALTHY" printed over a
363
+ // superseded INDEX.md that was still live and retrievable). Identity catches exactly that.
364
+ if (!Array.isArray(syncStateRaw.synced_blob_shas)) {
365
+ console.log(chalk.yellow(` ⚠ sync-state records no synced_blob_shas at ${statePathForOrigin} — the walked set is UNKNOWN.`));
366
+ process.exit(2);
368
367
  }
369
-
370
- // (c) Report
371
- const delta = vectorCount - localChunks;
372
- const ratio = localChunks === 0 ? Infinity : Math.abs(delta) / localChunks;
373
- const direction = delta === 0 ? 'EXACT'
374
- : delta < 0 ? 'LOST (Pinecone missing chunks)'
375
- : 'ORPHANS (Pinecone has extra vectors)';
376
- const healthy = Math.abs(delta) / Math.max(localChunks, 1) <= DRIFT_THRESHOLD;
377
-
378
- console.log(` Pinecone vectorCount : ${chalk.white(vectorCount)}`);
379
- console.log(` Local total_chunks : ${chalk.white(localChunks)}`);
380
- console.log(` Drift : ${chalk.white((delta >= 0 ? '+' : '') + delta)} (${(ratio * 100).toFixed(1)}%)`);
381
- console.log(` Direction : ${delta === 0 ? chalk.green(direction) : (healthy ? chalk.yellow(direction) : chalk.red(direction))}`);
368
+ const report = diagnoseCorpusIdentity({
369
+ liveFileIds: (await listRemoteFileIds(apiClient, appId, kbName)).file_ids,
370
+ walkedBlobShas: syncStateRaw.synced_blob_shas,
371
+ zeroChunkBlobShas: syncStateRaw.zero_chunk_blob_shas,
372
+ });
373
+
374
+ console.log(` Walked by last sync : ${chalk.white(report.walked)} file(s)` +
375
+ (report.zeroKnown ? chalk.gray(` (${report.zeroChunk} empty by design)`) : ''));
376
+ console.log(` Live corpus files : ${chalk.white(report.live)}`);
377
+ console.log(` Orphans (live, not walked) : ${report.orphans.length ? chalk.red(report.orphans.length) : chalk.green(0)}`);
378
+ report.orphans.slice(0, 10).forEach((id) => console.log(chalk.red(` ${id}`)));
379
+ if (report.zeroKnown) {
380
+ console.log(` Missing (walked, not live) : ${report.missing.length ? chalk.red(report.missing.length) : chalk.green(0)}`);
381
+ report.missing.slice(0, 10).forEach((id) => console.log(chalk.red(` ${id}`)));
382
+ } else {
383
+ console.log(chalk.yellow(' Missing (walked, not live) : UNVERIFIABLE — this state predates the record of empty files; the next sync writes it.'));
384
+ }
385
+ console.log(chalk.gray(` Pinecone vectorCount : ${vectorCount} (informational — it lags deletes and is not a health signal)`));
382
386
  console.log(` Index : ${ragStatus.indexName || '(unknown)'}`);
383
387
  console.log(` Last sync (server) : ${ragStatus.lastSync || '(unknown)'}`);
384
388
  console.log(` Last sync (local) : ${syncStateRaw.timestamp || '(unknown)'}`);
@@ -420,19 +424,37 @@ export async function runKbDoctor(apiClient, options) {
420
424
 
421
425
  // (e) Exit
422
426
  console.log();
423
- if (!healthy) {
424
- console.log(chalk.red(` ✗ DRIFT detected: ${(ratio * 100).toFixed(1)}% exceeds ${(DRIFT_THRESHOLD * 100).toFixed(1)}% threshold.\n`));
425
- if (delta < 0) {
426
- console.log(chalk.gray(` Recommended: invalidate sync-state and re-sync:`));
427
- console.log(chalk.gray(` rm "${syncStatePath}"`));
428
- console.log(chalk.gray(` descix kb corpus sync -a ${appId} -k ${kbName}\n`));
429
- } else {
430
- console.log(chalk.gray(` Recommended: clear stale vectors and re-sync to rebuild Pinecone from local state.\n`));
431
- }
432
- process.exit(1);
427
+ if (report.orphans.length > 0) {
428
+ console.log(chalk.red(` ✗ ORPHANS: ${report.orphans.length} corpus file(s) are live and retrievable but were not in the last walk.`));
429
+ console.log(chalk.gray(` A plain sync purges them (it reconciles against the live KB). Preview it first:`));
430
+ console.log(chalk.gray(` descix kb corpus sync -a ${appId} -k ${kbName} --dry-run\n`));
433
431
  }
432
+ if (report.missing.length > 0) {
433
+ console.log(chalk.red(` ✗ MISSING: ${report.missing.length} walked file(s) hold chunks but are not live.`));
434
+ console.log(chalk.gray(` A plain sync skips files its state already records, so rebuild:`));
435
+ console.log(chalk.gray(` descix kb corpus sync -a ${appId} -k ${kbName} --rebuild --dry-run\n`));
436
+ }
437
+ if (report.orphans.length > 0 || report.missing.length > 0) process.exit(1);
434
438
 
435
- console.log(chalk.green(` ✓ HEALTHY (drift within ${(DRIFT_THRESHOLD * 100).toFixed(1)}% threshold)\n`));
439
+ if (report.zeroKnown) {
440
+ console.log(chalk.green(` ✓ HEALTHY — every live corpus file was walked, and every walked file with content is live.\n`));
441
+ } else {
442
+ console.log(chalk.green(` ✓ No orphans.`) + chalk.yellow(` Missing-content check pending the next sync.\n`));
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Compare the live corpus file set against the last walk. Pure, so the verdict is testable
448
+ * without a store. Only `corpus:` ids are this manifest's to judge; other schemes are ignored.
449
+ */
450
+ export function diagnoseCorpusIdentity({ liveFileIds, walkedBlobShas, zeroChunkBlobShas }) {
451
+ const live = new Set((liveFileIds || []).filter((id) => typeof id === 'string' && id.startsWith('corpus:')));
452
+ const walked = new Set((walkedBlobShas || []).map((sha) => `corpus:${sha}`));
453
+ const zeroKnown = Array.isArray(zeroChunkBlobShas);
454
+ const zero = new Set((zeroChunkBlobShas || []).map((sha) => `corpus:${sha}`));
455
+ const orphans = [...live].filter((id) => !walked.has(id)).sort();
456
+ const missing = zeroKnown ? [...walked].filter((id) => !zero.has(id) && !live.has(id)).sort() : [];
457
+ return { live: live.size, walked: walked.size, zeroKnown, zeroChunk: zero.size, orphans, missing };
436
458
  }
437
459
 
438
460
  export default {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * What a site upload must never publish. Hard deny — the upload fails loud before any file
3
+ * leaves the machine, dry-run included.
4
+ *
5
+ * WHY (measured, 2026-09-18): egpt-godsworld's DEV site had been publicly serving, as static
6
+ * files, the app's persona (`assets/system_instructions.md`) and its whole backend source tree
7
+ * (`microservice/app.js`, `Dockerfile`, `services/*`, config examples). An earlier upload used a
8
+ * broad manifest, and `site upload` publishes whatever its manifest names. The KB sync already
9
+ * refuses private content (CorpusDenyLint); the site upload had no equivalent.
10
+ *
11
+ * A DIFFERENT PATTERN SET from the corpus, on purpose: the corpus lint is about editorial
12
+ * publishability (drafts, patents, test dumps), and a site legitimately ships some of those. This
13
+ * set is about what is private at RUNTIME — backend source, an agent's persona, credentials,
14
+ * workspace and tooling state. The MATCHER is shared (CorpusDenyLint.matchPathPattern), so there
15
+ * is one definition of how a pattern meets a path.
16
+ *
17
+ * No exemption flag: every class here is repaired by not naming the file in the manifest, which
18
+ * is always possible. An escape hatch would be the one path the next leak takes.
19
+ */
20
+
21
+ import { matchPathPattern } from './CorpusDenyLint.js';
22
+
23
+ export const SITE_DENY_PATH_PATTERNS = [
24
+ { id: 1, class: 'app-source', patterns: ['**/microservice/**', 'microservice'],
25
+ reason: 'backend source — an app\'s microservice is deployed as a service, never served as files' },
26
+ { id: 2, class: 'agent-persona', patterns: ['system_instructions.md'],
27
+ reason: 'the agent\'s persona / system instructions — its prompt is not a public asset' },
28
+ { id: 3, class: 'credential', caseFold: true,
29
+ patterns: ['wallet*.json', '.env', '.env.*', 'dev-overrides*.json', 'secret*.json', '*.pem', '*.key',
30
+ 'service-account*.json', 'credentials*.json', 'id_rsa*', '*.p12'],
31
+ reason: 'credential or credential-shaped config (a placeholder copy teaches the real layout)' },
32
+ { id: 4, class: 'workspace-state', patterns: ['**/.descix/**', '.descix'],
33
+ reason: 'DeSciX workspace / session state' },
34
+ { id: 5, class: 'tooling-state', patterns: ['**/.claude/**', '**/.git/**', '.claude', '.git'],
35
+ reason: 'agent or VCS internals' },
36
+ ];
37
+
38
+ /**
39
+ * Every published path that matches a deny class.
40
+ * @param {string[]} publishedPaths - paths relative to the site root, as they would be served
41
+ * @returns {Array<{path:string, class:string, glob:string, reason:string}>}
42
+ */
43
+ export function lintSiteFiles(publishedPaths) {
44
+ const violations = [];
45
+ for (const p of publishedPaths) {
46
+ for (const pattern of SITE_DENY_PATH_PATTERNS) {
47
+ const glob = matchPathPattern(p, pattern);
48
+ if (glob) {
49
+ violations.push({ path: p, class: pattern.class, glob, reason: pattern.reason });
50
+ break;
51
+ }
52
+ }
53
+ }
54
+ return violations;
55
+ }
56
+
57
+ /** Throw, naming every offending file and the repair, when any path is denied. */
58
+ export function assertSitePublishable(publishedPaths) {
59
+ const violations = lintSiteFiles(publishedPaths);
60
+ if (violations.length === 0) return;
61
+ const lines = violations.map((v) => ` - ${v.path} [${v.class}] ${v.reason}`);
62
+ const err = new Error(
63
+ `site upload REFUSED: ${violations.length} file(s) must never be publicly served:\n${lines.join('\n')}\n` +
64
+ 'Nothing was uploaded. Remove these from the site manifest\'s sources (.descix/manifests/site.json) ' +
65
+ 'or from the build output, then upload again.');
66
+ err.code = 'SITE_DENY_PATH';
67
+ err.violations = violations;
68
+ throw err;
69
+ }
@@ -143,7 +143,8 @@
143
143
  "delete_knowledge_base"
144
144
  ],
145
145
  "kb doctor": [
146
- "get_kb_rag_status"
146
+ "get_kb_rag_status",
147
+ "kb_list_file_ids"
147
148
  ],
148
149
  "kb list": [
149
150
  "list_knowledge_bases"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descix/cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
5
5
  "repository": {
6
6
  "type": "git",