@clipform/mcp-server 3.0.0 → 3.2.1

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.
@@ -6,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-KCF7ZVDI.js";
9
+ } from "./chunk-EQ6WNIA5.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -15,7 +15,7 @@ import {
15
15
  getGuideUri,
16
16
  guideFallbackText,
17
17
  registerResources
18
- } from "./chunk-EXV5M7YV.js";
18
+ } from "./chunk-236IGTEY.js";
19
19
  import {
20
20
  ACTIVE_NODE_TYPES,
21
21
  CONTACT_FIELDS,
@@ -35,11 +35,12 @@ import {
35
35
  resolveFormType,
36
36
  structuredResult,
37
37
  textResult
38
- } from "./chunk-KNOCYZHG.js";
38
+ } from "./chunk-VZKMOMFJ.js";
39
39
  import {
40
40
  getMcpAuth,
41
+ runWithMcpAuth,
41
42
  runWithMcpTool
42
- } from "./chunk-HZMMOSEM.js";
43
+ } from "./chunk-A5CFGL27.js";
43
44
  import {
44
45
  __commonJS,
45
46
  __export,
@@ -114,7 +115,7 @@ var require_code = __commonJS({
114
115
  }
115
116
  exports._ = _;
116
117
  var plus = new _Code("+");
117
- function str(strs, ...args) {
118
+ function str2(strs, ...args) {
118
119
  const expr = [safeStringify(strs[0])];
119
120
  let i = 0;
120
121
  while (i < args.length) {
@@ -125,7 +126,7 @@ var require_code = __commonJS({
125
126
  optimize(expr);
126
127
  return new _Code(expr);
127
128
  }
128
- exports.str = str;
129
+ exports.str = str2;
129
130
  function addCodeArg(code, arg) {
130
131
  if (arg instanceof _Code)
131
132
  code.push(...arg._items);
@@ -168,7 +169,7 @@ var require_code = __commonJS({
168
169
  return;
169
170
  }
170
171
  function strConcat(c1, c2) {
171
- return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`;
172
+ return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
172
173
  }
173
174
  exports.strConcat = strConcat;
174
175
  function interpolate(x) {
@@ -1130,22 +1131,22 @@ var require_util = __commonJS({
1130
1131
  return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
1131
1132
  }
1132
1133
  exports.schemaRefOrVal = schemaRefOrVal;
1133
- function unescapeFragment(str) {
1134
- return unescapeJsonPointer(decodeURIComponent(str));
1134
+ function unescapeFragment(str2) {
1135
+ return unescapeJsonPointer(decodeURIComponent(str2));
1135
1136
  }
1136
1137
  exports.unescapeFragment = unescapeFragment;
1137
- function escapeFragment(str) {
1138
- return encodeURIComponent(escapeJsonPointer(str));
1138
+ function escapeFragment(str2) {
1139
+ return encodeURIComponent(escapeJsonPointer(str2));
1139
1140
  }
1140
1141
  exports.escapeFragment = escapeFragment;
1141
- function escapeJsonPointer(str) {
1142
- if (typeof str == "number")
1143
- return `${str}`;
1144
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
1142
+ function escapeJsonPointer(str2) {
1143
+ if (typeof str2 == "number")
1144
+ return `${str2}`;
1145
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
1145
1146
  }
1146
1147
  exports.escapeJsonPointer = escapeJsonPointer;
1147
- function unescapeJsonPointer(str) {
1148
- return str.replace(/~1/g, "/").replace(/~0/g, "~");
1148
+ function unescapeJsonPointer(str2) {
1149
+ return str2.replace(/~1/g, "/").replace(/~0/g, "~");
1149
1150
  }
1150
1151
  exports.unescapeJsonPointer = unescapeJsonPointer;
1151
1152
  function eachItem(xs, f) {
@@ -2170,8 +2171,8 @@ var require_json_schema_traverse = __commonJS({
2170
2171
  post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
2171
2172
  }
2172
2173
  }
2173
- function escapeJsonPtr(str) {
2174
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
2174
+ function escapeJsonPtr(str2) {
2175
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
2175
2176
  }
2176
2177
  }
2177
2178
  });
@@ -3235,10 +3236,10 @@ var require_utils = __commonJS({
3235
3236
  return { host, isIPV6: false };
3236
3237
  }
3237
3238
  }
3238
- function findToken(str, token) {
3239
+ function findToken(str2, token) {
3239
3240
  let ind = 0;
3240
- for (let i = 0; i < str.length; i++) {
3241
- if (str[i] === token) ind++;
3241
+ for (let i = 0; i < str2.length; i++) {
3242
+ if (str2[i] === token) ind++;
3242
3243
  }
3243
3244
  return ind;
3244
3245
  }
@@ -3975,7 +3976,7 @@ var require_core = __commonJS({
3975
3976
  var util_1 = require_util();
3976
3977
  var $dataRefSchema = require_data();
3977
3978
  var uri_1 = require_uri();
3978
- var defaultRegExp = (str, flags) => new RegExp(str, flags);
3979
+ var defaultRegExp = (str2, flags) => new RegExp(str2, flags);
3979
3980
  defaultRegExp.code = "new RegExp";
3980
3981
  var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
3981
3982
  var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
@@ -4770,16 +4771,16 @@ var require_ucs2length = __commonJS({
4770
4771
  "../../node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/ucs2length.js"(exports) {
4771
4772
  "use strict";
4772
4773
  Object.defineProperty(exports, "__esModule", { value: true });
4773
- function ucs2length(str) {
4774
- const len = str.length;
4774
+ function ucs2length(str2) {
4775
+ const len = str2.length;
4775
4776
  let length = 0;
4776
4777
  let pos = 0;
4777
4778
  let value;
4778
4779
  while (pos < len) {
4779
4780
  length++;
4780
- value = str.charCodeAt(pos++);
4781
+ value = str2.charCodeAt(pos++);
4781
4782
  if (value >= 55296 && value <= 56319 && pos < len) {
4782
- value = str.charCodeAt(pos);
4783
+ value = str2.charCodeAt(pos);
4783
4784
  if ((value & 64512) === 56320)
4784
4785
  pos++;
4785
4786
  }
@@ -6662,8 +6663,8 @@ var require_formats = __commonJS({
6662
6663
  }
6663
6664
  var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
6664
6665
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
6665
- function date3(str) {
6666
- const matches = DATE.exec(str);
6666
+ function date3(str2) {
6667
+ const matches = DATE.exec(str2);
6667
6668
  if (!matches)
6668
6669
  return false;
6669
6670
  const year = +matches[1];
@@ -6682,8 +6683,8 @@ var require_formats = __commonJS({
6682
6683
  }
6683
6684
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6684
6685
  function getTime(strictTimeZone) {
6685
- return function time3(str) {
6686
- const matches = TIME.exec(str);
6686
+ return function time3(str2) {
6687
+ const matches = TIME.exec(str2);
6687
6688
  if (!matches)
6688
6689
  return false;
6689
6690
  const hr = +matches[1];
@@ -6729,8 +6730,8 @@ var require_formats = __commonJS({
6729
6730
  var DATE_TIME_SEPARATOR = /t|\s/i;
6730
6731
  function getDateTime(strictTimeZone) {
6731
6732
  const time3 = getTime(strictTimeZone);
6732
- return function date_time(str) {
6733
- const dateTime = str.split(DATE_TIME_SEPARATOR);
6733
+ return function date_time(str2) {
6734
+ const dateTime = str2.split(DATE_TIME_SEPARATOR);
6734
6735
  return dateTime.length === 2 && date3(dateTime[0]) && time3(dateTime[1]);
6735
6736
  };
6736
6737
  }
@@ -6755,13 +6756,13 @@ var require_formats = __commonJS({
6755
6756
  }
6756
6757
  var NOT_URI_FRAGMENT = /\/|:/;
6757
6758
  var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
6758
- function uri(str) {
6759
- return NOT_URI_FRAGMENT.test(str) && URI.test(str);
6759
+ function uri(str2) {
6760
+ return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
6760
6761
  }
6761
6762
  var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
6762
- function byte(str) {
6763
+ function byte(str2) {
6763
6764
  BYTE.lastIndex = 0;
6764
- return BYTE.test(str);
6765
+ return BYTE.test(str2);
6765
6766
  }
6766
6767
  var MIN_INT32 = -(2 ** 31);
6767
6768
  var MAX_INT32 = 2 ** 31 - 1;
@@ -6775,11 +6776,11 @@ var require_formats = __commonJS({
6775
6776
  return true;
6776
6777
  }
6777
6778
  var Z_ANCHOR = /[^\\]\\Z/;
6778
- function regex(str) {
6779
- if (Z_ANCHOR.test(str))
6779
+ function regex(str2) {
6780
+ if (Z_ANCHOR.test(str2))
6780
6781
  return false;
6781
6782
  try {
6782
- new RegExp(str);
6783
+ new RegExp(str2);
6783
6784
  return true;
6784
6785
  } catch (e) {
6785
6786
  return false;
@@ -7115,14 +7116,14 @@ function promiseAllObject(promisesObj) {
7115
7116
  }
7116
7117
  function randomString(length = 10) {
7117
7118
  const chars = "abcdefghijklmnopqrstuvwxyz";
7118
- let str = "";
7119
+ let str2 = "";
7119
7120
  for (let i = 0; i < length; i++) {
7120
- str += chars[Math.floor(Math.random() * chars.length)];
7121
+ str2 += chars[Math.floor(Math.random() * chars.length)];
7121
7122
  }
7122
- return str;
7123
+ return str2;
7123
7124
  }
7124
- function esc(str) {
7125
- return JSON.stringify(str);
7125
+ function esc(str2) {
7126
+ return JSON.stringify(str2);
7126
7127
  }
7127
7128
  var captureStackTrace = Error.captureStackTrace ? Error.captureStackTrace : (..._args) => {
7128
7129
  };
@@ -7210,8 +7211,8 @@ var getParsedType = (data) => {
7210
7211
  };
7211
7212
  var propertyKeyTypes = /* @__PURE__ */ new Set(["string", "number", "symbol"]);
7212
7213
  var primitiveTypes = /* @__PURE__ */ new Set(["string", "number", "bigint", "boolean", "symbol", "undefined"]);
7213
- function escapeRegex(str) {
7214
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7214
+ function escapeRegex(str2) {
7215
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7215
7216
  }
7216
7217
  function clone(inst, def, params) {
7217
7218
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -17187,6 +17188,48 @@ function applyNodeDefaults(node) {
17187
17188
  }
17188
17189
  }
17189
17190
 
17191
+ // src/lib/workspace-target.ts
17192
+ var PINNED_WORKSPACE_ERROR = "This connection is pinned to one workspace, so a workspace argument can't be used. Omit it to act in the connection's workspace.";
17193
+ async function runWithWorkspaceTarget(args, run) {
17194
+ const input = args && typeof args === "object" ? args : {};
17195
+ const workspaceId = typeof input.workspace_id === "string" && input.workspace_id ? input.workspace_id : void 0;
17196
+ const formId = typeof input.form_id === "string" && input.form_id ? input.form_id : void 0;
17197
+ if (!workspaceId && !formId) return run();
17198
+ const ctx = getMcpAuth();
17199
+ const resolve = ctx?.resolveWorkspace;
17200
+ if (!resolve) {
17201
+ if (workspaceId) return errorResult(PINNED_WORKSPACE_ERROR);
17202
+ return run();
17203
+ }
17204
+ if (workspaceId && !formId && workspaceId === ctx.default_workspace_id) {
17205
+ return run();
17206
+ }
17207
+ let resolved;
17208
+ try {
17209
+ resolved = await resolve({
17210
+ ...workspaceId ? { workspace_id: workspaceId } : {},
17211
+ ...formId ? { form_id: formId } : {}
17212
+ });
17213
+ } catch (err) {
17214
+ return errorResult(err instanceof Error ? err.message : "Could not resolve that workspace.");
17215
+ }
17216
+ if (!resolved) return run();
17217
+ return runWithMcpAuth(
17218
+ {
17219
+ ...ctx,
17220
+ workspace_id: resolved.workspace_id,
17221
+ api_key: resolved.api_key
17222
+ },
17223
+ run
17224
+ );
17225
+ }
17226
+ var workspaceIdInput = external_exports.string().uuid().optional().describe(
17227
+ "Workspace to act in (id from clipform_whoami workspaces). Omit to use the connection's default workspace."
17228
+ );
17229
+ var formIdTargetInput = external_exports.string().uuid().optional().describe(
17230
+ "The form this is for (from clipform_create_form) - a routing hint only, to pick the right workspace. The result still lands in the workspace library, not on a node. Required on an anonymous session, or this lands in a different workspace and clipform_attach_node_media will not find it."
17231
+ );
17232
+
17190
17233
  // src/tools/create-form.ts
17191
17234
  function registerCreateFormTool(server) {
17192
17235
  server.registerTool(
@@ -17211,6 +17254,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17211
17254
  ]
17212
17255
  }`,
17213
17256
  inputSchema: {
17257
+ workspace_id: workspaceIdInput,
17214
17258
  title: external_exports.string().describe("Form title"),
17215
17259
  nodes: external_exports.array(NodeSchema).min(1).describe("Ordered list of nodes/steps for the form"),
17216
17260
  show_step_counter: external_exports.boolean().optional().describe("Show step counter (e.g. '1/5'). Set true for quizzes."),
@@ -17229,7 +17273,8 @@ Example: A form that asks a question, collects contact info, then finishes:
17229
17273
  viewer_url: external_exports.string().describe("Form URL for respondents - a live public URL once authenticated/published, or a scoped preview-only link for an anonymous session"),
17230
17274
  dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit)"),
17231
17275
  nodes: external_exports.array(external_exports.object({ id: external_exports.string(), type: external_exports.string(), prompt: external_exports.string() })).describe("Created nodes in order - IDs for attach_node_media / update_node"),
17232
- plan: external_exports.object({ name: external_exports.string(), auth_mode: external_exports.string() }).optional().describe("Plan and auth context for this session")
17276
+ plan: external_exports.object({ name: external_exports.string(), auth_mode: external_exports.string() }).optional().describe("Plan and auth context for this session"),
17277
+ workspace_name: external_exports.string().optional().describe("Name of the workspace the form was created in")
17233
17278
  },
17234
17279
  annotations: {
17235
17280
  readOnlyHint: false,
@@ -17342,6 +17387,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17342
17387
  isPreview ? `Form created as a preview draft.` : `Form created and live.`,
17343
17388
  ``,
17344
17389
  `Title: ${title}`,
17390
+ ...planContext?.is_authenticated && planContext.workspace_name ? [`Workspace: ${planContext.workspace_name}`] : [],
17345
17391
  `Form ID: ${formId}`,
17346
17392
  ``,
17347
17393
  `Node IDs (use directly with attach_node_media / update_node - no get_form round-trip needed):`,
@@ -17389,15 +17435,111 @@ Example: A form that asks a question, collects contact info, then finishes:
17389
17435
  // runtime. structuredContent should return only relevant data (ChatGPT
17390
17436
  // App Directory response-hygiene policy, #797).
17391
17437
  nodes: createdNodes.map((n) => ({ id: n.id, type: n.type, prompt: n.prompt })),
17392
- ...planContext ? { plan: { name: planContext.plan_name, auth_mode: planContext.auth_mode } } : {}
17438
+ ...planContext ? { plan: { name: planContext.plan_name, auth_mode: planContext.auth_mode } } : {},
17439
+ ...planContext?.is_authenticated && planContext.workspace_name ? { workspace_name: planContext.workspace_name } : {}
17393
17440
  }
17394
17441
  };
17395
17442
  }
17396
17443
  );
17397
17444
  }
17398
17445
 
17399
- // src/tools/import-form.ts
17400
- function detectImportPlatform(url) {
17446
+ // ../form-sources/dist/types.js
17447
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
17448
+ var str = (v) => typeof v === "string" ? v : "";
17449
+ var DEFAULT_QUESTION_CAP = 50;
17450
+ var MAX_OPTIONS = 10;
17451
+
17452
+ // ../form-sources/dist/patterns.js
17453
+ var firstMatch = (url, patterns) => {
17454
+ for (const re of patterns) {
17455
+ const id = re.exec(url)?.[1];
17456
+ if (id)
17457
+ return id;
17458
+ }
17459
+ return null;
17460
+ };
17461
+ var VIDEOASK_PATTERNS = [
17462
+ /^https?:\/\/api\.videoask\.com\/forms\/sharing\/(f[a-z0-9]{8})(?:[/?#]|$)/i,
17463
+ /^https?:\/\/(?:www\.)?videoask\.com\/(f[a-z0-9]{8})(?:[/?#]|$)/i
17464
+ ];
17465
+ var detectVideoAskId = (url) => firstMatch(url, VIDEOASK_PATTERNS);
17466
+ var TYPEFORM_ATTR_KINDS = {
17467
+ "data-tf-widget": "inline",
17468
+ "data-tf-popup": "popup",
17469
+ "data-tf-slider": "slider",
17470
+ "data-tf-sidetab": "slider",
17471
+ "data-tf-popover": "popover"
17472
+ };
17473
+ var TALLY_ATTR_KINDS = {
17474
+ "data-tally-src": "inline",
17475
+ "data-tally-open": "popup"
17476
+ };
17477
+ var EMBED_ATTR_NAMES = [...Object.keys(TYPEFORM_ATTR_KINDS), ...Object.keys(TALLY_ATTR_KINDS)];
17478
+ function pushUnique(out, ref) {
17479
+ if (!out.some((r) => r.id === ref.id))
17480
+ out.push(ref);
17481
+ }
17482
+ function detectVideoAskScript(text) {
17483
+ const out = [];
17484
+ if (!/VIDEOASK_EMBED_CONFIG/.test(text))
17485
+ return out;
17486
+ for (const m of text.matchAll(/videoask\.com\/(f[a-z0-9]{8})(?![a-z0-9])/gi))
17487
+ pushUnique(out, { id: m[1].toLowerCase() });
17488
+ return out;
17489
+ }
17490
+
17491
+ // ../form-sources/dist/providers/videoask.js
17492
+ function optionText(o) {
17493
+ if (typeof o === "string")
17494
+ return o;
17495
+ if (isObj(o))
17496
+ return str(o.label) || str(o.text) || str(o.value) || str(o.title);
17497
+ return "";
17498
+ }
17499
+ var videoask = {
17500
+ id: "videoask",
17501
+ detect: detectVideoAskId,
17502
+ detectScript: detectVideoAskScript,
17503
+ fetchSpec: (id) => ({ url: `https://api.videoask.com/forms/sharing/${id}`, responseType: "json" }),
17504
+ /**
17505
+ * `GET api.videoask.com/forms/sharing/<id>`. Personal/account fields
17506
+ * (`author_id`, `organization_id`, ...) are dropped by construction: only the
17507
+ * fields below are copied.
17508
+ */
17509
+ summarize(id, json, opts) {
17510
+ if (!isObj(json) || !Array.isArray(json.questions))
17511
+ throw new Error("not a VideoAsk form payload");
17512
+ const transcriptCap = opts?.transcriptCap;
17513
+ const questions = json.questions.slice(0, opts?.questionCap ?? DEFAULT_QUESTION_CAP).map((q) => {
17514
+ const o = isObj(q) ? q : {};
17515
+ const transcript = str(o.transcription);
17516
+ return {
17517
+ label: str(o.label),
17518
+ type: str(o.type),
17519
+ options: (Array.isArray(o.options) ? o.options : []).map(optionText).filter(Boolean).slice(0, MAX_OPTIONS),
17520
+ transcript: transcriptCap === void 0 ? transcript : transcript.slice(0, transcriptCap)
17521
+ };
17522
+ });
17523
+ return {
17524
+ provider: "videoask",
17525
+ id,
17526
+ title: str(json.title),
17527
+ questions,
17528
+ meta: {
17529
+ ...str(json.created_at) ? { created_at: str(json.created_at) } : {},
17530
+ ...str(json.updated_at) ? { last_edited_at: str(json.updated_at) } : {},
17531
+ // hide_branding is a paid-plan feature.
17532
+ paid_plan: json.hide_branding === true,
17533
+ asks_contact: ["requires_contact_email", "requires_contact_info", "requires_contact_name", "requires_contact_phone_number"].some((k) => json[k] === true),
17534
+ ...str(json.status) ? { status: str(json.status) } : {}
17535
+ }
17536
+ };
17537
+ }
17538
+ };
17539
+
17540
+ // ../form-sources/dist/index.js
17541
+ var IMPORTABLE_PROVIDERS = ["google", "typeform", "tally"];
17542
+ function detectProvider(url) {
17401
17543
  let parsed;
17402
17544
  try {
17403
17545
  parsed = new URL(url);
@@ -17405,17 +17547,22 @@ function detectImportPlatform(url) {
17405
17547
  return null;
17406
17548
  }
17407
17549
  const hostname2 = parsed.hostname.toLowerCase();
17408
- if (hostname2 === "docs.google.com" || hostname2 === "forms.gle") {
17550
+ if (hostname2 === "docs.google.com" || hostname2 === "forms.gle")
17409
17551
  return "google";
17410
- }
17411
- if (hostname2.includes("typeform.com") && parsed.pathname.includes("/to/")) {
17552
+ if (hostname2.includes("typeform.com") && parsed.pathname.includes("/to/"))
17412
17553
  return "typeform";
17413
- }
17414
- if (hostname2.includes("tally.so")) {
17554
+ if (hostname2.includes("tally.so"))
17415
17555
  return "tally";
17416
- }
17556
+ if (videoask.detect(url))
17557
+ return "videoask";
17417
17558
  return null;
17418
17559
  }
17560
+
17561
+ // src/tools/import-form.ts
17562
+ function detectImportPlatform(url) {
17563
+ const provider = detectProvider(url);
17564
+ return provider && IMPORTABLE_PROVIDERS.includes(provider) ? provider : null;
17565
+ }
17419
17566
  function pluralQuestions(n) {
17420
17567
  return `${n} question${n === 1 ? "" : "s"}`;
17421
17568
  }
@@ -17426,6 +17573,7 @@ function registerImportFormTool(server) {
17426
17573
  title: "Import Form",
17427
17574
  description: `Convert a public Google Form, Typeform, or Tally form into a new Clipform. Supported URLs: Google Forms (docs.google.com or forms.gle - must be shared as "Anyone with the link"), Typeform (form.typeform.com/to/...), and Tally (tally.so - must be public). The source form must be publicly accessible; a private form's questions cannot be read. The new Clipform is created as a DRAFT - review it, then republish with clipform_update_form (is_live: true) once it's ready. Some question types don't map cleanly to a Clipform node and are skipped rather than guessed at; the result always reports exactly what was imported vs skipped, and why.`,
17428
17575
  inputSchema: {
17576
+ workspace_id: workspaceIdInput,
17429
17577
  url: external_exports.string().url().describe("Public URL of the Google Form, Typeform, or Tally form to import")
17430
17578
  },
17431
17579
  outputSchema: {
@@ -17438,7 +17586,8 @@ function registerImportFormTool(server) {
17438
17586
  skipped: external_exports.number().describe("Questions that could not be converted"),
17439
17587
  skipped_details: external_exports.array(external_exports.record(external_exports.string(), external_exports.string())).describe("Per-skipped-question title, reason, and source question type"),
17440
17588
  warnings: external_exports.array(external_exports.string()).describe("Non-fatal issues surfaced during the import")
17441
- })
17589
+ }),
17590
+ workspace_name: external_exports.string().optional().describe("Name of the workspace the form was created in")
17442
17591
  },
17443
17592
  annotations: {
17444
17593
  readOnlyHint: false,
@@ -17462,6 +17611,7 @@ function registerImportFormTool(server) {
17462
17611
  }
17463
17612
  const me = meResult.data;
17464
17613
  const workspaceId = me.workspace?.id ?? null;
17614
+ const workspaceName = me.auth_mode && me.auth_mode !== "anonymous" ? me.workspace?.name ?? void 0 : void 0;
17465
17615
  if (!workspaceId) {
17466
17616
  return errorResult(
17467
17617
  "No workspace available. This usually means authentication is misconfigured - check your API key or OAuth connection."
@@ -17504,7 +17654,9 @@ function registerImportFormTool(server) {
17504
17654
  `Pass form_id (the UUID above, not a share_id) to follow-up tools (get_form, add_node, update_node, etc.).`
17505
17655
  );
17506
17656
  }
17657
+ if (formId && workspaceName) lines.push(``, `Created in workspace '${workspaceName}'.`);
17507
17658
  return structuredResult(lines.join("\n"), {
17659
+ ...workspaceName ? { workspace_name: workspaceName } : {},
17508
17660
  form_id: formId,
17509
17661
  viewer_url: viewerUrl,
17510
17662
  import_summary: {
@@ -17528,6 +17680,7 @@ function registerListFormsTool(server) {
17528
17680
  title: "List Clipforms",
17529
17681
  description: `List forms in your workspace with optional filtering. Returns paginated results (cursor-based). next_cursor is included when more results exist. To find or open a form by name, pass search with part of its title instead of paging through results.`,
17530
17682
  inputSchema: {
17683
+ workspace_id: workspaceIdInput,
17531
17684
  limit: external_exports.number().int().min(1).max(100).optional().describe("Number of forms to return (default 25, max 100)"),
17532
17685
  cursor: external_exports.string().optional().describe(
17533
17686
  "Pagination cursor from previous response's next_cursor"
@@ -18347,7 +18500,8 @@ function registerWhoamiTool(server) {
18347
18500
  node_limit: external_exports.number().nullable().describe("Max nodes per form, or null if unlimited"),
18348
18501
  form_limit: external_exports.number().nullable().describe("Max forms in the workspace, or null if unlimited")
18349
18502
  }).describe("Plan limits for the active workspace"),
18350
- workspaces: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string() })).optional().describe("All workspaces in the caller's company - only present for session (dashboard) auth"),
18503
+ workspaces: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string() })).optional().describe("Workspaces the caller can target with workspace_id - present for session (dashboard) auth and the OAuth connector"),
18504
+ default_workspace_id: external_exports.string().optional().describe("OAuth connector only: the workspace used when a tool call omits workspace_id"),
18351
18505
  links: external_exports.object({
18352
18506
  dashboard: external_exports.string().describe("Dashboard home for the active workspace."),
18353
18507
  forms: external_exports.string().describe("Forms list for the active workspace.")
@@ -18385,11 +18539,26 @@ function registerWhoamiTool(server) {
18385
18539
  if (Array.isArray(me.workspaces)) {
18386
18540
  structured.workspaces = me.workspaces.map((w) => ({ id: w.id, name: w.name }));
18387
18541
  }
18542
+ const ctx = getMcpAuth();
18543
+ if (!structured.workspaces && ctx?.listWorkspaces) {
18544
+ try {
18545
+ const list = await ctx.listWorkspaces();
18546
+ structured.workspaces = list.map((w) => ({ id: w.id, name: w.name }));
18547
+ if (ctx.default_workspace_id) structured.default_workspace_id = ctx.default_workspace_id;
18548
+ } catch {
18549
+ }
18550
+ }
18388
18551
  if (me.links && typeof me.links.dashboard === "string" && typeof me.links.forms === "string") {
18389
18552
  structured.links = { dashboard: me.links.dashboard, forms: me.links.forms };
18390
18553
  }
18391
18554
  const workspaceLine = workspace ? `workspace '${workspace.name}'` : "no workspace (anonymous)";
18392
18555
  const lines = [`Auth: ${authMode}. ${workspaceLine}. Plan: ${plan.name}.`];
18556
+ if (Array.isArray(structured.workspaces) && ctx?.listWorkspaces) {
18557
+ const list = structured.workspaces;
18558
+ lines.push(
18559
+ `Workspaces you can act in (pass one of these ids as the workspace argument to target it; omit it to use the default): ` + list.map((w) => `${w.name} (${w.id})`).join(", ")
18560
+ );
18561
+ }
18393
18562
  if (structured.links) {
18394
18563
  const links = structured.links;
18395
18564
  lines.push(`Dashboard: ${links.dashboard}`, `Forms list: ${links.forms}`);
@@ -18578,7 +18747,8 @@ function registerDeleteNodeTool(server) {
18578
18747
 
18579
18748
  // src/tools/upload-media-asset.ts
18580
18749
  var MediaAssetItemSchema = external_exports.object({
18581
- media_type: external_exports.enum(["video", "still"]).describe("Type of media"),
18750
+ media_type: external_exports.enum(["video", "still", "document"]).describe("Type of media. Use document for a consent-agreement PDF (url required, PDF only)."),
18751
+ filename: external_exports.string().max(200).optional().describe("Display filename for a document (media_type document). Defaults to the URL's file name."),
18582
18752
  media_source: external_exports.enum(["uploaded", "recorded"]).default("uploaded").describe("How the media was created"),
18583
18753
  url: external_exports.string().url().optional().describe("Public URL that links directly to the media file itself. When provided, the server fetches and stores it directly."),
18584
18754
  captions: external_exports.array(
@@ -18624,9 +18794,16 @@ function mapFileEntryToItem(file) {
18624
18794
  if (category === "video") {
18625
18795
  return { ok: true, item: { media_type: "video", media_source: "uploaded", url: file.download_url } };
18626
18796
  }
18797
+ if (mimeType === "application/pdf") {
18798
+ const filename = file.name ?? file.file_name;
18799
+ return {
18800
+ ok: true,
18801
+ item: { media_type: "document", media_source: "uploaded", url: file.download_url, ...filename ? { filename } : {} }
18802
+ };
18803
+ }
18627
18804
  return {
18628
18805
  ok: false,
18629
- error: `Attached file type "${mimeType || "unknown"}" is not supported. Attach an image or video file, or provide a direct URL instead.`
18806
+ error: `Attached file type "${mimeType || "unknown"}" is not supported. Attach an image, video or PDF file, or provide a direct URL instead.`
18630
18807
  };
18631
18808
  }
18632
18809
  function registerUploadMediaAssetTool(server) {
@@ -18636,38 +18813,55 @@ function registerUploadMediaAssetTool(server) {
18636
18813
  title: "Upload Media Asset",
18637
18814
  description: `Put one or more media files into your workspace media library (max 10, uploaded sequentially). This is step one of attaching media to a node - follow up with clipform_attach_node_media to place the returned media_asset_id on a node.
18638
18815
 
18639
- When a public URL is provided, the media is fetched and stored automatically. For video: ingested via Mux. For image: stored in Supabase. Word-level captions from clipform_generate_tts enable per-word highlighting in the viewer once attached - pass the caption_ref it returned instead of hand-copying the captions array.
18816
+ When a public URL is provided, the media is fetched and stored automatically. The API picks where each asset is stored based on its size and type. Word-level captions from clipform_generate_tts enable per-word highlighting in the viewer once attached - pass the caption_ref it returned instead of hand-copying the captions array.
18817
+
18818
+ PDFs (media_type document) are for consent agreements: they return a document descriptor, not a media_asset_id.
18640
18819
 
18641
18820
  Provide media via \`items\` (explicit type + URL), \`files\` (files attached by the host client), or both. At least one is required.`,
18642
18821
  inputSchema: {
18822
+ workspace_id: workspaceIdInput,
18823
+ form_id: formIdTargetInput,
18643
18824
  items: external_exports.array(MediaAssetItemSchema).min(1).max(10).optional().describe("One or more media items to create as workspace library assets"),
18644
- files: external_exports.array(FileEntrySchema).min(1).max(10).optional().describe("Files attached by the host client. Each becomes a video or still library asset based on its declared mime_type.")
18825
+ files: external_exports.array(FileEntrySchema).min(1).max(10).optional().describe("Files attached by the host client. Each becomes a video or still library asset (or a consent-agreement document for application/pdf) based on its declared mime_type.")
18645
18826
  },
18646
18827
  outputSchema: {
18647
18828
  results: external_exports.array(
18648
18829
  external_exports.object({
18649
- media_asset_id: external_exports.string().optional(),
18830
+ media_asset_id: external_exports.string().optional().describe("Absent for documents"),
18650
18831
  media_type: external_exports.string(),
18832
+ document: external_exports.object({
18833
+ storage_path: external_exports.string(),
18834
+ filename: external_exports.string(),
18835
+ url: external_exports.string(),
18836
+ version: external_exports.string()
18837
+ }).optional().describe("Consent-agreement descriptor (documents only) - set as consent_items[i].document via clipform_update_node"),
18651
18838
  stored: external_exports.boolean().describe("Bytes already stored (URL-based); false when an upload_url is pending"),
18652
18839
  upload_url: external_exports.string().optional(),
18653
18840
  upload_method: external_exports.string().optional(),
18654
18841
  ok: external_exports.boolean(),
18655
18842
  error: external_exports.string().optional()
18656
18843
  })
18657
- ).describe("One result per item, in order")
18844
+ ).describe("One result per item, in order"),
18845
+ workspace_name: external_exports.string().optional().describe("Name of the workspace the media library entries were created in")
18658
18846
  },
18659
18847
  annotations: {
18660
18848
  readOnlyHint: false,
18661
18849
  destructiveHint: false,
18662
18850
  idempotentHint: false,
18663
- openWorldHint: false
18851
+ // Open-world: fetches an arbitrary caller-supplied URL (and may hand
18852
+ // larger video to a third-party streaming provider) (#4434).
18853
+ openWorldHint: true
18664
18854
  },
18665
18855
  // Apps SDK convention (openai-apps-sdk-examples#185): a host client only
18666
18856
  // hydrates a file attachment into a TOP-LEVEL tool argument named here -
18667
18857
  // same mechanism create-form.ts uses for anthropic/alwaysLoad.
18668
18858
  _meta: { "openai/fileParams": ["files"] }
18669
18859
  },
18670
- async ({ items, files }) => {
18860
+ // `form_id` is consumed centrally by `runWithWorkspaceTarget` (server.ts)
18861
+ // BEFORE this handler runs - it's a routing hint only (#4621), never
18862
+ // forwarded to the API: this route takes no form_id and /me below still
18863
+ // resolves the (now-targeted) workspace.
18864
+ async ({ items, files, form_id: _form_id }) => {
18671
18865
  if ((!items || items.length === 0) && (!files || files.length === 0)) {
18672
18866
  return errorResult("Provide at least one media item via `items` or `files`.");
18673
18867
  }
@@ -18681,6 +18875,7 @@ Provide media via \`items\` (explicit type + URL), \`files\` (files attached by
18681
18875
  }
18682
18876
  const me = meResult.data;
18683
18877
  const workspaceId = me.workspace?.id ?? null;
18878
+ const workspaceName = me.auth_mode && me.auth_mode !== "anonymous" ? me.workspace?.name ?? void 0 : void 0;
18684
18879
  if (!workspaceId) {
18685
18880
  return errorResult(
18686
18881
  "No workspace available. This usually means authentication is misconfigured - check your API key or OAuth connection."
@@ -18712,6 +18907,7 @@ Provide media via \`items\` (explicit type + URL), \`files\` (files attached by
18712
18907
  media_source: item.media_source
18713
18908
  };
18714
18909
  if (item.url) createBody.url = item.url;
18910
+ if (item.filename) createBody.filename = item.filename;
18715
18911
  if (item.captions) createBody.captions = item.captions;
18716
18912
  if (item.caption_ref) createBody.caption_ref = item.caption_ref;
18717
18913
  if (item.ai_generated !== void 0) createBody.ai_generated = item.ai_generated;
@@ -18722,6 +18918,33 @@ Provide media via \`items\` (explicit type + URL), \`files\` (files attached by
18722
18918
  results.push({ media_type: item.media_type, stored: false, ok: false, error: created.error });
18723
18919
  continue;
18724
18920
  }
18921
+ if (item.media_type === "document") {
18922
+ const d = created.data;
18923
+ if (typeof d.storage_path !== "string" || typeof d.url !== "string" || typeof d.version !== "string" || !d.version) {
18924
+ const error2 = "document upload did not return a document descriptor.";
18925
+ lines.push(`FAILED: ${error2}`);
18926
+ lines.push("");
18927
+ results.push({ media_type: "document", stored: false, ok: false, error: error2 });
18928
+ continue;
18929
+ }
18930
+ const document = {
18931
+ storage_path: d.storage_path,
18932
+ filename: String(d.filename ?? item.filename ?? "agreement.pdf"),
18933
+ url: d.url,
18934
+ version: d.version
18935
+ };
18936
+ successCount++;
18937
+ results.push({ media_type: "document", stored: true, ok: true, document });
18938
+ lines.push(
18939
+ [
18940
+ `Document stored: ${document.filename}`,
18941
+ `Descriptor: ${JSON.stringify(document)}`,
18942
+ `Set this on a Details node consent item via clipform_update_node: consent_items[i].document = <descriptor>, require_scroll_to_accept: true. Do not use clipform_attach_node_media.`
18943
+ ].join("\n")
18944
+ );
18945
+ lines.push("");
18946
+ continue;
18947
+ }
18725
18948
  const mediaAssetId = created.data.media_asset_id;
18726
18949
  if (!mediaAssetId) {
18727
18950
  const error2 = `upload did not return a media asset id (route ${String(created.data.route ?? "unknown")}).`;
@@ -18763,9 +18986,12 @@ Provide media via \`items\` (explicit type + URL), \`files\` (files attached by
18763
18986
  lines.unshift(`Media asset upload: ${successCount}/${entries.length} succeeded
18764
18987
  `);
18765
18988
  }
18766
- lines.push(`Attach these to nodes with clipform_attach_node_media.`);
18989
+ if (results.some((r) => r.ok && r.media_asset_id)) {
18990
+ lines.push(`Attach these to nodes with clipform_attach_node_media.`);
18991
+ }
18767
18992
  if (successCount === 0) return errorResult(lines.join("\n"));
18768
- return structuredResult(lines.join("\n"), { results });
18993
+ if (workspaceName) lines.push(`Workspace: ${workspaceName}`);
18994
+ return structuredResult(lines.join("\n"), { results, ...workspaceName ? { workspace_name: workspaceName } : {} });
18769
18995
  }
18770
18996
  );
18771
18997
  }
@@ -18776,9 +19002,10 @@ function registerCompleteMediaUploadTool(server) {
18776
19002
  "clipform_complete_media_upload",
18777
19003
  {
18778
19004
  title: "Complete Media Upload",
18779
- description: `Confirm a signed-PUT still image upload finished, after PUTting the bytes to the upload_url returned by clipform_upload_media_asset. The API verifies the object actually landed in storage before flipping the asset from processing to ready - call this right after the PUT succeeds, or the asset stays invisible in the library. Not needed for video uploads (TUS/Mux settle automatically).`,
19005
+ description: `Confirm a signed-PUT still image upload finished, after PUTting the bytes to the upload_url returned by clipform_upload_media_asset. The API verifies the object actually landed in storage before flipping the asset from processing to ready - call this right after the PUT succeeds, or the asset stays invisible in the library. Not needed for video uploads (they settle automatically).`,
18780
19006
  inputSchema: {
18781
- media_asset_id: external_exports.string().uuid().describe("The media_asset_id returned by clipform_upload_media_asset for the still image")
19007
+ media_asset_id: external_exports.string().uuid().describe("The media_asset_id returned by clipform_upload_media_asset for the still image"),
19008
+ form_id: formIdTargetInput
18782
19009
  },
18783
19010
  outputSchema: {
18784
19011
  media_asset_id: external_exports.string(),
@@ -18791,7 +19018,10 @@ function registerCompleteMediaUploadTool(server) {
18791
19018
  openWorldHint: false
18792
19019
  }
18793
19020
  },
18794
- async ({ media_asset_id }) => {
19021
+ // `form_id` (#4621) is a routing hint only, consumed centrally by
19022
+ // `runWithWorkspaceTarget` (server.ts) before this handler runs - never
19023
+ // forwarded to the API below (this route takes no form_id).
19024
+ async ({ media_asset_id, form_id: _form_id }) => {
18795
19025
  const meResult = await callApi("/me", { method: "GET" });
18796
19026
  if (!meResult.ok) {
18797
19027
  return errorResult(`Unable to determine workspace: ${meResult.error}`);
@@ -18970,7 +19200,7 @@ function registerDeleteNodeMediaTool(server) {
18970
19200
  "clipform_delete_node_media",
18971
19201
  {
18972
19202
  title: "Delete Node Media",
18973
- description: `Remove media from a node. Deletes the media record and cleans up external resources (Mux video asset, storage file). Requires user confirmation before it runs.`,
19203
+ description: `Remove media from a node. Deletes the media record and the stored media asset (including any copy held by a third-party video provider). Requires user confirmation before it runs.`,
18974
19204
  inputSchema: {
18975
19205
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
18976
19206
  node_id: external_exports.string().describe("The node ID"),
@@ -18982,7 +19212,9 @@ function registerDeleteNodeMediaTool(server) {
18982
19212
  // Deleting already-removed media has no further effect - textbook
18983
19213
  // idempotent delete (#2976).
18984
19214
  idempotentHint: true,
18985
- openWorldHint: false
19215
+ // Open-world: also deletes any copy held by a third-party video
19216
+ // provider (#4434).
19217
+ openWorldHint: true
18986
19218
  }
18987
19219
  },
18988
19220
  async ({ form_id, node_id, confirm }) => {
@@ -19124,10 +19356,10 @@ function registerSearchNewsTool(server) {
19124
19356
  server.registerTool(
19125
19357
  "clipform_search_news",
19126
19358
  {
19127
- title: "Search News (fallback)",
19128
- description: `Fallback news lookup for clients without native web search. Returns structured current-news articles from NewsAPI and The Guardian.
19359
+ title: "Search Recent News",
19360
+ description: `Search recent news articles from NewsAPI and The Guardian and return them as structured results.
19129
19361
 
19130
- Coverage: recent events, people, and topics (post-May-2025). Does NOT cover timeless topics (history, geography, science). Narrower and less current than native web search tools (WebSearch, web fetch) when available.
19362
+ Coverage: recent events, people, and topics (post-May-2025). Does NOT cover timeless topics (history, geography, science).
19131
19363
 
19132
19364
  Returns: article title, source, author, date, URL, description, and image URL per result.`,
19133
19365
  inputSchema: {
@@ -19212,6 +19444,8 @@ Use the tone parameter to direct HOW the voice speaks. Always set a tone that ma
19212
19444
 
19213
19445
  Pass one item or many (max 10) - multiple items run in parallel. Returns audio_url and caption_ref per item - pass caption_ref downstream to clipform_render_composition / clipform_render_video_template / clipform_upload_media_asset to attach this run's word-level captions instead of hand-copying the captions array.`,
19214
19446
  inputSchema: {
19447
+ workspace_id: workspaceIdInput,
19448
+ form_id: formIdTargetInput,
19215
19449
  items: external_exports.array(TtsItemSchema).min(1).max(10).describe("One or more TTS items to generate")
19216
19450
  },
19217
19451
  outputSchema: {
@@ -19227,7 +19461,11 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio_u
19227
19461
  },
19228
19462
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19229
19463
  },
19230
- async ({ items }) => {
19464
+ // `form_id` (#4621) is a routing hint only, consumed centrally by
19465
+ // `runWithWorkspaceTarget` (server.ts) before this handler runs - it
19466
+ // swaps the auth context's workspace_id read below, but is never
19467
+ // forwarded to the API itself (the /internal/tts route takes no form_id).
19468
+ async ({ items, form_id: _form_id }) => {
19231
19469
  const workspace_id = getMcpAuth()?.workspace_id;
19232
19470
  const results = await Promise.allSettled(
19233
19471
  items.map(
@@ -20632,7 +20870,7 @@ function createServer(options) {
20632
20870
  config2,
20633
20871
  (...args) => runWithMcpTool(name, async () => {
20634
20872
  try {
20635
- return await handler(...args);
20873
+ return await runWithWorkspaceTarget(args[0], () => handler(...args));
20636
20874
  } catch (err) {
20637
20875
  const error2 = err instanceof Error ? err : new Error(String(err));
20638
20876
  options?.onToolError?.(name, error2);
@@ -20685,4 +20923,4 @@ export {
20685
20923
  buildInstructions,
20686
20924
  createServer
20687
20925
  };
20688
- //# sourceMappingURL=chunk-QBPE32AV.js.map
20926
+ //# sourceMappingURL=chunk-J7727374.js.map