@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 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;
@@ -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
- constructor(client, directory = join(tmpdir(), `relvo-runs-${process.getuid?.() ?? "user"}`), resultFiles, topUp) {
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 & 0o077 || process.getuid && root.uid !== process.getuid())
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 & 0o077 || process.getuid && scoped.uid !== process.getuid())
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 & 0o077)
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
- const waited = await this.client.waitExecution(execution.id, waitSeconds * 1000);
620
- timedOut = waited.timedOut;
621
- execution = waited.data ?? execution;
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
- return file && execution.status === "succeeded" ? { ...view, file: await this.exportFile(execution.id, execution.output, selection?.path ?? [], selection?.fields) } : view;
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" ? { result: agentResultView(execution.output, selection, pick) } : {}),
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
- let execution = (await this.client.execution(id)).data, timedOut = false;
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")
@@ -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 idempotencySchema = z.string().regex(/^[A-Za-z0-9._:-]{8,128}$/);
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
- if (body && new TextEncoder().encode(body).byteLength > (upload ? PLATFORM_UPLOAD_MAX_BYTES : meta ? PLATFORM_META_LIMITS.requestBytes : 65536))
287
- throw new Error(upload ? "The upload exceeds 12 MiB." : meta ? "The meta tool request exceeds 1 MiB." : "The tool request exceeds 64 KiB.");
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 (!idempotencySchema.safeParse(idempotencyKey).success)
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 (!idempotencySchema.safeParse(idempotencyKey).success)
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 readPrivateText(path, maximumBytes = 65536) {
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("utf8");
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 writePrivateText(path, value, privateParent = true, maximumBytes = 65536) {
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 (Buffer.byteLength(value, "utf8") > maximumBytes)
70
+ if (value.byteLength > maximumBytes)
70
71
  throw new Error("Relvo private file exceeds its size limit.");
71
72
  try {
72
- await readPrivateText(path, maximumBytes);
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
- return localProfileSchema.parse(await readPrivateJson(path));
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
- throw new Error("Relvo local profile is unsafe or invalid. No credentials were changed.");
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 Error("Relvo login is missing, incomplete or expired. Run relvo setup to resume.");
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 };