@mgsoftwarebv/mg-dashboard-mcp 7.4.2 → 7.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { sql } from 'drizzle-orm';
20
20
  import { once } from 'events';
21
21
  import { lookup } from 'dns/promises';
22
22
  import { connect } from 'tls';
23
- import { pgTable, timestamp, jsonb, uuid, boolean, text, pgEnum, integer, uniqueIndex, index, check, bigint } from 'drizzle-orm/pg-core';
23
+ import { pgTable, timestamp, jsonb, uuid, boolean, text, pgEnum, integer, uniqueIndex, index, check, bigint, foreignKey } from 'drizzle-orm/pg-core';
24
24
  import { HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, S3Client, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
25
25
 
26
26
  var __defProp = Object.defineProperty;
@@ -990,9 +990,9 @@ ${raw2.substring(0, 500)}` }] };
990
990
  return { content: [{ type: "text", text: `No environment variables for ${project}/${env}.` }] };
991
991
  }
992
992
  const lines = vars.map((v) => `${v.isSecret ? "[secret]" : " "} ${v.name}`).sort();
993
- const text9 = `Env vars for ${project}/${env} (${vars.length}) \u2014 values hidden, use action="get" with a key:
993
+ const text10 = `Env vars for ${project}/${env} (${vars.length}) \u2014 values hidden, use action="get" with a key:
994
994
  ` + "-".repeat(50) + "\n" + lines.join("\n");
995
- return { content: [{ type: "text", text: text9 }] };
995
+ return { content: [{ type: "text", text: text10 }] };
996
996
  }
997
997
  if (action === "get") {
998
998
  const key2 = String(args2.key ?? "");
@@ -1079,9 +1079,9 @@ async function fetchAndFormatRun(conn, proxy, sshExec2, instance, runId) {
1079
1079
  return { content: [{ type: "text", text: `Invalid API response:
1080
1080
  ${rawJson.substring(0, 500)}` }] };
1081
1081
  }
1082
- let text9 = formatRunDetail(run);
1083
- if (logs) text9 += "\n\n--- Logs ---\n" + logs;
1084
- return { content: [{ type: "text", text: text9 }] };
1082
+ let text10 = formatRunDetail(run);
1083
+ if (logs) text10 += "\n\n--- Logs ---\n" + logs;
1084
+ return { content: [{ type: "text", text: text10 }] };
1085
1085
  }
1086
1086
  async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSeconds) {
1087
1087
  const pollInterval = 3e3;
@@ -1103,10 +1103,10 @@ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSec
1103
1103
  continue;
1104
1104
  }
1105
1105
  if (TERMINAL_STATUSES.has(run.status)) {
1106
- let text9 = formatRunDetail(run);
1106
+ let text10 = formatRunDetail(run);
1107
1107
  const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
1108
- if (logs) text9 += "\n\n--- Logs ---\n" + logs;
1109
- return { content: [{ type: "text", text: text9 }] };
1108
+ if (logs) text10 += "\n\n--- Logs ---\n" + logs;
1109
+ return { content: [{ type: "text", text: text10 }] };
1110
1110
  }
1111
1111
  }
