@openontology/opencode-palantir 0.1.4-next.9 → 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 +11 -8
  2. package/dist/index.js +456 -92
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,7 +36,7 @@ Restart OpenCode.
36
36
  After enabling the plugin, OpenCode will automatically register:
37
37
 
38
38
  - Tools: `get_doc_page`, `list_all_docs`
39
- - Commands: `/refresh-docs`, `/setup-palantir-mcp`, `/rescan-palantir-mcp-tools`
39
+ - Commands: `/refresh-docs`, `/refresh-docs-rescrape`, `/setup-palantir-mcp`, `/rescan-palantir-mcp-tools`
40
40
  - Agents: `foundry-librarian`, `foundry`
41
41
 
42
42
  ### Versions: how to get the latest
@@ -131,17 +131,22 @@ it’s `export`ed in the environment where OpenCode is launched.
131
131
 
132
132
  ## Docs tools (Palantir public docs)
133
133
 
134
- This package does **not** ship with docs bundled. The docs DB is a local file:
134
+ The docs DB is a local file:
135
135
 
136
136
  - `data/docs.parquet` (in your repo root)
137
137
 
138
- ### Fetch docs
138
+ ### First-run behavior
139
139
 
140
- In OpenCode, run:
140
+ On startup and tool usage, the plugin automatically ensures `data/docs.parquet` exists by using a
141
+ prebuilt snapshot (download/copy). In most repos, docs tools should work without any manual setup.
141
142
 
142
- - `/refresh-docs`
143
+ ### Refresh commands
143
144
 
144
- This downloads the docs and writes `data/docs.parquet`.
145
+ - `/refresh-docs` (recommended)
146
+ - Force refresh from a prebuilt snapshot (no live rescrape)
147
+ - `/refresh-docs-rescrape` (unsafe/experimental fallback)
148
+ - Live-rescrapes palantir.com docs and rebuilds `data/docs.parquet`
149
+ - Use only when snapshot download/copy is blocked
145
150
 
146
151
  ### Tools
147
152
 
@@ -150,8 +155,6 @@ This downloads the docs and writes `data/docs.parquet`.
150
155
  - `list_all_docs`
151
156
  - List docs with pagination and optional query/scope filtering
152
157
 
153
- If `data/docs.parquet` is missing, both tools will tell you to run `/refresh-docs`.
154
-
155
158
  ## Foundry MCP helpers
156
159
 
157
160
  This plugin registers Foundry commands and agents automatically at startup (config-driven).
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // src/index.ts
3
- import path4 from "path";
3
+ import path5 from "path";
4
4
  import { tool } from "@opencode-ai/plugin/tool";
5
5
 
6
6
  // node_modules/hyparquet/src/constants.js
@@ -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,7 +8063,7 @@ 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) {
@@ -7912,7 +8112,7 @@ async function resolveProfile(worktree) {
7912
8112
  } catch (err) {
7913
8113
  return {
7914
8114
  profile: "unknown",
7915
- reasons: [`Repo scan failed; falling back to unknown: ${formatError3(err)}`]
8115
+ reasons: [`Repo scan failed; falling back to unknown: ${formatError5(err)}`]
7916
8116
  };
7917
8117
  }
7918
8118
  }
