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

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 +8 -11
  2. package/dist/index.js +92 -456
  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`, `/refresh-docs-rescrape`, `/setup-palantir-mcp`, `/rescan-palantir-mcp-tools`
39
+ - Commands: `/refresh-docs`, `/setup-palantir-mcp`, `/rescan-palantir-mcp-tools`
40
40
  - Agents: `foundry-librarian`, `foundry`
41
41
 
42
42
  ### Versions: how to get the latest
@@ -131,22 +131,17 @@ it’s `export`ed in the environment where OpenCode is launched.
131
131
 
132
132
  ## Docs tools (Palantir public docs)
133
133
 
134
- The docs DB is a local file:
134
+ This package does **not** ship with docs bundled. The docs DB is a local file:
135
135
 
136
136
  - `data/docs.parquet` (in your repo root)
137
137
 
138
- ### First-run behavior
138
+ ### Fetch docs
139
139
 
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.
140
+ In OpenCode, run:
142
141
 
143
- ### Refresh commands
142
+ - `/refresh-docs`
144
143
 
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
144
+ This downloads the docs and writes `data/docs.parquet`.
150
145
 
151
146
  ### Tools
152
147
 
@@ -155,6 +150,8 @@ prebuilt snapshot (download/copy). In most repos, docs tools should work without
155
150
  - `list_all_docs`
156
151
  - List docs with pagination and optional query/scope filtering
157
152
 
153
+ If `data/docs.parquet` is missing, both tools will tell you to run `/refresh-docs`.
154
+
158
155
  ## Foundry MCP helpers
159
156
 
160
157
  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 path5 from "path";
3
+ import path4 from "path";
4
4
  import { tool } from "@opencode-ai/plugin/tool";
5
5
 
6
6
  // node_modules/hyparquet/src/constants.js
@@ -6088,9 +6088,6 @@ 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
- }
6094
6091
  function decompressPagefind(data) {
6095
6092
  const decompressed = gunzipSync(Buffer.from(data));
6096
6093
  if (decompressed.length < PAGEFIND_HEADER_SIZE) {
@@ -6205,11 +6202,8 @@ async function withConcurrencyLimit(tasks, limit) {
6205
6202
  next();
6206
6203
  });
6207
6204
  }
6208
- async function fetchAllDocs(dbPath, options = {}) {
6205
+ async function fetchAllDocs(dbPath) {
6209
6206
  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;
6213
6207
  const langKey = Object.keys(entry.languages)[0];
6214
6208
  if (!langKey) {
6215
6209
  throw new Error("No languages found in Pagefind entry");
@@ -6217,25 +6211,17 @@ async function fetchAllDocs(dbPath, options = {}) {
6217
6211
  const langHash = entry.languages[langKey].hash;
6218
6212
  const pageHashes = await fetchAndParseMeta(langHash);
6219
6213
  const totalPages = pageHashes.length;
6220
- onProgress?.({ type: "discovered", totalPages });
6221
6214
  const fetchedRecords = [];
6222
6215
  const failedUrls = [];
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 });
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...`);
6236
6221
  }
6222
+ return record;
6237
6223
  }));