1112
1112
  return {
@@ -3793,10 +3793,10 @@ var ZodObject = class _ZodObject extends ZodType {
3793
3793
  // }) as any;
3794
3794
  // return merged;
3795
3795
  // }
3796
- catchall(index7) {
3796
+ catchall(index8) {
3797
3797
  return new _ZodObject({
3798
3798
  ...this._def,
3799
- catchall: index7
3799
+ catchall: index8
3800
3800
  });
3801
3801
  }
3802
3802
  pick(mask) {
@@ -4114,9 +4114,9 @@ function mergeValues(a, b) {
4114
4114
  return { valid: false };
4115
4115
  }
4116
4116
  const newArray = [];
4117
- for (let index7 = 0; index7 < a.length; index7++) {
4118
- const itemA = a[index7];
4119
- const itemB = b[index7];
4117
+ for (let index8 = 0; index8 < a.length; index8++) {
4118
+ const itemA = a[index8];
4119
+ const itemB = b[index8];
4120
4120
  const sharedValue = mergeValues(itemA, itemB);
4121
4121
  if (!sharedValue.valid) {
4122
4122
  return { valid: false };
@@ -4322,10 +4322,10 @@ var ZodMap = class extends ZodType {
4322
4322
  }
4323
4323
  const keyType = this._def.keyType;
4324
4324
  const valueType = this._def.valueType;
4325
- const pairs = [...ctx.data.entries()].map(([key, value], index7) => {
4325
+ const pairs = [...ctx.data.entries()].map(([key, value], index8) => {
4326
4326
  return {
4327
- key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index7, "key"])),
4328
- value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index7, "value"]))
4327
+ key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index8, "key"])),
4328
+ value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index8, "value"]))
4329
4329
  };
4330
4330
  });
4331
4331
  if (ctx.common.async) {
@@ -6123,7 +6123,7 @@ pgTable(
6123
6123
  index("dns_migration_mailbox_migration_idx").on(table.migrationId)
6124
6124
  ]
6125
6125
  );