@@ -7976,7 +8176,7 @@ async function autoBootstrapPalantirMcpIfConfigured(worktree) {
7976
8176
  const changed = needsMigration || stableJsonStringify(merged) !== stableJsonStringify(patch.data);
7977
8177
  if (!changed)
7978
8178
  return;
7979
- const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8179
+ const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
7980
8180
  const text = stringifyJsonc(patch.data);
7981
8181
  await writeFileAtomic(outPath, text);
7982
8182
  if (readLegacy.ok) {
@@ -8037,7 +8237,7 @@ async function setupPalantirMcp(worktree, rawArgs) {
8037
8237
  try {
8038
8238
  toolNames = await listPalantirMcpTools(discoveryUrl);
8039
8239
  } catch (err) {
8040
- return `[ERROR] ${formatError3(err)}`;
8240
+ return `[ERROR] ${formatError5(err)}`;
8041
8241
  }
8042
8242
  if (toolNames.length === 0)
8043
8243
  return "[ERROR] palantir-mcp tool discovery returned no tools.";
@@ -8048,12 +8248,12 @@ async function setupPalantirMcp(worktree, rawArgs) {
8048
8248
  profile,
8049
8249
  allowlist
8050
8250
  });
8051
- const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8251
+ const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
8052
8252
  const text = stringifyJsonc(patch.data);
8053
8253
  try {
8054
8254
  await writeFileAtomic(outPath, text);
8055
8255
  } catch (err) {
8056
- return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError3(err)}`;
8256
+ return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError5(err)}`;
8057
8257
  }
8058
8258
  let bakInfo = "";
8059
8259
  if (readLegacy.ok) {
@@ -8064,7 +8264,7 @@ async function setupPalantirMcp(worktree, rawArgs) {
8064
8264
  Migrated legacy ${readLegacy.path} -> ${bakPath}`;
8065
8265
  } catch (err) {
8066
8266
  bakInfo = `
8067
- [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)}`;
8068
8268
  }
8069
8269
  }
8070
8270
  const warnings = [...normalized.warnings, ...patch.warnings];
@@ -8118,18 +8318,18 @@ async function rescanPalantirMcpTools(worktree) {
8118
8318
  try {
8119
8319
  toolNames = await listPalantirMcpTools(normalized.url);
8120
8320
  } catch (err) {
8121
- return `[ERROR] ${formatError3(err)}`;
8321
+ return `[ERROR] ${formatError5(err)}`;
8122
8322
  }
8123
8323
  if (toolNames.length === 0)
8124
8324
  return "[ERROR] palantir-mcp tool discovery returned no tools.";
8125
8325
  const allowlist = computeAllowedTools(profile, toolNames);
8126
8326
  const patch = patchConfigForRescan(baseData, { toolNames, profile, allowlist });
8127
- const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8327
+ const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
8128
8328
  const text = stringifyJsonc(patch.data);
8129
8329
  try {
8130
8330
  await writeFileAtomic(outPath, text);
8131
8331
  } catch (err) {
8132
- return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError3(err)}`;
8332
+ return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError5(err)}`;
8133
8333
  }
8134
8334
  const warnings = [...normalized.warnings, ...patch.warnings];
8135
8335
  return [
@@ -8142,18 +8342,41 @@ async function rescanPalantirMcpTools(worktree) {
8142
8342
  }
8143
8343
 
8144
8344
  // src/index.ts
8145
- var NO_DB_MESSAGE = "Documentation database not found. Run /refresh-docs to download Palantir Foundry documentation.";
8146
8345
  var plugin = async (input) => {
8147
- const dbPath = path4.join(input.worktree, "data", "docs.parquet");
8346
+ const dbPath = path5.join(input.worktree, "data", "docs.parquet");
8148
8347
  let dbInstance = null;
8149
- let autoBootstrapStarted = false;
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
+ }
8150
8367
  function ensureCommandDefinitions(cfg) {
8151
8368
  if (!cfg.command)
8152
8369
  cfg.command = {};
8153
8370
  if (!cfg.command["refresh-docs"]) {
8154
8371
  cfg.command["refresh-docs"] = {
8155
- template: "Refresh Palantir documentation database.",
8156
- description: "Download Palantir docs and write data/docs.parquet (local)."
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."
8157
8380
  };
8158
8381
  }
8159
8382
  if (!cfg.command["setup-palantir-mcp"]) {
@@ -8220,8 +8443,8 @@ var plugin = async (input) => {
8220
8443
  ensureAgentDefaults2(foundry, "foundry");
8221
8444
  cfg.agent.foundry = foundry;
8222
8445
  }
8223
- function maybeStartAutoBootstrap() {
8224
- if (autoBootstrapStarted)
8446
+ function maybeStartAutoBootstrapMcp() {
8447
+ if (autoBootstrapMcpStarted)
8225
8448
  return;
8226
8449
  const token = process.env.FOUNDRY_TOKEN;
8227
8450
  const url = process.env.FOUNDRY_URL;
@@ -8229,21 +8452,106 @@ var plugin = async (input) => {
8229
8452
  return;
8230
8453
  if (!url || url.trim().length === 0)
8231
8454
  return;
8232
- autoBootstrapStarted = true;
8455
+ autoBootstrapMcpStarted = true;
8233
8456
  autoBootstrapPalantirMcpIfConfigured(input.worktree);
8234
8457
  }
8235
- async function getDb() {
8236
- if (!dbInstance) {
8237
- dbInstance = await createDatabase(dbPath);
8238
- }
8239
- return dbInstance;
8458
+ function maybeStartAutoBootstrapDocs() {
8459
+ if (autoBootstrapDocsStarted)
8460
+ return;
8461
+ autoBootstrapDocsStarted = true;
8462
+ ensureDocsAvailable().catch(() => {});
8240
8463
  }
8241
- async function dbExists() {
8242
- return Bun.file(dbPath).exists();
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;
8243
8482
  }
8244
8483
  function pushText(output, text) {
8245
8484
  output.parts.push({ type: "text", text });
8246
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
+ }
8247
8555
  function toPathname(inputUrl) {
8248
8556
  const trimmed = inputUrl.trim();
8249
8557
  if (trimmed.length === 0)
@@ -8297,8 +8605,8 @@ var plugin = async (input) => {
8297
8605
  function isInScope(pageUrl, scope) {
8298
8606
  if (scope === "all")
8299
8607
  return true;
8300
- const path5 = toPathname(pageUrl);
8301
- return path5.startsWith(`/${scope}/`) || path5.startsWith(`/docs/${scope}/`);
8608
+ const path6 = toPathname(pageUrl);
8609
+ return path6.startsWith(`/${scope}/`) || path6.startsWith(`/docs/${scope}/`);
8302
8610
  }
8303
8611
  function tokenizeQuery(query) {
8304
8612
  const tokens = query.toLowerCase().trim().split(/[\s/._-]+/g).map((t) => t.trim()).filter((t) => t.length > 0);
@@ -8308,13 +8616,13 @@ var plugin = async (input) => {
8308
8616
  const q = query.toLowerCase().trim();
8309
8617
  if (q.length === 0)
8310
8618
  return 0;
8311
- const path5 = toPathname(page.url).toLowerCase();
8619
+ const path6 = toPathname(page.url).toLowerCase();
8312
8620
  const title = page.title.toLowerCase();
8313
- if (path5 === q)
8621
+ if (path6 === q)
8314
8622
  return 2000;
8315
- if (path5 === toPathname(q).toLowerCase())
8623
+ if (path6 === toPathname(q).toLowerCase())
8316
8624
  return 2000;
8317
- if (path5.includes(q))
8625
+ if (path6.includes(q))
8318
8626
  return 1200;
8319
8627
  if (title.includes(q))
8320
8628
  return 1000;
@@ -8325,10 +8633,10 @@ var plugin = async (input) => {
8325
8633
  for (const t of tokens) {
8326
8634
  if (title.includes(t))
8327
8635
  score += 40;
8328
- if (path5.includes(t))
8636
+ if (path6.includes(t))
8329
8637
  score += 30;
8330
8638
  }
8331
- if (path5.startsWith(q))
8639
+ if (path6.startsWith(q))
8332
8640
  score += 100;
8333
8641
  if (title.startsWith(q))
8334
8642
  score += 100;
@@ -8338,7 +8646,8 @@ var plugin = async (input) => {
8338
8646
  config: async (cfg) => {
8339
8647
  ensureCommandDefinitions(cfg);
8340
8648
  ensureAgentDefinitions(cfg);
8341
- maybeStartAutoBootstrap();
8649
+ maybeStartAutoBootstrapMcp();
8650
+ maybeStartAutoBootstrapDocs();
8342
8651
  },
8343
8652
  tool: {
8344
8653
  get_doc_page: tool({
@@ -8349,8 +8658,9 @@ var plugin = async (input) => {
8349
8658
  scope: tool.schema.enum(["foundry", "apollo", "gotham", "all"]).optional().describe("Scope to search within when using query or fuzzy matching (default: foundry).")
8350
8659
  },
8351
8660
  async execute(args) {
8352
- if (!await dbExists())
8353
- return NO_DB_MESSAGE;
8661
+ const docsError = await ensureDocsReadyForTool();
8662
+ if (docsError)
8663
+ return docsError;
8354
8664
  const scope = parseScope(args.scope);
8355
8665
  if (!scope) {
8356
8666
  return [
@@ -8422,8 +8732,9 @@ ${bestPage.content}`;
8422
8732
  query: tool.schema.string().optional().describe("Optional query to filter/rank results by title/URL (case-insensitive).")
8423
8733
  },
