@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.
- package/README.md +11 -8
- package/dist/index.js +456 -92
- 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
|
-
|
|
134
|
+
The docs DB is a local file:
|
|
135
135
|
|
|
136
136
|
- `data/docs.parquet` (in your repo root)
|
|
137
137
|
|
|
138
|
-
###
|
|
138
|
+
### First-run behavior
|
|
139
139
|
|
|
140
|
-
|
|
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
|
-
|
|
143
|
+
### Refresh commands
|
|
143
144
|
|
|
144
|
-
|
|
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
|
|
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
|
|
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,7 +8063,7 @@ 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) {
|
|
@@ -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: ${
|
|
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 =
|
|
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] ${
|
|
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 =
|
|
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}: ${
|
|
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}: ${
|
|
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] ${
|
|
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 =
|
|
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}: ${
|
|
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 =
|
|
8346
|
+
const dbPath = path5.join(input.worktree, "data", "docs.parquet");
|
|
8148
8347
|
let dbInstance = null;
|
|
8149
|
-
let
|
|
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
|
|
8156
|
-
description: "
|
|
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
|
|
8224
|
-
if (
|
|
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
|
-
|
|
8455
|
+
autoBootstrapMcpStarted = true;
|
|
8233
8456
|
autoBootstrapPalantirMcpIfConfigured(input.worktree);
|
|
8234
8457
|
}
|
|
8235
|
-
|
|
8236
|
-
if (
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8458
|
+
function maybeStartAutoBootstrapDocs() {
|
|
8459
|
+
if (autoBootstrapDocsStarted)
|
|
8460
|
+
return;
|
|
8461
|
+
autoBootstrapDocsStarted = true;
|
|
8462
|
+
ensureDocsAvailable().catch(() => {});
|
|
8240
8463
|
}
|
|
8241
|
-
|
|
8242
|
-
|
|
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
|
|
8301
|
-
return
|
|
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
|
|
8619
|
+
const path6 = toPathname(page.url).toLowerCase();
|
|
8312
8620
|
const title = page.title.toLowerCase();
|
|
8313
|
-
if (
|
|
8621
|
+
if (path6 === q)
|
|
8314
8622
|
return 2000;
|
|
8315
|
-
if (
|
|
8623
|
+
if (path6 === toPathname(q).toLowerCase())
|
|
8316
8624
|
return 2000;
|
|
8317
|
-
if (
|
|
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 (
|
|
8636
|
+
if (path6.includes(t))
|
|
8329
8637
|
score += 30;
|
|
8330
8638
|
}
|
|
8331
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
8353
|
-
|
|
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
|
-
|
|
8426
|
-
|
|
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
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
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") {
|