6238
- const results = await withConcurrencyLimit(tasks, concurrency);
6224
+ const results = await withConcurrencyLimit(tasks, DEFAULT_CONCURRENCY);
6239
6225
  for (let i = 0;i < results.length; i++) {
6240
6226
  const result = results[i];
6241
6227
  if (result.status === "fulfilled") {
@@ -6243,15 +6229,10 @@ async function fetchAllDocs(dbPath, options = {}) {
6243
6229
  } else {
6244
6230
  const url = `${PAGEFIND_BASE}/fragment/${pageHashes[i]}.pf_fragment`;
6245
6231
  failedUrls.push(url);
6232
+ console.log(`[ERROR] Failed to fetch ${url}: ${result.reason.message}`);
6246
6233
  }
6247
6234
  }
6248
6235
  await writeParquet(fetchedRecords, dbPath);
6249
- onProgress?.({
6250
- type: "completed",
6251
- totalPages,
6252
- fetchedPages: fetchedRecords.length,
6253
- failedPages: failedUrls.length
6254
- });
6255
6236
  return {
6256
6237
  totalPages,
6257
6238
  fetchedPages: fetchedRecords.length,
@@ -6260,189 +6241,8 @@ async function fetchAllDocs(dbPath, options = {}) {
6260
6241
  };
6261
6242
  }
6262
6243
 
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
-
6444
6244
  // src/palantir-mcp/commands.ts
6445
- import path4 from "path";
6245
+ import path3 from "path";
6446
6246
 
6447
6247
  // src/palantir-mcp/allowlist.ts
6448
6248
  function isMutatingTool(toolName) {
@@ -6504,7 +6304,7 @@ function computeAllowedTools(profile, toolNames) {
6504
6304
  }
6505
6305
 
6506
6306
  // src/palantir-mcp/mcp-client.ts
6507
- function formatError3(err) {
6307
+ function formatError(err) {
6508
6308
  return err instanceof Error ? err.toString() : String(err);
6509
6309
  }
6510
6310
  function withTimeout(p, ms, label) {
@@ -6645,7 +6445,7 @@ ${errText}`));
6645
6445
  return Array.from(new Set(names)).sort((a, b) => a.localeCompare(b));
6646
6446
  } catch (err) {
6647
6447
  const stderrText = stderrChunks.join("");
6648
- throw new Error(`[ERROR] Failed to list palantir-mcp tools: ${formatError3(err)}
6448
+ throw new Error(`[ERROR] Failed to list palantir-mcp tools: ${formatError(err)}
6649
6449
  ${stderrText}`);
6650
6450
  } finally {
6651
6451
  try {
@@ -6684,8 +6484,8 @@ function normalizeFoundryBaseUrl(raw) {
6684
6484
  }
6685
6485
 
6686
6486
  // src/palantir-mcp/opencode-config.ts
6687
- import fs2 from "fs/promises";
6688
- import path2 from "path";
6487
+ import fs from "fs/promises";
6488
+ import path from "path";
6689
6489
 
6690
6490
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
6691
6491
  function createScanner(text, ignoreTrivia = false) {
@@ -7497,28 +7297,28 @@ var OPENCODE_JSON_FILENAME = "opencode.json";
7497
7297
  function isRecord(value) {
7498
7298
  return !!value && typeof value === "object" && !Array.isArray(value);
7499
7299
  }
7500
- function formatError4(err) {
7300
+ function formatError2(err) {
7501
7301
  return err instanceof Error ? err.toString() : String(err);
7502
7302
  }
7503
7303
  async function pathExists(p) {
7504
7304
  try {
7505
- await fs2.access(p);
7305
+ await fs.access(p);
7506
7306
  return true;
7507
7307
  } catch {
7508
7308
  return false;
7509
7309
  }
7510
7310
  }
7511
7311
  async function readOpencodeJsonc(worktree) {
7512
- const configPath = path2.join(worktree, OPENCODE_JSONC_FILENAME);
7312
+ const configPath = path.join(worktree, OPENCODE_JSONC_FILENAME);
7513
7313
  if (!await pathExists(configPath))
7514
7314
  return { ok: false, missing: true };
7515
7315
  let text;
7516
7316
  try {
7517
- text = await fs2.readFile(configPath, "utf8");
7317
+ text = await fs.readFile(configPath, "utf8");
7518
7318
  } catch (err) {
7519
7319
  return {
7520
7320
  ok: false,
7521
- error: `[ERROR] Failed reading ${OPENCODE_JSONC_FILENAME}: ${formatError4(err)}`
7321
+ error: `[ERROR] Failed reading ${OPENCODE_JSONC_FILENAME}: ${formatError2(err)}`
7522
7322
  };
7523
7323
  }
7524
7324
  const errors = [];
@@ -7533,16 +7333,16 @@ async function readOpencodeJsonc(worktree) {
7533
7333
  return { ok: true, path: configPath, text, data };
7534
7334
  }
7535
7335
  async function readLegacyOpencodeJson(worktree) {
7536
- const legacyPath = path2.join(worktree, OPENCODE_JSON_FILENAME);
7336
+ const legacyPath = path.join(worktree, OPENCODE_JSON_FILENAME);
7537
7337
  if (!await pathExists(legacyPath))
7538
7338
  return { ok: false, missing: true };
7539
7339
  let text;
7540
7340
  try {
7541
- text = await fs2.readFile(legacyPath, "utf8");
7341
+ text = await fs.readFile(legacyPath, "utf8");
7542
7342
  } catch (err) {
7543
7343
  return {
7544
7344
  ok: false,
7545
- error: `[ERROR] Failed reading ${OPENCODE_JSON_FILENAME}: ${formatError4(err)}`
7345
+ error: `[ERROR] Failed reading ${OPENCODE_JSON_FILENAME}: ${formatError2(err)}`
7546
7346
  };
7547
7347
  }
7548
7348
  let data;
@@ -7551,7 +7351,7 @@ async function readLegacyOpencodeJson(worktree) {
7551
7351
  } catch (err) {
7552
7352
  return {
7553
7353
  ok: false,
7554
- error: `[ERROR] Failed parsing ${OPENCODE_JSON_FILENAME}: ${formatError4(err)}`
7354
+ error: `[ERROR] Failed parsing ${OPENCODE_JSON_FILENAME}: ${formatError2(err)}`
7555
7355
  };
7556
7356
  }
7557
7357
  return { ok: true, path: legacyPath, text, data };
@@ -7580,24 +7380,24 @@ function mergeLegacyIntoJsonc(legacyData, jsoncData) {
7580
7380
  return deepMergePreferTarget(base, legacy);
7581
7381
  }
7582
7382
  async function writeFileAtomic(filePath, text) {
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);
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);
7588
7388
  }
7589
7389
  async function renameLegacyToBak(worktree) {
7590
- const legacyPath = path2.join(worktree, OPENCODE_JSON_FILENAME);
7390
+ const legacyPath = path.join(worktree, OPENCODE_JSON_FILENAME);
7591
7391
  if (!await pathExists(legacyPath))
7592
7392
  return null;
7593
- const baseBak = path2.join(worktree, `${OPENCODE_JSON_FILENAME}.bak`);
7393
+ const baseBak = path.join(worktree, `${OPENCODE_JSON_FILENAME}.bak`);
7594
7394
  let bakPath = baseBak;
7595
7395
  let i = 1;
7596
7396
  while (await pathExists(bakPath)) {
7597
7397
  bakPath = `${baseBak}.${i}`;
7598
7398
  i += 1;
7599
7399
  }
7600
- await fs2.rename(legacyPath, bakPath);
7400
+ await fs.rename(legacyPath, bakPath);
7601
7401
  return bakPath;
7602
7402
  }
7603
7403
  function toolKey(toolName) {
@@ -7837,11 +7637,11 @@ function stringifyJsonc(data) {
7837
7637
  }
7838
7638
 
7839
7639
  // src/palantir-mcp/repo-scan.ts
7840
- import fs3 from "fs/promises";
7841
- import path3 from "path";
7640
+ import fs2 from "fs/promises";
7641
+ import path2 from "path";
7842
7642
  async function pathExists2(p) {
7843
7643
  try {
7844
- await fs3.access(p);
7644
+ await fs2.access(p);
7845
7645
  return true;
7846
7646
  } catch {
7847
7647
  return false;
@@ -7849,7 +7649,7 @@ async function pathExists2(p) {
7849
7649
  }
7850
7650
  async function readTextFileBounded(p, maxBytes) {
7851
7651
  try {
7852
- const file = await fs3.open(p, "r");
7652
+ const file = await fs2.open(p, "r");
7853
7653
  try {
7854
7654
  const buf = Buffer.alloc(maxBytes);
7855
7655
  const { bytesRead } = await file.read(buf, 0, maxBytes, 0);
@@ -7932,14 +7732,14 @@ async function collectSampleFiles(root, limit) {
7932
7732
  visitedDirs += 1;
7933
7733
  let entries;
7934
7734
  try {
7935
- entries = await fs3.readdir(dir, { withFileTypes: true });
7735
+ entries = await fs2.readdir(dir, { withFileTypes: true });
7936
7736
  } catch {
7937
7737
  continue;
7938
7738
  }
7939
7739
  for (const ent of entries) {
7940
7740
  if (results.length >= limit)
7941
7741
  break;
7942
- const full = path3.join(dir, ent.name);
7742
+ const full = path2.join(dir, ent.name);
7943
7743
  if (ent.isDirectory()) {
7944
7744
  if (ignoreDirs.has(ent.name))
7945
7745
  continue;
@@ -7948,7 +7748,7 @@ async function collectSampleFiles(root, limit) {
7948
7748
  }
7949
7749
  if (!ent.isFile())
7950
7750
  continue;
7951
- const ext = path3.extname(ent.name);
7751
+ const ext = path2.extname(ent.name);
7952
7752
  if (!allowedExts.has(ext))
7953
7753
  continue;
7954
7754
  results.push(full);
@@ -7971,13 +7771,13 @@ async function scanRepoForProfile(root) {
7971
7771
  { p: "lerna.json", profile: "all", score: 3, reason: "Found lerna.json" }
7972
7772
  ];
7973
7773
  for (const c of candidates) {
7974
- if (await pathExists2(path3.join(root, c.p))) {
7774
+ if (await pathExists2(path2.join(root, c.p))) {
7975
7775
  addScore(scores, reasons, c.profile, c.score, c.reason);
7976
7776
  }
7977
7777
  }
7978
- const packageJsonPath = path3.join(root, "package.json");
7979
- const pyprojectPath = path3.join(root, "pyproject.toml");
7980
- const requirementsPath = path3.join(root, "requirements.txt");
7778
+ const packageJsonPath = path2.join(root, "package.json");
7779
+ const pyprojectPath = path2.join(root, "pyproject.toml");
7780
+ const requirementsPath = path2.join(root, "requirements.txt");
7981
7781
  const hasPackageJson = await pathExists2(packageJsonPath);
7982
7782
  const hasPyproject = await pathExists2(pyprojectPath);
7983
7783
  if (hasPackageJson && hasPyproject) {
@@ -8017,22 +7817,22 @@ async function scanRepoForProfile(root) {
8017
7817
  }
8018
7818
  }
8019
7819
  }
8020
- if (await pathExists2(path3.join(root, "pipelines"))) {
7820
+ if (await pathExists2(path2.join(root, "pipelines"))) {
8021
7821
  addScore(scores, reasons, "pipelines_transforms", 3, "Found pipelines/ directory");
8022
7822
  }
8023
- if (await pathExists2(path3.join(root, "transforms"))) {
7823
+ if (await pathExists2(path2.join(root, "transforms"))) {
8024
7824
  addScore(scores, reasons, "pipelines_transforms", 3, "Found transforms/ directory");
8025
7825
  }
8026
- if (await pathExists2(path3.join(root, "internal", "pipeline"))) {
7826
+ if (await pathExists2(path2.join(root, "internal", "pipeline"))) {
8027
7827
  addScore(scores, reasons, "pipelines_transforms", 3, "Found internal/pipeline/ directory");
8028
7828
  }
8029
- if (await pathExists2(path3.join(root, "internal", "transforms"))) {
7829
+ if (await pathExists2(path2.join(root, "internal", "transforms"))) {
8030
7830
  addScore(scores, reasons, "pipelines_transforms", 3, "Found internal/transforms/ directory");
8031
7831
  }
8032
- if (await pathExists2(path3.join(root, "functions"))) {
7832
+ if (await pathExists2(path2.join(root, "functions"))) {
8033
7833
  addScore(scores, reasons, "osdk_functions_ts", 2, "Found functions/ directory");
8034
7834
  }
8035
- if (await pathExists2(path3.join(root, "src", "functions"))) {
7835
+ if (await pathExists2(path2.join(root, "src", "functions"))) {
8036
7836
  addScore(scores, reasons, "osdk_functions_ts", 2, "Found src/functions/ directory");
8037
7837
  }
8038
7838
  const sampleFiles = await collectSampleFiles(root, 50);
@@ -8063,7 +7863,7 @@ async function scanRepoForProfile(root) {
8063
7863
  }
8064
7864
 
8065
7865
  // src/palantir-mcp/commands.ts
8066
- function formatError5(err) {
7866
+ function formatError3(err) {
8067
7867
  return err instanceof Error ? err.toString() : String(err);
8068
7868
  }
8069
7869
  function isRecord2(value) {
@@ -8112,7 +7912,7 @@ async function resolveProfile(worktree) {
8112
7912
  } catch (err) {
8113
7913
  return {
8114
7914
  profile: "unknown",
8115
- reasons: [`Repo scan failed; falling back to unknown: ${formatError5(err)}`]
7915
+ reasons: [`Repo scan failed; falling back to unknown: ${formatError3(err)}`]
8116
7916
  };
8117
7917
  }
8118
7918
  }
@@ -8176,7 +7976,7 @@ async function autoBootstrapPalantirMcpIfConfigured(worktree) {
8176
7976
  const changed = needsMigration || stableJsonStringify(merged) !== stableJsonStringify(patch.data);
8177
7977
  if (!changed)
8178
7978
  return;
8179
- const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
7979
+ const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8180
7980
  const text = stringifyJsonc(patch.data);
8181
7981
  await writeFileAtomic(outPath, text);
8182
7982
  if (readLegacy.ok) {
@@ -8237,7 +8037,7 @@ async function setupPalantirMcp(worktree, rawArgs) {
8237
8037
  try {
8238
8038
  toolNames = await listPalantirMcpTools(discoveryUrl);
8239
8039
  } catch (err) {
8240
- return `[ERROR] ${formatError5(err)}`;
8040
+ return `[ERROR] ${formatError3(err)}`;
8241
8041
  }
8242
8042
  if (toolNames.length === 0)
8243
8043
  return "[ERROR] palantir-mcp tool discovery returned no tools.";
@@ -8248,12 +8048,12 @@ async function setupPalantirMcp(worktree, rawArgs) {
8248
8048
  profile,
8249
8049
  allowlist
8250
8050
  });
8251
- const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
8051
+ const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8252
8052
  const text = stringifyJsonc(patch.data);
8253
8053
  try {
8254
8054
  await writeFileAtomic(outPath, text);
8255
8055
  } catch (err) {
8256
- return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError5(err)}`;
8056
+ return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError3(err)}`;
8257
8057
  }
8258
8058
  let bakInfo = "";
8259
8059
  if (readLegacy.ok) {
@@ -8264,7 +8064,7 @@ async function setupPalantirMcp(worktree, rawArgs) {
8264
8064
  Migrated legacy ${readLegacy.path} -> ${bakPath}`;
8265
8065
  } catch (err) {
8266
8066
  bakInfo = `
8267
- [ERROR] Wrote ${OPENCODE_JSONC_FILENAME}, but failed to rename legacy ${readLegacy.path}: ${formatError5(err)}`;
8067
+ [ERROR] Wrote ${OPENCODE_JSONC_FILENAME}, but failed to rename legacy ${readLegacy.path}: ${formatError3(err)}`;
8268
8068
  }
8269
8069
  }
8270
8070
  const warnings = [...normalized.warnings, ...patch.warnings];
@@ -8318,18 +8118,18 @@ async function rescanPalantirMcpTools(worktree) {
8318
8118
  try {
8319
8119
  toolNames = await listPalantirMcpTools(normalized.url);
8320
8120
  } catch (err) {
8321
- return `[ERROR] ${formatError5(err)}`;
8121
+ return `[ERROR] ${formatError3(err)}`;
8322
8122
  }
8323
8123
  if (toolNames.length === 0)
8324
8124
  return "[ERROR] palantir-mcp tool discovery returned no tools.";
8325
8125
  const allowlist = computeAllowedTools(profile, toolNames);
8326
8126
  const patch = patchConfigForRescan(baseData, { toolNames, profile, allowlist });
8327
- const outPath = path4.join(worktree, OPENCODE_JSONC_FILENAME);
8127
+ const outPath = path3.join(worktree, OPENCODE_JSONC_FILENAME);
8328
8128
  const text = stringifyJsonc(patch.data);
8329
8129
  try {
8330
8130
  await writeFileAtomic(outPath, text);
8331
8131
  } catch (err) {
8332
- return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError5(err)}`;
8132
+ return `[ERROR] Failed writing ${OPENCODE_JSONC_FILENAME}: ${formatError3(err)}`;
8333
8133
  }
8334
8134
  const warnings = [...normalized.warnings, ...patch.warnings];
8335
8135
  return [
@@ -8342,41 +8142,18 @@ async function rescanPalantirMcpTools(worktree) {
8342
8142
  }
8343
8143
 
8344
8144
  // src/index.ts
8145
+ var NO_DB_MESSAGE = "Documentation database not found. Run /refresh-docs to download Palantir Foundry documentation.";
8345
8146
  var plugin = async (input) => {
8346
- const dbPath = path5.join(input.worktree, "data", "docs.parquet");
8147
+ const dbPath = path4.join(input.worktree, "data", "docs.parquet");
8347
8148
  let dbInstance = null;
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
- }
8149
+ let autoBootstrapStarted = false;
8367
8150
  function ensureCommandDefinitions(cfg) {
8368
8151
  if (!cfg.command)
8369
8152
  cfg.command = {};
8370
8153
  if (!cfg.command["refresh-docs"]) {
8371
8154
  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."
8155
+ template: "Refresh Palantir documentation database.",
8156
+ description: "Download Palantir docs and write data/docs.parquet (local)."
8380
8157
  };
8381
8158
  }
8382
8159
  if (!cfg.command["setup-palantir-mcp"]) {
@@ -8443,8 +8220,8 @@ var plugin = async (input) => {
8443
8220
  ensureAgentDefaults2(foundry, "foundry");
8444
8221
  cfg.agent.foundry = foundry;
8445
8222
  }
8446
- function maybeStartAutoBootstrapMcp() {
8447
- if (autoBootstrapMcpStarted)
8223
+ function maybeStartAutoBootstrap() {
8224
+ if (autoBootstrapStarted)
8448
8225
  return;
8449
8226
  const token = process.env.FOUNDRY_TOKEN;
8450
8227
  const url = process.env.FOUNDRY_URL;
@@ -8452,105 +8229,20 @@ var plugin = async (input) => {
8452
8229
  return;
8453
8230
  if (!url || url.trim().length === 0)
8454
8231
  return;
8455
- autoBootstrapMcpStarted = true;
8232
+ autoBootstrapStarted = true;
8456
8233
  autoBootstrapPalantirMcpIfConfigured(input.worktree);
8457
8234
  }
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
8235
  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;
8482
- }
8483
- function pushText(output, text) {
8484
- output.parts.push({ type: "text", text });
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);
8236
+ if (!dbInstance) {
8237
+ dbInstance = await createDatabase(dbPath);
8527
8238
  }
8239
+ return dbInstance;
8528
8240
  }
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
- `);
8241
+ async function dbExists() {
8242
+ return Bun.file(dbPath).exists();
8538
8243
  }
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
- }
8244
+ function pushText(output, text) {
8245
+ output.parts.push({ type: "text", text });
8554
8246
  }
8555
8247
  function toPathname(inputUrl) {
8556
8248
  const trimmed = inputUrl.trim();
@@ -8605,8 +8297,8 @@ var plugin = async (input) => {
8605
8297
  function isInScope(pageUrl, scope) {
8606
8298
  if (scope === "all")
8607
8299
  return true;
8608
- const path6 = toPathname(pageUrl);
8609
- return path6.startsWith(`/${scope}/`) || path6.startsWith(`/docs/${scope}/`);
8300
+ const path5 = toPathname(pageUrl);
8301
+ return path5.startsWith(`/${scope}/`) || path5.startsWith(`/docs/${scope}/`);
8610
8302
  }
8611
8303
  function tokenizeQuery(query) {
8612
8304
  const tokens = query.toLowerCase().trim().split(/[\s/._-]+/g).map((t) => t.trim()).filter((t) => t.length > 0);
@@ -8616,13 +8308,13 @@ var plugin = async (input) => {
8616
8308
  const q = query.toLowerCase().trim();
8617
8309
  if (q.length === 0)
8618
8310
  return 0;
8619
- const path6 = toPathname(page.url).toLowerCase();
8311
+ const path5 = toPathname(page.url).toLowerCase();
8620
8312
  const title = page.title.toLowerCase();
8621
- if (path6 === q)
8313
+ if (path5 === q)
8622
8314
  return 2000;
8623
- if (path6 === toPathname(q).toLowerCase())
8315
+ if (path5 === toPathname(q).toLowerCase())
8624
8316
  return 2000;
8625
- if (path6.includes(q))
8317
+ if (path5.includes(q))
8626
8318
  return 1200;
8627
8319
  if (title.includes(q))
8628
8320
  return 1000;
@@ -8633,10 +8325,10 @@ var plugin = async (input) => {
8633
8325
  for (const t of tokens) {
8634
8326
  if (title.includes(t))
8635
8327
  score += 40;
8636
- if (path6.includes(t))
8328
+ if (path5.includes(t))
8637
8329
  score += 30;
8638
8330
  }
8639
- if (path6.startsWith(q))
8331
+ if (path5.startsWith(q))
8640
8332
  score += 100;
8641
8333
  if (title.startsWith(q))
8642
8334
  score += 100;
@@ -8646,8 +8338,7 @@ var plugin = async (input) => {
8646
8338
  config: async (cfg) => {
8647
8339
  ensureCommandDefinitions(cfg);
8648
8340
  ensureAgentDefinitions(cfg);
8649
- maybeStartAutoBootstrapMcp();
8650
- maybeStartAutoBootstrapDocs();
8341
+ maybeStartAutoBootstrap();
8651
8342
  },
8652
8343
  tool: {
8653
8344
  get_doc_page: tool({
@@ -8658,9 +8349,8 @@ var plugin = async (input) => {
8658
8349
  scope: tool.schema.enum(["foundry", "apollo", "gotham", "all"]).optional().describe("Scope to search within when using query or fuzzy matching (default: foundry).")
8659
8350
  },
8660
8351
  async execute(args) {
8661
- const docsError = await ensureDocsReadyForTool();
8662
- if (docsError)
8663
- return docsError;
8352
+ if (!await dbExists())
8353
+ return NO_DB_MESSAGE;
8664
8354
  const scope = parseScope(args.scope);
8665
8355
  if (!scope) {
8666
8356
  return [
@@ -8732,9 +8422,8 @@ ${bestPage.content}`;
8732
8422
  query: tool.schema.string().optional().describe("Optional query to filter/rank results by title/URL (case-insensitive).")
8733
8423
  },
8734
8424
  async execute(args) {
8735
- const docsError = await ensureDocsReadyForTool();
8736
- if (docsError)
8737
- return docsError;
8425
+ if (!await dbExists())
8426
+ return NO_DB_MESSAGE;
8738
8427
  const scope = parseScope(args.scope);
8739
8428
  if (!scope) {
8740
8429
  return [
@@ -8813,65 +8502,12 @@ ${bestPage.content}`;
8813
8502
  },
8814
8503
  "command.execute.before": async (hookInput, output) => {
8815
8504
  if (hookInput.command === "refresh-docs") {
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));
8505
+ const result = await fetchAllDocs(dbPath);
8506
+ if (dbInstance) {
8507
+ closeDatabase(dbInstance);
8508
+ dbInstance = null;
8874
8509
  }
8510
+ pushText(output, `Refreshed documentation: ${result.fetchedPages}/${result.totalPages} pages fetched. ${result.failedUrls.length} failures.`);
8875
8511
  return;
8876
8512
  }
8877
8513
  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.5-next.4",
3
+ "version": "0.1.5",
4
4
  "description": "collection of tools, agents, hooks to supercharge development in foundry",
5
5
  "license": "MIT",
6
6
  "author": {