@openontology/opencode-palantir 0.1.4 → 0.1.5-next.4

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.
Files changed (3) hide show
  1. package/README.md +161 -117
  2. package/dist/index.js +629 -84
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // src/index.ts
3
+ import path5 from "path";
3
4
  import { tool } from "@opencode-ai/plugin/tool";
4
- import path4 from "path";
5
5
 
6
6
  // node_modules/hyparquet/src/constants.js
7
7
  var ParquetTypes = [
@@ -6088,6 +6088,9 @@ var BASE_DELAY_MS = 1000;
6088
6088
  var BACKOFF_FACTOR = 2;
6089
6089
  var JITTER_RANGE = 0.25;
6090
6090
  var BATCH_SIZE = 100;
6091
+ function formatError(error) {
6092
+ return error instanceof Error ? error.toString() : String(error);
6093
+ }
6091
6094
  function decompressPagefind(data) {
6092
6095
  const decompressed = gunzipSync(Buffer.from(data));
6093
6096
  if (decompressed.length < PAGEFIND_HEADER_SIZE) {
@@ -6202,8 +6205,11 @@ async function withConcurrencyLimit(tasks, limit) {
6202
6205
  next();
6203
6206
  });
6204
6207
  }
6205
- async function fetchAllDocs(dbPath) {
6208
+ async function fetchAllDocs(dbPath, options = {}) {
6206
6209
  const entry = await fetchEntryPoint();
6210
+ const onProgress = options.onProgress;
6211
+ const concurrency = typeof options.concurrency === "number" && options.concurrency > 0 ? options.concurrency : DEFAULT_CONCURRENCY;
6212
+ const progressEvery = typeof options.progressEvery === "number" && options.progressEvery > 0 ? Math.floor(options.progressEvery) : BATCH_SIZE;
6207
6213
  const langKey = Object.keys(entry.languages)[0];
6208
6214
  if (!langKey) {
6209
6215
  throw new Error("No languages found in Pagefind entry");
@@ -6211,17 +6217,25 @@ async function fetchAllDocs(dbPath) {
6211
6217
  const langHash = entry.languages[langKey].hash;
6212
6218
  const pageHashes = await fetchAndParseMeta(langHash);
6213
6219
  const totalPages = pageHashes.length;
6220
+ onProgress?.({ type: "discovered", totalPages });
6214
6221
  const fetchedRecords = [];
6215
6222
  const failedUrls = [];
6216
- let done = 0;
6217
- const tasks = pageHashes.map((hash) => () => fetchFragment(hash).then((record) => {
6218
- done++;
6219
- if (done % BATCH_SIZE === 0 || done === totalPages) {
6220
- console.log(`Fetched ${done}/${totalPages} pages...`);
6223
+ let processedPages = 0;
6224
+ const tasks = pageHashes.map((hash) => () => fetchFragment(hash).catch((error) => {
6225
+ const url = `${PAGEFIND_BASE}/fragment/${hash}.pf_fragment`;
6226
+ onProgress?.({
6227
+ type: "page-failed",
6228
+ url,
6229
+ error: formatError(error)
6230
+ });
6231
+ throw error;
6232
+ }).finally(() => {
6233
+ processedPages += 1;
6234
+ if (processedPages % progressEvery === 0 || processedPages === totalPages) {
6235
+ onProgress?.({ type: "progress", processedPages, totalPages });
6221
6236
  }
6222
- return record;
6223
6237
  }));
6224
- const results = await withConcurrencyLimit(tasks, DEFAULT_CONCURRENCY);
6238
+ const results = await withConcurrencyLimit(tasks, concurrency);
6225
6239
  for (let i = 0;i < results.length; i++) {
6226
6240
  const result = results[i];
6227
6241
  if (result.status === "fulfilled") {
@@ -6229,10 +6243,15 @@ async function fetchAllDocs(dbPath) {
6229
6243
  } else {
6230
6244
  const url = `${PAGEFIND_BASE}/fragment/${pageHashes[i]}.pf_fragment`;
6231
6245
  failedUrls.push(url);
6232
- console.log(`[ERROR] Failed to fetch ${url}: ${result.reason.message}`);
6233
6246
  }
6234
6247
  }
6235
6248
  await writeParquet(fetchedRecords, dbPath);
6249
+ onProgress?.({
6250
+ type: "completed",
6251
+ totalPages,
6252
+ fetchedPages: fetchedRecords.length,
6253
+ failedPages: failedUrls.length
6254
+ });
6236
6255
  return {
6237
6256
  totalPages,
6238
6257
  fetchedPages: fetchedRecords.length,
@@ -6241,8 +6260,189 @@ async function fetchAllDocs(dbPath) {
6241
6260
  };
6242
6261
  }
6243
6262
 
6263
+ // src/docs/snapshot.ts
6264
+ import fs from "fs/promises";
6265
+ import path from "path";
6266
+ var DEFAULT_DOCS_SNAPSHOT_URLS = [
6267
+ "https://raw.githubusercontent.com/anand-testcompare/opencode-palantir/main/data/docs.parquet"
6268
+ ];
6269
+ var MIN_SNAPSHOT_BYTES = 64;
6270
+ var inFlightByPath = new Map;
6271
+ function formatError2(err) {
6272
+ return err instanceof Error ? err.toString() : String(err);
6273
+ }
6274
+ function emit(onEvent, event) {
6275
+ if (!onEvent)
6276
+ return;
6277
+ onEvent(event);
6278
+ }
6279
+ function normalizeSnapshotUrls(customUrls) {
6280
+ const envSingleRaw = process.env.OPENCODE_PALANTIR_DOCS_SNAPSHOT_URL;
6281
+ const envManyRaw = process.env.OPENCODE_PALANTIR_DOCS_SNAPSHOT_URLS;
6282
+ const envSingle = typeof envSingleRaw === "string" && envSingleRaw.trim().length > 0 ? [envSingleRaw.trim()] : [];
6283
+ const envMany = typeof envManyRaw === "string" && envManyRaw.trim().length > 0 ? envManyRaw.split(",").map((x) => x.trim()).filter((x) => x.length > 0) : [];
6284
+ const resolved = customUrls ?? [...envMany, ...envSingle, ...DEFAULT_DOCS_SNAPSHOT_URLS];
6285
+ return Array.from(new Set(resolved));
6286
+ }
6287
+ async function ensureDirectoryExists(dbPath) {
6288
+ await fs.mkdir(path.dirname(dbPath), { recursive: true });
6289
+ }
6290
+ async function statIfExists(filePath) {
6291
+ try {
6292
+ return await fs.stat(filePath);
6293
+ } catch (err) {
6294
+ if (err.code === "ENOENT")
6295
+ return null;
6296
+ throw err;
6297
+ }
6298
+ }
6299
+ function assertValidSnapshotSize(bytes, source) {
6300
+ if (bytes < MIN_SNAPSHOT_BYTES) {
6301
+ throw new Error(`Snapshot from ${source} is unexpectedly small (${bytes} bytes). Expected at least ${MIN_SNAPSHOT_BYTES} bytes.`);
6302
+ }
6303
+ }
6304
+ function tempPathFor(dbPath) {
6305
+ const base = path.basename(dbPath);
6306
+ return path.join(path.dirname(dbPath), `.${base}.tmp.${process.pid}.${Date.now()}`);
6307
+ }
6308
+ async function writeBufferAtomic(dbPath, bytes) {
6309
+ const tmp = tempPathFor(dbPath);
6310
+ await fs.writeFile(tmp, bytes);
6311
+ await fs.rename(tmp, dbPath);
6312
+ }
6313
+ async function copyFileAtomic(sourcePath, dbPath) {
6314
+ const tmp = tempPathFor(dbPath);
6315
+ await fs.copyFile(sourcePath, tmp);
6316
+ await fs.rename(tmp, dbPath);
6317
+ }
6318
+ function bundledSnapshotCandidates(dbPath, pluginDirectory) {
6319
+ const candidates = [];
6320
+ if (pluginDirectory && pluginDirectory.trim().length > 0) {
6321
+ candidates.push(path.resolve(pluginDirectory, "data", "docs.parquet"));
6322
+ } else {
6323
+ candidates.push(path.resolve(import.meta.dir, "..", "..", "data", "docs.parquet"));
6324
+ }
6325
+ const target = path.resolve(dbPath);
6326
+ const deduped = Array.from(new Set(candidates.map((x) => path.resolve(x))));
6327
+ return deduped.filter((candidate) => candidate !== target);
6328
+ }
6329
+ async function tryDownloadSnapshot(dbPath, urls, onEvent) {
6330
+ const errors = [];
6331
+ for (const url of urls) {
6332
+ emit(onEvent, { type: "download-start", url });
6333
+ try {
6334
+ const response = await fetch(url);
6335
+ if (!response.ok) {
6336
+ const reason = `HTTP ${response.status} ${response.statusText}`.trim();
6337
+ emit(onEvent, { type: "download-failed", url, error: reason });
6338
+ errors.push(`${url}: ${reason}`);
6339
+ continue;
6340
+ }
6341
+ const bytes = new Uint8Array(await response.arrayBuffer());
6342
+ assertValidSnapshotSize(bytes.byteLength, url);
6343
+ await writeBufferAtomic(dbPath, bytes);
6344
+ emit(onEvent, { type: "download-success", url, bytes: bytes.byteLength });
6345
+ return {
6346
+ dbPath,
6347
+ changed: true,
6348
+ source: "download",
6349
+ bytes: bytes.byteLength,
6350
+ downloadUrl: url
6351
+ };
6352
+ } catch (err) {
6353
+ const reason = formatError2(err);
6354
+ emit(onEvent, { type: "download-failed", url, error: reason });
6355
+ errors.push(`${url}: ${reason}`);
6356
+ }
6357
+ }
6358
+ if (errors.length === 0)
6359
+ return null;
6360
+ throw new Error([
6361
+ "Unable to download prebuilt docs snapshot from configured source URLs.",
6362
+ ...errors.map((line) => `- ${line}`)
6363
+ ].join(`
6364
+ `));
6365
+ }
6366
+ async function tryCopyBundledSnapshot(dbPath, pluginDirectory, onEvent) {
6367
+ const candidates = bundledSnapshotCandidates(dbPath, pluginDirectory);
6368
+ for (const sourcePath of candidates) {
6369
+ const stat = await statIfExists(sourcePath);
6370
+ if (!stat || !stat.isFile())
6371
+ continue;
6372
+ emit(onEvent, { type: "copy-start", sourcePath });
6373
+ assertValidSnapshotSize(stat.size, sourcePath);
6374
+ await copyFileAtomic(sourcePath, dbPath);
6375
+ emit(onEvent, { type: "copy-success", sourcePath, bytes: stat.size });
6376
+ return {
6377
+ dbPath,
6378
+ changed: true,
6379
+ source: "bundled-copy",
6380
+ bytes: stat.size
6381
+ };
6382
+ }
6383
+ return null;
6384
+ }
6385
+ async function ensureDocsParquetInternal(options) {
6386
+ const dbPath = path.resolve(options.dbPath);
6387
+ const force = options.force === true;
6388
+ const onEvent = options.onEvent;
6389
+ emit(onEvent, { type: "start", force });
6390
+ await ensureDirectoryExists(dbPath);
6391
+ if (!force) {
6392
+ const existing = await statIfExists(dbPath);
6393
+ if (existing && existing.isFile()) {
6394
+ assertValidSnapshotSize(existing.size, dbPath);
6395
+ const result = {
6396
+ dbPath,
6397
+ changed: false,
6398
+ source: "existing",
6399
+ bytes: existing.size
6400
+ };
6401
+ emit(onEvent, { type: "skip-existing", bytes: existing.size });
6402
+ emit(onEvent, { type: "done", result });
6403
+ return result;
6404
+ }
6405
+ }
6406
+ const snapshotUrls = normalizeSnapshotUrls(options.snapshotUrls);
6407
+ let downloadError = null;
6408
+ try {
6409
+ const downloaded = await tryDownloadSnapshot(dbPath, snapshotUrls, onEvent);
6410
+ if (downloaded) {
6411
+ emit(onEvent, { type: "done", result: downloaded });
6412
+ return downloaded;
6413
+ }
6414
+ } catch (err) {
6415
+ downloadError = err instanceof Error ? err : new Error(String(err));
6416
+ }
6417
+ const copied = await tryCopyBundledSnapshot(dbPath, options.pluginDirectory, onEvent);
6418
+ if (copied) {
6419
+ emit(onEvent, { type: "done", result: copied });
6420
+ return copied;
6421
+ }
6422
+ const fallbackHint = "No bundled snapshot was found. You can run /refresh-docs-rescrape as a fallback.";
6423
+ if (downloadError) {
6424
+ throw new Error(`${downloadError.message}
6425
+ ${fallbackHint}`);
6426
+ }
6427
+ throw new Error(`No docs snapshot sources were available. ${fallbackHint} ` + `Checked URLs=${snapshotUrls.length}, bundled candidates=${bundledSnapshotCandidates(dbPath, options.pluginDirectory).length}.`);
6428
+ }
6429
+ async function ensureDocsParquet(options) {
6430
+ const dbPath = path.resolve(options.dbPath);
6431
+ const existing = inFlightByPath.get(dbPath);
6432
+ if (existing)
6433
+ return existing;
6434
+ let promise;
6435
+ promise = ensureDocsParquetInternal({ ...options, dbPath }).finally(() => {
6436
+ if (inFlightByPath.get(dbPath) === promise) {
6437
+ inFlightByPath.delete(dbPath);
6438
+ }
6439
+ });
6440
+ inFlightByPath.set(dbPath, promise);
6441
+ return promise;
6442
+ }
6443
+
6244
6444
  // src/palantir-mcp/commands.ts
6245
- import path3 from "path";
6445
+ import path4 from "path";
6246
6446
 
6247
6447
  // src/palantir-mcp/allowlist.ts
6248
6448
  function isMutatingTool(toolName) {
@@ -6304,7 +6504,7 @@ function computeAllowedTools(profile, toolNames) {
6304
6504
  }
6305
6505
 
6306
6506
  // src/palantir-mcp/mcp-client.ts
6307
- function formatError(err) {
6507
+ function formatError3(err) {
6308
6508
  return err instanceof Error ? err.toString() : String(err);
6309
6509
  }
6310
6510
  function withTimeout(p, ms, label) {
@@ -6445,7 +6645,7 @@ ${errText}`));
6445
6645
  return Array.from(new Set(names)).sort((a, b) => a.localeCompare(b));
6446
6646
  } catch (err) {
6447
6647
  const stderrText = stderrChunks.join("");
6448
- throw new Error(`[ERROR] Failed to list palantir-mcp tools: ${formatError(err)}
6648
+ throw new Error(`[ERROR] Failed to list palantir-mcp tools: ${formatError3(err)}
6449
6649
  ${stderrText}`);
6450
6650
  } finally {
6451
6651
  try {
@@ -6484,8 +6684,8 @@ function normalizeFoundryBaseUrl(raw) {
6484
6684
  }
6485
6685
 
6486
6686
  // src/palantir-mcp/opencode-config.ts
6487
- import fs from "fs/promises";
6488
- import path from "path";
6687
+ import fs2 from "fs/promises";
6688
+ import path2 from "path";
6489
6689
 
6490
6690
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
6491
6691
  function createScanner(text, ignoreTrivia = false) {
@@ -7297,28 +7497,28 @@ var OPENCODE_JSON_FILENAME = "opencode.json";
7297
7497
  function isRecord(value) {
7298
7498
  return !!value && typeof value === "object" && !Array.isArray(value);
7299
7499
  }
7300
- function formatError2(err) {
7500
+ function formatError4(err) {
7301
7501
  return err instanceof Error ? err.toString() : String(err);
7302
7502
  }
7303
7503
  async function pathExists(p) {
7304
7504
  try {
7305
- await fs.access(p);
7505
+ await fs2.access(p);
7306
7506
  return true;
7307
7507
  } catch {
7308
7508
  return false;
7309
7509
  }
7310
7510
  }
7311
7511
  async function readOpencodeJsonc(worktree) {
7312
- const configPath = path.join(worktree, OPENCODE_JSONC_FILENAME);
7512
+ const configPath = path2.join(worktree, OPENCODE_JSONC_FILENAME);
7313
7513
  if (!await pathExists(configPath))
7314
7514
  return { ok: false, missing: true };
7315
7515
  let text;
7316
7516
  try {
7317
- text = await fs.readFile(configPath, "utf8");
7517
+ text = await fs2.readFile(configPath, "utf8");
7318
7518
  } catch (err) {
7319
7519
  return {
7320
7520
  ok: false,
7321
- error: `[ERROR] Failed reading ${OPENCODE_JSONC_FILENAME}: ${formatError2(err)}`
7521
+ error: `[ERROR] Failed reading ${OPENCODE_JSONC_FILENAME}: ${formatError4(err)}`
7322
7522
  };
7323
7523
  }
7324
7524
  const errors = [];
@@ -7333,16 +7533,16 @@ async function readOpencodeJsonc(worktree) {
7333
7533
  return { ok: true, path: configPath, text, data };
7334
7534
  }
7335
7535
  async function readLegacyOpencodeJson(worktree) {
7336
- const legacyPath = path.join(worktree, OPENCODE_JSON_FILENAME);
7536
+ const legacyPath = path2.join(worktree, OPENCODE_JSON_FILENAME);
7337
7537
  if (!await pathExists(legacyPath))
7338
7538
  return { ok: false, missing: true };
7339
7539
  let text;
7340
7540
  try {
7341
- text = await fs.readFile(legacyPath, "utf8");
7541
+ text = await fs2.readFile(legacyPath, "utf8");
7342
7542
  } catch (err) {
7343
7543
  return {
7344
7544
  ok: false,
7345
- error: `[ERROR] Failed reading ${OPENCODE_JSON_FILENAME}: ${formatError2(err)}`
7545
+ error: `[ERROR] Failed reading ${OPENCODE_JSON_FILENAME}: ${formatError4(err)}`
7346
7546
  };
7347
7547
  }
7348
7548
  let data;
@@ -7351,7 +7551,7 @@ async function readLegacyOpencodeJson(worktree) {
7351
7551
  } catch (err) {
7352
7552
  return {
7353
7553
  ok: false,
7354
- error: `[ERROR] Failed parsing ${OPENCODE_JSON_FILENAME}: ${formatError2(err)}`
7554
+ error: `[ERROR] Failed parsing ${OPENCODE_JSON_FILENAME}: ${formatError4(err)}`
7355
7555
  };
7356
7556
  }
7357
7557
  return { ok: true, path: legacyPath, text, data };
@@ -7380,24 +7580,24 @@ function mergeLegacyIntoJsonc(legacyData, jsoncData) {
7380
7580
  return deepMergePreferTarget(base, legacy);
7381
7581
  }
7382
7582
  async function writeFileAtomic(filePath, text) {
7383
- const dir = path.dirname(filePath);
7384
- const base = path.basename(filePath);
7385
- const tmp = path.join(dir, `.${base}.tmp.${process.pid}.${Date.now()}`);
7386
- await fs.writeFile(tmp, text, "utf8");
7387
- await fs.rename(tmp, filePath);
7583
+ const dir = path2.dirname(filePath);
7584
+ const base = path2.basename(filePath);
7585
+ const tmp = path2.join(dir, `.${base}.tmp.${process.pid}.${Date.now()}`);
7586
+ await fs2.writeFile(tmp, text, "utf8");
7587
+ await fs2.rename(tmp, filePath);
7388
7588
  }
7389
7589
  async function renameLegacyToBak(worktree) {
7390
- const legacyPath = path.join(worktree, OPENCODE_JSON_FILENAME);
7590
+ const legacyPath = path2.join(worktree, OPENCODE_JSON_FILENAME);
7391
7591
  if (!await pathExists(legacyPath))
7392
7592
  return null;
7393
- const baseBak = path.join(worktree, `${OPENCODE_JSON_FILENAME}.bak`);
7593
+ const baseBak = path2.join(worktree, `${OPENCODE_JSON_FILENAME}.bak`);
7394
7594
  let bakPath = baseBak;
7395
7595
  let i = 1;
7396
7596
  while (await pathExists(bakPath)) {
7397
7597
  bakPath = `${baseBak}.${i}`;
7398
7598
  i += 1;
7399
7599
  }
7400
- await fs.rename(legacyPath, bakPath);
7600
+ await fs2.rename(legacyPath, bakPath);
7401
7601
  return bakPath;
7402
7602
  }
7403
7603
  function toolKey(toolName) {
@@ -7637,11 +7837,11 @@ function stringifyJsonc(data) {
7637
7837
  }
7638
7838
 
7639
7839
  // src/palantir-mcp/repo-scan.ts
7640
- import fs2 from "fs/promises";
7641
- import path2 from "path";
7840
+ import fs3 from "fs/promises";
7841
+ import path3 from "path";
7642
7842
  async function pathExists2(p) {
7643
7843
  try {
7644
- await fs2.access(p);
7844
+ await fs3.access(p);
7645
7845
  return true;
7646
7846
  } catch {
7647
7847
  return false;
@@ -7649,7 +7849,7 @@ async function pathExists2(p) {
7649
7849
  }
7650
7850
  async function readTextFileBounded(p, maxBytes) {
7651
7851
  try {
7652
- const file = await fs2.open(p, "r");
7852
+ const file = await fs3.open(p, "r");
7653
7853
  try {
7654
7854
  const buf = Buffer.alloc(maxBytes);
7655
7855
  const { bytesRead } = await file.read(buf, 0, maxBytes, 0);
@@ -7732,14 +7932,14 @@ async function collectSampleFiles(root, limit) {
7732
7932
  visitedDirs += 1;
7733
7933
  let entries;
7734
7934
  try {
7735
- entries = await fs2.readdir(dir, { withFileTypes: true });
7935
+ entries = await fs3.readdir(dir, { withFileTypes: true });
7736
7936
  } catch {
7737
7937
  continue;
7738
7938
  }
7739
7939
  for (const ent of entries) {
7740
7940
  if (results.length >= limit)
7741
7941
  break;
7742
- const full = path2.join(dir, ent.name);
7942
+ const full = path3.join(dir, ent.name);
7743
7943
  if (ent.isDirectory()) {
7744
7944
  if (ignoreDirs.has(ent.name))
7745
7945
  continue;
@@ -7748,7 +7948,7 @@ async function collectSampleFiles(root, limit) {
7748
7948
  }
7749
7949
  if (!ent.isFile())
7750
7950
  continue;
7751
- const ext = path2.extname(ent.name);
7951
+ const ext = path3.extname(ent.name);
7752
7952
  if (!allowedExts.has(ext))
7753
7953
  continue;
7754
7954
  results.push(full);
@@ -7771,13 +7971,13 @@ async function scanRepoForProfile(root) {
7771
7971
  { p: "lerna.json", profile: "all", score: 3, reason: "Found lerna.json" }
7772
7972
  ];
7773
7973
  for (const c of candidates) {
7774
- if (await pathExists2(path2.join(root, c.p))) {
7974
+ if (await pathExists2(path3.join(root, c.p))) {
7775
7975
  addScore(scores, reasons, c.profile, c.score, c.reason);
7776
7976
  }
7777
7977
  }
7778
- const packageJsonPath = path2.join(root, "package.json");
7779
- const pyprojectPath = path2.join(root, "pyproject.toml");
7780
- const requirementsPath = path2.join(root, "requirements.txt");
7978
+ const packageJsonPath = path3.join(root, "package.json");
7979
+ const pyprojectPath = path3.join(root, "pyproject.toml");
7980
+ const requirementsPath = path3.join(root, "requirements.txt");
7781
7981
  const hasPackageJson = await pathExists2(packageJsonPath);
7782
7982
  const hasPyproject = await pathExists2(pyprojectPath);
7783
7983
  if (hasPackageJson && hasPyproject) {
@@ -7817,22 +8017,22 @@ async function scanRepoForProfile(root) {
7817
8017
  }
7818
8018
  }
7819
8019
  }
7820
- if (await pathExists2(path2.join(root, "pipelines"))) {
8020
+ if (await pathExists2(path3.join(root, "pipelines"))) {
7821
8021
  addScore(scores, reasons, "pipelines_transforms", 3, "Found pipelines/ directory");
7822
8022
  }
7823
- if (await pathExists2(path2.join(root, "transforms"))) {
8023
+ if (await pathExists2(path3.join(root, "transforms"))) {
7824
8024
  addScore(scores, reasons, "pipelines_transforms", 3, "Found transforms/ directory");
7825
8025
  }
7826
- if (await pathExists2(path2.join(root, "internal", "pipeline"))) {
8026
+ if (await pathExists2(path3.join(root, "internal", "pipeline"))) {
7827
8027
  addScore(scores, reasons, "pipelines_transforms", 3, "Found internal/pipeline/ directory");
7828
8028
  }
7829
- if (await pathExists2(path2.join(root, "internal", "transforms"))) {
8029
+ if (await pathExists2(path3.join(root, "internal", "transforms"))) {
7830
8030
  addScore(scores, reasons, "pipelines_transforms", 3, "Found internal/transforms/ directory");
7831
8031
  }
7832
- if (await pathExists2(path2.join(root, "functions"))) {
8032
+ if (await pathExists2(path3.join(root, "functions"))) {
7833
8033
  addScore(scores, reasons, "osdk_functions_ts", 2, "Found functions/ directory");
7834
8034
  }
7835
- if (await pathExists2(path2.join(root, "src", "functions"))) {
8035
+ if (await pathExists2(path3.join(root, "src", "functions"))) {
7836
8036
  addScore(scores, reasons, "osdk_functions_ts", 2, "Found src/functions/ directory");
7837
8037
  }
7838
8038
  const sampleFiles = await collectSampleFiles(root, 50);
@@ -7863,12 +8063,26 @@ async function scanRepoForProfile(root) {
7863
8063
  }
7864
8064
 
7865
8065
  // src/palantir-mcp/commands.ts
7866
- function formatError3(err) {
8066
+ function formatError5(err) {
7867
8067
  return err instanceof Error ? err.toString() : String(err);
7868
8068
  }
7869
8069
  function isRecord2(value) {
7870
8070
  return !!value && typeof value === "object" && !Array.isArray(value);
7871
8071
  }
8072
+ function stableSortJson(value) {
8073
+ if (Array.isArray(value))
8074
+ return value.map(stableSortJson);
8075
+ if (!isRecord2(value))
8076
+ return value;
8077
+ const out = {};
8078
+ for (const k of Object.keys(value).sort((a, b) => a.localeCompare(b))) {
8079
+ out[k] = stableSortJson(value[k]);
8080
+ }
8081
+ return out;
8082
+ }
8083
+ function stableJsonStringify(value) {
8084
+ return JSON.stringify(stableSortJson(value));
8085
+ }
7872
8086
  function formatWarnings(warnings) {
7873
8087
  if (warnings.length === 0)
7874
8088
  return "";
@@ -7898,12 +8112,85 @@ async function resolveProfile(worktree) {
7898
8112
  } catch (err) {
7899
8113
  return {
7900
8114
  profile: "unknown",
7901
- reasons: [`Repo scan failed; falling back to unknown: ${formatError3(err)}`]
8115
+ reasons: [`Repo scan failed; falling back to unknown: ${formatError5(err)}`]
7902
8116
  };
7903
8117
  }
7904
8118
  }
8119
+ function hasPalantirToolToggles(data, agentName) {
8120
+ const agents = data["agent"];
8121
+ if (!isRecord2(agents))
8122
+ return false;
8123
+ const agent = agents[agentName];
8124
+ if (!isRecord2(agent))
8125
+ return false;
8126
+ const tools = agent["tools"];
8127
+ if (!isRecord2(tools))
8128
+ return false;
8129
+ return Object.keys(tools).some((k) => k.startsWith("palantir-mcp_"));
8130
+ }
8131
+ function isAutoBootstrapAlreadyComplete(data) {
8132
+ const foundryUrl = extractFoundryApiUrlFromMcpConfig(data);
8133
+ const toolsRoot = data["tools"];
8134
+ const hasGlobalDeny = isRecord2(toolsRoot) && toolsRoot["palantir-mcp_*"] === false;
8135
+ const hasAgentToggles = hasPalantirToolToggles(data, "foundry-librarian") && hasPalantirToolToggles(data, "foundry");
8136
+ return !!foundryUrl && hasGlobalDeny && hasAgentToggles;
8137
+ }
8138
+ async function autoBootstrapPalantirMcpIfConfigured(worktree) {
8139
+ try {
8140
+ const tokenRaw = process.env.FOUNDRY_TOKEN;
8141
+ const urlRaw = process.env.FOUNDRY_URL;
8142
+ if (!tokenRaw || tokenRaw.trim().length === 0)
8143
+ return;
8144
+ if (!urlRaw || urlRaw.trim().length === 0)
8145
+ return;
8146
+ const normalized = normalizeFoundryBaseUrl(urlRaw);
8147
+ if ("error" in normalized)
8148
+ return;
8149
+ const readJsonc = await readOpencodeJsonc(worktree);
8150
+ if (!readJsonc.ok && !("missing" in readJsonc))
8151
+ return;
8152
+ const readLegacy = await readLegacyOpencodeJson(worktree);
8153
+ if (!readLegacy.ok && !("missing" in readLegacy))
8154
+ return;
8155
+ const baseJsoncData = readJsonc.ok ? readJsonc.data : {};
8156
+ const base = isRecord2(baseJsoncData) ? baseJsoncData : {};
8157
+ const merged = readLegacy.ok ? mergeLegacyIntoJsonc(readLegacy.data, base) : { ...base };
8158
+ if (isAutoBootstrapAlreadyComplete(merged))
8159
+ return;
8160
+ const existingMcpUrlRaw = extractFoundryApiUrlFromMcpConfig(merged);
8161
+ const existingMcpUrlNorm = existingMcpUrlRaw ? normalizeFoundryBaseUrl(existingMcpUrlRaw) : null;
8162
+ const { profile } = await resolveProfile(worktree);
8163
+ const discoveryUrl = existingMcpUrlNorm && "url" in existingMcpUrlNorm ? existingMcpUrlNorm.url : normalized.url;
8164
+ const toolNames = await listPalantirMcpTools(discoveryUrl);
8165
+ if (toolNames.length === 0)
8166
+ return;
8167
+ const allowlist = computeAllowedTools(profile, toolNames);
8168
+ const patch = patchConfigForSetup(merged, {
8169
+ foundryApiUrl: normalized.url,
8170
+ toolNames,
8171
+ profile,
8172
+ allowlist
8173
+ });
8174
+ const jsoncMissing = !readJsonc.ok && "missing" in readJsonc;
8175
+ const needsMigration = jsoncMissing && readLegacy.ok;
8176
+ const changed = needsMigration || stableJsonStringify(merged) !== stableJsonStringify(patch.data);
8177
+ if (!changed)
8178
+ return;
8179
+ const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
8180
+ const text = stringifyJsonc(patch.data);
8181
+ await writeFileAtomic(outPath, text);
8182
+ if (readLegacy.ok) {
8183
+ await renameLegacyToBak(worktree);
8184
+ }
8185
+ } catch (err) {
8186
+ return;
8187
+ }
8188
+ }
7905
8189
  async function setupPalantirMcp(worktree, rawArgs) {
7906
- const urlArg = rawArgs.trim();
8190
+ const urlFromArgs = rawArgs.trim();
8191
+ const urlFromEnvRaw = process.env.FOUNDRY_URL;
8192
+ const urlFromEnv = typeof urlFromEnvRaw === "string" ? urlFromEnvRaw.trim() : "";
8193
+ const urlArg = urlFromArgs || urlFromEnv;
7907
8194
  if (!urlArg) {
7908
8195
  return [
7909
8196
  "[ERROR] Missing Foundry base URL.",
@@ -7911,6 +8198,9 @@ async function setupPalantirMcp(worktree, rawArgs) {
7911
8198
  "Usage:",
7912
8199
  " /setup-palantir-mcp <foundry_api_url>",
7913
8200
  "",
8201
+ "Or set:",
8202
+ " export FOUNDRY_URL=<foundry_api_url>",
8203
+ "",
7914
8204
  "Example:",
7915
8205
  " /setup-palantir-mcp https://23dimethyl.usw-3.palantirfoundry.com"
7916
8206
  ].join(`
@@ -7947,7 +8237,7 @@ async function setupPalantirMcp(worktree, rawArgs) {
7947
8237
  try {
7948
8238
  toolNames = await listPalantirMcpTools(discoveryUrl);
7949
8239
  } catch (err) {
7950
- return `[ERROR] ${formatError3(err)}`;
8240
+ return `[ERROR] ${formatError5(err)}`;
7951
8241
  }
7952
8242
  if (toolNames.length === 0)
7953
8243
  return "[ERROR] palantir-mcp tool discovery returned no tools.";
@@ -7958,12 +8248,12 @@ async function setupPalantirMcp(worktree, rawArgs) {
7958
8248
  profile,
7959
8249
  allowlist
7960
8250
  });
7961
- const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8251
+ const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
7962
8252
  const text = stringifyJsonc(patch.data);
7963
8253
  try {
7964
8254
  await writeFileAtomic(outPath, text);
7965
8255
  } catch (err) {
7966
- return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError3(err)}`;
8256
+ return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError5(err)}`;
7967
8257
  }
7968
8258
  let bakInfo = "";
7969
8259
  if (readLegacy.ok) {
@@ -7974,7 +8264,7 @@ async function setupPalantirMcp(worktree, rawArgs) {
7974
8264
  Migrated legacy ${readLegacy.path} -> ${bakPath}`;
7975
8265
  } catch (err) {
7976
8266
  bakInfo = `
7977
- [ERROR] Wrote ${OPENCODE_JSONC_FILENAME}, but failed to rename legacy ${readLegacy.path}: ${formatError3(err)}`;
8267
+ [ERROR] Wrote ${OPENCODE_JSONC_FILENAME}, but failed to rename legacy ${readLegacy.path}: ${formatError5(err)}`;
7978
8268
  }
7979
8269
  }
7980
8270
  const warnings = [...normalized.warnings, ...patch.warnings];
@@ -8028,18 +8318,18 @@ async function rescanPalantirMcpTools(worktree) {
8028
8318
  try {
8029
8319
  toolNames = await listPalantirMcpTools(normalized.url);
8030
8320
  } catch (err) {
8031
- return `[ERROR] ${formatError3(err)}`;
8321
+ return `[ERROR] ${formatError5(err)}`;
8032
8322
  }
8033
8323
  if (toolNames.length === 0)
8034
8324
  return "[ERROR] palantir-mcp tool discovery returned no tools.";
8035
8325
  const allowlist = computeAllowedTools(profile, toolNames);
8036
8326
  const patch = patchConfigForRescan(baseData, { toolNames, profile, allowlist });
8037
- const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8327
+ const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
8038
8328
  const text = stringifyJsonc(patch.data);
8039
8329
  try {
8040
8330
  await writeFileAtomic(outPath, text);
8041
8331
  } catch (err) {
8042
- return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError3(err)}`;
8332
+ return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError5(err)}`;
8043
8333
  }
8044
8334
  const warnings = [...normalized.warnings, ...patch.warnings];
8045
8335
  return [
@@ -8052,22 +8342,216 @@ async function rescanPalantirMcpTools(worktree) {
8052
8342
  }
8053
8343
 
8054
8344
  // src/index.ts
8055
- var NO_DB_MESSAGE = "Documentation database not found. Run /refresh-docs to download Palantir Foundry documentation.";
8056
8345
  var plugin = async (input) => {
8057
- const dbPath = path4.join(input.worktree, "data", "docs.parquet");
8346
+ const dbPath = path5.join(input.worktree, "data", "docs.parquet");
8058
8347
  let dbInstance = null;
8059
- async function getDb() {
8060
- if (!dbInstance) {
8061
- dbInstance = await createDatabase(dbPath);
8348
+ let dbInitPromise = null;
8349
+ let autoBootstrapMcpStarted = false;
8350
+ let autoBootstrapDocsStarted = false;
8351
+ function formatError6(err) {
8352
+ return err instanceof Error ? err.toString() : String(err);
8353
+ }
8354
+ function formatBytes(bytes) {
8355
+ if (!Number.isFinite(bytes) || bytes < 0)
8356
+ return "0 B";
8357
+ const units = ["B", "KB", "MB", "GB"];
8358
+ let value = bytes;
8359
+ let index = 0;
8360
+ while (value >= 1024 && index < units.length - 1) {
8361
+ value /= 1024;
8362
+ index += 1;
8363
+ }
8364
+ const decimals = value >= 10 || index === 0 ? 0 : 1;
8365
+ return `${value.toFixed(decimals)} ${units[index]}`;
8366
+ }
8367
+ function ensureCommandDefinitions(cfg) {
8368
+ if (!cfg.command)
8369
+ cfg.command = {};
8370
+ if (!cfg.command["refresh-docs"]) {
8371
+ cfg.command["refresh-docs"] = {
8372
+ template: "Refresh Palantir docs snapshot (recommended).",
8373
+ description: "Force refresh data/docs.parquet from a prebuilt snapshot (download/copy; no rescrape)."
8374
+ };
8375
+ }
8376
+ if (!cfg.command["refresh-docs-rescrape"]) {
8377
+ cfg.command["refresh-docs-rescrape"] = {
8378
+ template: "Refresh docs by live rescrape (unsafe/experimental).",
8379
+ description: "Explicit fallback: rescrape palantir.com docs and rebuild data/docs.parquet. Slower and less reliable than /refresh-docs."
8380
+ };
8381
+ }
8382
+ if (!cfg.command["setup-palantir-mcp"]) {
8383
+ cfg.command["setup-palantir-mcp"] = {
8384
+ template: "Set up palantir-mcp for this repo.",
8385
+ description: "Guided MCP setup for Foundry. Usage: /setup-palantir-mcp <foundry_api_url>. Requires FOUNDRY_TOKEN for tool discovery."
8386
+ };
8387
+ }
8388
+ if (!cfg.command["rescan-palantir-mcp-tools"]) {
8389
+ cfg.command["rescan-palantir-mcp-tools"] = {
8390
+ template: "Re-scan palantir-mcp tools and patch tool gating.",
8391
+ description: "Re-discovers the palantir-mcp tool list and adds missing palantir-mcp_* toggles (does not overwrite existing toggles). Requires FOUNDRY_TOKEN."
8392
+ };
8393
+ }
8394
+ }
8395
+ function ensureAgentDefaults2(agent, agentName) {
8396
+ const defaultDescription = agentName === "foundry-librarian" ? "Foundry exploration and context gathering (parallel-friendly)" : "Foundry execution agent (uses only enabled palantir-mcp tools)";
8397
+ if (agent.mode !== "subagent" && agent.mode !== "primary" && agent.mode !== "all") {
8398
+ agent.mode = "subagent";
8399
+ }
8400
+ if (typeof agent["hidden"] !== "boolean")
8401
+ agent["hidden"] = false;
8402
+ if (typeof agent.description !== "string")
8403
+ agent.description = defaultDescription;
8404
+ if (typeof agent.prompt !== "string") {
8405
+ agent.prompt = agentName === "foundry-librarian" ? [
8406
+ "You are the Foundry librarian.",
8407
+ "",
8408
+ "- Focus on exploration and context gathering.",
8409
+ "- Split independent exploration tasks and run them in parallel when possible.",
8410
+ "- Return compact summaries and cite the tool calls you ran.",
8411
+ "- Avoid dumping massive schemas unless explicitly asked."
8412
+ ].join(`
8413
+ `) : [
8414
+ "You are the Foundry execution agent.",
8415
+ "",
8416
+ "- Use only enabled palantir-mcp tools.",
8417
+ "- Prefer working from summaries produced by @foundry-librarian.",
8418
+ "- Keep operations focused and deterministic."
8419
+ ].join(`
8420
+ `);
8062
8421
  }
8063
- return dbInstance;
8422
+ if (!agent.tools)
8423
+ agent.tools = {};
8424
+ if (agentName === "foundry-librarian") {
8425
+ if (agent.tools.get_doc_page === undefined)
8426
+ agent.tools.get_doc_page = true;
8427
+ if (agent.tools.list_all_docs === undefined)
8428
+ agent.tools.list_all_docs = true;
8429
+ return;
8430
+ }
8431
+ if (agent.tools.get_doc_page === undefined)
8432
+ agent.tools.get_doc_page = false;
8433
+ if (agent.tools.list_all_docs === undefined)
8434
+ agent.tools.list_all_docs = false;
8435
+ }
8436
+ function ensureAgentDefinitions(cfg) {
8437
+ if (!cfg.agent)
8438
+ cfg.agent = {};
8439
+ const librarian = cfg.agent["foundry-librarian"] ?? {};
8440
+ ensureAgentDefaults2(librarian, "foundry-librarian");
8441
+ cfg.agent["foundry-librarian"] = librarian;
8442
+ const foundry = cfg.agent.foundry ?? {};
8443
+ ensureAgentDefaults2(foundry, "foundry");
8444
+ cfg.agent.foundry = foundry;
8445
+ }
8446
+ function maybeStartAutoBootstrapMcp() {
8447
+ if (autoBootstrapMcpStarted)
8448
+ return;
8449
+ const token = process.env.FOUNDRY_TOKEN;
8450
+ const url = process.env.FOUNDRY_URL;
8451
+ if (!token || token.trim().length === 0)
8452
+ return;
8453
+ if (!url || url.trim().length === 0)
8454
+ return;
8455
+ autoBootstrapMcpStarted = true;
8456
+ autoBootstrapPalantirMcpIfConfigured(input.worktree);
8064
8457
  }
8065
- async function dbExists() {
8066
- return Bun.file(dbPath).exists();
8458
+ function maybeStartAutoBootstrapDocs() {
8459
+ if (autoBootstrapDocsStarted)
8460
+ return;
8461
+ autoBootstrapDocsStarted = true;
8462
+ ensureDocsAvailable().catch(() => {});
8463
+ }
8464
+ function resetDb() {
8465
+ if (dbInstance)
8466
+ closeDatabase(dbInstance);
8467
+ dbInstance = null;
8468
+ dbInitPromise = null;
8469
+ }
8470
+ async function getDb() {
8471
+ if (dbInstance)
8472
+ return dbInstance;
8473
+ if (dbInitPromise)
8474
+ return dbInitPromise;
8475
+ dbInitPromise = createDatabase(dbPath).then((created) => {
8476
+ dbInstance = created;
8477
+ return created;
8478
+ }).finally(() => {
8479
+ dbInitPromise = null;
8480
+ });
8481
+ return dbInitPromise;
8067
8482
  }
8068
8483
  function pushText(output, text) {
8069
8484
  output.parts.push({ type: "text", text });
8070
8485
  }
8486
+ function formatSnapshotFailure(err) {
8487
+ return [
8488
+ "[ERROR] Unable to obtain Palantir docs snapshot.",
8489
+ "",
8490
+ `Reason: ${formatError6(err)}`,
8491
+ "",
8492
+ "Next steps:",
8493
+ "- Retry /refresh-docs (recommended prebuilt snapshot path).",
8494
+ "- If snapshot download is blocked, run /refresh-docs-rescrape (unsafe/experimental)."
8495
+ ].join(`
8496
+ `);
8497
+ }
8498
+ function formatSnapshotRefreshEvent(event) {
8499
+ if (event.type === "skip-existing")
8500
+ return `snapshot_status=already_present bytes=${event.bytes}`;
8501
+ if (event.type === "download-start")
8502
+ return `download_attempt url=${event.url}`;
8503
+ if (event.type === "download-failed")
8504
+ return `download_failed url=${event.url} error=${event.error}`;
8505
+ if (event.type === "download-success")
8506
+ return `download_succeeded url=${event.url} bytes=${event.bytes}`;
8507
+ if (event.type === "copy-start")
8508
+ return `copy_attempt source=${event.sourcePath}`;
8509
+ if (event.type === "copy-success")
8510
+ return `copy_succeeded source=${event.sourcePath} bytes=${event.bytes}`;
8511
+ return null;
8512
+ }
8513
+ async function ensureDocsAvailable(options = {}) {
8514
+ return ensureDocsParquet({
8515
+ dbPath,
8516
+ force: options.force === true,
8517
+ pluginDirectory: input.directory,
8518
+ onEvent: options.onEvent
8519
+ });
8520
+ }
8521
+ async function ensureDocsReadyForTool() {
8522
+ try {
8523
+ await ensureDocsAvailable();
8524
+ return null;
8525
+ } catch (err) {
8526
+ return formatSnapshotFailure(err);
8527
+ }
8528
+ }
8529
+ function formatRescrapeFailure(err) {
8530
+ return [
8531
+ "[ERROR] /refresh-docs-rescrape failed.",
8532
+ "",
8533
+ `Reason: ${formatError6(err)}`,
8534
+ "",
8535
+ "Try /refresh-docs for the recommended prebuilt snapshot flow."
8536
+ ].join(`
8537
+ `);
8538
+ }
8539
+ function formatRescrapeProgressEvent(event, progressLines, failureSamples) {
8540
+ if (event.type === "discovered") {
8541
+ progressLines.push(`discovered_pages=${event.totalPages}`);
8542
+ return;
8543
+ }
8544
+ if (event.type === "progress") {
8545
+ progressLines.push(`processed_pages=${event.processedPages}/${event.totalPages}`);
8546
+ return;
8547
+ }
8548
+ if (event.type === "page-failed") {
8549
+ if (failureSamples.length < 5) {
8550
+ failureSamples.push(`url=${event.url} error=${event.error}`);
8551
+ }
8552
+ return;
8553
+ }
8554
+ }
8071
8555
  function toPathname(inputUrl) {
8072
8556
  const trimmed = inputUrl.trim();
8073
8557
  if (trimmed.length === 0)
@@ -8121,8 +8605,8 @@ var plugin = async (input) => {
8121
8605
  function isInScope(pageUrl, scope) {
8122
8606
  if (scope === "all")
8123
8607
  return true;
8124
- const path5 = toPathname(pageUrl);
8125
- return path5.startsWith(`/${scope}/`) || path5.startsWith(`/docs/${scope}/`);
8608
+ const path6 = toPathname(pageUrl);
8609
+ return path6.startsWith(`/${scope}/`) || path6.startsWith(`/docs/${scope}/`);
8126
8610
  }
8127
8611
  function tokenizeQuery(query) {
8128
8612
  const tokens = query.toLowerCase().trim().split(/[\s/._-]+/g).map((t) => t.trim()).filter((t) => t.length > 0);
@@ -8132,13 +8616,13 @@ var plugin = async (input) => {
8132
8616
  const q = query.toLowerCase().trim();
8133
8617
  if (q.length === 0)
8134
8618
  return 0;
8135
- const path5 = toPathname(page.url).toLowerCase();
8619
+ const path6 = toPathname(page.url).toLowerCase();
8136
8620
  const title = page.title.toLowerCase();
8137
- if (path5 === q)
8621
+ if (path6 === q)
8138
8622
  return 2000;
8139
- if (path5 === toPathname(q).toLowerCase())
8623
+ if (path6 === toPathname(q).toLowerCase())
8140
8624
  return 2000;
8141
- if (path5.includes(q))
8625
+ if (path6.includes(q))
8142
8626
  return 1200;
8143
8627
  if (title.includes(q))
8144
8628
  return 1000;
@@ -8149,16 +8633,22 @@ var plugin = async (input) => {
8149
8633
  for (const t of tokens) {
8150
8634
  if (title.includes(t))
8151
8635
  score += 40;
8152
- if (path5.includes(t))
8636
+ if (path6.includes(t))
8153
8637
  score += 30;
8154
8638
  }
8155
- if (path5.startsWith(q))
8639
+ if (path6.startsWith(q))
8156
8640
  score += 100;
8157
8641
  if (title.startsWith(q))
8158
8642
  score += 100;
8159
8643
  return score;
8160
8644
  }
8161
8645
  return {
8646
+ config: async (cfg) => {
8647
+ ensureCommandDefinitions(cfg);
8648
+ ensureAgentDefinitions(cfg);
8649
+ maybeStartAutoBootstrapMcp();
8650
+ maybeStartAutoBootstrapDocs();
8651
+ },
8162
8652
  tool: {
8163
8653
  get_doc_page: tool({
8164
8654
  description: "Retrieve a Palantir documentation page. Provide either a URL path (preferred) or a free-text query; the tool will handle common URL variants (full URLs, missing /docs prefix, trailing slashes).",
@@ -8168,8 +8658,9 @@ var plugin = async (input) => {
8168
8658
  scope: tool.schema.enum(["foundry", "apollo", "gotham", "all"]).optional().describe("Scope to search within when using query or fuzzy matching (default: foundry).")
8169
8659
  },
8170
8660
  async execute(args) {
8171
- if (!await dbExists())
8172
- return NO_DB_MESSAGE;
8661
+ const docsError = await ensureDocsReadyForTool();
8662
+ if (docsError)
8663
+ return docsError;
8173
8664
  const scope = parseScope(args.scope);
8174
8665
  if (!scope) {
8175
8666
  return [
@@ -8241,8 +8732,9 @@ ${bestPage.content}`;
8241
8732
  query: tool.schema.string().optional().describe("Optional query to filter/rank results by title/URL (case-insensitive).")
8242
8733
  },
8243
8734
  async execute(args) {
8244
- if (!await dbExists())
8245
- return NO_DB_MESSAGE;
8735
+ const docsError = await ensureDocsReadyForTool();
8736
+ if (docsError)
8737
+ return docsError;
8246
8738
  const scope = parseScope(args.scope);
8247
8739
  if (!scope) {
8248
8740
  return [
@@ -8321,12 +8813,65 @@ ${bestPage.content}`;
8321
8813
  },
8322
8814
  "command.execute.before": async (hookInput, output) => {
8323
8815
  if (hookInput.command === "refresh-docs") {
8324
- const result = await fetchAllDocs(dbPath);
8325
- if (dbInstance) {
8326
- closeDatabase(dbInstance);
8327
- dbInstance = null;
8816
+ const progressLines = [];
8817
+ try {
8818
+ const result = await ensureDocsAvailable({
8819
+ force: true,
8820
+ onEvent: (event) => {
8821
+ const line = formatSnapshotRefreshEvent(event);
8822
+ if (line)
8823
+ progressLines.push(line);
8824
+ }
8825
+ });
8826
+ resetDb();
8827
+ const db = await getDb();
8828
+ const indexedPages = getAllPages(db).length;
8829
+ pushText(output, [
8830
+ "refresh-docs complete (recommended snapshot path).",
8831
+ "",
8832
+ ...progressLines.map((line) => `- ${line}`),
8833
+ ...progressLines.length > 0 ? [""] : [],
8834
+ `snapshot_source=${result.source}`,
8835
+ result.downloadUrl ? `snapshot_url=${result.downloadUrl}` : null,
8836
+ `snapshot_bytes=${result.bytes} (${formatBytes(result.bytes)})`,
8837
+ `indexed_pages=${indexedPages}`
8838
+ ].filter((line) => !!line).join(`
8839
+ `));
8840
+ } catch (err) {
8841
+ pushText(output, formatSnapshotFailure(err));
8842
+ }
8843
+ return;
8844
+ }
8845
+ if (hookInput.command === "refresh-docs-rescrape") {
8846
+ const progressLines = [];
8847
+ const failureSamples = [];
8848
+ try {
8849
+ const result = await fetchAllDocs(dbPath, {
8850
+ progressEvery: 250,
8851
+ onProgress: (event) => {
8852
+ formatRescrapeProgressEvent(event, progressLines, failureSamples);
8853
+ }
8854
+ });
8855
+ resetDb();
8856
+ const db = await getDb();
8857
+ const indexedPages = getAllPages(db).length;
8858
+ pushText(output, [
8859
+ "refresh-docs-rescrape complete (unsafe/experimental).",
8860
+ "",
8861
+ "Warning: this command live-scrapes palantir.com and is slower/less reliable than /refresh-docs.",
8862
+ "",
8863
+ ...progressLines.map((line) => `- ${line}`),
8864
+ ...progressLines.length > 0 ? [""] : [],
8865
+ `total_pages=${result.totalPages}`,
8866
+ `fetched_pages=${result.fetchedPages}`,
8867
+ `failed_pages=${result.failedUrls.length}`,
8868
+ `indexed_pages=${indexedPages}`,
8869
+ ...failureSamples.length > 0 ? ["", "failure_samples:", ...failureSamples.map((line) => `- ${line}`)] : []
8870
+ ].join(`
8871
+ `));
8872
+ } catch (err) {
8873
+ pushText(output, formatRescrapeFailure(err));
8328
8874
  }
8329
- pushText(output, `Refreshed documentation: ${result.fetchedPages}/${result.totalPages} pages fetched. ${result.failedUrls.length} failures.`);
8330
8875
  return;
8331
8876
  }
8332
8877
  if (hookInput.command === "setup-palantir-mcp") {