6126
- pgTable(
6126
+ var githubToken = pgTable(
6127
6127
  "github_token",
6128
6128
  {
6129
6129
  id: uuid("id").primaryKey().defaultRandom(),
@@ -6253,6 +6253,60 @@ pgTable(
6253
6253
  index("content_ingestion_runs_project_idx").on(table.projectKey)
6254
6254
  ]
6255
6255
  );
6256
+ var repoFunnel = pgTable(
6257
+ "repo_funnel",
6258
+ {
6259
+ id: uuid("id").primaryKey().defaultRandom(),
6260
+ name: text("name").notNull(),
6261
+ enabled: boolean("enabled").notNull().default(true),
6262
+ sourceRepoFullName: text("source_repo_full_name").notNull(),
6263
+ sourceBranch: text("source_branch").notNull().default("main"),
6264
+ targetRepoFullName: text("target_repo_full_name").notNull(),
6265
+ targetBranch: text("target_branch").notNull().default("main"),
6266
+ githubTokenId: uuid("github_token_id"),
6267
+ sshServerId: uuid("ssh_server_id").notNull(),
6268
+ lastSyncedSha: text("last_synced_sha"),
6269
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
6270
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
6271
+ },
6272
+ (table) => [
6273
+ index("repo_funnel_source_idx").on(
6274
+ table.sourceRepoFullName,
6275
+ table.sourceBranch
6276
+ ),
6277
+ index("repo_funnel_enabled_idx").on(table.enabled),
6278
+ foreignKey({
6279
+ columns: [table.githubTokenId],
6280
+ foreignColumns: [githubToken.id],
6281
+ name: "repo_funnel_github_token_id_fk"
6282
+ }).onDelete("set null"),
6283
+ foreignKey({
6284
+ columns: [table.sshServerId],
6285
+ foreignColumns: [managedServer.id],
6286
+ name: "repo_funnel_ssh_server_id_fk"
6287
+ }).onDelete("restrict")
6288
+ ]
6289
+ );
6290
+ pgTable(
6291
+ "repo_funnel_log",
6292
+ {
6293
+ id: uuid("id").primaryKey().defaultRandom(),
6294
+ repoFunnelId: uuid("repo_funnel_id").notNull(),
6295
+ sourceCommitSha: text("source_commit_sha").notNull(),
6296
+ status: text("status").notNull().default("pending"),
6297
+ errorMessage: text("error_message"),
6298
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
6299
+ },
6300
+ (table) => [
6301
+ index("repo_funnel_log_funnel_idx").on(table.repoFunnelId),
6302
+ index("repo_funnel_log_status_idx").on(table.status),
6303
+ foreignKey({
6304
+ columns: [table.repoFunnelId],
6305
+ foreignColumns: [repoFunnel.id],
6306
+ name: "repo_funnel_log_repo_funnel_id_fk"
6307
+ }).onDelete("cascade")
6308
+ ]
6309
+ );
6256
6310
  var GITHUB_API = "https://api.github.com";
6257
6311
  var ReleaseProfileQuickCreateError = class extends Error {
6258
6312
  constructor(message, code = "INTERNAL_SERVER_ERROR") {
@@ -6619,6 +6673,10 @@ async function updateReleaseProfileSettings(profileId, fields) {
6619
6673
  setExprs.push(sql`has_trigger_dev = ${fields.hasTriggerDev}`);
6620
6674
  if (fields.workDirectory !== void 0)
6621
6675
  setExprs.push(sql`work_directory = ${fields.workDirectory}`);
6676
+ if (fields.internalTicketProjectId !== void 0)
6677
+ setExprs.push(
6678
+ sql`internal_ticket_project_id = ${fields.internalTicketProjectId}`
6679
+ );
6622
6680
  if (setExprs.length === 0) {
6623
6681
  throw new ReleaseProfileQuickCreateError(
6624
6682
  "No fields to update",
@@ -7652,7 +7710,7 @@ function getEncryptionKey() {
7652
7710
  throw new Error("ENCRYPTION_KEY must be a 64-character hex string");
7653
7711
  return buf;
7654
7712
  }
7655
- function encrypt(text9) {
7713
+ function encrypt(text10) {
7656
7714
  const key = getEncryptionKey();
7657
7715
  const iv = randomBytes(ENC_IV_LENGTH);
7658
7716
  const cipher = createCipheriv(
@@ -7660,7 +7718,7 @@ function encrypt(text9) {
7660
7718
  new Uint8Array(key),
7661
7719
  new Uint8Array(iv)
7662
7720
  );
7663
- let encrypted = cipher.update(text9, "utf8", "hex");
7721
+ let encrypted = cipher.update(text10, "utf8", "hex");
7664
7722
  encrypted += cipher.final("hex");
7665
7723
  const authTag = cipher.getAuthTag();
7666
7724
  return Buffer.concat([
@@ -8380,10 +8438,10 @@ async function r2GetObjectRange(bucket, key, range) {
8380
8438
  const body = result.Body;
8381
8439
  if (!body?.transformToString)
8382
8440
  throw new Error("R2 returned no readable body");
8383
- const text9 = await body.transformToString();
8441
+ const text10 = await body.transformToString();
8384
8442
  const header = `# range: bytes ${range.offset}-${end} of ${size} (${effectiveLen} bytes)`;
8385
8443
  return `${header}
8386
- ${text9}`;
8444
+ ${text10}`;
8387
8445
  } catch (e) {
8388
8446
  throw r2WrapError(bucket, key, e);
8389
8447
  }
@@ -8746,15 +8804,15 @@ async function sftpRead(opts, filePath, proxy, options) {
8746
8804
  clearTimeout(timer);
8747
8805
  cleanup?.();
8748
8806
  cleanup = void 0;
8749
- const text9 = Buffer.concat(
8807
+ const text10 = Buffer.concat(
8750
8808
  chunks.map((ch) => new Uint8Array(ch))
8751
8809
  ).toString("utf-8");
8752
8810
  if (!isWholeFileRequest) {
8753
8811
  const header = `# range: bytes ${offset}-${offset + effectiveLen - 1} of ${total} (${effectiveLen} bytes)`;
8754
8812
  resolve(`${header}
8755
- ${text9}`);
8813
+ ${text10}`);
8756
8814
  } else {
8757
- resolve(text9);
8815
+ resolve(text10);
8758
8816
  }
8759
8817
  });
8760
8818
  rs.on("error", (e) => {
@@ -8901,11 +8959,11 @@ function getKnownContainers(serverId, maxAgeMs = 5 * 6e4) {
8901
8959
  if (Date.now() - e.capturedAt > maxAgeMs) return void 0;
8902
8960
  return e.names;
8903
8961
  }
8904
- function truncateForLLM(text9, maxBytes) {
8905
- const totalBytes = Buffer.byteLength(text9, "utf8");
8962
+ function truncateForLLM(text10, maxBytes) {
8963
+ const totalBytes = Buffer.byteLength(text10, "utf8");
8906
8964
  if (totalBytes <= maxBytes)
8907
- return { text: text9, truncated: false, totalBytes, shownBytes: totalBytes };
8908
- const buf = Buffer.from(text9, "utf8");
8965
+ return { text: text10, truncated: false, totalBytes, shownBytes: totalBytes };
8966
+ const buf = Buffer.from(text10, "utf8");
8909
8967
  let cut = maxBytes;
8910
8968
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
8911
8969
  const head = buf.subarray(0, cut).toString("utf8");
@@ -8936,10 +8994,10 @@ function postprocessResult(result, meta) {
8936
8994
  if (!result.content?.length) return result;
8937
8995
  if (RAW_JSON_TOOLS.has(meta.toolName)) return result;
8938
8996
  const block = result.content[0];
8939
- let text9 = String(block.text ?? "");
8940
- const trunc = truncateForLLM(text9, RESPONSE_MAX_BYTES);
8997
+ let text10 = String(block.text ?? "");
8998
+ const trunc = truncateForLLM(text10, RESPONSE_MAX_BYTES);
8941
8999
  if (trunc.truncated) {
8942
- text9 = trunc.text + "\n\n... " + buildTruncationHint(
9000
+ text10 = trunc.text + "\n\n... " + buildTruncationHint(
8943
9001
  meta.toolName,
8944
9002
  meta.args,
8945
9003
  trunc.totalBytes,
@@ -8953,11 +9011,11 @@ function postprocessResult(result, meta) {
8953
9011
  const parts = [`took ${tookStr}`, sizeStr];
8954
9012
  if (meta.serverIdLabel) parts.push(`server: ${meta.serverIdLabel}`);
8955
9013
  if (meta.cached) parts.push("cached");
8956
- text9 = `${text9}
9014
+ text10 = `${text10}
8957
9015
 
8958
9016
  [${parts.join(", ")}]`;
8959
9017
  }
8960
- return { ...result, content: [{ ...block, text: text9 }] };
9018
+ return { ...result, content: [{ ...block, text: text10 }] };
8961
9019
  }
8962
9020
  function buildPipelineScript(commands, shell, marker, stopOnError) {
8963
9021
  if (shell === "powershell") {
@@ -10756,7 +10814,7 @@ var TOOLS = [
10756
10814
  },
10757
10815
  {
10758
10816
  name: "release-profile",
10759
- description: 'Create or update a release profile. Pick the operation with `action`:\n- "create": one-step creation \u2014 scans the GitHub repo, detects apps (Next.js/Docker/PM2), creates the profile plus dev/prod stages with smart defaults (dev: release-dev + PR webhook + auto-approve, prod: release-prod + manual + DB backup) and seeds the stage apps. Release branches are created on GitHub automatically. Required: repoFullName. Optional: githubKey, name, releaseType. Assign deployment servers afterwards in the dashboard (Releases \u2192 Profiles).\n- "update": change settings of an existing profile located by `releaseProfile` (name or repo). Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Required: releaseProfile. Optional: name, enabled, releaseType, githubKey, useChangesBranch, hasTriggerDev, workDirectory.\n\nRequires ci_cd permission.',
10817
+ description: 'Create or update a release profile. Pick the operation with `action`:\n- "create": one-step creation \u2014 scans the GitHub repo, detects apps (Next.js/Docker/PM2), creates the profile plus dev/prod stages with smart defaults (dev: release-dev + PR webhook + auto-approve, prod: release-prod + manual + DB backup) and seeds the stage apps. Release branches are created on GitHub automatically. Required: repoFullName. Optional: githubKey, name, releaseType. Assign deployment servers afterwards in the dashboard (Releases \u2192 Profiles).\n- "update": change settings of an existing profile located by `releaseProfile` (name or repo). Changing releaseType reconfigures the stages (missing stages are created with smart defaults, out-of-scope stages are disabled). Required: releaseProfile. Optional: name, enabled, releaseType, githubKey, useChangesBranch, hasTriggerDev, workDirectory, internalTicketProjectId.\n\nRequires ci_cd permission.',
10760
10818
  inputSchema: {
10761
10819
  type: "object",
10762
10820
  properties: {
@@ -10801,6 +10859,10 @@ var TOOLS = [
10801
10859
  workDirectory: {
10802
10860
  type: "string",
10803
10861
  description: 'Working directory within the repo (e.g. "."). Update only.'
10862
+ },
10863
+ internalTicketProjectId: {
10864
+ type: "string",
10865
+ description: "Refront project UUID for escalating fundamental/platform issues (update only). Pass empty string to clear."
10804
10866
  }
10805
10867
  },
10806
10868
  required: ["action"]
@@ -11769,7 +11831,7 @@ async function executeToolCall(name, a, _serverId) {
11769
11831
  ]
11770
11832
  };
11771
11833
  }
11772
- const lines = data.hits.map((hit, index7) => {
11834
+ const lines = data.hits.map((hit, index8) => {
11773
11835
  const when = hit.lastMessageAt ? new Date(hit.lastMessageAt).toISOString().slice(0, 10) : "unknown date";
11774
11836
  const repo = hit.repo ?? "unknown repo";
11775
11837
  const sim = hit.similarity !== null ? ` \xB7 ${(hit.similarity * 100).toFixed(0)}% match` : "";
@@ -11779,7 +11841,7 @@ async function executeToolCall(name, a, _serverId) {
11779
11841
  ...Array.isArray(hit.tech) ? hit.tech.slice(0, 4) : []
11780
11842
  ].filter(Boolean);
11781
11843
  const tags = facets.length > 0 ? ` \xB7 ${facets.join(", ")}` : "";
11782
- return `${index7 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
11844
+ return `${index8 + 1}. [${repo} \xB7 ${when}${sim}${tags}] ${hit.title}
11783
11845
  id: ${hit.id}
11784
11846
  ${(hit.summary ?? hit.snippet).replace(/\s+/g, " ").slice(0, 500)}`;
11785
11847
  });
@@ -12964,8 +13026,8 @@ ${sample.join("\n")}${files.length > 5 ? `
12964
13026
  };
12965
13027
  const filtered = sortRows(applyFilter(only.rows));
12966
13028
  if (format === "json") {
12967
- const text10 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
12968
- return { content: [{ type: "text", text: text10 }] };
13029
+ const text11 = filtered.map((c) => JSON.stringify(c)).join("\n") || "(no containers)";
13030
+ return { content: [{ type: "text", text: text11 }] };
12969
13031
  }
12970
13032
  if (groupByProject) {
12971
13033
  const groups = /* @__PURE__ */ new Map();
@@ -12992,8 +13054,8 @@ ${sample.join("\n")}${files.length > 5 ? `
12992
13054
  }
12993
13055
  const header = `${"NAMES".padEnd(36)} ${"IMAGE".padEnd(40)} ${"STATUS".padEnd(24)} ${"HEALTH".padEnd(10)} ${"PORTS".padEnd(40)} PROJECT`;
12994
13056
  const body = filtered.map((r) => `${fmtRow(r)} ${r.Project || ""}`);
12995
- const text9 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
12996
- return { content: [{ type: "text", text: text9 }] };
13057
+ const text10 = filtered.length === 0 ? "(no containers match)" : [header, ...body].join("\n");
13058
+ return { content: [{ type: "text", text: text10 }] };
12997
13059
  }
12998
13060
  if (format === "json") {
12999
13061
  const lines = [];
@@ -14181,6 +14243,7 @@ LIMIT ${limit};
14181
14243
  SELECT
14182
14244
  p.id, p.name, p.repo_full_name, p.enabled, p.release_type,
14183
14245
  p.work_directory, p.use_changes_branch, p.has_trigger_dev,
14246
+ p.internal_ticket_project_id,
14184
14247
  gt.label AS github_key,
14185
14248
  (
14186
14249
  SELECT string_agg(
@@ -14207,7 +14270,8 @@ LIMIT ${limit};
14207
14270
  `type=${p.release_type ?? "both"}`,
14208
14271
  `key=${p.github_key ?? "MG Software (default)"}`,
14209
14272
  p.use_changes_branch ? "changes-branch" : null,
14210
- p.has_trigger_dev ? "trigger.dev" : null
14273
+ p.has_trigger_dev ? "trigger.dev" : null,
14274
+ p.internal_ticket_project_id ? `internalTicketProject=${p.internal_ticket_project_id}` : null
14211
14275
  ].filter(Boolean).join(", ");
14212
14276
  return `${p.name} \u2014 ${p.repo_full_name} [${flags}]
14213
14277
  stages: ${p.stages ?? "(none)"}`;
@@ -14324,7 +14388,8 @@ ${lines.join("\n\n")}`
14324
14388
  githubTokenId,
14325
14389
  useChangesBranch: a.useChangesBranch !== void 0 ? a.useChangesBranch === true : void 0,
14326
14390
  hasTriggerDev: a.hasTriggerDev !== void 0 ? a.hasTriggerDev === true : void 0,
14327
- workDirectory: a.workDirectory !== void 0 ? String(a.workDirectory) : void 0
14391
+ workDirectory: a.workDirectory !== void 0 ? String(a.workDirectory) : void 0,
14392
+ internalTicketProjectId: a.internalTicketProjectId !== void 0 ? String(a.internalTicketProjectId).trim() || null : void 0
14328
14393
  });
14329
14394
  return {
14330
14395
  content: [
@@ -14335,7 +14400,8 @@ ${lines.join("\n\n")}`
14335
14400
  profileId: profile.id,
14336
14401
  name: result.profile.name,
14337
14402
  updated: true,
14338
- stagesReconfigured: result.stagesReconfigured
14403
+ stagesReconfigured: result.stagesReconfigured,
14404
+ internalTicketProjectId: result.profile.internal_ticket_project_id ?? null
14339
14405
  },
14340
14406
  null,
14341
14407
  2
@@ -14749,5 +14815,3 @@ main().catch((err) => {
14749
14815
  console.error("Fatal error:", err);
14750
14816
  process.exit(1);
14751
14817
  });
14752
- //# sourceMappingURL=index.js.map
14753
- //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgsoftwarebv/mg-dashboard-mcp",
3
- "version": "7.4.2",
3
+ "version": "7.4.4",
4
4
  "description": "MCP Server for MG Dashboard - SSH, SFTP, Docker, domains, DNS, and environment config tools for Cursor",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "@types/node": "^20.0.0",
42
42
  "@types/ssh2": "^1.15.0",
43
43
  "tsup": "^8.0.0",
44
- "typescript": "^5.0.0"
44
+ "typescript": "^7.0.2"
45
45
  },
46
46
  "files": [
47
47
  "dist"
package/dist/index.d.ts DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env node
2
- interface SshConnectionOptions {
3
- hostname: string;
4
- port: number;
5
- username: string;
6
- password?: string;
7
- privateKey?: string;
8
- passphrase?: string;
9
- timeout?: number;
10
- }
11
- type SftpWriteInput = {
12
- content: string;
13
- } | {
14
- sourcePath: string;
15
- };
16
- type CopyEndpoint = {
17
- kind: "ssh";
18
- opts: SshConnectionOptions;
19
- proxy?: SshConnectionOptions;
20
- path: string;
21
- } | {
22
- kind: "r2";
23
- bucket: string;
24
- key: string;
25
- };
26
-
27
- export type { CopyEndpoint, SftpWriteInput };