@relvo/mcp 0.1.4 → 0.1.6
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 +14 -1
- package/bin/relvo-mcp.mjs +1 -1
- package/dist/client/agent-run.js +43 -10
- package/dist/client/browser.js +38 -21
- 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 +43 -21
- 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 +25 -4
- package/dist/schema/platformCustomTools.js +27 -7
- package/dist/schema/platformHttpConstraints.js +4 -0
- package/dist/schema/platformLocalSetup.js +1 -1
- package/dist/schema/platformMetaTools.js +2 -2
- package/dist/schema/platformPublicExecution.js +3 -1
- package/dist/schema/platformTeamSkills.js +250 -7
- 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
|
@@ -37,18 +37,27 @@ This option does not require a local npm package.
|
|
|
37
37
|
## First check
|
|
38
38
|
|
|
39
39
|
Ask your agent to check the Relvo connection without running a paid tool.
|
|
40
|
-
By default, the server exposes
|
|
40
|
+
By default, the server exposes four tools:
|
|
41
41
|
|
|
42
42
|
| Tool | Purpose |
|
|
43
43
|
| --- | --- |
|
|
44
44
|
| `platform_tools` | Find capabilities and their input schemas. |
|
|
45
45
|
| `platform_run` | Quote and run approved work, then wait for the result. |
|
|
46
46
|
| `platform` | Inspect schemas, get quotes, read results, or resume a run. |
|
|
47
|
+
| `platform_team_write` | Create or change what the whole team shares: skills, custom tools, workflows, triggers and monitors. |
|
|
47
48
|
|
|
48
49
|
For a price-only check, use `platform` to call `platform_quote` before any run.
|
|
49
50
|
Schemas load on demand. Set `RELVO_MCP_MODE=full` only if your host needs every
|
|
50
51
|
original operation listed as a separate tool.
|
|
51
52
|
|
|
53
|
+
## Team skill references
|
|
54
|
+
|
|
55
|
+
Use `platform` with operation `team_skills` to list your team's procedures. Operation `team_skill` reads one by `slug` and lists
|
|
56
|
+
its files. Add a relative `path` to read `SKILL.md`, a reference document, an image or a script. Images return MCP image content;
|
|
57
|
+
long text and binary files return pages with `nextOffset`. The same access permissions apply to every file.
|
|
58
|
+
|
|
59
|
+
For local folder installation, use **Use in your environment** in [Customize → Skills](https://app.relvo.si/customization).
|
|
60
|
+
|
|
52
61
|
## Costs and retries
|
|
53
62
|
|
|
54
63
|
Quotes are free. Searches count toward your team's monthly free calls and are
|
|
@@ -62,5 +71,9 @@ and key or resume its `runId`. Read stored results instead of purchasing them ag
|
|
|
62
71
|
Local file uploads are disabled by default. Enable only the directories you need
|
|
63
72
|
with `RELVO_MCP_ALLOWED_UPLOAD_ROOTS`, and upload only user-authorized files.
|
|
64
73
|
|
|
74
|
+
The server reads the npm registry once per start. When a newer release has been
|
|
75
|
+
public for 72 hours, a one-time note in a tool result says how to update; nothing
|
|
76
|
+
is installed. `RELVO_AUTO_UPDATE=0` turns this off.
|
|
77
|
+
|
|
65
78
|
See [Relvo docs](https://app.relvo.si/docs) for workflows and configuration,
|
|
66
79
|
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, platformTeamSkillListKindSchema, 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, teamSkillKindNeedsAdmin } 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.",
|
|
@@ -160,6 +160,7 @@ const publicErrors = {
|
|
|
160
160
|
platform_team_skill_not_editable: "This member may change only the team skills they wrote. Create a new skill instead, or ask its creator or a team admin.",
|
|
161
161
|
platform_team_skill_limit_reached: "This project has reached its team skill limit. Merge or delete skills first.",
|
|
162
162
|
platform_team_skill_version_not_found: "That version of the team skill is not kept. List its versions first.",
|
|
163
|
+
platform_team_item_admin_only: "This acts on every teammate's machine that installs it (a hook, MCP server or permission, or a skill or agent whose frontmatter holds hooks, allowed-tools, mcpServers or permissionMode): an organization owner or admin changes it in the Relvo console, never with an API key.",
|
|
163
164
|
platform_workflow_not_found: "No workflow with that slug exists in this project. List workflows first.",
|
|
164
165
|
platform_workflow_exists: "This project already has a workflow with that slug. Read it and update it instead, or choose another slug.",
|
|
165
166
|
platform_workflow_invalid: "The workflow definition is invalid. Fix what details.fields or details.reason names: every tool must be in the project's catalog, and a reference names a declared input or an earlier step.",
|
|
@@ -283,8 +284,9 @@ export class PlatformClient {
|
|
|
283
284
|
async request(path, schema, options) {
|
|
284
285
|
const body = options?.body === undefined ? undefined : JSON.stringify(options.body);
|
|
285
286
|
const upload = path === PLATFORM_ROUTES.upload, meta = PLATFORM_META_ROUTES.includes(path);
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
const maximumRequestBytes = options?.maximumRequestBytes ?? (upload ? PLATFORM_UPLOAD_MAX_BYTES : meta ? PLATFORM_META_LIMITS.requestBytes : options?.toolInput ? PLATFORM_TOOL_REQUEST_MAX_BYTES : 65536);
|
|
288
|
+
if (body && new TextEncoder().encode(body).byteLength > maximumRequestBytes)
|
|
289
|
+
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
290
|
let response;
|
|
289
291
|
const timeoutMs = options?.timeoutMs ?? 20000;
|
|
290
292
|
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 +381,7 @@ export class PlatformClient {
|
|
|
379
381
|
}
|
|
380
382
|
searchTools(options = {}) {
|
|
381
383
|
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 })
|
|
384
|
+
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
385
|
.then(result => { this.toolNameHints.remember(result.data.tools.map(tool => tool.name), credential); return result; });
|
|
384
386
|
}
|
|
385
387
|
searchMetaTools(input) {
|
|
@@ -471,6 +473,18 @@ export class PlatformClient {
|
|
|
471
473
|
query.set("connectionId", parsed.connectionId);
|
|
472
474
|
return this.request(`${route(PLATFORM_ROUTES.triggerTypes)}?${query}`, z.object({ data: platformTriggerTypesSchema }));
|
|
473
475
|
}
|
|
476
|
+
addMonitorDestination(id, destination) {
|
|
477
|
+
if (!PLATFORM_MONITOR_ID.test(id))
|
|
478
|
+
throw new Error("Provide a valid monitor ID.");
|
|
479
|
+
return this.request(route(PLATFORM_ROUTES.monitorDestinations, { id }), z.object({ data: z.object({ monitor: platformMonitorSchema }) }), { method: "POST", body: platformMonitorDestinationInputSchema.parse(destination) });
|
|
480
|
+
}
|
|
481
|
+
removeMonitorDestination(id, destinationId) {
|
|
482
|
+
if (!PLATFORM_MONITOR_ID.test(id))
|
|
483
|
+
throw new Error("Provide a valid monitor ID.");
|
|
484
|
+
if (!/^dst_[A-Za-z0-9_-]{1,100}$/.test(destinationId))
|
|
485
|
+
throw new Error("Provide a destination ID such as dst_….");
|
|
486
|
+
return this.request(route(PLATFORM_ROUTES.monitorDestinationDelete, { id, destinationId }), z.object({ data: z.object({ monitor: platformMonitorSchema }) }), { method: "POST", body: {} });
|
|
487
|
+
}
|
|
474
488
|
addTriggerDestination(id, destination) {
|
|
475
489
|
return this.request(route(PLATFORM_ROUTES.triggerDestinations, { id: triggerId(id) }), triggerResult, { method: "POST", body: platformTriggerDestinationInputSchema.parse(destination) });
|
|
476
490
|
}
|
|
@@ -483,10 +497,10 @@ export class PlatformClient {
|
|
|
483
497
|
customTools() { return this.request(route(PLATFORM_ROUTES.customTools), z.object({ data: z.object({ tools: z.array(platformCustomToolSummarySchema) }) })); }
|
|
484
498
|
customTool(name) { return this.request(route(PLATFORM_ROUTES.customTool, { name: customToolName(name) }), customToolResult); }
|
|
485
499
|
createCustomTool(input) {
|
|
486
|
-
return this.request(route(PLATFORM_ROUTES.customTools), customToolResult, { method: "POST", body: platformCustomToolCreateSchema.parse(input) });
|
|
500
|
+
return this.request(route(PLATFORM_ROUTES.customTools), customToolResult, { method: "POST", body: platformCustomToolCreateSchema.parse(input), toolInput: true });
|
|
487
501
|
}
|
|
488
502
|
updateCustomTool(name, input) {
|
|
489
|
-
return this.request(route(PLATFORM_ROUTES.customToolUpdate, { name: customToolName(name) }), customToolResult, { method: "POST", body: platformCustomToolUpdateSchema.parse(input) });
|
|
503
|
+
return this.request(route(PLATFORM_ROUTES.customToolUpdate, { name: customToolName(name) }), customToolResult, { method: "POST", body: platformCustomToolUpdateSchema.parse(input), toolInput: true });
|
|
490
504
|
}
|
|
491
505
|
deleteCustomTool(name) {
|
|
492
506
|
return this.request(route(PLATFORM_ROUTES.customToolDelete, { name: customToolName(name) }), z.object({ data: z.object({ success: z.literal(true) }) }), { method: "POST", body: {} });
|
|
@@ -497,13 +511,16 @@ export class PlatformClient {
|
|
|
497
511
|
restoreCustomTool(name, input) {
|
|
498
512
|
return this.request(route(PLATFORM_ROUTES.customToolRestore, { name: customToolName(name) }), customToolResult, { method: "POST", body: platformCustomToolRestoreSchema.parse(input) });
|
|
499
513
|
}
|
|
500
|
-
teamSkills() {
|
|
501
|
-
|
|
514
|
+
teamSkills(kind) {
|
|
515
|
+
const query = kind ? `?${new URLSearchParams({ kind: platformTeamSkillListKindSchema.parse(kind) })}` : "";
|
|
516
|
+
return this.request(`${route(PLATFORM_ROUTES.teamSkills)}${query}`, z.object({ data: z.object({ skills: z.array(platformTeamSkillSummarySchema) }) }));
|
|
517
|
+
}
|
|
518
|
+
teamSkill(slug) { return this.request(route(PLATFORM_ROUTES.teamSkill, { slug: teamSkillSlug(slug) }), teamSkillResult, { maximumResponseBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes }); }
|
|
502
519
|
createTeamSkill(input) {
|
|
503
|
-
return this.request(route(PLATFORM_ROUTES.teamSkills), teamSkillResult, { method: "POST", body: platformTeamSkillCreateSchema.parse(input) });
|
|
520
|
+
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
521
|
}
|
|
505
522
|
updateTeamSkill(slug, input) {
|
|
506
|
-
return this.request(route(PLATFORM_ROUTES.teamSkillUpdate, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillUpdateSchema.parse(input) });
|
|
523
|
+
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
524
|
}
|
|
508
525
|
deleteTeamSkill(slug) {
|
|
509
526
|
return this.request(route(PLATFORM_ROUTES.teamSkillDelete, { slug: teamSkillSlug(slug) }), z.object({ data: z.object({ success: z.literal(true) }) }), { method: "POST", body: {} });
|
|
@@ -512,7 +529,7 @@ export class PlatformClient {
|
|
|
512
529
|
return this.request(route(PLATFORM_ROUTES.teamSkillVersions, { slug: teamSkillSlug(slug) }), z.object({ data: z.object({ versions: z.array(platformTeamSkillVersionSchema) }) }));
|
|
513
530
|
}
|
|
514
531
|
restoreTeamSkill(slug, input) {
|
|
515
|
-
return this.request(route(PLATFORM_ROUTES.teamSkillRestore, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillRestoreSchema.parse(input) });
|
|
532
|
+
return this.request(route(PLATFORM_ROUTES.teamSkillRestore, { slug: teamSkillSlug(slug) }), teamSkillResult, { method: "POST", body: platformTeamSkillRestoreSchema.parse(input), maximumResponseBytes: PLATFORM_TEAM_SKILL_LIMITS.requestBytes });
|
|
516
533
|
}
|
|
517
534
|
suggestions() { return this.request(route(PLATFORM_ROUTES.suggestions), z.object({ data: platformSuggestionsSchema })); }
|
|
518
535
|
access() { return this.request(route(PLATFORM_ROUTES.access), z.object({ data: platformTeamAccessSchema })); }
|
|
@@ -536,7 +553,7 @@ export class PlatformClient {
|
|
|
536
553
|
return this.request(route(PLATFORM_ROUTES.workflowVersions, { slug: workflowSlug(slug) }), z.object({ data: z.object({ versions: z.array(platformWorkflowVersionSchema) }) }));
|
|
537
554
|
}
|
|
538
555
|
runWorkflow(slug, input, idempotencyKey) {
|
|
539
|
-
if (!
|
|
556
|
+
if (!platformIdempotencyKeySchema.safeParse(idempotencyKey).success)
|
|
540
557
|
throw new Error("Keep an idempotency key of 8–128 letters, digits, periods, underscores, colons or hyphens for this run.");
|
|
541
558
|
return this.request(route(PLATFORM_ROUTES.workflowRuns, { slug: workflowSlug(slug) }), workflowRunResult, { method: "POST", body: platformWorkflowRunCreateSchema.parse(input), idempotencyKey });
|
|
542
559
|
}
|
|
@@ -553,7 +570,7 @@ export class PlatformClient {
|
|
|
553
570
|
const parsed = platformQuoteRequestSchema.safeParse(input);
|
|
554
571
|
if (!parsed.success)
|
|
555
572
|
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 => {
|
|
573
|
+
return this.request(route(PLATFORM_ROUTES.quote), quoteResultSchema, { method: "POST", body: parsed.data, toolInput: true }).then(value => {
|
|
557
574
|
const receivedAt = Date.now(), expiresAt = Date.parse(value.data.expiresAt);
|
|
558
575
|
return { ...value, timing: { receivedAtUtc: new Date(receivedAt).toISOString(),
|
|
559
576
|
expiresInSeconds: Number.isFinite(expiresAt) ? Math.max(0, Math.floor((expiresAt - receivedAt) / 1000)) : null } };
|
|
@@ -563,9 +580,9 @@ export class PlatformClient {
|
|
|
563
580
|
const parsed = platformCreateExecutionSchema.safeParse(input);
|
|
564
581
|
if (!parsed.success)
|
|
565
582
|
throw new Error("Provide a tool, arguments, the quote's priceVersion and maximumChargeMicros, and an optional valid provider identifier.");
|
|
566
|
-
if (!
|
|
583
|
+
if (!platformIdempotencyKeySchema.safeParse(idempotencyKey).success)
|
|
567
584
|
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 });
|
|
585
|
+
return this.request(route(PLATFORM_ROUTES.tool, { tool: parsed.data.tool }), executionResultSchema, { method: "POST", body: parsed.data, idempotencyKey, toolInput: true });
|
|
569
586
|
}
|
|
570
587
|
recoverExecution(input) {
|
|
571
588
|
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 };
|