@noimosai/platform-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/dist/cli/cli.js CHANGED
@@ -8,15 +8,11 @@ import { CliInputError, cliInputReason, isCliInputFailure, jsonOption } from "./
8
8
  import { uploadLocalInput } from "../client/local-input.js";
9
9
  import { localProfilePath, runtimeConfig } from "../client/local-profile.js";
10
10
  import { localProjectConfig } from "../client/local-projects.js";
11
- import { runLocalCommand } from "./setup.js";
12
11
  import { isTeamResource, teamResourceCommand } from "./team-resources.js";
13
- import { runWalletCommand } from "./wallet.js";
14
12
  import { PLATFORM_CLI_VERSION } from "./version.js";
15
13
  import { AgentRun, agentRunSchema, agentParallelSchema, agentResultSchema, agentResultsSchema, agentResumeSchema, resultFilesFor as resultFiles } from "../client/agent-run.js";
16
14
  import { platformTeamQuerySchema } from "../client/index.js";
17
15
  import { platformProjectStatusQuerySchema } from "../client/index.js";
18
- import { deactivateProjectFiles } from "./project-files.js";
19
- import { projectSyncFactory } from "./project-sync.js";
20
16
  import { PLATFORM_META_LIMITS, PLATFORM_TOOL_REQUEST_MAX_BYTES, platformMetaSearchSchema, platformMetaSchemasSchema, platformMetaConnectionsSchema, platformMetaExecuteSchema, platformMetaWorkbenchSchema, platformMetaBashSchema, platformIdempotencyKeySchema, platformJsonObjectSchema, withEvidentValues } from "../client/index.js";
