@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.
- package/README.md +161 -117
- package/dist/index.js +629 -84
- 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
|
|
6217
|
-
const tasks = pageHashes.map((hash) => () => fetchFragment(hash).
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
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,
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
6488
|
-
import
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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}: ${
|
|
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 =
|
|
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
|
|
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}: ${
|
|
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}: ${
|
|
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 =
|
|
7384
|
-
const base =
|
|
7385
|
-
const tmp =
|
|
7386
|
-
await
|
|
7387
|
-
await
|
|
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 =
|
|
7590
|
+
const legacyPath = path2.join(worktree, OPENCODE_JSON_FILENAME);
|
|
7391
7591
|
if (!await pathExists(legacyPath))
|
|
7392
7592
|
return null;
|
|
7393
|
-
const baseBak =
|
|
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
|
|
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
|
|
7641
|
-
import
|
|
7840
|
+
import fs3 from "fs/promises";
|
|
7841
|
+
import path3 from "path";
|
|
7642
7842
|
async function pathExists2(p) {
|
|
7643
7843
|
try {
|
|
7644
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
7779
|
-
const pyprojectPath =
|
|
7780
|
-
const requirementsPath =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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: ${
|
|
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
|
|
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] ${
|
|
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 =
|
|
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}: ${
|
|
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}: ${
|
|
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] ${
|
|
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 =
|
|
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}: ${
|
|
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 =
|
|
8346
|
+
const dbPath = path5.join(input.worktree, "data", "docs.parquet");
|
|
8058
8347
|
let dbInstance = null;
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8066
|
-
|
|
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
|
|
8125
|
-
return
|
|
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
|
|
8619
|
+
const path6 = toPathname(page.url).toLowerCase();
|
|
8136
8620
|
const title = page.title.toLowerCase();
|
|
8137
|
-
if (
|
|
8621
|
+
if (path6 === q)
|
|
8138
8622
|
return 2000;
|
|
8139
|
-
if (
|
|
8623
|
+
if (path6 === toPathname(q).toLowerCase())
|
|
8140
8624
|
return 2000;
|
|
8141
|
-
if (
|
|
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 (
|
|
8636
|
+
if (path6.includes(t))
|
|
8153
8637
|
score += 30;
|
|
8154
8638
|
}
|
|
8155
|
-
if (
|
|
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
|
-
|
|
8172
|
-
|
|
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
|
-
|
|
8245
|
-
|
|
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
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
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") {
|