@formfeed/mcp 0.1.0 → 0.2.0

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.
Files changed (2) hide show
  1. package/bin/formfeed-mcp.mjs +410 -53
  2. package/package.json +1 -1
@@ -1155,16 +1155,16 @@ var require_numeric_data = __commonJS({
1155
1155
  return NumericData.getBitsLength(this.data.length);
1156
1156
  };
1157
1157
  NumericData.prototype.write = function write(bitBuffer) {
1158
- let i, group, value;
1158
+ let i, group2, value;
1159
1159
  for (i = 0; i + 3 <= this.data.length; i += 3) {
1160
- group = this.data.substr(i, 3);
1161
- value = parseInt(group, 10);
1160
+ group2 = this.data.substr(i, 3);
1161
+ value = parseInt(group2, 10);
1162
1162
  bitBuffer.put(value, 10);
1163
1163
  }
1164
1164
  const remainingNum = this.data.length - i;
1165
1165
  if (remainingNum > 0) {
1166
- group = this.data.substr(i);
1167
- value = parseInt(group, 10);
1166
+ group2 = this.data.substr(i);
1167
+ value = parseInt(group2, 10);
1168
1168
  bitBuffer.put(value, remainingNum * 3 + 1);
1169
1169
  }
1170
1170
  };
@@ -20735,14 +20735,14 @@ var require_liquid_node = __commonJS({
20735
20735
  constructor(token, remainTokens, liquid) {
20736
20736
  super(token, remainTokens, liquid);
20737
20737
  this.candidates = [];
20738
- const group = this.tokenizer.readValue();
20738
+ const group2 = this.tokenizer.readValue();
20739
20739
  this.tokenizer.skipBlank();
20740
- if (group) {
20740
+ if (group2) {
20741
20741
  if (this.tokenizer.peek() === ":") {
20742
- this.group = group;
20742
+ this.group = group2;
20743
20743
  this.tokenizer.advance();
20744
20744
  } else
20745
- this.candidates.push(group);
20745
+ this.candidates.push(group2);
20746
20746
  }
20747
20747
  while (!this.tokenizer.end()) {
20748
20748
  const value = this.tokenizer.readValue();
@@ -20753,8 +20753,8 @@ var require_liquid_node = __commonJS({
20753
20753
  this.tokenizer.assert(this.candidates.length, () => `empty candidates: "${token.getText()}"`);
20754
20754
  }
20755
20755
  *render(ctx, emitter) {
20756
- const group = yield evalToken(this.group, ctx);
20757
- const fingerprint = `cycle:${group}:` + this.candidates.join(",");
20756
+ const group2 = yield evalToken(this.group, ctx);
20757
+ const fingerprint = `cycle:${group2}:` + this.candidates.join(",");
20758
20758
  const groups = ctx.getRegister("cycle", {});
20759
20759
  let idx = groups[fingerprint];
20760
20760
  if (idx === void 0) {
@@ -77815,7 +77815,7 @@ var formatHelpers = [
77815
77815
  name: "asset",
77816
77816
  doc: {
77817
77817
  signature: "asset(name)",
77818
- description: "URL of a workspace asset (logo, background) on the CDN.",
77818
+ description: "URL of a file in the workspace library (logo, background) on the CDN. Upload it on the Files page or with `POST /files` and use the name it has there; relative image paths resolve against the same library.",
77819
77819
  example: `<img src="{{ asset('logo.png') }}">`,
77820
77820
  category: "document"
77821
77821
  },
@@ -78308,6 +78308,12 @@ function defaultHelpers() {
78308
78308
  // packages/engine/src/lib/engines/handlebars.ts
78309
78309
  var import_handlebars = __toESM(require_lib2());
78310
78310
 
78311
+ // packages/engine/src/lib/unavailable.ts
78312
+ var UNAVAILABLE = {};
78313
+ function unavailableHelper(name) {
78314
+ return UNAVAILABLE[name];
78315
+ }
78316
+
78311
78317
  // packages/engine/src/lib/analysis/tags.ts
78312
78318
  function rangeAt(line, column, length) {
78313
78319
  return {
@@ -78487,7 +78493,10 @@ function missingPathDiagnostics(variables, sampleData, loopSources, builtinRoots
78487
78493
  const root = v2.path[0];
78488
78494
  if (!root || builtinRoots4.has(root)) continue;
78489
78495
  let path = v2.path;
78490
- if (loopSources.has(root)) {
78496
+ if (v2.source !== void 0) {
78497
+ if (!v2.source) continue;
78498
+ path = [...v2.source, ...v2.path.slice(1)];
78499
+ } else if (loopSources.has(root)) {
78491
78500
  const source = loopSources.get(root);
78492
78501
  if (!source) continue;
78493
78502
  path = [...source, ...v2.path.slice(1)];
@@ -78746,6 +78755,13 @@ function analyzeHandlebars(source, opts = {}) {
78746
78755
  range: f2.range,
78747
78756
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
78748
78757
  });
78758
+ } else if (unavailableHelper(f2.name)) {
78759
+ diagnostics.push({
78760
+ severity: "warning",
78761
+ code: "unavailable-helper",
78762
+ message: unavailableHelper(f2.name),
78763
+ range: f2.range
78764
+ });
78749
78765
  } else if (!f2.known) {
78750
78766
  diagnostics.push({
78751
78767
  severity: "error",
@@ -78854,6 +78870,151 @@ var handlebarsEngine = {
78854
78870
 
78855
78871
  // packages/engine/src/lib/engines/jinja2.ts
78856
78872
  var import_nunjucks = __toESM(require_nunjucks());
78873
+
78874
+ // packages/engine/src/lib/engines/python-format.ts
78875
+ var SPEC = /^(?:([\s\S])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?([,_])?(?:\.(\d+))?([bcdeEfFgGnosxX%])?$/;
78876
+ function group(digits, separator) {
78877
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
78878
+ }
78879
+ function pythonExponent(text2) {
78880
+ return text2.replace(/e([+-])(\d)$/i, (m2, sign, digit) => `${m2[0]}${sign}0${digit}`);
78881
+ }
78882
+ function formatNumber(value, type, precision, alternate) {
78883
+ if (!Number.isFinite(value)) return Number.isNaN(value) ? "nan" : "inf";
78884
+ const abs = Math.abs(value);
78885
+ switch (type) {
78886
+ case "f":
78887
+ case "F":
78888
+ return abs.toFixed(precision ?? 6);
78889
+ case "%":
78890
+ return `${(abs * 100).toFixed(precision ?? 6)}%`;
78891
+ case "e":
78892
+ case "E": {
78893
+ const text2 = pythonExponent(abs.toExponential(precision ?? 6));
78894
+ return type === "E" ? text2.toUpperCase() : text2;
78895
+ }
78896
+ case "g":
78897
+ case "G": {
78898
+ const p = precision === 0 ? 1 : precision ?? 6;
78899
+ const exp = abs === 0 ? 0 : Math.floor(Math.log10(abs));
78900
+ let text2 = exp < -4 || exp >= p ? pythonExponent(abs.toExponential(p - 1)) : abs.toFixed(Math.max(0, p - 1 - exp));
78901
+ if (!alternate) {
78902
+ const [mantissa = "", exponent] = text2.split("e");
78903
+ const trimmed = mantissa.includes(".") ? mantissa.replace(/0+$/, "").replace(/\.$/, "") : mantissa;
78904
+ text2 = exponent === void 0 ? trimmed : `${trimmed}e${exponent}`;
78905
+ }
78906
+ return type === "G" ? text2.toUpperCase() : text2;
78907
+ }
78908
+ case "d":
78909
+ case "n":
78910
+ return String(Math.trunc(abs));
78911
+ case "b":
78912
+ return (alternate ? "0b" : "") + Math.trunc(abs).toString(2);
78913
+ case "o":
78914
+ return (alternate ? "0o" : "") + Math.trunc(abs).toString(8);
78915
+ case "x":
78916
+ return (alternate ? "0x" : "") + Math.trunc(abs).toString(16);
78917
+ case "X":
78918
+ return (alternate ? "0X" : "") + Math.trunc(abs).toString(16).toUpperCase();
78919
+ default:
78920
+ return precision === void 0 ? String(abs) : formatNumber(abs, "g", precision, alternate);
78921
+ }
78922
+ }
78923
+ function formatValue(value, spec) {
78924
+ const m2 = SPEC.exec(spec);
78925
+ if (!m2) return String(value ?? "");
78926
+ const [, fillChar, alignChar, sign, alt, zero, widthText, grouping, precisionText, type = ""] = m2;
78927
+ const precision = precisionText === void 0 ? void 0 : Number(precisionText);
78928
+ const width = widthText === void 0 ? 0 : Number(widthText);
78929
+ const numericType = /[bdeEfFgGnoxX%]/.test(type);
78930
+ const asNumber = typeof value === "number" ? value : typeof value === "boolean" ? Number(value) : numericType && value !== null && value !== void 0 && String(value).trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : void 0;
78931
+ let body;
78932
+ let prefix = "";
78933
+ if (asNumber !== void 0 && type !== "s" && type !== "c") {
78934
+ body = formatNumber(asNumber, type, precision, Boolean(alt));
78935
+ if (grouping) {
78936
+ const [int = "", ...rest] = body.split(".");
78937
+ const radix = /^0[box]/i.exec(int)?.[0] ?? "";
78938
+ body = radix + group(int.slice(radix.length), grouping) + (rest.length ? `.${rest.join(".")}` : "");
78939
+ }
78940
+ const negative = asNumber < 0 || Object.is(asNumber, -0);
78941
+ prefix = negative ? "-" : sign === "+" ? "+" : sign === " " ? " " : "";
78942
+ } else if (type === "c" && asNumber !== void 0) {
78943
+ body = String.fromCodePoint(asNumber);
78944
+ } else {
78945
+ body = String(value ?? "None");
78946
+ if (precision !== void 0 && !numericType) body = body.slice(0, precision);
78947
+ }
78948
+ const isNumber = asNumber !== void 0 && type !== "s";
78949
+ const align = alignChar ?? (zero && isNumber ? "=" : isNumber ? ">" : "<");
78950
+ const fill = fillChar ?? (zero && !alignChar ? "0" : " ");
78951
+ const pad = Math.max(0, width - prefix.length - body.length);
78952
+ switch (align) {
78953
+ case "<":
78954
+ return prefix + body + fill.repeat(pad);
78955
+ case "^":
78956
+ return fill.repeat(Math.floor(pad / 2)) + prefix + body + fill.repeat(Math.ceil(pad / 2));
78957
+ case "=":
78958
+ return prefix + fill.repeat(pad) + body;
78959
+ default:
78960
+ return fill.repeat(pad) + prefix + body;
78961
+ }
78962
+ }
78963
+ function lookupField(name, positional, named, next) {
78964
+ const [head = "", ...rest] = name.split(/(?=[.[])/);
78965
+ let value = head === "" ? positional[next()] : /^\d+$/.test(head) ? positional[Number(head)] : named[head];
78966
+ for (const part of rest) {
78967
+ const key = part.startsWith("[") ? part.slice(1, -1) : part.slice(1);
78968
+ value = value !== null && typeof value === "object" ? value[Array.isArray(value) && /^\d+$/.test(key) ? Number(key) : key] : void 0;
78969
+ }
78970
+ return value;
78971
+ }
78972
+ function pythonFormat(template, positional, named = {}) {
78973
+ let auto = 0;
78974
+ const next = () => auto++;
78975
+ const render = (text2) => {
78976
+ let out = "";
78977
+ for (let i = 0; i < text2.length; i++) {
78978
+ const ch = text2[i];
78979
+ if (ch === "}") {
78980
+ out += "}";
78981
+ if (text2[i + 1] === "}") i++;
78982
+ continue;
78983
+ }
78984
+ if (ch !== "{") {
78985
+ out += ch;
78986
+ continue;
78987
+ }
78988
+ if (text2[i + 1] === "{") {
78989
+ out += "{";
78990
+ i++;
78991
+ continue;
78992
+ }
78993
+ let depth = 1;
78994
+ let j2 = i + 1;
78995
+ for (; j2 < text2.length && depth > 0; j2++) {
78996
+ if (text2[j2] === "{") depth++;
78997
+ else if (text2[j2] === "}") depth--;
78998
+ }
78999
+ if (depth > 0) {
79000
+ out += text2.slice(i);
79001
+ break;
79002
+ }
79003
+ const field = text2.slice(i + 1, j2 - 1);
79004
+ const colon = field.indexOf(":");
79005
+ const head = colon < 0 ? field : field.slice(0, colon);
79006
+ const name = head.replace(/![rsa]$/, "");
79007
+ const value = lookupField(name, positional, named, next);
79008
+ const spec = colon < 0 ? "" : render(field.slice(colon + 1));
79009
+ out += value === void 0 ? "" : formatValue(value, spec);
79010
+ i = j2 - 1;
79011
+ }
79012
+ return out;
79013
+ };
79014
+ return render(template);
79015
+ }
79016
+
79017
+ // packages/engine/src/lib/engines/jinja2.ts
78857
79018
  var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
78858
79019
  "abs",
78859
79020
  "batch",
@@ -78908,7 +79069,12 @@ var builtinRoots2 = /* @__PURE__ */ new Set([
78908
79069
  "joiner",
78909
79070
  "lipsum",
78910
79071
  "varargs",
78911
- "kwargs"
79072
+ "kwargs",
79073
+ "namespace",
79074
+ // Jinja2's spelling of the literals, which the compat layer resolves at runtime
79075
+ "True",
79076
+ "False",
79077
+ "None"
78912
79078
  ]);
78913
79079
  var stringMethods = {
78914
79080
  upper: (s) => s.toUpperCase(),
@@ -78937,12 +79103,8 @@ var stringMethods = {
78937
79103
  split: (s, sep, max) => sep === void 0 || sep === null ? s.trim().split(/\s+/) : s.split(String(sep), max === void 0 ? void 0 : Number(max) + 1),
78938
79104
  join: (s, list2) => Array.isArray(list2) ? list2.join(s) : String(list2),
78939
79105
  format: (s, ...args2) => {
78940
- let i = 0;
78941
- const named = args2.at(-1) && typeof args2.at(-1) === "object" ? args2.at(-1) : {};
78942
- return s.replace(
78943
- /\{(\w*)\}/g,
78944
- (_2, key) => String(key ? named[key] ?? "" : args2[i++] ?? "")
78945
- );
79106
+ const named = keywordArgs(args2);
79107
+ return pythonFormat(s, named ? args2.slice(0, -1) : args2, named ?? {});
78946
79108
  },
78947
79109
  zfill: (s, width) => s.padStart(Number(width), "0"),
78948
79110
  count: (s, sub) => s.split(String(sub)).length - 1,
@@ -78957,6 +79119,92 @@ var stringMethods = {
78957
79119
  return f2.repeat(left) + s + f2.repeat(total - left);
78958
79120
  }
78959
79121
  };
79122
+ function keywordArgs(args2) {
79123
+ const last = args2.at(-1);
79124
+ if (!last || typeof last !== "object" || !Object.prototype.hasOwnProperty.call(last, "__keywords")) return void 0;
79125
+ const { __keywords: _marker, ...named } = last;
79126
+ return named;
79127
+ }
79128
+ var JinjaNamespace = class {
79129
+ };
79130
+ function namespace(...args2) {
79131
+ const ns = new JinjaNamespace();
79132
+ const named = keywordArgs(args2);
79133
+ for (const arg of named ? [...args2.slice(0, -1), named] : args2)
79134
+ if (arg && typeof arg === "object") Object.assign(ns, arg);
79135
+ return ns;
79136
+ }
79137
+ function setNamespaceAttribute(target, key, value) {
79138
+ if (!(target instanceof JinjaNamespace)) throw new Error("Cannot assign attribute on non-namespace object");
79139
+ target[String(key)] = value;
79140
+ }
79141
+ var toList = (v2) => Array.isArray(v2) ? v2 : v2 === void 0 || v2 === null ? [] : typeof v2 === "string" ? [...v2] : [v2];
79142
+ var attributeOf = (item, path) => String(path ?? "").split(".").reduce((acc, key) => acc !== null && typeof acc === "object" ? acc[key] : void 0, item);
79143
+ var jinjaTests = {
79144
+ none: (v2) => v2 === null,
79145
+ true: (v2) => v2 === true,
79146
+ false: (v2) => v2 === false,
79147
+ in: (v2, container) => typeof container === "string" ? container.includes(String(v2)) : Array.isArray(container) ? container.includes(v2) : !!container && typeof container === "object" && String(v2) in container,
79148
+ "==": (v2, a) => v2 === a,
79149
+ "!=": (v2, a) => v2 !== a,
79150
+ ">": (v2, a) => v2 > a,
79151
+ ">=": (v2, a) => v2 >= a,
79152
+ "<": (v2, a) => v2 < a,
79153
+ "<=": (v2, a) => v2 <= a
79154
+ };
79155
+ function runTest(self2, name, value, arg) {
79156
+ const own = jinjaTests[name];
79157
+ if (own) return own(value, arg);
79158
+ return self2.env.getTest(name).call(self2, value, arg) === true;
79159
+ }
79160
+ function installJinjaFilters(env) {
79161
+ const lookup = env;
79162
+ const helperTruncate = lookup.getFilter("truncate");
79163
+ lookup.addFilter("truncate", function(value, ...args2) {
79164
+ const named = keywordArgs(args2);
79165
+ const p = named ? args2.slice(0, -1) : args2;
79166
+ if (!named && typeof p[1] !== "boolean" && p.length < 3) return helperTruncate.call(this, value, ...args2);
79167
+ const s = String(value ?? "");
79168
+ const length = Number(p[0] ?? named?.["length"] ?? 255);
79169
+ const killwords = Boolean(p[1] ?? named?.["killwords"] ?? false);
79170
+ const end = String(p[2] ?? named?.["end"] ?? "...");
79171
+ const leeway = Number(p[3] ?? named?.["leeway"] ?? 5);
79172
+ if (s.length <= length + leeway) return s;
79173
+ const cut = s.slice(0, Math.max(0, length - end.length));
79174
+ if (killwords) return cut + end;
79175
+ const space = cut.lastIndexOf(" ");
79176
+ return (space >= 0 ? cut.slice(0, space) : cut) + end;
79177
+ });
79178
+ const helperMap = lookup.getFilter("map");
79179
+ lookup.addFilter("map", function(value, ...args2) {
79180
+ const named = keywordArgs(args2);
79181
+ const p = named ? args2.slice(0, -1) : args2;
79182
+ if (named && "attribute" in named)
79183
+ return toList(value).map((item) => {
79184
+ const v2 = attributeOf(item, named["attribute"]);
79185
+ return v2 === void 0 && "default" in named ? named["default"] : v2;
79186
+ });
79187
+ const items = toList(value);
79188
+ if (typeof p[0] === "string" && !items.some((item) => item !== null && typeof item === "object")) {
79189
+ let filter;
79190
+ try {
79191
+ filter = lookup.getFilter(p[0]);
79192
+ } catch {
79193
+ filter = void 0;
79194
+ }
79195
+ if (filter) return items.map((item) => filter.call(this, item, ...p.slice(1)));
79196
+ }
79197
+ return helperMap.call(this, value, ...args2);
79198
+ });
79199
+ const byAttribute = (keep) => function(value, attribute, test, arg) {
79200
+ return toList(value).filter((item) => {
79201
+ const v2 = attributeOf(item, attribute);
79202
+ return (test === void 0 ? Boolean(v2) : runTest(this, String(test), v2, arg)) === keep;
79203
+ });
79204
+ };
79205
+ lookup.addFilter("selectattr", byAttribute(true));
79206
+ lookup.addFilter("rejectattr", byAttribute(false));
79207
+ }
78960
79208
  function trimChars(s, chars, side) {
78961
79209
  if (chars === void 0 || chars === null)
78962
79210
  return side === "both" ? s.trim() : side === "left" ? s.trimStart() : s.trimEnd();
@@ -78977,6 +79225,20 @@ function installCompat() {
78977
79225
  this.compile(node.target, frame);
78978
79226
  this._emit(")");
78979
79227
  };
79228
+ const compileSet = import_nunjucks.default.compiler.Compiler.prototype.compileSet;
79229
+ import_nunjucks.default.runtime["formfeedSetAttribute"] = setNamespaceAttribute;
79230
+ import_nunjucks.default.compiler.Compiler.prototype.compileSet = function(node, frame) {
79231
+ const [target, ...more] = node.targets;
79232
+ if (!target || more.length || target.typename !== "LookupVal") return compileSet.call(this, node, frame);
79233
+ this._emit("runtime.formfeedSetAttribute(");
79234
+ this._compileExpression(target["target"], frame);
79235
+ this._emit(", ");
79236
+ this._compileExpression(target["val"], frame);
79237
+ this._emit(", ");
79238
+ if (node.value) this._compileExpression(node.value, frame);
79239
+ else this.compile(node.body, frame);
79240
+ this._emitLine(");");
79241
+ };
78980
79242
  const original = import_nunjucks.default.runtime.memberLookup;
78981
79243
  import_nunjucks.default.runtime.memberLookup = (obj, val2, ...rest) => {
78982
79244
  if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
@@ -79023,6 +79285,8 @@ function createEnvironment(ctx) {
79023
79285
  env.addFilter(name, wrapped);
79024
79286
  env.addGlobal(name, wrapped);
79025
79287
  }
79288
+ env.addGlobal("namespace", namespace);
79289
+ installJinjaFilters(env);
79026
79290
  return env;
79027
79291
  }
79028
79292
  var validationContext2 = {
@@ -79063,6 +79327,13 @@ function pathOf(node) {
79063
79327
  function inScope(w, name) {
79064
79328
  return w.scopes.some((s) => s.has(name));
79065
79329
  }
79330
+ function sourceOf(w, name) {
79331
+ for (let i = w.scopes.length - 1; i >= 0; i--) {
79332
+ const scope = w.scopes[i];
79333
+ if (scope?.has(name)) return scope.get(name) ?? null;
79334
+ }
79335
+ return null;
79336
+ }
79066
79337
  function walk2(node, w) {
79067
79338
  if (!node || typeof node !== "object") return;
79068
79339
  switch (node.typename) {
@@ -79071,10 +79342,12 @@ function walk2(node, w) {
79071
79342
  const p = pathOf(node);
79072
79343
  if (p) {
79073
79344
  const root = p.path[0] ?? "";
79345
+ const bound = inScope(w, root);
79074
79346
  w.variables.push({
79075
79347
  path: p.path,
79076
79348
  range: nodeRange(p.root, p.path.join(".").length),
79077
- kind: inScope(w, root) ? "loop-var" : "read"
79349
+ kind: bound ? "loop-var" : "read",
79350
+ ...bound ? { source: sourceOf(w, root) } : {}
79078
79351
  });
79079
79352
  }
79080
79353
  if (node.typename === "LookupVal" && !p) {
@@ -79120,18 +79393,18 @@ function walk2(node, w) {
79120
79393
  const source = pathOf(arr);
79121
79394
  const nameNode = node["name"];
79122
79395
  const names = nameNode.typename === "Array" ? nameNode.children ?? [] : [nameNode];
79123
- const scope = /* @__PURE__ */ new Set();
79396
+ const root = source?.path[0] ?? "";
79397
+ const items = !source ? null : inScope(w, root) ? ((s) => s ? [...s, ...source.path.slice(1)] : null)(sourceOf(w, root)) : source.path;
79398
+ const scope = /* @__PURE__ */ new Map();
79124
79399
  for (const n of names) {
79125
79400
  const name = String(n.value);
79126
- scope.add(name);
79127
- w.loopSources.set(
79128
- name,
79129
- names.length === 1 && source && !inScope(w, source.path[0] ?? "") ? source.path : names.length === 1 && source ? resolveLoopSource(w, source.path) : null
79130
- );
79401
+ const bound = names.length === 1 ? items : null;
79402
+ scope.set(name, bound);
79131
79403
  w.variables.push({
79132
79404
  path: [name],
79133
79405
  range: nodeRange(n, name.length),
79134
- kind: "loop-var"
79406
+ kind: "loop-var",
79407
+ source: bound
79135
79408
  });
79136
79409
  }
79137
79410
  w.scopes.push(scope);
@@ -79149,8 +79422,7 @@ function walk2(node, w) {
79149
79422
  range: nodeRange(p.root, p.path.join(".").length),
79150
79423
  kind: "assigned"
79151
79424
  });
79152
- w.scopes[0]?.add(p.path[0] ?? "");
79153
- w.loopSources.set(p.path[0] ?? "", null);
79425
+ if (p.path.length === 1 || !inScope(w, p.path[0] ?? "")) w.scopes[0]?.set(p.path[0] ?? "", null);
79154
79426
  }
79155
79427
  }
79156
79428
  walk2(node["value"], w);
@@ -79159,17 +79431,27 @@ function walk2(node, w) {
79159
79431
  }
79160
79432
  case "Macro": {
79161
79433
  const args2 = node["args"];
79162
- const scope = /* @__PURE__ */ new Set();
79163
- for (const child of args2.children ?? [])
79164
- if (child.typename === "Symbol") scope.add(String(child.value));
79434
+ const scope = /* @__PURE__ */ new Map();
79435
+ for (const child of args2.children ?? []) {
79436
+ if (child.typename === "Symbol") scope.set(String(child.value), null);
79437
+ if (child.typename === "KeywordArgs")
79438
+ for (const pair of child.children ?? []) {
79439
+ const key = pair["key"];
79440
+ if (key) scope.set(String(key.value), null);
79441
+ walk2(pair["value"], w);
79442
+ }
79443
+ }
79165
79444
  const nameNode = node["name"];
79166
- w.scopes[0]?.add(String(nameNode.value));
79167
- w.loopSources.set(String(nameNode.value), null);
79445
+ w.scopes[0]?.set(String(nameNode.value), null);
79168
79446
  w.scopes.push(scope);
79169
79447
  walk2(node["body"], w);
79170
79448
  w.scopes.pop();
79171
79449
  return;
79172
79450
  }
79451
+ case "Pair": {
79452
+ walk2(node["value"], w);
79453
+ return;
79454
+ }
79173
79455
  case "Include":
79174
79456
  case "Extends":
79175
79457
  case "Import":
@@ -79182,15 +79464,17 @@ function walk2(node, w) {
79182
79464
  });
79183
79465
  else walk2(tpl, w);
79184
79466
  if (node.typename === "Import")
79185
- w.scopes[0]?.add(
79186
- String(node["target"]?.value ?? "")
79467
+ w.scopes[0]?.set(
79468
+ String(node["target"]?.value ?? ""),
79469
+ null
79187
79470
  );
79188
79471
  if (node.typename === "FromImport")
79189
79472
  for (const n of node["names"].children ?? [])
79190
- w.scopes[0]?.add(
79473
+ w.scopes[0]?.set(
79191
79474
  String(
79192
79475
  n.typename === "Pair" ? n["value"].value : n.value
79193
- )
79476
+ ),
79477
+ null
79194
79478
  );
79195
79479
  return;
79196
79480
  }
@@ -79208,12 +79492,6 @@ function walk2(node, w) {
79208
79492
  walk2(value, w);
79209
79493
  }
79210
79494
  }
79211
- function resolveLoopSource(w, path) {
79212
- const root = path[0] ?? "";
79213
- if (!w.loopSources.has(root)) return path;
79214
- const source = w.loopSources.get(root);
79215
- return source ? [...source, ...path.slice(1)] : null;
79216
- }
79217
79495
  function analyzeJinja2(source, opts = {}) {
79218
79496
  const helpers = opts.helpers ?? defaultHelpers();
79219
79497
  const { blocks, diagnostics } = scanBlocks(source, jinjaTags);
@@ -79221,8 +79499,7 @@ function analyzeJinja2(source, opts = {}) {
79221
79499
  variables: [],
79222
79500
  filters: [],
79223
79501
  includes: [],
79224
- loopSources: /* @__PURE__ */ new Map(),
79225
- scopes: [/* @__PURE__ */ new Set()],
79502
+ scopes: [/* @__PURE__ */ new Map()],
79226
79503
  helpers
79227
79504
  };
79228
79505
  try {
@@ -79251,6 +79528,13 @@ function analyzeJinja2(source, opts = {}) {
79251
79528
  range: f2.range,
79252
79529
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
79253
79530
  });
79531
+ } else if (unavailableHelper(f2.name)) {
79532
+ diagnostics.push({
79533
+ severity: "warning",
79534
+ code: "unavailable-helper",
79535
+ message: unavailableHelper(f2.name),
79536
+ range: f2.range
79537
+ });
79254
79538
  } else if (!f2.known) {
79255
79539
  diagnostics.push({
79256
79540
  severity: "error",
@@ -79264,7 +79548,7 @@ function analyzeJinja2(source, opts = {}) {
79264
79548
  const dataDiagnostics = missingPathDiagnostics(
79265
79549
  w.variables,
79266
79550
  opts.sampleData,
79267
- w.loopSources,
79551
+ /* @__PURE__ */ new Map(),
79268
79552
  builtin
79269
79553
  );
79270
79554
  return {
@@ -79397,6 +79681,10 @@ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
79397
79681
  "url_decode",
79398
79682
  "url_encode",
79399
79683
  "where",
79684
+ "where_exp",
79685
+ "find_exp",
79686
+ "reject_exp",
79687
+ "group_by_exp",
79400
79688
  "raw",
79401
79689
  "sum",
79402
79690
  "group_by",
@@ -79651,6 +79939,13 @@ function analyzeLiquid(source, opts = {}) {
79651
79939
  range: f2.range,
79652
79940
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
79653
79941
  });
79942
+ } else if (unavailableHelper(f2.name)) {
79943
+ diagnostics.push({
79944
+ severity: "warning",
79945
+ code: "unavailable-helper",
79946
+ message: unavailableHelper(f2.name),
79947
+ range: f2.range
79948
+ });
79654
79949
  } else if (!f2.known) {
79655
79950
  diagnostics.push({
79656
79951
  severity: "error",
@@ -80069,6 +80364,11 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
80069
80364
  }
80070
80365
  ];
80071
80366
 
80367
+ // packages/api-types/src/lib/assets.ts
80368
+ var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
80369
+ var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
80370
+ var ASSET_MAX_BYTES = 10 * 1024 * 1024;
80371
+
80072
80372
  // packages/sdk-ts/src/lib/client.ts
80073
80373
  var FormfeedError = class extends Error {
80074
80374
  constructor(code, message2, status, problem = null, requestId = null) {
@@ -80096,6 +80396,8 @@ function randomKey() {
80096
80396
  if (c?.randomUUID) return c.randomUUID();
80097
80397
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
80098
80398
  }
80399
+ var withKey = (options) => ({ ...options, idempotencyKey: options.idempotencyKey ?? randomKey() });
80400
+ var idOf = (source) => typeof source === "string" ? source : source.id;
80099
80401
  var Formfeed = class {
80100
80402
  baseUrl;
80101
80403
  apiKey;
@@ -80164,6 +80466,54 @@ var Formfeed = class {
80164
80466
  idempotencyKey: options.idempotencyKey ?? randomKey()
80165
80467
  })
80166
80468
  };
80469
+ /**
80470
+ * PDF tools on the outputs of earlier renders (a `Render` or its id) and on PDFs of the file library
80471
+ * (a `LibraryFile`, its id or its name). merge, protect and watermark return a new render and cost
80472
+ * 0.5 units each on live keys; info is free.
80473
+ */
80474
+ pdf = {
80475
+ merge: (sources, output = {}, options = {}) => this.request("POST", "/pdf/merge", { ...output, sources: sources.map(idOf) }, withKey(options)),
80476
+ protect: (source, protect, options = {}) => this.request("POST", "/pdf/protect", { ...protect, source: idOf(source) }, withKey(options)),
80477
+ watermark: (source, watermark, options = {}) => this.request("POST", "/pdf/watermark", { ...watermark, source: idOf(source) }, withKey(options)),
80478
+ info: (source, options = {}) => this.request("POST", "/pdf/info", { source: idOf(source) }, options)
80479
+ };
80480
+ /**
80481
+ * The workspace file library (`/files`): images and PDFs a template references by name with
80482
+ * `asset('logo.png')`, and the sources of image watermarks and merges. Needs `file:read`,
80483
+ * `file:write` or `file:delete`. Files are served without a signature, so keep confidential
80484
+ * documents out of it.
80485
+ */
80486
+ files = {
80487
+ /** Uploads a file; the same name replaces the existing file in place and keeps its URL. */
80488
+ upload: (file, options = {}) => {
80489
+ const blob = file.data instanceof Blob ? file.data : new Blob([file.data], file.contentType ? { type: file.contentType } : {});
80490
+ const form = new FormData();
80491
+ form.set("file", blob, file.name);
80492
+ form.set("name", file.name);
80493
+ return this.request("POST", "/files", form, options);
80494
+ },
80495
+ /** Page of files, newest first. */
80496
+ list: (query = {}, options = {}) => {
80497
+ const params = new URLSearchParams();
80498
+ for (const [k2, v2] of Object.entries(query)) if (v2 !== void 0 && v2 !== null && v2 !== "") params.set(k2, String(v2));
80499
+ const qs = params.toString();
80500
+ return this.request("GET", `/files${qs ? `?${qs}` : ""}`, void 0, options);
80501
+ },
80502
+ /** Every file matching the query, following the cursor. */
80503
+ all: async (query = {}, options = {}) => {
80504
+ const out = [];
80505
+ let cursor = null;
80506
+ do {
80507
+ const page = await this.files.list({ ...query, cursor }, options);
80508
+ out.push(...page.data);
80509
+ cursor = page.next_cursor;
80510
+ } while (cursor);
80511
+ return out;
80512
+ },
80513
+ get: (id, options = {}) => this.request("GET", `/files/${encodeURIComponent(id)}`, void 0, options),
80514
+ /** Removes the file and its bytes; templates that name it render a missing image afterwards. */
80515
+ delete: (file, options = {}) => this.request("DELETE", `/files/${encodeURIComponent(idOf(file))}`, void 0, options)
80516
+ };
80167
80517
  jobs = {
80168
80518
  get: (id, options = {}) => this.request("GET", `/jobs/${encodeURIComponent(id)}`, void 0, options),
80169
80519
  waitFor: async (id, options = {}) => {
@@ -80214,7 +80564,13 @@ var Formfeed = class {
80214
80564
  create: (idOrSlug, input, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions`, input, options),
80215
80565
  publish: (idOrSlug, number, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions/${number}/publish`, void 0, options)
80216
80566
  },
80217
- schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options)
80567
+ schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options),
80568
+ /**
80569
+ * Checks a version with data without rendering: syntax, header and footer, missing partials,
80570
+ * runtime errors and the data against the stored schema. Free. Without `data`, the version's
80571
+ * sample data is checked.
80572
+ */
80573
+ validate: (idOrSlug, input = {}, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/validate`, input, options)
80218
80574
  };
80219
80575
  account = {
80220
80576
  get: (options = {}) => this.request("GET", "/account", void 0, options),
@@ -80227,9 +80583,10 @@ var Formfeed = class {
80227
80583
  const headers = {
80228
80584
  authorization: `Bearer ${this.apiKey}`,
80229
80585
  accept: "application/json",
80230
- "user-agent": "formfeed-sdk-ts/0.1"
80586
+ "user-agent": "formfeed-sdk-ts/0.2"
80231
80587
  };
80232
- if (body !== void 0) headers["content-type"] = "application/json";
80588
+ const isForm = typeof FormData !== "undefined" && body instanceof FormData;
80589
+ if (body !== void 0 && !isForm) headers["content-type"] = "application/json";
80233
80590
  if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
80234
80591
  let attempt = 0;
80235
80592
  for (; ; ) {
@@ -80241,7 +80598,7 @@ var Formfeed = class {
80241
80598
  res = await this.fetchImpl(url, {
80242
80599
  method,
80243
80600
  headers,
80244
- body: body === void 0 ? void 0 : JSON.stringify(body),
80601
+ body: body === void 0 ? void 0 : isForm ? body : JSON.stringify(body),
80245
80602
  signal: controller.signal
80246
80603
  });
80247
80604
  } catch (e) {
@@ -80287,7 +80644,7 @@ var encoder2 = new TextEncoder();
80287
80644
 
80288
80645
  // packages/mcp/src/lib/server.ts
80289
80646
  import { z as z2 } from "zod";
80290
- var SERVER_INFO = { name: "formfeed", version: "0.1.0" };
80647
+ var SERVER_INFO = { name: "formfeed", version: "0.2.0" };
80291
80648
  var engineSchema = z2.enum(["jinja2", "liquid", "handlebars"]);
80292
80649
  var outputSchema = z2.enum(["pdf", "png", "jpg"]);
80293
80650
  var templateSummary = (t) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Model Context Protocol server for Formfeed: list templates, read data schemas, validate and render documents from AI agents.",
5
5
  "license": "MIT",
6
6
  "author": {