21
17
  function metaInput(schema, input) {
22
18
  const json = platformJsonObjectSchema.safeParse(input);
@@ -427,7 +423,7 @@ export async function runCli(args, env, output, error, transport = fetch, stdin
427
423
  else if (args.length === 5 && args[0] === "download" && args[3] === "--out" && args[4].trim()) {
428
424
  const id = platformExecutionIdSchema.parse(args[1]);
429
425
  const artifactId = platformArtifactIdSchema.parse(args[2]);
430
- command = (client) => downloadArtifact(client, id, artifactId, args[4], transport);
426
+ command = (client) => downloadArtifact(client, id, artifactId, args[4], transport, cliCommand);
431
427
  }
432
428
  else if (args.length === 3 && args[0] === "artifact-url") {
433
429
  const id = platformExecutionIdSchema.parse(args[1]);
@@ -526,7 +522,9 @@ export async function runRelvoCli(args, env, output, error, transport = fetch, s
526
522
  const { values } = parseArgs({ args: args.slice(1), options: { profile: { type: "string" }, mode: { type: "string" }, project: { type: "string" } } });
527
523
  if (!values.profile)
528
524
  throw new Error("relvo mcp requires the managed --profile path.");
529
- const { startPlatformMcp } = await import("../mcp/runtime.js");
525
+ const [{ startPlatformMcp }, { deactivateProjectFiles }, { projectSyncFactory }] = await Promise.all([
526
+ import("../mcp/runtime.js"), import("./project-files.js"), import("./project-sync.js")
527
+ ]);
530
528
  const profileHome = dirname(dirname(dirname(values.profile)));
531
529
  await startPlatformMcp(options.version, values.profile, values.mode, values.project, id => deactivateProjectFiles(id, env, profileHome), projectSyncFactory(env, profileHome));
532
530
  return process.exitCode ? 1 : 0;
@@ -546,7 +544,7 @@ export async function runRelvoCli(args, env, output, error, transport = fetch, s
546
544
  };
547
545
  if (args[0] === "wallet" && !args.some((arg) => ["--help", "-h"].includes(arg))) {
548
546
  try {
549
- await runWalletCommand(args, env, output, transport, options);
547
+ await (await import("./wallet.js")).runWalletCommand(args, env, output, transport, options);
550
548
  return 0;
551
549
  }
552
550
  catch (failure) {
@@ -556,7 +554,7 @@ export async function runRelvoCli(args, env, output, error, transport = fetch, s
556
554
  const local = ["setup", "login", "doctor", "logout", "skills"].includes(args[0]) || args[0] === "team-skills" && ["sync", "push", "check", "apply", "dismiss", "auto-sync"].includes(args[1]) || args[0] === "project" && ["list", "add", "use"].includes(args[1]);
557
555
  if (local && !args.some((arg) => ["--help", "-h"].includes(arg))) {
558
556
  try {
559
- await runLocalCommand(args, env, output, transport, options);
557
+ await (await import("./setup.js")).runLocalCommand(args, env, output, transport, options);
560
558
  return 0;
561
559
  }
562
560
  catch (failure) {
@@ -1,15 +1,23 @@
1
1
  import { createHash } from "node:crypto";
2
- import { link, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import { link, lstat, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { outputArtifacts } from "../client/agent-run.js";
5
5
  const STALL_MS = 60_000;
6
- export async function downloadArtifact(client, id, artifactId, filename, transport = fetch) {
6
+ const errorCode = (error) => error instanceof Error && "code" in error ? error.code : undefined;
7
+ const existingOutput = (path) => new Error(`The --out file already exists: ${path}. Choose a new file name; nothing was overwritten.`);
8
+ export async function downloadArtifact(client, id, artifactId, filename, transport, cliCommand) {
7
9
  const execution = (await client.execution(id)).data;
8
10
  if (["queued", "running", "waiting"].includes(execution.status))
9
- throw new Error(`This execution is still ${execution.status}. Wait for it with relvo wait ${id}, then download.`);
11
+ throw new Error(`This execution is still ${execution.status}. Wait for it with ${cliCommand} wait ${id}, then download.`);
10
12
  const artifact = outputArtifacts(execution.output).find((entry) => entry.id === artifactId);
11
13
  if (!artifact)
12
14
  throw new Error("Artifact is not present in this execution. Inspect execution output for available artifact IDs.");
15
+ const path = resolve(filename);
16
+ if (!await stat(dirname(path)).then(entry => entry.isDirectory(), () => false))
17
+ throw new Error(`The folder for --out does not exist: ${dirname(path)}. No file was saved.`);
18
+ if (await lstat(path).then(() => true, (error) => { if (errorCode(error) === "ENOENT")
19
+ return false; throw error; }))
20
+ throw existingOutput(path);
13
21
  const { data } = await client.artifactDownload(id, artifactId);
14
22
  const url = new URL(data.url);
15
23
  if (url.hostname !== "storage.googleapis.com" && !url.hostname.endsWith(".storage.googleapis.com"))
@@ -25,7 +33,7 @@ export async function downloadArtifact(client, id, artifactId, filename, transpo
25
33
  clearTimeout(watchdog);
26
34
  throw new Error("Artifact download failed. Retrieve a fresh URL and try again.");
27
35
  }
28
- const reader = response.body.getReader(), chunks = [];
36
+ const reader = response.body.getReader(), chunks = [], digest = createHash("sha256");
29
37
  let bytes = 0, oversized = false;
30
38
  try {
31
39
  for (;;) {
@@ -40,6 +48,7 @@ export async function downloadArtifact(client, id, artifactId, filename, transpo
40
48
  break;
41
49
  }
42
50
  chunks.push(part.value);
51
+ digest.update(part.value);
43
52
  }
44
53
  }
45
54
  catch {
@@ -51,19 +60,17 @@ export async function downloadArtifact(client, id, artifactId, filename, transpo
51
60
  }
52
61
  if (oversized)
53
62
  throw new Error("Artifact exceeded its recorded size; no file was saved.");
54
- const buffer = Buffer.concat(chunks);
55
- if (bytes !== artifact.sizeBytes || createHash("sha256").update(buffer).digest("hex") !== artifact.sha256)
63
+ if (bytes !== artifact.sizeBytes || digest.digest("hex") !== artifact.sha256)
56
64
  throw new Error("Artifact integrity check failed; no file was saved.");
57
- const path = resolve(filename);
58
65
  const temporary = await mkdtemp(join(dirname(path), ".noimos-platform-")).catch((error) => {
59
- if (error instanceof Error && "code" in error && error.code === "ENOENT")
66
+ if (errorCode(error) === "ENOENT")
60
67
  throw new Error(`The folder for --out does not exist: ${dirname(path)}. No file was saved.`);
61
68
  throw error;
62
69
  });
63
70
  try {
64
71
  const staged = join(temporary, "artifact");
65
- await writeFile(staged, buffer, { flag: "wx", mode: 0o600 });
66
- await link(staged, path);
72
+ await writeFile(staged, Buffer.concat(chunks, bytes), { flag: "wx", mode: 0o600 });
73
+ await link(staged, path).catch((error) => { throw errorCode(error) === "EEXIST" ? existingOutput(path) : error; });
67
74
  }
68
75
  finally {
69
76
  await rm(temporary, { recursive: true, force: true });
@@ -140,10 +140,12 @@ async function context(path, env, options = {}) {
140
140
  const roots = harnessRoots(home, env, await readCustomHarnesses(home));
141
141
  return { profile, roots, clients: await targets(roots, profile, options.selected), state: await readState(path, profile) };
142
142
  }
143
- async function inspect(client, profile) {
144
- const { data } = await client.project();
145
- if (data.developerProjectId !== profile.grant.developerProjectId || data.developerOrganizationId !== profile.grant.developerOrganizationId)
146
- throw new Error("The saved login does not match this project. No project files were changed.");
143
+ async function inspect(client, profile, verifyProject = true) {
144
+ if (verifyProject) {
145
+ const { data } = await client.project();
146
+ if (data.developerProjectId !== profile.grant.developerProjectId || data.developerOrganizationId !== profile.grant.developerOrganizationId)
147
+ throw new Error("The saved login does not match this project. No project files were changed.");
148
+ }
147
149
  const items = (await client.teamSkills("all")).data.skills.sort((a, b) => a.slug.localeCompare(b.slug));
148
150
  if (new Set(items.map(item => item.slug)).size !== items.length)
149
151
  throw new Error("The project returned duplicate items. No project files were changed.");
@@ -199,8 +201,12 @@ export function localProjectSync(path, env, options = {}) {
199
201
  const client = options.connected?.client ?? new PlatformClient({ apiUrl: ctx.profile.apiUrl, apiKey: ctx.profile.grant.apiKey, local: ctx.profile.local, source: "cli" }, options.transport);
200
202
  return { ...ctx, client };
201
203
  };
202
- const capture = async (ctx) => {
203
- const items = (await inspect(ctx.client, ctx.profile)).filter(item => ctx.clients.some(name => teamItemTargets(item.harnesses, harnessView(ctx.roots, name).like)));
204
+ let polledLogin;
205
+ const capture = async (ctx, poll = false) => {
206
+ const login = `${ctx.profile.grant.apiKey}\n${ctx.profile.grant.developerOrganizationId}\n${ctx.profile.grant.developerProjectId}`;
207
+ const items = (await inspect(ctx.client, ctx.profile, !poll || polledLogin !== login)).filter(item => ctx.clients.some(name => teamItemTargets(item.harnesses, harnessView(ctx.roots, name).like)));
208
+ if (poll)
209
+ polledLogin = login;
204
210
  const scriptsPreviouslyAllowed = await teamSkillsUseScripts(ctx.roots, ctx.clients, ctx.profile.grant.developerProjectId)
205
211
  || await teamHarnessUsesScripts(ctx.roots, ctx.clients, ctx.profile.grant.developerProjectId);
206
212
  const current = await readLocalProfile(path);
@@ -222,7 +228,7 @@ export function localProjectSync(path, env, options = {}) {
222
228
  if (env.RELVO_PROJECT_AUTO_SYNC === "0")
223
229
  return undefined;
224
230
  const ctx = await prepare();
225
- return !ctx.state.enabled ? undefined : (await capture(ctx)).review;
231
+ return !ctx.state.enabled ? undefined : (await capture(ctx, true)).review;
226
232
  },
227
233
  apply: (snapshot, confirmedByUser, allowScripts, removeExecutableSettings = false) => withProfileLock(path, async () => {
228
234
  confirmed(confirmedByUser);
@@ -319,6 +319,10 @@ const publicErrors = {
319
319
  platform_voice_provider_unavailable: "The voice provider for this model is unavailable. Choose another model.",
320
320
  platform_voice_session_cancelled: "The voice session was cancelled or has ended.",
321
321
  };
322
+ const noimosPublicErrors = {
323
+ platform_invalid_developer_api_key: "This API key is no longer valid: it expired, was revoked or rotated, or its member lost access. Create a new key in the console.",
324
+ platform_developer_api_key_required: "This is not a valid API key for this service. Check the key in the console.",
325
+ };
322
326
  const PLATFORM_ERROR_CODE = /^platform_[a-z0-9_]{1,100}$/;
323
327
  const REFUSALS_WITH_DETAILS = new Set(["platform_invalid_tool_arguments", "platform_custom_tool_invalid", "platform_custom_tool_schema_invalid", "platform_custom_tool_version_conflict",
324
328
  "platform_team_skill_invalid", "platform_team_skill_version_conflict", "platform_workflow_invalid", "platform_workflow_version_conflict", "platform_workflow_inputs_invalid",
@@ -326,9 +330,9 @@ const REFUSALS_WITH_DETAILS = new Set(["platform_invalid_tool_arguments", "platf
326
330
  const refusalDetailsSchema = z.object({ fields: platformInvalidArgumentFieldsSchema.optional(), reason: z.string().max(200).optional(), version: z.number().int().optional(),
327
331
  chargeCeilingMicros: z.number().int().nonnegative().optional() });
328
332
  const usd = (micros) => `$${(Number(micros) / 1_000_000).toFixed(micros.length > 4 ? 2 : 4)}`;
329
- function shortfallText({ reason, requiredMicros, availableMicros }) {
333
+ function shortfallText({ reason, requiredMicros, availableMicros }, relvo) {
330
334
  if (reason === "balance")
331
- return ` It needs ${usd(requiredMicros)} and the balance has ${usd(availableMicros)}. Top up the balance (relvo wallet top-up, or the console), then retry with the same idempotency key.`;
335
+ return ` It needs ${usd(requiredMicros)} and the balance has ${usd(availableMicros)}. Top up the balance (${relvo ? "relvo wallet top-up, or the console" : "in the console"}), then retry with the same idempotency key.`;
332
336
  const scope = reason === "project_monthly_limit" ? "project's monthly" : reason === "member_monthly_limit" ? "member's monthly" : reason === "api_key_daily_limit" ? "API key's daily" : reason === "organization_daily_limit" ? "organization's daily" : "organization's monthly";
333
337
  const reset = reason.endsWith("_daily_limit") ? " The daily count starts again at 00:00 UTC." : "";
334
338
  return ` It needs ${usd(requiredMicros)} and ${usd(availableMicros)} remains of the ${scope} spending limit.${reset} Topping up does not help; a person must raise the limit in the console.`;
@@ -349,9 +353,11 @@ export class PlatformApiError extends Error {
349
353
  summary;
350
354
  billing;
351
355
  constructor(status, code, productName = PLATFORM_PRODUCT_NAMES.relvo, fields = [], shortfall, provider, scope, reconnectUrl, billingUrl, actionUrl, connectionProviders = [], noExecutionCharge = false, refusal = {}) {
352
- const summary = code && Object.hasOwn(publicErrors, code) ? publicErrors[code] : code ? `The API refused this request: ${code}.`
353
- : status >= 500 ? "The service failed this request. Retry shortly; the key and project do not need to change." : "Check the developer key, project access and limits in the console.";
354
- super(`${productName} API request failed (${status}). ${summary}${refusal.retryAfterSeconds ? ` Retry after ${refusal.retryAfterSeconds} seconds; retrying sooner counts against this key.` : ""}${fields.length ? ` Invalid fields: ${fields.join(", ")}.` : ""}${refusal.reason ? ` Reason: ${refusal.reason}` : ""}${refusal.version === undefined ? "" : ` Current version: ${refusal.version}.`}${refusal.chargeCeilingMicros === undefined ? "" : ` chargeCeilingMicros: ${refusal.chargeCeilingMicros}.`}${shortfall ? shortfallText(shortfall) : ""}${provider ? ` Provider key needed: ${provider}.` : ""}${scope ? ` Missing scope: ${scope}; obtain the user's additional approval before retrying.` : ""}${reconnectUrl ? ` Show the user this page to reconnect it, then retry: ${reconnectUrl}` : ""}${billingUrl ? ` Billing: ${billingUrl}` : ""}${actionUrl ? ` Show the user this page to complete the required approval or configuration, then retry the original request: ${actionUrl}` : ""}`);
356
+ const relvo = productName === PLATFORM_PRODUCT_NAMES.relvo;
357
+ const summary = code && !relvo && Object.hasOwn(noimosPublicErrors, code) ? noimosPublicErrors[code]
358
+ : code && Object.hasOwn(publicErrors, code) ? publicErrors[code] : code ? `The API refused this request: ${code}.`
359
+ : status >= 500 ? "The service failed this request. Retry shortly; the key and project do not need to change." : "Check the developer key, project access and limits in the console.";
360
+ super(`${productName} API request failed (${status}). ${summary}${refusal.retryAfterSeconds ? ` Retry after ${refusal.retryAfterSeconds} seconds; retrying sooner counts against this key.` : ""}${fields.length ? ` Invalid fields: ${fields.join(", ")}.` : ""}${refusal.reason ? ` Reason: ${refusal.reason}` : ""}${refusal.version === undefined ? "" : ` Current version: ${refusal.version}.`}${refusal.chargeCeilingMicros === undefined ? "" : ` chargeCeilingMicros: ${refusal.chargeCeilingMicros}.`}${shortfall ? shortfallText(shortfall, relvo) : ""}${provider ? ` Provider key needed: ${provider}.` : ""}${scope ? ` Missing scope: ${scope}; obtain the user's additional approval before retrying.` : ""}${reconnectUrl ? ` Show the user this page to reconnect it, then retry: ${reconnectUrl}` : ""}${billingUrl ? ` Billing: ${billingUrl}` : ""}${actionUrl ? ` Show the user this page to complete the required approval or configuration, then retry the original request: ${actionUrl}` : ""}`);
355
361
  this.status = status;
356
362
  this.code = code;
357
363
  this.fields = fields;
@@ -1,4 +1,4 @@
1
- import { PlatformApiError, wrapUntrusted, TOOL_RESULT_CONTEXT_MAX_BYTES, toolResultSelectionSchema } from "../client/index.js";
1
+ import { PlatformApiError, PLATFORM_DISTRIBUTION, wrapUntrusted, TOOL_RESULT_CONTEXT_MAX_BYTES, toolResultSelectionSchema } from "../client/index.js";
2
2
  import { RELVO_TOOL_RESULT_RULE } from "./instructions.js";
3
3
  export const MCP_RESULT_MAX_BYTES = TOOL_RESULT_CONTEXT_MAX_BYTES - 256;
4
4
  const bytes = (value) => Buffer.byteLength(JSON.stringify(value));
@@ -20,7 +20,7 @@ export function fitRows(rows, reply, fits, oversized) {
20
20
  kept.pop();
21
21
  return oversized && kept.length && !fits(reply(kept)) ? reply(kept.map(oversized)) : reply(kept);
22
22
  }
23
- export function mcpRequestFailure(error, message = error instanceof Error ? error.message : "Relvo request failed.") {
23
+ export function mcpRequestFailure(error, message = error instanceof Error ? error.message : `${PLATFORM_DISTRIBUTION.name} request failed.`) {
24
24
  const reply = error instanceof PlatformApiError
25
25
  ? boundedMcpResult({ error: { code: error.code, message: error.guidance ? error.summary : message,
26
26
  ...(error.shortfall ? { shortfall: error.shortfall } : {}), ...(error.billingUrl ? { billingUrl: error.billingUrl } : {}),
@@ -1,3 +1,4 @@
1
+ import { PLATFORM_DISTRIBUTION } from "../client/index.js";
1
2
  import { withUpdateNotice } from "./update-notice.js";
2
3
  import { mcpRequestFailure } from "./context.js";
3
4
  export class PlatformOperations {
@@ -29,7 +30,7 @@ export class PlatformOperations {
29
30
  return await tool.call(input, context);
30
31
  }
31
32
  catch (error) {
32
- return mcpRequestFailure(error, error instanceof Error ? error.message : "Relvo operation failed.");
33
+ return mcpRequestFailure(error, error instanceof Error ? error.message : `${PLATFORM_DISTRIBUTION.name} operation failed.`);
33
34
  }
34
35
  }
35
36
  attach(server, notice, only) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@noimosai/platform-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@noimosai/platform-mcp",
9
- "version": "0.1.4",
9
+ "version": "0.1.5",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/server": "^2.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noimosai/platform-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "NoimosAI Developer Platform stdio MCP server for research, SEO, social data and media generation.",
5
5
  "type": "module",
6
6
  "bin": {