8424
8734
  async execute(args) {
8425
- if (!await dbExists())
8426
- return NO_DB_MESSAGE;
8735
+ const docsError = await ensureDocsReadyForTool();
8736
+ if (docsError)
8737
+ return docsError;
8427
8738
  const scope = parseScope(args.scope);
8428
8739
  if (!scope) {
8429
8740
  return [
@@ -8502,12 +8813,65 @@ ${bestPage.content}`;
8502
8813
  },
8503
8814
  "command.execute.before": async (hookInput, output) => {
8504
8815
  if (hookInput.command === "refresh-docs") {
8505
- const result = await fetchAllDocs(dbPath);
8506
- if (dbInstance) {
8507
- closeDatabase(dbInstance);
8508
- 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));
8509
8874
  }
8510
- pushText(output, `Refreshed documentation: ${result.fetchedPages}/${result.totalPages} pages fetched. ${result.failedUrls.length} failures.`);
8511
8875
  return;
8512
8876
  }
8513
8877
  if (hookInput.command === "setup-palantir-mcp") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openontology/opencode-palantir",
3
- "version": "0.1.4-next.9",
3
+ "version": "0.1.5-next.4",
4
4
  "description": "collection of tools, agents, hooks to supercharge development in foundry",
5
5
  "license": "MIT",
6
6
  "author": {