@relvo/mcp 0.1.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.
- package/README.md +12 -0
- package/bin/relvo-mcp.mjs +1 -1
- package/dist/client/agent-run.js +43 -10
- package/dist/client/browser.js +33 -20
- package/dist/client/local-input.js +15 -1
- package/dist/client/local-profile.js +35 -8
- package/dist/mcp/agent-server.js +54 -44
- package/dist/mcp/icon.js +1 -0
- package/dist/mcp/instructions.js +1 -1
- package/dist/mcp/local-upload.js +3 -17
- package/dist/mcp/operations.js +4 -3
- package/dist/mcp/runtime.js +7 -6
- package/dist/mcp/server.js +27 -15
- package/dist/mcp/team-skill.js +62 -0
- package/dist/mcp/update-notice.js +67 -0
- package/dist/mcp/version.js +1 -1
- package/dist/schema/platformCapabilityFamilies.js +1 -1
- package/dist/schema/platformLocalSetup.js +1 -1
- package/dist/schema/platformMetaTools.js +2 -2
- package/dist/schema/platformPublicExecution.js +1 -0
- package/dist/schema/platformTeamSkills.js +84 -4
- package/dist/schema/toolInputRepair.js +22 -16
- package/dist/schema/untrustedContent.js +1 -1
- package/npm-shrinkwrap.json +73 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -49,6 +49,14 @@ For a price-only check, use `platform` to call `platform_quote` before any run.
|
|
|
49
49
|
Schemas load on demand. Set `RELVO_MCP_MODE=full` only if your host needs every
|
|
50
50
|
original operation listed as a separate tool.
|
|
51
51
|
|
|
52
|
+
## Team skill references
|
|
53
|
+
|
|
54
|
+
Use `platform` with operation `team_skills` to list your team's procedures. Operation `team_skill` reads one by `slug` and lists
|
|
55
|
+
its files. Add a relative `path` to read `SKILL.md`, a reference document, an image or a script. Images return MCP image content;
|
|
56
|
+
long text and binary files return pages with `nextOffset`. The same access permissions apply to every file.
|
|
57
|
+
|
|
58
|
+
For local folder installation, use **Use in your environment** in [Customize → Skills](https://app.relvo.si/customization).
|
|
59
|
+
|
|
52
60
|
## Costs and retries
|
|
53
61
|
|
|
54
62
|
Quotes are free. Searches count toward your team's monthly free calls and are
|
|
@@ -62,5 +70,9 @@ and key or resume its `runId`. Read stored results instead of purchasing them ag
|
|
|
62
70
|
Local file uploads are disabled by default. Enable only the directories you need
|
|
63
71
|
with `RELVO_MCP_ALLOWED_UPLOAD_ROOTS`, and upload only user-authorized files.
|
|
64
72
|
|
|
73
|
+
The server reads the npm registry once per start. When a newer release has been
|
|
74
|
+
public for 72 hours, a one-time note in a tool result says how to update; nothing
|
|
75
|
+
is installed. `RELVO_AUTO_UPDATE=0` turns this off.
|
|
76
|
+
|
|
65
77
|
See [Relvo docs](https://app.relvo.si/docs) for workflows and configuration,
|
|
66
78
|
or run `npx --yes @relvo/mcp --help` for server options.
|
package/bin/relvo-mcp.mjs
CHANGED
|
@@ -7,7 +7,7 @@ if (args.length === 1 && args[0] === "--version") {
|
|
|
7
7
|
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
8
8
|
console.log(manifest.version);
|
|
9
9
|
} else if (args.length === 1 && ["--help", "-h", "help"].includes(args[0])) {
|
|
10
|
-
console.log("Usage: relvo-mcp\nStart the Relvo MCP server over stdio.\nEnvironment: RELVO_API_URL, RELVO_API_KEY\nOptional: RELVO_MCP_MODE=full loads every operation as its own tool; the compact agent surface is the default.\nOptional: RELVO_MCP_TOOLS=web_search,serp_lookup also lists those catalog tools as tools of their own, to call without discovery (agent surface, at most 20).\nLocal uploads are disabled unless RELVO_MCP_ALLOWED_UPLOAD_ROOTS lists allowed absolute directories.\nExample: npx -y @relvo/mcp\nStandard output is reserved for MCP protocol messages.");
|
|
10
|
+
console.log("Usage: relvo-mcp\nStart the Relvo MCP server over stdio.\nEnvironment: RELVO_API_URL, RELVO_API_KEY\nOptional: RELVO_MCP_MODE=full loads every operation as its own tool; the compact agent surface is the default.\nOptional: RELVO_MCP_TOOLS=web_search,serp_lookup also lists those catalog tools as tools of their own, to call without discovery (agent surface, at most 20).\nLocal uploads are disabled unless RELVO_MCP_ALLOWED_UPLOAD_ROOTS lists allowed absolute directories.\nThe server reads the npm registry once per start and adds a one-time note to a tool result when a newer release exists. RELVO_AUTO_UPDATE=0 turns this off.\nExample: npx -y @relvo/mcp\nStandard output is reserved for MCP protocol messages.");
|
|
11
11
|
} else if (args.length && !(args.length === 2 && args[0] === "--profile")) {
|
|
12
12
|
console.error("Usage: relvo-mcp [--help | --version]");
|
|
13
13
|
process.exitCode = 2;
|
package/dist/client/agent-run.js
CHANGED
|
@@ -8,6 +8,12 @@ import { fitToolResultPage, toolResultSelectionSchema, TOOL_RESULT_PAGE_MAX_CHAR
|
|
|
8
8
|
import { platformFundingShortfallSchema } from "../schema/platformFunding.js";
|
|
9
9
|
import { AUTO_CAPABILITY_VARIANTS } from "../schema/platformCapabilityFamilies.js";
|
|
10
10
|
const keySchema = z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/);
|
|
11
|
+
export const RETAINED_READ_MS = 5 * 60 * 1000;
|
|
12
|
+
function lastFinishedExecution() {
|
|
13
|
+
let kept;
|
|
14
|
+
return { read: id => kept?.execution.id === id && Date.now() < kept.until ? kept.execution : undefined,
|
|
15
|
+
keep: execution => { kept = { until: Date.now() + RETAINED_READ_MS, execution }; } };
|
|
16
|
+
}
|
|
11
17
|
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
12
18
|
const recoveryReferencePattern = /^run:([a-f0-9]{64}):([A-Za-z0-9._:-]{8,128})$/;
|
|
13
19
|
const recoveryReferenceSchema = z.string().regex(recoveryReferencePattern);
|
|
@@ -351,16 +357,20 @@ export function agentResultView(value, raw, picks, rowsOnly = false) {
|
|
|
351
357
|
export function resultFilesFor(env, cwd = process.cwd()) {
|
|
352
358
|
return env.RELVO_RESULT_DIRECTORY ? { directory: env.RELVO_RESULT_DIRECTORY } : { directory: join(cwd, ".relvo", "results"), within: cwd };
|
|
353
359
|
}
|
|
360
|
+
const readableByOthers = (mode) => process.platform !== "win32" && (mode & 0o077) !== 0;
|
|
361
|
+
const failureText = (error) => error instanceof Error ? error.message : "The result could not be read.";
|
|
354
362
|
export class AgentRun {
|
|
355
363
|
client;
|
|
356
364
|
directory;
|
|
357
365
|
resultFiles;
|
|
358
366
|
topUp;
|
|
359
|
-
|
|
367
|
+
retained;
|
|
368
|
+
constructor(client, directory = join(tmpdir(), `relvo-runs-${process.getuid?.() ?? "user"}`), resultFiles, topUp, retained = lastFinishedExecution()) {
|
|
360
369
|
this.client = client;
|
|
361
370
|
this.directory = directory;
|
|
362
371
|
this.resultFiles = resultFiles;
|
|
363
372
|
this.topUp = topUp;
|
|
373
|
+
this.retained = retained;
|
|
364
374
|
}
|
|
365
375
|
async resultFolder() {
|
|
366
376
|
if (!this.resultFiles) {
|
|
@@ -436,12 +446,12 @@ export class AgentRun {
|
|
|
436
446
|
async folder() {
|
|
437
447
|
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
438
448
|
const root = await lstat(this.directory);
|
|
439
|
-
if (!root.isDirectory() || root.isSymbolicLink() || root.mode
|
|
449
|
+
if (!root.isDirectory() || root.isSymbolicLink() || readableByOthers(root.mode) || process.getuid && root.uid !== process.getuid())
|
|
440
450
|
throw new Error("The private run directory must belong to this user and allow owner access only.");
|
|
441
451
|
const path = join(this.directory, await this.client.requestJournalScope());
|
|
442
452
|
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
443
453
|
const scoped = await lstat(path);
|
|
444
|
-
if (!scoped.isDirectory() || scoped.isSymbolicLink() || scoped.mode
|
|
454
|
+
if (!scoped.isDirectory() || scoped.isSymbolicLink() || readableByOthers(scoped.mode) || process.getuid && scoped.uid !== process.getuid())
|
|
445
455
|
throw new Error("Invalid private run directory.");
|
|
446
456
|
return path;
|
|
447
457
|
}
|
|
@@ -449,7 +459,7 @@ export class AgentRun {
|
|
|
449
459
|
const path = join(await this.folder(), digestSchema.parse(runId) + ".json");
|
|
450
460
|
try {
|
|
451
461
|
const info = await lstat(path);
|
|
452
|
-
if (!info.isFile() || info.isSymbolicLink() || info.mode
|
|
462
|
+
if (!info.isFile() || info.isSymbolicLink() || readableByOthers(info.mode))
|
|
453
463
|
throw new Error("Invalid saved run file.");
|
|
454
464
|
return savedSchema.parse(JSON.parse(await readFile(path, "utf8")));
|
|
455
465
|
}
|
|
@@ -616,17 +626,37 @@ export class AgentRun {
|
|
|
616
626
|
async finish(runId, execution, quote, { waitSeconds, selection, pick, file }, toppedUp = null) {
|
|
617
627
|
let timedOut = false;
|
|
618
628
|
if (PENDING.includes(execution.status)) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
629
|
+
try {
|
|
630
|
+
const waited = await this.client.waitExecution(execution.id, waitSeconds * 1000);
|
|
631
|
+
timedOut = waited.timedOut;
|
|
632
|
+
execution = waited.data ?? execution;
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
timedOut = true;
|
|
636
|
+
}
|
|
622
637
|
}
|
|
623
638
|
const view = { ...this.view(runId, execution, quote, timedOut, selection, pick), ...(toppedUp ? { toppedUp } : {}) };
|
|
624
|
-
|
|
639
|
+
if (!file || execution.status !== "succeeded")
|
|
640
|
+
return view;
|
|
641
|
+
try {
|
|
642
|
+
return { ...view, file: await this.exportFile(execution.id, execution.output, selection?.path ?? [], selection?.fields) };
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
return { ...view, fileError: `${failureText(error)} The run succeeded and is retained: operation:result {id} reads it.` };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
readableResult(execution, selection, pick) {
|
|
649
|
+
try {
|
|
650
|
+
return { result: agentResultView(execution.output, selection, pick) };
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
return { resultError: `${failureText(error)} The run succeeded and is retained: operation:result {id} reads it with another selection.` };
|
|
654
|
+
}
|
|
625
655
|
}
|
|
626
656
|
view(runId, execution, quote, timedOut, selection, pick) {
|
|
627
657
|
return { runId, admitted: true, ...(quote?.plan ? { quote: { plan: quote.plan } } : {}), id: execution.id, status: execution.status, timedOut,
|
|
628
658
|
reservedMicros: execution.reservedMicros, chargedMicros: ["succeeded", "failed", "cancelled"].includes(execution.status) ? execution.chargedMicros : null,
|
|
629
|
-
errorCode: execution.errorCode, ...(execution.status === "succeeded" ?
|
|
659
|
+
errorCode: execution.errorCode, ...(execution.status === "succeeded" ? this.readableResult(execution, selection, pick) : {}),
|
|
630
660
|
...(PENDING.includes(execution.status) ? { next: "Still running and paid for: operation:result {id} waits for it again. Answer from it only after it succeeds or fails." } : {}),
|
|
631
661
|
retained: { readWith: "result", input: { id: execution.id } } };
|
|
632
662
|
}
|
|
@@ -665,12 +695,15 @@ export class AgentRun {
|
|
|
665
695
|
const { id, pick, file, waitSeconds, ...selection } = agentResultSchema.parse(raw);
|
|
666
696
|
if (file && pick)
|
|
667
697
|
throw new Error("file writes one selected path; use path and fields, not pick.");
|
|
668
|
-
|
|
698
|
+
const retained = this.retained.read(id);
|
|
699
|
+
let execution = retained ?? (await this.client.execution(id)).data, timedOut = false;
|
|
669
700
|
if (PENDING.includes(execution.status)) {
|
|
670
701
|
const waited = await this.client.waitExecution(id, waitSeconds * 1000);
|
|
671
702
|
timedOut = waited.timedOut;
|
|
672
703
|
execution = waited.data ?? execution;
|
|
673
704
|
}
|
|
705
|
+
if (execution.status === "succeeded" && !retained)
|
|
706
|
+
this.retained.keep(execution);
|
|
674
707
|
if (execution.status === "succeeded" && file)
|
|
675
708
|
return { id: execution.id, status: execution.status, file: await this.exportFile(execution.id, execution.output, selection.path, selection.fields) };
|
|
676
709
|
if (execution.status === "succeeded")
|
package/dist/client/browser.js
CHANGED
|
@@ -10,7 +10,7 @@ export { executionReceiptContext, executionPageContext } from "./execution-conte
|
|
|
10
10
|
export { UNTRUSTED_CONTENT_RULE, wrapUntrusted } from "../schema/untrustedContent.js";
|
|
11
11
|
export { fitToolResultPage, TOOL_RESULT_CONTEXT_MAX_BYTES, TOOL_RESULT_PAGE_MAX_ITEMS, toolResultSelectionSchema } from "../schema/toolResultContext.js";
|
|
12
12
|
export { withEvidentValues } from "../schema/toolInputRepair.js";
|
|
13
|
-
export { platformJsonObjectSchema } from "../schema/platformPublicExecution.js";
|
|
13
|
+
export { platformJsonObjectSchema, PLATFORM_TOOL_REQUEST_MAX_BYTES } from "../schema/platformPublicExecution.js";
|
|
14
14
|
import { toolDiscoveryInputSchema, toolDiscoveryResultSchema, TOOL_DISCOVERY_REQUEST_TIMEOUT_MS } from "../schema/toolDiscovery.js";
|
|
15
15
|
export { toolDiscoveryInputSchema } from "../schema/toolDiscovery.js";
|
|
16
16
|
export { PLATFORM_MCP_DIRECT_TOOLS_MAX, platformMcpDirectToolNames } from "../schema/platformMetaTools.js";
|
|
@@ -20,19 +20,19 @@ import { platformFundingShortfallSchema } from "../schema/platformFunding.js";
|
|
|
20
20
|
import { PLATFORM_META_ROUTES, PLATFORM_ROUTES, platformToolsPath as route } from "../schema/platformRoutes.js";
|
|
21
21
|
import { platformSkillsBundleSchema, platformSkillSchema } from "../schema/platformSkills.js";
|
|
22
22
|
export { platformSkillsBundleSchema, platformSkillSchema } from "../schema/platformSkills.js";
|
|
23
|
-
import { platformMonitorCreateSchema, platformMonitorSchema, platformMonitorEventsSchema, platformMonitorEventsQuerySchema, platformPopularMonitorSchema, platformMonitorableToolSchema, platformMonitorQuoteSchema, platformMonitorQuoteResultSchema, platformMonitorResumeSchema, PLATFORM_MONITOR_ID } from "../schema/platformMonitors.js";
|
|
24
|
-
export { platformMonitorCreateSchema, platformMonitorEventsQuerySchema, platformMonitorQuoteSchema, platformMonitorResumeSchema, PLATFORM_MONITOR_ID } from "../schema/platformMonitors.js";
|
|
23
|
+
import { platformMonitorCreateSchema, platformMonitorSchema, platformMonitorEventsSchema, platformMonitorEventsQuerySchema, platformPopularMonitorSchema, platformMonitorableToolSchema, platformMonitorQuoteSchema, platformMonitorQuoteResultSchema, platformMonitorResumeSchema, platformMonitorDestinationInputSchema, PLATFORM_MONITOR_ID } from "../schema/platformMonitors.js";
|
|
24
|
+
export { platformMonitorCreateSchema, platformMonitorEventsQuerySchema, platformMonitorQuoteSchema, platformMonitorResumeSchema, platformMonitorDestinationInputSchema, PLATFORM_MONITOR_ID } from "../schema/platformMonitors.js";
|
|
25
25
|
import { platformCustomToolCreateSchema, platformCustomToolNameSchema, platformCustomToolRestoreSchema, platformCustomToolSchema, platformCustomToolSummarySchema, platformCustomToolUpdateSchema, platformCustomToolVersionSchema, PLATFORM_CUSTOM_TOOL_PREFIX } from "../schema/platformCustomTools.js";
|
|
26
26
|
export { platformCustomEndpointDefinitionSchema, platformCustomToolCreateSchema, platformCustomToolDefinitionSchema, platformCustomToolNameSchema, platformCustomToolSlugSchema, platformCustomToolRestoreSchema, platformCustomToolUpdateSchema, PLATFORM_CUSTOM_TOOL_PREFIX } from "../schema/platformCustomTools.js";
|
|
27
|
-
import { platformSuggestionsSchema, platformTeamSkillCreateSchema, platformTeamSkillRestoreSchema, platformTeamSkillSchema, platformTeamSkillSummarySchema, platformTeamSkillUpdateSchema, platformTeamSkillVersionSchema, PLATFORM_TEAM_SKILL_SLUG } from "../schema/platformTeamSkills.js";
|
|
28
|
-
export { platformTeamSkillCreateSchema, platformTeamSkillDefinitionSchema, platformTeamSkillRestoreSchema, platformTeamSkillUpdateSchema, PLATFORM_TEAM_SKILL_SLUG } from "../schema/platformTeamSkills.js";
|
|
27
|
+
import { platformSuggestionsSchema, platformTeamSkillCreateSchema, platformTeamSkillRestoreSchema, platformTeamSkillSchema, platformTeamSkillSummarySchema, platformTeamSkillUpdateSchema, platformTeamSkillVersionSchema, PLATFORM_TEAM_SKILL_SLUG, PLATFORM_TEAM_SKILL_LIMITS } from "../schema/platformTeamSkills.js";
|
|
28
|
+
export { platformTeamSkillCreateSchema, platformTeamSkillDefinitionSchema, platformTeamSkillRestoreSchema, platformTeamSkillUpdateSchema, PLATFORM_TEAM_SKILL_SLUG, PLATFORM_TEAM_SKILL_LIMITS, platformSkillPathSchema, renderSkillMarkdown, skillFileBytes } from "../schema/platformTeamSkills.js";
|
|
29
29
|
import { platformWorkflowCreateSchema, platformWorkflowRunCreateSchema, platformWorkflowRunIdSchema, platformWorkflowRunListQuerySchema, platformWorkflowRunListSchema, platformWorkflowRunSchema, platformWorkflowSchema, platformWorkflowSummarySchema, platformWorkflowUpdateSchema, platformWorkflowVersionSchema, PLATFORM_WORKFLOW_SLUG } from "../schema/platformWorkflows.js";
|
|
30
30
|
export { platformWorkflowCreateSchema, platformWorkflowDefinitionSchema, platformWorkflowRunCreateSchema, platformWorkflowRunIdSchema, platformWorkflowRunListQuerySchema, platformWorkflowSlugSchema, platformWorkflowStepSchema, platformWorkflowUpdateSchema, PLATFORM_WORKFLOW_SLUG } from "../schema/platformWorkflows.js";
|
|
31
31
|
import { platformTriggerCreateSchema, platformTriggerDestinationInputSchema, platformTriggerEventsQuerySchema, platformTriggerEventsSchema, platformTriggerIdSchema, platformTriggerSchema, platformTriggerTypesQuerySchema, platformTriggerTypesSchema, platformTriggerUpdateSchema } from "../schema/platformTriggers.js";
|
|
32
32
|
export { platformTriggerCreateSchema, platformTriggerDestinationInputSchema, platformTriggerEventsQuerySchema, platformTriggerIdSchema, platformTriggerIntervalSchema, platformTriggerTypesQuerySchema, platformTriggerUpdateSchema } from "../schema/platformTriggers.js";
|
|
33
33
|
import { platformMemberAccessUpdateSchema, platformTeamAccessSchema } from "../schema/platformToolAccess.js";
|
|
34
34
|
export { platformMemberAccessUpdateSchema } from "../schema/platformToolAccess.js";
|
|
35
|
-
import { platformInputFileSchema, platformInputUploadSchema, PLATFORM_UPLOAD_MAX_BYTES } from "../schema/platformPublicExecution.js";
|
|
35
|
+
import { platformInputFileSchema, platformInputUploadSchema, PLATFORM_UPLOAD_MAX_BYTES, PLATFORM_TOOL_REQUEST_MAX_BYTES } from "../schema/platformPublicExecution.js";
|
|
36
36
|
export { platformInputFileSchema, platformInputMimeSchema, platformInputFileMimeSchema, PLATFORM_INPUT_MAX_BYTES } from "../schema/platformPublicExecution.js";
|
|
37
37
|
import { PLATFORM_EXECUTION_MAX_WAIT_SECONDS, PLATFORM_EXECUTION_RESPONSE_MAX_BYTES, platformCatalogSchema, platformCatalogPageQuerySchema, platformCatalogPageSchema, platformQuoteSchema, platformExecutionSchema, platformExecutionContextSchema, platformExecutionResultPageSchema, platformExecutionIdSchema, platformCreateExecutionSchema, platformRecoverExecutionSchema, platformQuoteRequestSchema, platformInvalidArgumentFieldsSchema, platformToolNameSchema, platformArtifactIdSchema, platformArtifactDownloadSchema, platformExecutionListQuerySchema, platformExecutionListSchema } from "../schema/platformPublicExecution.js";
|
|
38
38
|
import { PLATFORM_EXECUTION_CONTEXT_BUDGET_ERROR, PlatformExecutionContextBudgetError, platformExecutionResultPageContext } from "../schema/platformPublicExecution.js";
|
|
@@ -83,7 +83,7 @@ function triggerId(id) {
|
|
|
83
83
|
throw new Error("Provide a trigger ID such as trg_….");
|
|
84
84
|
return id;
|
|
85
85
|
}
|
|
86
|
-
const
|
|
86
|
+
export const platformIdempotencyKeySchema = z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/);
|
|
87
87
|
function normalizeToolSearch(value) { return value.toLowerCase().replace(/[\s_-]+/g, ""); }
|
|
88
88
|
const publicErrors = {
|
|
89
89
|
platform_quote_changed: "The priceVersion is stale or the maximum charge is below the current quote. This request was not admitted. Request a new quote for the same arguments/provider, copy its priceVersion and use an authorized maximum covering it. Keep the same idempotency key.",
|
|
@@ -283,8 +283,9 @@ export class PlatformClient {
|
|
|
283
283
|
async request(path, schema, options) {
|
|
284
284
|
const body = options?.body === undefined ? undefined : JSON.stringify(options.body);
|
|
285
285
|
const upload = path === PLATFORM_ROUTES.upload, meta = PLATFORM_META_ROUTES.includes(path);
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
const maximumRequestBytes = options?.maximumRequestBytes ?? (upload ? PLATFORM_UPLOAD_MAX_BYTES : meta ? PLATFORM_META_LIMITS.requestBytes : options?.toolInput ? PLATFORM_TOOL_REQUEST_MAX_BYTES : 65536);
|
|
287
|
+
if (body && new TextEncoder().encode(body).byteLength > maximumRequestBytes)
|
|
288
|
+
throw new Error(options?.maximumRequestBytes ? "The skill package exceeds 20 MiB." : upload ? "The upload exceeds 12 MiB." : meta ? "The meta tool request exceeds 1 MiB." : options?.toolInput ? "The tool request exceeds 1 MiB." : "The tool request exceeds 64 KiB.");
|
|
288
289
|
let response;
|
|
289
290
|
const timeoutMs = options?.timeoutMs ?? 20000;
|
|
290
291
|
const timeoutMessage = `The ${this.productName} API did not answer within ${Math.round(timeoutMs / 1000)} s. If submitting an execution, retry with the same body and idempotency key.`;
|
|
@@ -379,7 +380,7 @@ export class PlatformClient {
|
|
|
379
380
|
}
|
|
380
381
|
searchTools(options = {}) {
|
|
381
382
|
const credential = this.config.apiKey;
|
|
382
|
-
return this.request(PLATFORM_ROUTES.discover, z.object({ data: toolDiscoveryResultSchema }), { method: "POST", body: toolDiscoveryInputSchema.parse(options), timeoutMs: TOOL_DISCOVERY_REQUEST_TIMEOUT_MS })
|
|
383
|
+
return this.request(PLATFORM_ROUTES.discover, z.object({ data: toolDiscoveryResultSchema }), { method: "POST", body: toolDiscoveryInputSchema.parse(options), timeoutMs: TOOL_DISCOVERY_REQUEST_TIMEOUT_MS, toolInput: true })
|
|
383
384
|
.then(result => { this.toolNameHints.remember(result.data.tools.map(tool => tool.name), credential); return result; });
|
|
384
385
|
}
|
|
385
386
|
searchMetaTools(input) {
|
|
@@ -471,6 +472,18 @@ export class PlatformClient {
|
|
|
471
472
|
query.set("connectionId", parsed.connectionId);
|
|
472
473
|
return this.request(`${route(PLATFORM_ROUTES.triggerTypes)}?${query}`, z.object({ data: platformTriggerTypesSchema }));
|
|
473
474
|
}
|
|
475
|
+
addMonitorDestination(id, destination) {
|
|
476
|
+
if (!PLATFORM_MONITOR_ID.test(id))
|
|
477
|
+
throw new Error("Provide a valid monitor ID.");
|
|
478
|
+
return this.request(route(PLATFORM_ROUTES.monitorDestinations, { id }), z.object({ data: z.object({ monitor: platformMonitorSchema }) }), { method: "POST", body: platformMonitorDestinationInputSchema.parse(destination) });
|
|
479
|
+
}
|
|
480
|
+
removeMonitorDestination(id, destinationId) {
|
|
481
|
+
if (!PLATFORM_MONITOR_ID.test(id))
|
|
482
|
+
throw new Error("Provide a valid monitor ID.");
|
|
483
|
+
if (!/^dst_[A-Za-z0-9_-]{1,100}$/.test(destinationId))
|
|
484
|
+
throw new Error("Provide a destination ID such as dst_….");
|
|
485
|
+
return this.request(route(PLATFORM_ROUTES.monitorDestinationDelete, { id, destinationId }), z.object({ data: z.object({ monitor: platformMonitorSchema }) }), { method: "POST", body: {} });
|
|
486
|
+
}
|
|
474
487
|
addTriggerDestination(id, destination) {
|
|
475
488
|
return this.request(route(PLATFORM_ROUTES.triggerDestinations, { id: triggerId(id) }), triggerResult, { method: "POST", body: platformTriggerDestinationInputSchema.parse(destination) });
|
|
476
489
|
}
|
|
@@ -483,10 +496,10 @@ export class PlatformClient {
|
|
|
483
496
|
customTools() { return this.request(route(PLATFORM_ROUTES.customTools), z.object({ data: z.object({ tools: z.array(platformCustomToolSummarySchema) }) })); }
|
|
484
497
|
customTool(name) { return this.request(route(PLATFORM_ROUTES.customTool, { name: customToolName(name) }), customToolResult); }
|
|
485
498
|
createCustomTool(input) {
|
|
486
|
-
return this.request(route(PLATFORM_ROUTES.customTools), customToolResult, { method: "POST", body: platformCustomToolCreateSchema.parse(input) });
|
|
499
|
+
return this.request(route(PLATFORM_ROUTES.customTools), customToolResult, { method: "POST", body: platformCustomToolCreateSchema.parse(input), toolInput: true });
|
|
487
500
|
}
|
|
488
501
|
updateCustomTool(name, input) {
|
|
489
|
-
return this.request(route(PLATFORM_ROUTES.customToolUpdate, { name: customToolName(name) }), customToolResult, { method: "POST", body: platformCustomToolUpdateSchema.parse(input) });
|
|
502
|
+
return this.request(route(PLATFORM_ROUTES.customToolUpdate, { name: customToolName(name) }), customToolResult, { method: "POST", body: platformCustomToolUpdateSchema.parse(input), toolInput: true });
|
|
490
503
|
}
|
|
491
504
|
deleteCustomTool(name) {
|
|
492
505
|
return this.request(route(PLATFORM_ROUTES.customToolDelete, { name: customToolName(name) }), z.object({ data: z.object({ success: z.literal(true) }) }), { method: "POST", body: {} });
|
|
@@ -498,12 +511,12 @@ export class PlatformClient {
|
|
|
498
511
|
return this.request(route(PLATFORM_ROUTES.customToolRestore, { name: customToolName(name) }), customToolResult, { method: "POST", body: platformCustomToolRestoreSchema.parse(input) });
|
|
499
512
|
}
|
|
500
513
|
teamSkills() { return this.request(route(PLATFORM_ROUTES.teamSkills), z.object({ data: z.object({ skills: z.array(platformTeamSkillSummarySchema) }) })); }
|
|
501
|
-
teamSkill(slug) { return this.request(route(PLATFORM_ROUTES.teamSkill, { slug: teamSkillSlug(slug) }), teamSkillResult); }
|
|
514
|
+
teamSkill(slug) { return this.request(route(PLATFORM_ROUTES.teamSkill, { slug: teamSkillSlug(slug) }), teamSkillResult, { maximumResponseBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes }); }
|
|
502
515
|
createTeamSkill(input) {
|
|
503
|
-
return this.request(route(PLATFORM_ROUTES.teamSkills), teamSkillResult, { method: "POST", body: platformTeamSkillCreateSchema.parse(input) });
|
|
516
|
+
return this.request(route(PLATFORM_ROUTES.teamSkills), teamSkillResult, { method: "POST", body: platformTeamSkillCreateSchema.parse(input), maximumRequestBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes, maximumResponseBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes });
|
|
504
517
|
}
|
|
505
518
|
updateTeamSkill(slug, input) {
|
|
506
|
-
return this.request(route(PLATFORM_ROUTES.teamSkillUpdate, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillUpdateSchema.parse(input) });
|
|
519
|
+
return this.request(route(PLATFORM_ROUTES.teamSkillUpdate, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillUpdateSchema.parse(input), maximumRequestBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes, maximumResponseBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes });
|
|
507
520
|
}
|
|
508
521
|
deleteTeamSkill(slug) {
|
|
509
522
|
return this.request(route(PLATFORM_ROUTES.teamSkillDelete, { slug: teamSkillSlug(slug) }), z.object({ data: z.object({ success: z.literal(true) }) }), { method: "POST", body: {} });
|
|
@@ -512,7 +525,7 @@ export class PlatformClient {
|
|
|
512
525
|
return this.request(route(PLATFORM_ROUTES.teamSkillVersions, { slug: teamSkillSlug(slug) }), z.object({ data: z.object({ versions: z.array(platformTeamSkillVersionSchema) }) }));
|
|
513
526
|
}
|
|
514
527
|
restoreTeamSkill(slug, input) {
|
|
515
|
-
return this.request(route(PLATFORM_ROUTES.teamSkillRestore, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillRestoreSchema.parse(input) });
|
|
528
|
+
return this.request(route(PLATFORM_ROUTES.teamSkillRestore, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillRestoreSchema.parse(input), maximumResponseBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes });
|
|
516
529
|
}
|
|
517
530
|
suggestions() { return this.request(route(PLATFORM_ROUTES.suggestions), z.object({ data: platformSuggestionsSchema })); }
|
|
518
531
|
access() { return this.request(route(PLATFORM_ROUTES.access), z.object({ data: platformTeamAccessSchema })); }
|
|
@@ -536,7 +549,7 @@ export class PlatformClient {
|
|
|
536
549
|
return this.request(route(PLATFORM_ROUTES.workflowVersions, { slug: workflowSlug(slug) }), z.object({ data: z.object({ versions: z.array(platformWorkflowVersionSchema) }) }));
|
|
537
550
|
}
|
|
538
551
|
runWorkflow(slug, input, idempotencyKey) {
|
|
539
|
-
if (!
|
|
552
|
+
if (!platformIdempotencyKeySchema.safeParse(idempotencyKey).success)
|
|
540
553
|
throw new Error("Keep an idempotency key of 8–128 letters, digits, periods, underscores, colons or hyphens for this run.");
|
|
541
554
|
return this.request(route(PLATFORM_ROUTES.workflowRuns, { slug: workflowSlug(slug) }), workflowRunResult, { method: "POST", body: platformWorkflowRunCreateSchema.parse(input), idempotencyKey });
|
|
542
555
|
}
|
|
@@ -553,7 +566,7 @@ export class PlatformClient {
|
|
|
553
566
|
const parsed = platformQuoteRequestSchema.safeParse(input);
|
|
554
567
|
if (!parsed.success)
|
|
555
568
|
throw new Error("Provide a tool name, a JSON object of arguments and an optional valid provider identifier.");
|
|
556
|
-
return this.request(route(PLATFORM_ROUTES.quote), quoteResultSchema, { method: "POST", body: parsed.data }).then(value => {
|
|
569
|
+
return this.request(route(PLATFORM_ROUTES.quote), quoteResultSchema, { method: "POST", body: parsed.data, toolInput: true }).then(value => {
|
|
557
570
|
const receivedAt = Date.now(), expiresAt = Date.parse(value.data.expiresAt);
|
|
558
571
|
return { ...value, timing: { receivedAtUtc: new Date(receivedAt).toISOString(),
|
|
559
572
|
expiresInSeconds: Number.isFinite(expiresAt) ? Math.max(0, Math.floor((expiresAt - receivedAt) / 1000)) : null } };
|
|
@@ -563,9 +576,9 @@ export class PlatformClient {
|
|
|
563
576
|
const parsed = platformCreateExecutionSchema.safeParse(input);
|
|
564
577
|
if (!parsed.success)
|
|
565
578
|
throw new Error("Provide a tool, arguments, the quote's priceVersion and maximumChargeMicros, and an optional valid provider identifier.");
|
|
566
|
-
if (!
|
|
579
|
+
if (!platformIdempotencyKeySchema.safeParse(idempotencyKey).success)
|
|
567
580
|
throw new Error("Keep an idempotency key of 8–128 letters, digits, periods, underscores, colons or hyphens for this execution.");
|
|
568
|
-
return this.request(route(PLATFORM_ROUTES.tool, { tool: parsed.data.tool }), executionResultSchema, { method: "POST", body: parsed.data, idempotencyKey });
|
|
581
|
+
return this.request(route(PLATFORM_ROUTES.tool, { tool: parsed.data.tool }), executionResultSchema, { method: "POST", body: parsed.data, idempotencyKey, toolInput: true });
|
|
569
582
|
}
|
|
570
583
|
recoverExecution(input) {
|
|
571
584
|
return this.request(route(PLATFORM_ROUTES.executionRecovery), z.object({ data: z.object({ execution: platformExecutionSchema.nullable() }) }), { method: "POST", body: platformRecoverExecutionSchema.parse(input) });
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { open } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { extname } from "node:path";
|
|
3
|
+
import { basename, extname } from "node:path";
|
|
4
4
|
import { platformInputMimeSchema, platformInputFileMimeSchema, PLATFORM_INPUT_MAX_BYTES } from "./index.js";
|
|
5
|
+
const sensitiveDirectories = new Set([
|
|
6
|
+
".ssh", ".aws", ".gcloud", ".kube", ".config", ".git", ".docker", ".gnupg", ".azure", ".password-store",
|
|
7
|
+
".claude", ".agents", ".codex", ".cursor", "_sessions",
|
|
8
|
+
]);
|
|
9
|
+
const sensitiveNames = new Set([
|
|
10
|
+
".env", ".npmrc", ".netrc", ".git-credentials", ".pgpass", ".pypirc",
|
|
11
|
+
"credentials.json", "application_default_credentials.json", "id_rsa", "id_ed25519", "logins.json", "secrets.json",
|
|
12
|
+
]);
|
|
13
|
+
const sensitiveExtensions = new Set([".key", ".pem", ".p12", ".pfx", ".kdbx", ".db"]);
|
|
14
|
+
export function isSensitiveLocalPath(path) {
|
|
15
|
+
const name = basename(path).toLowerCase();
|
|
16
|
+
return path.split(/[\\/]/).some((part) => sensitiveDirectories.has(part.toLowerCase()))
|
|
17
|
+
|| sensitiveNames.has(name) || name.startsWith(".env") || sensitiveExtensions.has(extname(name));
|
|
18
|
+
}
|
|
5
19
|
const extensions = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".mp4": "video/mp4", ".mov": "video/quicktime", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp",
|
|
6
20
|
".gif": "image/gif", ".bmp": "image/bmp", ".pdf": "application/pdf", ".csv": "text/csv", ".tsv": "text/tab-separated-values", ".txt": "text/plain", ".json": "application/json", ".xml": "application/xml",
|
|
7
21
|
".zip": "application/zip", ".gz": "application/gzip", ".glb": "model/gltf-binary", ".gltf": "model/gltf+json", ".srt": "application/x-subrip", ".vtt": "text/vtt", ".bin": "application/octet-stream" };
|
|
@@ -49,27 +49,28 @@ export async function profileDirectory(path = localProfilePath(), create = false
|
|
|
49
49
|
await safeDirectory(dirname(path), create, true);
|
|
50
50
|
return dirname(path);
|
|
51
51
|
}
|
|
52
|
-
export async function
|
|
52
|
+
export async function readPrivateBytes(path, maximumBytes = 65536) {
|
|
53
53
|
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
54
54
|
try {
|
|
55
55
|
const info = await handle.stat();
|
|
56
56
|
owned(info.mode, info.uid, true);
|
|
57
57
|
if (!info.isFile() || info.size > maximumBytes || info.nlink !== 1)
|
|
58
58
|
throw new Error("Relvo private file is invalid.");
|
|
59
|
-
return await handle.readFile(
|
|
59
|
+
return await handle.readFile();
|
|
60
60
|
}
|
|
61
61
|
finally {
|
|
62
62
|
await handle.close();
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
export async function readPrivateText(path, maximumBytes = 65536) { return (await readPrivateBytes(path, maximumBytes)).toString("utf8"); }
|
|
65
66
|
export async function readPrivateJson(path, maximumBytes = 65536) { return JSON.parse(await readPrivateText(path, maximumBytes)); }
|
|
66
|
-
export async function
|
|
67
|
+
export async function writePrivateBytes(path, value, privateParent = true, maximumBytes = 65536) {
|
|
67
68
|
const directory = dirname(path);
|
|
68
69
|
await safeDirectory(directory, false, privateParent);
|
|
69
|
-
if (
|
|
70
|
+
if (value.byteLength > maximumBytes)
|
|
70
71
|
throw new Error("Relvo private file exceeds its size limit.");
|
|
71
72
|
try {
|
|
72
|
-
await
|
|
73
|
+
await readPrivateBytes(path, maximumBytes);
|
|
73
74
|
}
|
|
74
75
|
catch (error) {
|
|
75
76
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
@@ -101,18 +102,44 @@ export async function writePrivateText(path, value, privateParent = true, maximu
|
|
|
101
102
|
await handle.close();
|
|
102
103
|
}
|
|
103
104
|
}
|
|
105
|
+
export async function writePrivateText(path, value, privateParent = true, maximumBytes = 65536) { await writePrivateBytes(path, Buffer.from(value, "utf8"), privateParent, maximumBytes); }
|
|
104
106
|
export async function writePrivateJson(path, value, privateParent = true, maximumBytes = 65536) { await writePrivateText(path, JSON.stringify(value) + "\n", privateParent, maximumBytes); }
|
|
107
|
+
export class LocalProfileError extends Error {
|
|
108
|
+
reason;
|
|
109
|
+
name = "LocalProfileError";
|
|
110
|
+
constructor(reason, message) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.reason = reason;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const windowsLocalSetupRefusal = "Relvo's saved login and automatic setup need macOS or Linux. On Windows, connect your agent to the MCP URL shown under Getting started in the Relvo console.";
|
|
105
116
|
export async function readLocalProfile(path = localProfilePath()) {
|
|
106
117
|
try {
|
|
107
118
|
await profileDirectory(path);
|
|
108
|
-
|
|
119
|
+
const raw = await readPrivateJson(path), parsed = localProfileSchema.safeParse(raw);
|
|
120
|
+
if (parsed.success)
|
|
121
|
+
return parsed.data;
|
|
122
|
+
if (raw === null || typeof raw !== "object" || !("managedBy" in raw) || raw.managedBy !== "relvo-cli")
|
|
123
|
+
throw new Error("Foreign profile");
|
|
124
|
+
throw new SyntaxError("Invalid profile");
|
|
109
125
|
}
|
|
110
126
|
catch (error) {
|
|
111
127
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
112
128
|
return null;
|
|
113
|
-
|
|
129
|
+
if (process.platform === "win32")
|
|
130
|
+
throw new LocalProfileError("unsupported", windowsLocalSetupRefusal);
|
|
131
|
+
if (error instanceof SyntaxError)
|
|
132
|
+
throw new LocalProfileError("unreadable", `Relvo's saved login at ${path} is unreadable. Run relvo logout to remove it, then relvo setup. No credentials were changed.`);
|
|
133
|
+
throw new LocalProfileError("unsafe", `Relvo's saved login at ${path} is unsafe to use: it or a folder above it is a link, belongs to another user, is writable by others, or is not Relvo's file. Keep your home and .config folders writable only by you, then run chmod 700 ${dirname(path)} and chmod 600 ${path}. No credentials were changed.`);
|
|
114
134
|
}
|
|
115
135
|
}
|
|
136
|
+
export async function removeUnreadableProfile(path) {
|
|
137
|
+
await profileDirectory(path);
|
|
138
|
+
const info = await lstat(path);
|
|
139
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
140
|
+
throw new LocalProfileError("unsafe", `Relvo's saved login at ${path} is not a regular file, so it was left in place.`);
|
|
141
|
+
await unlink(path);
|
|
142
|
+
}
|
|
116
143
|
export async function writeLocalProfile(path, profile) {
|
|
117
144
|
await profileDirectory(path, true);
|
|
118
145
|
await readLocalProfile(path);
|
|
@@ -147,7 +174,7 @@ export async function runtimeConfig(env, source, profile, requireProfileMatch =
|
|
|
147
174
|
return environment;
|
|
148
175
|
const saved = await readLocalProfile(profile);
|
|
149
176
|
if (!saved || saved.state !== "ready" || saved.pendingAck || Date.parse(saved.grant.expiresAt) <= Date.now())
|
|
150
|
-
throw new
|
|
177
|
+
throw new LocalProfileError("missing", "Relvo login is missing, incomplete or expired. Run npx --yes @relvo/cli setup to resume.");
|
|
151
178
|
if (environment && (environment.apiUrl.replace(/\/$/, "") !== saved.apiUrl || environment.apiKey !== saved.grant.apiKey || Boolean(environment.local) !== saved.local))
|
|
152
179
|
throw new Error("Ambient Relvo credentials conflict with the explicitly selected managed profile. No project was selected.");
|
|
153
180
|
return { apiUrl: saved.apiUrl, apiKey: saved.grant.apiKey, local: saved.local, source };
|