@casys/mcp-erpnext 3.1.0 → 3.2.0-beta.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.
package/mcp-erpnext.mjs CHANGED
@@ -1784,14 +1784,14 @@ var require_code3 = __commonJS({
1784
1784
  }
1785
1785
  exports.callValidateCode = callValidateCode;
1786
1786
  var newRegExp = (0, codegen_1._)`new RegExp`;
1787
- function usePattern({ gen, it: { opts } }, pattern) {
1787
+ function usePattern({ gen, it: { opts } }, pattern2) {
1788
1788
  const u = opts.unicodeRegExp ? "u" : "";
1789
1789
  const { regExp } = opts.code;
1790
- const rx = regExp(pattern, u);
1790
+ const rx = regExp(pattern2, u);
1791
1791
  return gen.scopeValue("pattern", {
1792
1792
  key: rx.toString(),
1793
1793
  ref: rx,
1794
- code: (0, codegen_1._)`${regExp.code === "new RegExp" ? newRegExp : (0, util_2.useFunc)(gen, regExp)}(${pattern}, ${u})`
1794
+ code: (0, codegen_1._)`${regExp.code === "new RegExp" ? newRegExp : (0, util_2.useFunc)(gen, regExp)}(${pattern2}, ${u})`
1795
1795
  });
1796
1796
  }
1797
1797
  exports.usePattern = usePattern;
@@ -6902,7 +6902,7 @@ var require_discriminator2 = __commonJS({
6902
6902
  error: error2,
6903
6903
  code(cxt) {
6904
6904
  const { gen, data, schema, parentSchema, it } = cxt;
6905
- const { oneOf } = parentSchema;
6905
+ const { oneOf: oneOf2 } = parentSchema;
6906
6906
  if (!it.opts.discriminator) {
6907
6907
  throw new Error("discriminator: requires discriminator option");
6908
6908
  }
@@ -6911,7 +6911,7 @@ var require_discriminator2 = __commonJS({
6911
6911
  throw new Error("discriminator: requires propertyName");
6912
6912
  if (schema.mapping)
6913
6913
  throw new Error("discriminator: mapping is not supported");
6914
- if (!oneOf)
6914
+ if (!oneOf2)
6915
6915
  throw new Error("discriminator: requires oneOf keyword");
6916
6916
  const valid = gen.let("valid", false);
6917
6917
  const tag2 = gen.const("tag", (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(tagName)}`);
@@ -6939,8 +6939,8 @@ var require_discriminator2 = __commonJS({
6939
6939
  const oneOfMapping = {};
6940
6940
  const topRequired = hasRequired(parentSchema);
6941
6941
  let tagRequired = true;
6942
- for (let i = 0; i < oneOf.length; i++) {
6943
- let sch = oneOf[i];
6942
+ for (let i = 0; i < oneOf2.length; i++) {
6943
+ let sch = oneOf2[i];
6944
6944
  if ((sch === null || sch === void 0 ? void 0 : sch.$ref) && !(0, util_1.schemaHasRulesButRef)(sch, it.self.RULES)) {
6945
6945
  const ref = sch.$ref;
6946
6946
  sch = compile_1.resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref);
@@ -9022,9 +9022,9 @@ ${indent}`) + "'";
9022
9022
  return quotedString(value, ctx);
9023
9023
  }
9024
9024
  const indent = ctx.indent || (ctx.forceBlockIndent || containsDocumentMarker(value) ? " " : "");
9025
- const literal2 = blockQuote === "literal" ? true : blockQuote === "folded" || type === Scalar.Scalar.BLOCK_FOLDED ? false : type === Scalar.Scalar.BLOCK_LITERAL ? true : !lineLengthOverLimit(value, lineWidth, indent.length);
9025
+ const literal3 = blockQuote === "literal" ? true : blockQuote === "folded" || type === Scalar.Scalar.BLOCK_FOLDED ? false : type === Scalar.Scalar.BLOCK_LITERAL ? true : !lineLengthOverLimit(value, lineWidth, indent.length);
9026
9026
  if (!value)
9027
- return literal2 ? "|\n" : ">\n";
9027
+ return literal3 ? "|\n" : ">\n";
9028
9028
  let chomp;
9029
9029
  let endStart;
9030
9030
  for (endStart = value.length; endStart > 0; --endStart) {
@@ -9073,7 +9073,7 @@ ${indent}`) + "'";
9073
9073
  if (onComment)
9074
9074
  onComment();
9075
9075
  }
9076
- if (!literal2) {
9076
+ if (!literal3) {
9077
9077
  const foldedValue = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`);
9078
9078
  let literalFallback = false;
9079
9079
  const foldOptions = getFoldOptions(ctx, true);
@@ -16837,8 +16837,8 @@ var $ZodCheckUpperCase = /* @__PURE__ */ $constructor("$ZodCheckUpperCase", (ins
16837
16837
  var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst, def) => {
16838
16838
  $ZodCheck.init(inst, def);
16839
16839
  const escapedRegex = escapeRegex(def.includes);
16840
- const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position},}${escapedRegex}` : escapedRegex);
16841
- def.pattern = pattern;
16840
+ const pattern2 = new RegExp(typeof def.position === "number" ? `^.{${def.position},}${escapedRegex}` : escapedRegex);
16841
+ def.pattern = pattern2;
16842
16842
  inst._zod.check = (payload) => {
16843
16843
  if (payload.value.includes(def.includes, def.position))
16844
16844
  return;
@@ -16855,8 +16855,8 @@ var $ZodCheckIncludes = /* @__PURE__ */ $constructor("$ZodCheckIncludes", (inst,
16855
16855
  });
16856
16856
  var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (inst, def) => {
16857
16857
  $ZodCheck.init(inst, def);
16858
- const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
16859
- def.pattern ?? (def.pattern = pattern);
16858
+ const pattern2 = new RegExp(`^${escapeRegex(def.prefix)}.*`);
16859
+ def.pattern ?? (def.pattern = pattern2);
16860
16860
  inst._zod.check = (payload) => {
16861
16861
  if (payload.value.startsWith(def.prefix))
16862
16862
  return;
@@ -16873,8 +16873,8 @@ var $ZodCheckStartsWith = /* @__PURE__ */ $constructor("$ZodCheckStartsWith", (i
16873
16873
  });
16874
16874
  var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst, def) => {
16875
16875
  $ZodCheck.init(inst, def);
16876
- const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
16877
- def.pattern ?? (def.pattern = pattern);
16876
+ const pattern2 = new RegExp(`.*${escapeRegex(def.suffix)}$`);
16877
+ def.pattern ?? (def.pattern = pattern2);
16878
16878
  inst._zod.check = (payload) => {
16879
16879
  if (payload.value.endsWith(def.suffix))
16880
16880
  return;
@@ -18435,8 +18435,8 @@ var $ZodOptional = /* @__PURE__ */ $constructor("$ZodOptional", (inst, def) => {
18435
18435
  return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;
18436
18436
  });
18437
18437
  defineLazyInternal(inst, "pattern", (zod) => {
18438
- const pattern = zod.def.innerType._zod.pattern;
18439
- return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;
18438
+ const pattern2 = zod.def.innerType._zod.pattern;
18439
+ return pattern2 ? new RegExp(`^(${cleanRegex(pattern2.source)})?$`) : void 0;
18440
18440
  });
18441
18441
  inst._zod.parse = (payload, ctx) => {
18442
18442
  if (payload.value === void 0) {
@@ -18463,8 +18463,8 @@ var $ZodNullable = /* @__PURE__ */ $constructor("$ZodNullable", (inst, def) => {
18463
18463
  defineLazyInternal(inst, "optin", (zod) => zod.def.innerType._zod.optin);
18464
18464
  defineLazyInternal(inst, "optout", (zod) => zod.def.innerType._zod.optout);
18465
18465
  defineLazyInternal(inst, "pattern", (zod) => {
18466
- const pattern = zod.def.innerType._zod.pattern;
18467
- return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;
18466
+ const pattern2 = zod.def.innerType._zod.pattern;
18467
+ return pattern2 ? new RegExp(`^(${cleanRegex(pattern2.source)}|null)$`) : void 0;
18468
18468
  });
18469
18469
  defineLazyInternal(inst, "values", (zod) => {
18470
18470
  return zod.def.innerType._zod.values ? /* @__PURE__ */ new Set([...zod.def.innerType._zod.values, null]) : void 0;
@@ -19545,12 +19545,12 @@ function _length(length, params) {
19545
19545
  });
19546
19546
  }
19547
19547
  // @__NO_SIDE_EFFECTS__
19548
- function _regex(pattern, params) {
19548
+ function _regex(pattern2, params) {
19549
19549
  return new $ZodCheckRegex({
19550
19550
  check: "string_format",
19551
19551
  format: "regex",
19552
19552
  ...normalizeParams(params),
19553
- pattern
19553
+ pattern: pattern2
19554
19554
  });
19555
19555
  }
19556
19556
  // @__NO_SIDE_EFFECTS__
@@ -20236,9 +20236,9 @@ var addDivisor = (agg, value) => {
20236
20236
  if (!agg.multipleOf.includes(value))
20237
20237
  agg.multipleOf.push(value);
20238
20238
  };
20239
- var addPattern = (agg, pattern) => {
20239
+ var addPattern = (agg, pattern2) => {
20240
20240
  agg.patterns ?? (agg.patterns = /* @__PURE__ */ new Set());
20241
- agg.patterns.add(pattern);
20241
+ agg.patterns.add(pattern2);
20242
20242
  };
20243
20243
  var intersectMime = (agg, mime) => {
20244
20244
  agg.mime = agg.mime ? agg.mime.filter((m) => mime.includes(m)) : [...mime];
@@ -20303,8 +20303,8 @@ function aggregateChecks(schema) {
20303
20303
  }
20304
20304
  if (bag.mime)
20305
20305
  intersectMime(agg, bag.mime);
20306
- for (const pattern of bag.patterns ?? [])
20307
- addPattern(agg, pattern);
20306
+ for (const pattern2 of bag.patterns ?? [])
20307
+ addPattern(agg, pattern2);
20308
20308
  return agg;
20309
20309
  }
20310
20310
  var formatMap = {
@@ -20486,11 +20486,11 @@ var nanProcessor = (schema, ctx, json, params) => {
20486
20486
  };
20487
20487
  var templateLiteralProcessor = (schema, _ctx, json, _params) => {
20488
20488
  const _json = json;
20489
- const pattern = schema._zod.pattern;
20490
- if (!pattern)
20489
+ const pattern2 = schema._zod.pattern;
20490
+ if (!pattern2)
20491
20491
  throw new Error("Pattern not found in template literal");
20492
20492
  _json.type = "string";
20493
- _json.pattern = pattern.source;
20493
+ _json.pattern = pattern2.source;
20494
20494
  };
20495
20495
  var fileProcessor = (schema, _ctx, json, _params) => {
20496
20496
  const _json = json;
@@ -20730,8 +20730,8 @@ function rewriteKeyNames(ctx) {
20730
20730
  bySchema.set(entry.schema, entry);
20731
20731
  }
20732
20732
  const rewrites = /* @__PURE__ */ new Map();
20733
- for (const record2 of pendingRecords.get(ctx) ?? []) {
20734
- const seen = ctx.seen.get(record2);
20733
+ for (const record3 of pendingRecords.get(ctx) ?? []) {
20734
+ const seen = ctx.seen.get(record3);
20735
20735
  const names = (seen?.def ?? seen?.schema)?.propertyNames;
20736
20736
  if (!names || names === true || rewrites.has(names))
20737
20737
  continue;
@@ -20761,8 +20761,8 @@ var recordProcessor = (schema, ctx, _json, params) => {
20761
20761
  path: [...params.path, "patternProperties", "*"]
20762
20762
  });
20763
20763
  json.patternProperties = {};
20764
- for (const pattern of patterns) {
20765
- assignProp(json.patternProperties, exactPattern(pattern).source, valueSchema);
20764
+ for (const pattern2 of patterns) {
20765
+ assignProp(json.patternProperties, exactPattern(pattern2).source, valueSchema);
20766
20766
  }
20767
20767
  } else {
20768
20768
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
@@ -26313,8 +26313,8 @@ function zodEmittedPattern(schema) {
26313
26313
  return typeof jsonSchema.pattern === "string" ? jsonSchema.pattern : void 0;
26314
26314
  }
26315
26315
  var DATETIME_FRACTION_DIGITS = /\\\.\\d\{(\d+)\}/;
26316
- function datetimeReferenceSchemas(pattern) {
26317
- const fractionDigits = DATETIME_FRACTION_DIGITS.exec(pattern);
26316
+ function datetimeReferenceSchemas(pattern2) {
26317
+ const fractionDigits = DATETIME_FRACTION_DIGITS.exec(pattern2);
26318
26318
  const precisions = [
26319
26319
  void 0,
26320
26320
  -1,
@@ -26327,7 +26327,7 @@ function datetimeReferenceSchemas(pattern) {
26327
26327
  precision
26328
26328
  }))));
26329
26329
  }
26330
- function referencePatternsForFormat(format, pattern) {
26330
+ function referencePatternsForFormat(format, pattern2) {
26331
26331
  let referenceSchemas;
26332
26332
  switch (format) {
26333
26333
  case "email":
@@ -26340,14 +26340,14 @@ function referencePatternsForFormat(format, pattern) {
26340
26340
  referenceSchemas = [iso_exports.date()];
26341
26341
  break;
26342
26342
  case "date-time":
26343
- referenceSchemas = datetimeReferenceSchemas(pattern);
26343
+ referenceSchemas = datetimeReferenceSchemas(pattern2);
26344
26344
  break;
26345
26345
  }
26346
26346
  return new Set(referenceSchemas.map((schema) => zodEmittedPattern(schema)).filter((emitted) => emitted !== void 0));
26347
26347
  }
26348
- function isLibraryFormatPattern(format, pattern, vendor) {
26348
+ function isLibraryFormatPattern(format, pattern2, vendor) {
26349
26349
  if (vendor !== "zod") return true;
26350
- return referencePatternsForFormat(format, pattern).has(pattern);
26350
+ return referencePatternsForFormat(format, pattern2).has(pattern2);
26351
26351
  }
26352
26352
  function promptArgumentsFromStandardSchema(schema) {
26353
26353
  const jsonSchema = standardSchemaToJsonSchema(schema, "input");
@@ -29295,14 +29295,14 @@ var require_code = /* @__PURE__ */ __commonJSMin(((exports) => {
29295
29295
  }
29296
29296
  exports.callValidateCode = callValidateCode;
29297
29297
  const newRegExp = (0, codegen_1._)`new RegExp`;
29298
- function usePattern({ gen, it: { opts } }, pattern) {
29298
+ function usePattern({ gen, it: { opts } }, pattern2) {
29299
29299
  const u = opts.unicodeRegExp ? "u" : "";
29300
29300
  const { regExp } = opts.code;
29301
- const rx = regExp(pattern, u);
29301
+ const rx = regExp(pattern2, u);
29302
29302
  return gen.scopeValue("pattern", {
29303
29303
  key: rx.toString(),
29304
29304
  ref: rx,
29305
- code: (0, codegen_1._)`${regExp.code === "new RegExp" ? newRegExp : (0, util_2.useFunc)(gen, regExp)}(${pattern}, ${u})`
29305
+ code: (0, codegen_1._)`${regExp.code === "new RegExp" ? newRegExp : (0, util_2.useFunc)(gen, regExp)}(${pattern2}, ${u})`
29306
29306
  });
29307
29307
  }
29308
29308
  exports.usePattern = usePattern;
@@ -32965,12 +32965,12 @@ var require_discriminator = /* @__PURE__ */ __commonJSMin(((exports) => {
32965
32965
  },
32966
32966
  code(cxt) {
32967
32967
  const { gen, data, schema, parentSchema, it } = cxt;
32968
- const { oneOf } = parentSchema;
32968
+ const { oneOf: oneOf2 } = parentSchema;
32969
32969
  if (!it.opts.discriminator) throw new Error("discriminator: requires discriminator option");
32970
32970
  const tagName = schema.propertyName;
32971
32971
  if (typeof tagName != "string") throw new Error("discriminator: requires propertyName");
32972
32972
  if (schema.mapping) throw new Error("discriminator: mapping is not supported");
32973
- if (!oneOf) throw new Error("discriminator: requires oneOf keyword");
32973
+ if (!oneOf2) throw new Error("discriminator: requires oneOf keyword");
32974
32974
  const valid = gen.let("valid", false);
32975
32975
  const tag2 = gen.const("tag", (0, codegen_1._)`${data}${(0, codegen_1.getProperty)(tagName)}`);
32976
32976
  gen.if((0, codegen_1._)`typeof ${tag2} == "string"`, () => validateMapping(), () => cxt.error(false, {
@@ -33008,8 +33008,8 @@ var require_discriminator = /* @__PURE__ */ __commonJSMin(((exports) => {
33008
33008
  const oneOfMapping = {};
33009
33009
  const topRequired = hasRequired(parentSchema);
33010
33010
  let tagRequired = true;
33011
- for (let i = 0; i < oneOf.length; i++) {
33012
- let sch = oneOf[i];
33011
+ for (let i = 0; i < oneOf2.length; i++) {
33012
+ let sch = oneOf2[i];
33013
33013
  if ((sch === null || sch === void 0 ? void 0 : sch.$ref) && !(0, util_1.schemaHasRulesButRef)(sch, it.self.RULES)) {
33014
33014
  const ref = sch.$ref;
33015
33015
  sch = compile_1.resolveRef.call(it.self, it.schemaEnv.root, it.baseId, ref);
@@ -39676,7 +39676,7 @@ var Node = class Node2 {
39676
39676
  let node2 = this;
39677
39677
  for (let i = 0, len = tokens.length; i < len; i++) {
39678
39678
  const token = tokens[i];
39679
- const pattern = token.length === 1 ? token === "*" ? i === len - 1 ? [
39679
+ const pattern2 = token.length === 1 ? token === "*" ? i === len - 1 ? [
39680
39680
  "",
39681
39681
  "",
39682
39682
  ".*"
@@ -39690,10 +39690,10 @@ var Node = class Node2 {
39690
39690
  TAIL_WILDCARD_REG_EXP_STR
39691
39691
  ] : token.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);
39692
39692
  let nextNode;
39693
- if (pattern) {
39694
- const name = pattern[1];
39695
- let regexpStr = pattern[2] || "[^/]+";
39696
- if (name && pattern[2]) {
39693
+ if (pattern2) {
39694
+ const name = pattern2[1];
39695
+ let regexpStr = pattern2[2] || "[^/]+";
39696
+ if (name && pattern2[2]) {
39697
39697
  if (regexpStr === ".*") throw PATH_ERROR;
39698
39698
  regexpStr = regexpStr.replace(/^\((?!\?:)(?=[^)]+\)$)/, "(?:");
39699
39699
  if (/\((?!\?:)/.test(regexpStr)) throw PATH_ERROR;
@@ -39957,16 +39957,16 @@ var Node3 = class Node4 {
39957
39957
  let i = 0;
39958
39958
  for (const p of parts) {
39959
39959
  const nextP = parts[++i];
39960
- const pattern = getPattern(p, nextP) || (nextP === void 0 && p && p.indexOf("*") === p.length - 1 ? p : null);
39961
- const isParam = Array.isArray(pattern);
39962
- const key = isParam ? pattern[0] : pattern || p;
39960
+ const pattern2 = getPattern(p, nextP) || (nextP === void 0 && p && p.indexOf("*") === p.length - 1 ? p : null);
39961
+ const isParam = Array.isArray(pattern2);
39962
+ const key = isParam ? pattern2[0] : pattern2 || p;
39963
39963
  const child = curNode.#children[key] ||= new Node4();
39964
- if (pattern && !child.#pattern) {
39965
- child.#pattern = pattern;
39964
+ if (pattern2 && !child.#pattern) {
39965
+ child.#pattern = pattern2;
39966
39966
  curNode.#patterns.push(child);
39967
39967
  }
39968
39968
  curNode = child;
39969
- if (isParam) possibleKeys.add(pattern[1]);
39969
+ if (isParam) possibleKeys.add(pattern2[1]);
39970
39970
  }
39971
39971
  curNode.#methods.push({ [method]: {
39972
39972
  handler,
@@ -40011,19 +40011,19 @@ var Node3 = class Node4 {
40011
40011
  } else tempNodes.push(nextNode);
40012
40012
  }
40013
40013
  for (const child of node2.#patterns) {
40014
- const pattern = child.#pattern;
40014
+ const pattern2 = child.#pattern;
40015
40015
  const params = node2.#params === emptyParams ? {} : { ...node2.#params };
40016
- if (typeof pattern === "string") {
40017
- if (pattern === "*" || part.startsWith(pattern.slice(0, -1))) {
40016
+ if (typeof pattern2 === "string") {
40017
+ if (pattern2 === "*" || part.startsWith(pattern2.slice(0, -1))) {
40018
40018
  this.#pushHandlerSets(handlerSets, child, method, node2.#params);
40019
- if (pattern === "*") {
40019
+ if (pattern2 === "*") {
40020
40020
  child.#params = params;
40021
40021
  tempNodes.push(child);
40022
40022
  }
40023
40023
  }
40024
40024
  continue;
40025
40025
  }
40026
- const [, name, matcher] = pattern;
40026
+ const [, name, matcher] = pattern2;
40027
40027
  if (!part && matcher === true) continue;
40028
40028
  if (matcher !== true) {
40029
40029
  if (!partOffsets) {
@@ -48086,6 +48086,115 @@ function stableStringify(value) {
48086
48086
  var DEFAULT_RETRY_STATUSES = [408, 429, 502, 503, 504];
48087
48087
  var DEFAULT_RETRY_METHODS = ["GET"];
48088
48088
  var DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
48089
+ var DEFAULT_MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024;
48090
+ var MAX_ERROR_BODY_BYTES = 64 * 1024;
48091
+ var ResponseBodyLimitError = class extends Error {
48092
+ constructor(label, size, limit) {
48093
+ super(
48094
+ `[FrappeClient] ${label} size ${size} bytes exceeds the ${limit}-byte limit`
48095
+ );
48096
+ this.name = "ResponseBodyLimitError";
48097
+ }
48098
+ };
48099
+ async function readResponseBytes(response, maxBytes, label) {
48100
+ const declaredLength = response.headers.get("content-length")?.trim();
48101
+ if (declaredLength && /^\d+$/.test(declaredLength)) {
48102
+ const size = Number(declaredLength);
48103
+ if (size > maxBytes) {
48104
+ try {
48105
+ await response.body?.cancel();
48106
+ } catch {
48107
+ }
48108
+ throw new ResponseBodyLimitError(label, size, maxBytes);
48109
+ }
48110
+ }
48111
+ if (!response.body) return new Uint8Array();
48112
+ const reader = response.body.getReader();
48113
+ const chunks = [];
48114
+ let total = 0;
48115
+ try {
48116
+ while (true) {
48117
+ const { done, value } = await reader.read();
48118
+ if (done) break;
48119
+ total += value.byteLength;
48120
+ if (total > maxBytes) {
48121
+ try {
48122
+ await reader.cancel();
48123
+ } catch {
48124
+ }
48125
+ throw new ResponseBodyLimitError(label, total, maxBytes);
48126
+ }
48127
+ chunks.push(value);
48128
+ }
48129
+ } finally {
48130
+ reader.releaseLock();
48131
+ }
48132
+ const bytes = new Uint8Array(total);
48133
+ let offset = 0;
48134
+ for (const chunk of chunks) {
48135
+ bytes.set(chunk, offset);
48136
+ offset += chunk.byteLength;
48137
+ }
48138
+ return bytes;
48139
+ }
48140
+ function isUnsafeControl(character) {
48141
+ const codePoint = character.codePointAt(0) ?? 0;
48142
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297;
48143
+ }
48144
+ function validateLocalFileUrl(fileUrl, isPrivate) {
48145
+ if (typeof fileUrl !== "string" || fileUrl.length === 0) {
48146
+ throw new Error("[FrappeClient] File.file_url must be a non-empty string");
48147
+ }
48148
+ if (fileUrl !== fileUrl.trim()) {
48149
+ throw new Error(
48150
+ "[FrappeClient] File.file_url must not contain whitespace padding"
48151
+ );
48152
+ }
48153
+ const expectedPrefix = isPrivate ? "/private/files/" : "/files/";
48154
+ let decoded = fileUrl;
48155
+ for (let depth = 0; depth < 5; depth++) {
48156
+ if (Array.from(decoded).some(isUnsafeControl) || decoded.includes("\\") || decoded.includes("?") || decoded.includes("#")) {
48157
+ throw new Error(
48158
+ "[FrappeClient] File.file_url must be a local Frappe file path without query, fragment, NUL, or backslash"
48159
+ );
48160
+ }
48161
+ if (!decoded.startsWith(expectedPrefix)) {
48162
+ throw new Error(
48163
+ `[FrappeClient] File.file_url does not match is_private=${isPrivate ? 1 : 0}`
48164
+ );
48165
+ }
48166
+ const fileSegment = decoded.slice(expectedPrefix.length);
48167
+ if (!fileSegment || fileSegment === "." || fileSegment === ".." || fileSegment.includes("/")) {
48168
+ throw new Error(
48169
+ "[FrappeClient] File.file_url must contain exactly one safe file segment"
48170
+ );
48171
+ }
48172
+ let next;
48173
+ try {
48174
+ next = decodeURIComponent(decoded);
48175
+ } catch {
48176
+ throw new Error(
48177
+ "[FrappeClient] File.file_url contains malformed percent encoding"
48178
+ );
48179
+ }
48180
+ if (next === decoded) return fileUrl;
48181
+ decoded = next;
48182
+ }
48183
+ throw new Error(
48184
+ "[FrappeClient] File.file_url contains excessive nested percent encoding"
48185
+ );
48186
+ }
48187
+ function sanitizeDownloadFileName(raw2, fileId) {
48188
+ const fallbackId = fileId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80) || "file";
48189
+ if (typeof raw2 !== "string") return `erpnext-${fallbackId}`;
48190
+ const normalized = Array.from(raw2.normalize("NFC")).filter((character) => !isUnsafeControl(character)).join("").replace(/[\\/:*?"<>|]/g, "_").trim().replace(/[. ]+$/g, "");
48191
+ const bounded = Array.from(normalized).slice(0, 255).join("");
48192
+ return !bounded || bounded === "." || bounded === ".." ? `erpnext-${fallbackId}` : bounded;
48193
+ }
48194
+ function normalizeMimeType(raw2) {
48195
+ const candidate = (raw2 ?? "").split(";", 1)[0].trim().toLowerCase();
48196
+ return /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i.test(candidate) && candidate !== "application/unknown" ? candidate : "application/octet-stream";
48197
+ }
48089
48198
  function decodeBase64File(contentBase64, maxBytes) {
48090
48199
  if (contentBase64.length === 0) {
48091
48200
  throw new Error("[FrappeClient] File content must not be empty");
@@ -48113,6 +48222,24 @@ function decodeBase64File(contentBase64, maxBytes) {
48113
48222
  }
48114
48223
  return Uint8Array.from(binary, (character) => character.charCodeAt(0));
48115
48224
  }
48225
+ function normalizeFrappeSiteUrl(baseUrl) {
48226
+ if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
48227
+ throw new Error(
48228
+ "[FrappeClient] baseUrl is required to derive site identity"
48229
+ );
48230
+ }
48231
+ let url2;
48232
+ try {
48233
+ url2 = new URL(baseUrl);
48234
+ } catch {
48235
+ throw new Error("[FrappeClient] baseUrl must be an absolute URL");
48236
+ }
48237
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
48238
+ throw new Error("[FrappeClient] baseUrl must be http or https");
48239
+ }
48240
+ const path = url2.pathname.replace(/\/+$/, "");
48241
+ return `${url2.protocol}//${url2.host}${path}`;
48242
+ }
48116
48243
  var FrappeAPIError = class extends Error {
48117
48244
  /**
48118
48245
  * @param message - Human-readable error description
@@ -48171,6 +48298,7 @@ var FrappeClient = class {
48171
48298
  authHeader;
48172
48299
  timeoutMs;
48173
48300
  maxUploadBytes;
48301
+ maxDownloadBytes;
48174
48302
  retries;
48175
48303
  retryStatuses;
48176
48304
  retryBackoffMs;
@@ -48186,12 +48314,29 @@ var FrappeClient = class {
48186
48314
  "[FrappeClient] maxUploadBytes must be a positive integer"
48187
48315
  );
48188
48316
  }
48317
+ this.maxDownloadBytes = config2.maxDownloadBytes ?? DEFAULT_MAX_DOWNLOAD_BYTES;
48318
+ if (!Number.isInteger(this.maxDownloadBytes) || this.maxDownloadBytes <= 0) {
48319
+ throw new Error(
48320
+ "[FrappeClient] maxDownloadBytes must be a positive integer"
48321
+ );
48322
+ }
48189
48323
  this.retries = config2.retries ?? 3;
48190
48324
  this.retryStatuses = config2.retryStatuses ?? DEFAULT_RETRY_STATUSES;
48191
48325
  this.retryBackoffMs = config2.retryBackoffMs ?? 200;
48192
48326
  this.retryMethods = config2.retryMethods ?? DEFAULT_RETRY_METHODS;
48193
48327
  this.cache = config2.cache ?? new MemoryCache();
48194
48328
  }
48329
+ /**
48330
+ * Normalized site URL of this configured client: origin + site path, with
48331
+ * credentials, query, and fragment stripped, and without a trailing slash.
48332
+ *
48333
+ * This is the preimage of the Buy capture `sourceInstance.siteId` SHA-256.
48334
+ * It is not an ERP database UUID and must not be copied onto the wire when
48335
+ * the opaque site identifier is sufficient.
48336
+ */
48337
+ normalizedSiteUrl() {
48338
+ return normalizeFrappeSiteUrl(this.baseUrl);
48339
+ }
48195
48340
  // ── Private HTTP helpers ────────────────────────────────────────────────────
48196
48341
  buildHeaders(includeJsonContentType = true) {
48197
48342
  const headers = {
@@ -48300,6 +48445,152 @@ var FrappeClient = class {
48300
48445
  }
48301
48446
  return responseBody;
48302
48447
  }
48448
+ buildSameOriginUrl(path) {
48449
+ if (!path.startsWith("/")) {
48450
+ throw new Error("[FrappeClient] Download path must be root-relative");
48451
+ }
48452
+ let base;
48453
+ let target;
48454
+ try {
48455
+ base = new URL(this.baseUrl);
48456
+ target = new URL(`${this.baseUrl}${path}`);
48457
+ } catch {
48458
+ throw new Error("[FrappeClient] baseUrl must be a valid absolute URL");
48459
+ }
48460
+ if (target.origin !== base.origin) {
48461
+ throw new Error("[FrappeClient] Refusing a cross-origin download");
48462
+ }
48463
+ return target.href;
48464
+ }
48465
+ async requestBinary(path) {
48466
+ let lastError;
48467
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
48468
+ try {
48469
+ return await this.requestBinaryOnce(path);
48470
+ } catch (err) {
48471
+ lastError = err;
48472
+ if (attempt === this.retries || !this.isRetryable(err, "GET")) {
48473
+ throw err;
48474
+ }
48475
+ const delay = this.computeBackoff(attempt, err);
48476
+ if (delay > 0) {
48477
+ await new Promise((resolve) => setTimeout(resolve, delay));
48478
+ }
48479
+ }
48480
+ }
48481
+ throw lastError;
48482
+ }
48483
+ async requestBinaryOnce(path) {
48484
+ const url2 = this.buildSameOriginUrl(path);
48485
+ const controller = new AbortController();
48486
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
48487
+ let response;
48488
+ try {
48489
+ response = await fetch(url2, {
48490
+ method: "GET",
48491
+ headers: {
48492
+ "Authorization": this.authHeader,
48493
+ "Accept": "*/*",
48494
+ "Cache-Control": "no-store"
48495
+ },
48496
+ cache: "no-store",
48497
+ redirect: "manual",
48498
+ signal: controller.signal
48499
+ });
48500
+ } catch (err) {
48501
+ clearTimeout(timer);
48502
+ if (err instanceof Error && err.name === "AbortError") {
48503
+ throw new FrappeAPIError(
48504
+ `Request timed out after ${this.timeoutMs}ms: GET ${path}`,
48505
+ 408,
48506
+ null
48507
+ );
48508
+ }
48509
+ throw new FrappeAPIError(
48510
+ `Network error on GET ${path}: ${err.message}`,
48511
+ 0,
48512
+ null
48513
+ );
48514
+ }
48515
+ try {
48516
+ if (response.redirected || response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) {
48517
+ try {
48518
+ await response.body?.cancel();
48519
+ } catch {
48520
+ }
48521
+ throw new FrappeAPIError(
48522
+ `GET ${path} refused an HTTP redirect`,
48523
+ response.status,
48524
+ null
48525
+ );
48526
+ }
48527
+ if (!response.ok) {
48528
+ let rawText;
48529
+ try {
48530
+ const errorBytes = await readResponseBytes(
48531
+ response,
48532
+ MAX_ERROR_BODY_BYTES,
48533
+ "Error response body"
48534
+ );
48535
+ rawText = new TextDecoder().decode(errorBytes);
48536
+ } catch (err) {
48537
+ if (!(err instanceof ResponseBodyLimitError)) throw err;
48538
+ rawText = err.message;
48539
+ }
48540
+ const contentType = response.headers.get("content-type") ?? "";
48541
+ let responseBody = rawText;
48542
+ if (contentType.includes("application/json") && rawText.length > 0) {
48543
+ try {
48544
+ responseBody = JSON.parse(rawText);
48545
+ } catch {
48546
+ responseBody = rawText;
48547
+ }
48548
+ }
48549
+ let message2 = response.statusText;
48550
+ if (typeof responseBody === "object" && responseBody !== null) {
48551
+ const body = responseBody;
48552
+ const baseMessage = body.message ?? body.exc_type ?? response.statusText;
48553
+ const serverDetails = extractServerMessages(body._server_messages);
48554
+ message2 = serverDetails ? `${baseMessage}: ${serverDetails}` : baseMessage;
48555
+ } else if (typeof responseBody === "string" && responseBody.length > 0) {
48556
+ message2 = responseBody.slice(0, 200);
48557
+ }
48558
+ throw new FrappeAPIError(
48559
+ `GET ${path} failed: ${message2}`,
48560
+ response.status,
48561
+ responseBody,
48562
+ parseRetryAfter(response.headers.get("retry-after"))
48563
+ );
48564
+ }
48565
+ const bytes = await readResponseBytes(
48566
+ response,
48567
+ this.maxDownloadBytes,
48568
+ "Downloaded file"
48569
+ );
48570
+ return {
48571
+ bytes,
48572
+ mimeType: normalizeMimeType(response.headers.get("content-type"))
48573
+ };
48574
+ } catch (err) {
48575
+ if (err instanceof FrappeAPIError || err instanceof ResponseBodyLimitError) {
48576
+ throw err;
48577
+ }
48578
+ if (err instanceof Error && err.name === "AbortError") {
48579
+ throw new FrappeAPIError(
48580
+ `Request timed out after ${this.timeoutMs}ms: GET ${path}`,
48581
+ 408,
48582
+ null
48583
+ );
48584
+ }
48585
+ throw new FrappeAPIError(
48586
+ `Network error while reading GET ${path}: ${err.message}`,
48587
+ 0,
48588
+ null
48589
+ );
48590
+ } finally {
48591
+ clearTimeout(timer);
48592
+ }
48593
+ }
48303
48594
  // ── Resource CRUD ───────────────────────────────────────────────────────────
48304
48595
  /**
48305
48596
  * List documents of a DocType.
@@ -48421,6 +48712,73 @@ var FrappeClient = class {
48421
48712
  );
48422
48713
  this.invalidate(doctype, name);
48423
48714
  }
48715
+ /**
48716
+ * Download one File row through Frappe's authenticated download handler.
48717
+ *
48718
+ * The caller supplies the expected attachment owner as a confused-deputy
48719
+ * guard. The File row is fetched fresh, its local path is validated, and the
48720
+ * response stream is stopped as soon as it exceeds `maxDownloadBytes`.
48721
+ */
48722
+ async downloadFile(input) {
48723
+ const fileId = input.fileId.trim();
48724
+ const attachedToDoctype = input.attachedToDoctype.trim();
48725
+ const attachedToName = input.attachedToName.trim();
48726
+ if (!fileId) {
48727
+ throw new Error("[FrappeClient] fileId must not be empty");
48728
+ }
48729
+ if (!attachedToDoctype) {
48730
+ throw new Error("[FrappeClient] attachedToDoctype must not be empty");
48731
+ }
48732
+ if (!attachedToName) {
48733
+ throw new Error("[FrappeClient] attachedToName must not be empty");
48734
+ }
48735
+ const metadata = await this.request(
48736
+ "GET",
48737
+ `/api/resource/File/${encodeURIComponent(fileId)}`
48738
+ );
48739
+ const file = metadata.data;
48740
+ if (file.name !== fileId) {
48741
+ throw new Error("[FrappeClient] File response identity mismatch");
48742
+ }
48743
+ if (file.attached_to_doctype !== attachedToDoctype || file.attached_to_name !== attachedToName) {
48744
+ throw new Error(
48745
+ "[FrappeClient] File is not attached to the requested document"
48746
+ );
48747
+ }
48748
+ if (file.is_private !== 0 && file.is_private !== 1) {
48749
+ throw new Error("[FrappeClient] File.is_private must be 0 or 1");
48750
+ }
48751
+ const fileUrl = validateLocalFileUrl(
48752
+ file.file_url,
48753
+ file.is_private === 1
48754
+ );
48755
+ const fileSize = file.file_size;
48756
+ if (fileSize !== void 0 && fileSize !== null) {
48757
+ if (typeof fileSize !== "number" || !Number.isInteger(fileSize) || fileSize < 0) {
48758
+ throw new Error(
48759
+ "[FrappeClient] File.file_size must be a non-negative integer"
48760
+ );
48761
+ }
48762
+ if (fileSize > this.maxDownloadBytes) {
48763
+ throw new ResponseBodyLimitError(
48764
+ "File metadata",
48765
+ fileSize,
48766
+ this.maxDownloadBytes
48767
+ );
48768
+ }
48769
+ }
48770
+ const params = new URLSearchParams({ file_url: fileUrl });
48771
+ const download = await this.requestBinary(
48772
+ `/api/method/frappe.handler.download_file?${params.toString()}`
48773
+ );
48774
+ return {
48775
+ fileId,
48776
+ fileName: sanitizeDownloadFileName(file.file_name, fileId),
48777
+ mimeType: download.mimeType,
48778
+ bytes: download.bytes,
48779
+ isPrivate: file.is_private === 1
48780
+ };
48781
+ }
48424
48782
  /**
48425
48783
  * Upload file bytes and attach the native File document to another document.
48426
48784
  * POST /api/method/upload_file
@@ -48510,6 +48868,7 @@ function getFrappeClient() {
48510
48868
  const apiKey = env6("ERPNEXT_API_KEY");
48511
48869
  const apiSecret = env6("ERPNEXT_API_SECRET");
48512
48870
  const maxUploadBytesRaw = env6("ERPNEXT_MAX_UPLOAD_BYTES");
48871
+ const maxDownloadBytesRaw = env6("ERPNEXT_MAX_DOWNLOAD_BYTES");
48513
48872
  if (!url2) {
48514
48873
  throw new Error(
48515
48874
  "[lib/erpnext] ERPNEXT_URL is required. Set it to your ERPNext instance URL, e.g. http://localhost:8000"
@@ -48525,7 +48884,8 @@ function getFrappeClient() {
48525
48884
  apiKey,
48526
48885
  apiSecret,
48527
48886
  cache: getCache(),
48528
- maxUploadBytes: maxUploadBytesRaw?.trim() ? Number(maxUploadBytesRaw) : void 0
48887
+ maxUploadBytes: maxUploadBytesRaw?.trim() ? Number(maxUploadBytesRaw) : void 0,
48888
+ maxDownloadBytes: maxDownloadBytesRaw?.trim() ? Number(maxDownloadBytesRaw) : void 0
48529
48889
  });
48530
48890
  return _client;
48531
48891
  }
@@ -48644,12 +49004,14 @@ var viewer = (name) => ({
48644
49004
  ui: { resourceUri: `ui://mcp-erpnext/${name}` }
48645
49005
  });
48646
49006
  var DOCLIST_META = viewer("doclist-viewer");
49007
+ var DOC_META = viewer("doc-viewer");
48647
49008
  var INVOICE_META = viewer("invoice-viewer");
48648
49009
  var STOCK_META = viewer("stock-viewer");
48649
49010
  var CHART_META = viewer("chart-viewer");
48650
49011
  var KANBAN_META = viewer("kanban-viewer");
48651
49012
  var KPI_META = viewer("kpi-viewer");
48652
49013
  var FUNNEL_META = viewer("funnel-viewer");
49014
+ var BUY_EVIDENCE_META = viewer("buy-evidence-viewer");
48653
49015
 
48654
49016
  // src/api/resolve.ts
48655
49017
  var NEGATIVE_CACHE_TTL_MS = 15e3;
@@ -48779,10 +49141,10 @@ function roundedTotalFallbackWarning(original, patched) {
48779
49141
 
48780
49142
  // src/tools/sales.ts
48781
49143
  function viewerDocument(value, doctype, name) {
48782
- const record2 = typeof value === "object" && value !== null ? value : {};
49144
+ const record3 = typeof value === "object" && value !== null ? value : {};
48783
49145
  return {
48784
- ...record2,
48785
- ...typeof record2.name === "string" || !name ? {} : { name },
49146
+ ...record3,
49147
+ ...typeof record3.name === "string" || !name ? {} : { name },
48786
49148
  doctype
48787
49149
  };
48788
49150
  }
@@ -48871,6 +49233,7 @@ var salesTools = [
48871
49233
  {
48872
49234
  name: "erpnext_customer_get",
48873
49235
  annotations: { readOnlyHint: true },
49236
+ _meta: DOC_META,
48874
49237
  description: "Get a single ERPNext customer by name (ID). Returns all fields including contact details.",
48875
49238
  category: "sales",
48876
49239
  inputSchema: {
@@ -48885,7 +49248,7 @@ var salesTools = [
48885
49248
  throw new Error("[erpnext_customer_get] 'name' is required");
48886
49249
  }
48887
49250
  const doc = await ctx.client.get("Customer", input.name);
48888
- return { data: doc };
49251
+ return { data: { ...doc, doctype: "Customer" } };
48889
49252
  }
48890
49253
  },
48891
49254
  {
@@ -49797,6 +50160,7 @@ var inventoryTools = [
49797
50160
  {
49798
50161
  name: "erpnext_item_get",
49799
50162
  annotations: { readOnlyHint: true },
50163
+ _meta: DOC_META,
49800
50164
  description: "Get a single ERPNext Item by name/item_code. Returns all fields including pricing and stock details.",
49801
50165
  category: "inventory",
49802
50166
  inputSchema: {
@@ -49811,7 +50175,7 @@ var inventoryTools = [
49811
50175
  throw new Error("[erpnext_item_get] 'name' is required");
49812
50176
  }
49813
50177
  const doc = await ctx.client.get("Item", input.name);
49814
- return { data: doc };
50178
+ return { data: { ...doc, doctype: "Item" } };
49815
50179
  }
49816
50180
  },
49817
50181
  {
@@ -50012,12 +50376,16 @@ var inventoryTools = [
50012
50376
  name: "erpnext_stock_entry_list",
50013
50377
  annotations: { readOnlyHint: true },
50014
50378
  _meta: DOCLIST_META,
50015
- description: "List Stock Entries (material transfers, receipts, issues). Fields: name, stock_entry_type, posting_date, from_warehouse, to_warehouse, total_amount. Filterable by stock_entry_type, date range.",
50379
+ description: "List Stock Entries (material transfers, receipts, issues). Fields: name, stock_entry_type, posting_date, from_warehouse, to_warehouse, total_amount. Filterable by item_code, stock_entry_type, date range.",
50016
50380
  category: "inventory",
50017
50381
  inputSchema: {
50018
50382
  type: "object",
50019
50383
  properties: {
50020
50384
  limit: { type: "number", description: "Max results (default 20)" },
50385
+ item_code: {
50386
+ type: "string",
50387
+ description: "Exact item code present in a Stock Entry line"
50388
+ },
50021
50389
  stock_entry_type: {
50022
50390
  type: "string",
50023
50391
  description: "Filter by type (Material Issue, Material Receipt, Material Transfer, etc.)"
@@ -50032,6 +50400,14 @@ var inventoryTools = [
50032
50400
  handler: async (input, ctx) => {
50033
50401
  const limit = input.limit ?? 20;
50034
50402
  const filters = [];
50403
+ if (input.item_code) {
50404
+ filters.push([
50405
+ "Stock Entry Detail",
50406
+ "item_code",
50407
+ "=",
50408
+ input.item_code
50409
+ ]);
50410
+ }
50035
50411
  if (input.stock_entry_type) {
50036
50412
  filters.push([
50037
50413
  "stock_entry_type",
@@ -50069,6 +50445,7 @@ var inventoryTools = [
50069
50445
  {
50070
50446
  name: "erpnext_stock_entry_get",
50071
50447
  annotations: { readOnlyHint: true },
50448
+ _meta: DOC_META,
50072
50449
  description: "Get a single Stock Entry by name. Returns full document with item details.",
50073
50450
  category: "inventory",
50074
50451
  inputSchema: {
@@ -50086,7 +50463,7 @@ var inventoryTools = [
50086
50463
  throw new Error("[erpnext_stock_entry_get] 'name' is required");
50087
50464
  }
50088
50465
  const doc = await ctx.client.get("Stock Entry", input.name);
50089
- return { data: doc };
50466
+ return { data: { ...doc, doctype: "Stock Entry" } };
50090
50467
  }
50091
50468
  },
50092
50469
  {
@@ -50281,6 +50658,7 @@ var accountingTools = [
50281
50658
  {
50282
50659
  name: "erpnext_journal_entry_get",
50283
50660
  annotations: { readOnlyHint: true },
50661
+ _meta: DOC_META,
50284
50662
  description: "Get a single Journal Entry by name (e.g. JV-00001). Returns full document with accounts.",
50285
50663
  category: "accounting",
50286
50664
  inputSchema: {
@@ -50298,7 +50676,7 @@ var accountingTools = [
50298
50676
  throw new Error("[erpnext_journal_entry_get] 'name' is required");
50299
50677
  }
50300
50678
  const doc = await ctx.client.get("Journal Entry", input.name);
50301
- return { data: doc };
50679
+ return { data: { ...doc, doctype: "Journal Entry" } };
50302
50680
  }
50303
50681
  },
50304
50682
  // ── Payment Entries ───────────────────────────────────────────────────────
@@ -50386,6 +50764,7 @@ var accountingTools = [
50386
50764
  {
50387
50765
  name: "erpnext_payment_entry_get",
50388
50766
  annotations: { readOnlyHint: true },
50767
+ _meta: DOC_META,
50389
50768
  description: "Get a single Payment Entry by name (e.g. PE-00001). Returns full document including references.",
50390
50769
  category: "accounting",
50391
50770
  inputSchema: {
@@ -50403,7 +50782,7 @@ var accountingTools = [
50403
50782
  throw new Error("[erpnext_payment_entry_get] 'name' is required");
50404
50783
  }
50405
50784
  const doc = await ctx.client.get("Payment Entry", input.name);
50406
- return { data: doc };
50785
+ return { data: { ...doc, doctype: "Payment Entry" } };
50407
50786
  }
50408
50787
  },
50409
50788
  {
@@ -50529,6 +50908,7 @@ var hrTools = [
50529
50908
  {
50530
50909
  name: "erpnext_employee_get",
50531
50910
  annotations: { readOnlyHint: true },
50911
+ _meta: DOC_META,
50532
50912
  description: "Get a single Employee by name/ID (e.g. HR-EMP-00001). Returns all fields.",
50533
50913
  category: "hr",
50534
50914
  inputSchema: {
@@ -50553,7 +50933,7 @@ var hrTools = [
50553
50933
  { allowPartialMatch: false, inputPath: "name" }
50554
50934
  );
50555
50935
  const doc = await ctx.client.get("Employee", employeeId);
50556
- return { data: doc };
50936
+ return { data: { ...doc, doctype: "Employee" } };
50557
50937
  }
50558
50938
  },
50559
50939
  // ── Attendance ────────────────────────────────────────────────────────────
@@ -50704,6 +51084,7 @@ var hrTools = [
50704
51084
  {
50705
51085
  name: "erpnext_leave_application_get",
50706
51086
  annotations: { readOnlyHint: true },
51087
+ _meta: DOC_META,
50707
51088
  description: "Get a single Leave Application by name. Returns full document.",
50708
51089
  category: "hr",
50709
51090
  inputSchema: {
@@ -50721,7 +51102,7 @@ var hrTools = [
50721
51102
  "Leave Application",
50722
51103
  input.name
50723
51104
  );
50724
- return { data: doc };
51105
+ return { data: { ...doc, doctype: "Leave Application" } };
50725
51106
  }
50726
51107
  },
50727
51108
  {
@@ -50867,6 +51248,7 @@ var hrTools = [
50867
51248
  {
50868
51249
  name: "erpnext_salary_slip_get",
50869
51250
  annotations: { readOnlyHint: true },
51251
+ _meta: DOC_META,
50870
51252
  description: "Get a single Salary Slip by name/ID. Returns all fields including earnings and deductions.",
50871
51253
  category: "hr",
50872
51254
  inputSchema: {
@@ -50884,7 +51266,7 @@ var hrTools = [
50884
51266
  throw new Error("[erpnext_salary_slip_get] 'name' is required");
50885
51267
  }
50886
51268
  const doc = await ctx.client.get("Salary Slip", input.name);
50887
- return { data: doc };
51269
+ return { data: { ...doc, doctype: "Salary Slip" } };
50888
51270
  }
50889
51271
  },
50890
51272
  // ── Payroll Entries ───────────────────────────────────────────────────────
@@ -51274,8 +51656,8 @@ async function applyAssignment(doctype, name, assignment, ctx, failureContext) {
51274
51656
  throw new Error(`${failureContext}: ${reason}`, { cause: error2 });
51275
51657
  }
51276
51658
  const todos = Array.isArray(nativeResult) ? nativeResult.map((todo) => {
51277
- const record2 = todo;
51278
- return { owner: record2.owner, name: record2.name };
51659
+ const record3 = todo;
51660
+ return { owner: record3.owner, name: record3.name };
51279
51661
  }) : [];
51280
51662
  return { notify_user: true, assignees: assignment.assignees, todos };
51281
51663
  }
@@ -51292,8 +51674,8 @@ async function removeAssignment(doctype, name, assignee, ctx, failureContext) {
51292
51674
  throw new Error(`${failureContext}: ${reason}`, { cause: error2 });
51293
51675
  }
51294
51676
  const todos = Array.isArray(nativeResult) ? nativeResult.map((todo) => {
51295
- const record2 = todo;
51296
- return { owner: record2.owner, name: record2.name };
51677
+ const record3 = todo;
51678
+ return { owner: record3.owner, name: record3.name };
51297
51679
  }) : [];
51298
51680
  return { removed: assignee, remaining: todos };
51299
51681
  }
@@ -51375,6 +51757,7 @@ var projectTools = [
51375
51757
  {
51376
51758
  name: "erpnext_project_get",
51377
51759
  annotations: { readOnlyHint: true },
51760
+ _meta: DOC_META,
51378
51761
  description: "Get a single Project by name. Returns full document including tasks summary.",
51379
51762
  category: "project",
51380
51763
  inputSchema: {
@@ -51389,7 +51772,7 @@ var projectTools = [
51389
51772
  throw new Error("[erpnext_project_get] 'name' is required");
51390
51773
  }
51391
51774
  const doc = await ctx.client.get("Project", input.name);
51392
- return { data: doc };
51775
+ return { data: { ...doc, doctype: "Project" } };
51393
51776
  }
51394
51777
  },
51395
51778
  // ── Tasks ─────────────────────────────────────────────────────────────────
@@ -51554,6 +51937,7 @@ var projectTools = [
51554
51937
  {
51555
51938
  name: "erpnext_task_get",
51556
51939
  annotations: { readOnlyHint: true },
51940
+ _meta: DOC_META,
51557
51941
  description: "Get a single Task by name. Returns full document including description and dependencies.",
51558
51942
  category: "project",
51559
51943
  inputSchema: {
@@ -51568,7 +51952,7 @@ var projectTools = [
51568
51952
  throw new Error("[erpnext_task_get] 'name' is required");
51569
51953
  }
51570
51954
  const doc = await ctx.client.get("Task", input.name);
51571
- return { data: doc };
51955
+ return { data: { ...doc, doctype: "Task" } };
51572
51956
  }
51573
51957
  },
51574
51958
  {
@@ -51744,6 +52128,7 @@ var projectTools = [
51744
52128
  {
51745
52129
  name: "erpnext_timesheet_get",
51746
52130
  annotations: { readOnlyHint: true },
52131
+ _meta: DOC_META,
51747
52132
  description: "Get a single Timesheet by name. Returns full document with time log details.",
51748
52133
  category: "project",
51749
52134
  inputSchema: {
@@ -51758,7 +52143,7 @@ var projectTools = [
51758
52143
  throw new Error("[erpnext_timesheet_get] 'name' is required");
51759
52144
  }
51760
52145
  const doc = await ctx.client.get("Timesheet", input.name);
51761
- return { data: doc };
52146
+ return { data: { ...doc, doctype: "Timesheet" } };
51762
52147
  }
51763
52148
  },
51764
52149
  {
@@ -51880,6 +52265,7 @@ var purchasingTools = [
51880
52265
  {
51881
52266
  name: "erpnext_supplier_get",
51882
52267
  annotations: { readOnlyHint: true },
52268
+ _meta: DOC_META,
51883
52269
  description: "Get a single ERPNext supplier by name (ID). Returns all fields including contact details.",
51884
52270
  category: "purchasing",
51885
52271
  inputSchema: {
@@ -51894,7 +52280,7 @@ var purchasingTools = [
51894
52280
  throw new Error("[erpnext_supplier_get] 'name' is required");
51895
52281
  }
51896
52282
  const doc = await ctx.client.get("Supplier", input.name);
51897
- return { data: doc };
52283
+ return { data: { ...doc, doctype: "Supplier" } };
51898
52284
  }
51899
52285
  },
51900
52286
  {
@@ -52023,6 +52409,7 @@ var purchasingTools = [
52023
52409
  {
52024
52410
  name: "erpnext_purchase_order_get",
52025
52411
  annotations: { readOnlyHint: true },
52412
+ _meta: DOC_META,
52026
52413
  description: "Get a single Purchase Order by name (e.g. PO-00001). Returns full document with line items.",
52027
52414
  category: "purchasing",
52028
52415
  inputSchema: {
@@ -52040,7 +52427,7 @@ var purchasingTools = [
52040
52427
  throw new Error("[erpnext_purchase_order_get] 'name' is required");
52041
52428
  }
52042
52429
  const doc = await ctx.client.get("Purchase Order", input.name);
52043
- return { data: doc };
52430
+ return { data: { ...doc, doctype: "Purchase Order" } };
52044
52431
  }
52045
52432
  },
52046
52433
  {
@@ -52191,6 +52578,7 @@ var purchasingTools = [
52191
52578
  {
52192
52579
  name: "erpnext_purchase_invoice_get",
52193
52580
  annotations: { readOnlyHint: true },
52581
+ _meta: DOC_META,
52194
52582
  description: "Get a single Purchase Invoice by name (e.g. PINV-00001). Returns full document with line items.",
52195
52583
  category: "purchasing",
52196
52584
  inputSchema: {
@@ -52211,7 +52599,7 @@ var purchasingTools = [
52211
52599
  "Purchase Invoice",
52212
52600
  input.name
52213
52601
  );
52214
- return { data: doc };
52602
+ return { data: { ...doc, doctype: "Purchase Invoice" } };
52215
52603
  }
52216
52604
  },
52217
52605
  // ── Purchase Receipts ─────────────────────────────────────────────────────
@@ -52285,6 +52673,7 @@ var purchasingTools = [
52285
52673
  {
52286
52674
  name: "erpnext_purchase_receipt_get",
52287
52675
  annotations: { readOnlyHint: true },
52676
+ _meta: DOC_META,
52288
52677
  description: "Get a single Purchase Receipt by name (e.g. MAT-PRE-00001). Returns full document with received items.",
52289
52678
  category: "purchasing",
52290
52679
  inputSchema: {
@@ -52305,7 +52694,7 @@ var purchasingTools = [
52305
52694
  "Purchase Receipt",
52306
52695
  input.name
52307
52696
  );
52308
- return { data: doc };
52697
+ return { data: { ...doc, doctype: "Purchase Receipt" } };
52309
52698
  }
52310
52699
  },
52311
52700
  // ── Supplier Quotations ───────────────────────────────────────────────────
@@ -52450,6 +52839,7 @@ var deliveryTools = [
52450
52839
  {
52451
52840
  name: "erpnext_delivery_note_get",
52452
52841
  annotations: { readOnlyHint: true },
52842
+ _meta: DOC_META,
52453
52843
  description: "Get a single Delivery Note by name (e.g. MAT-DN-00001). Returns full document with delivered items.",
52454
52844
  category: "delivery",
52455
52845
  inputSchema: {
@@ -52467,7 +52857,7 @@ var deliveryTools = [
52467
52857
  throw new Error("[erpnext_delivery_note_get] 'name' is required");
52468
52858
  }
52469
52859
  const doc = await ctx.client.get("Delivery Note", input.name);
52470
- return { data: doc };
52860
+ return { data: { ...doc, doctype: "Delivery Note" } };
52471
52861
  }
52472
52862
  },
52473
52863
  {
@@ -52600,6 +52990,7 @@ var deliveryTools = [
52600
52990
  {
52601
52991
  name: "erpnext_shipment_get",
52602
52992
  annotations: { readOnlyHint: true },
52993
+ _meta: DOC_META,
52603
52994
  description: "Get a single Shipment by name. Returns full shipment details including parcels.",
52604
52995
  category: "delivery",
52605
52996
  inputSchema: {
@@ -52614,7 +53005,7 @@ var deliveryTools = [
52614
53005
  throw new Error("[erpnext_shipment_get] 'name' is required");
52615
53006
  }
52616
53007
  const doc = await ctx.client.get("Shipment", input.name);
52617
- return { data: doc };
53008
+ return { data: { ...doc, doctype: "Shipment" } };
52618
53009
  }
52619
53010
  }
52620
53011
  ];
@@ -52694,6 +53085,7 @@ var manufacturingTools = [
52694
53085
  {
52695
53086
  name: "erpnext_bom_get",
52696
53087
  annotations: { readOnlyHint: true },
53088
+ _meta: DOC_META,
52697
53089
  description: "Get a single BOM by name (e.g. BOM-ITEM-00001). Returns full document with raw materials and operations.",
52698
53090
  category: "manufacturing",
52699
53091
  inputSchema: {
@@ -52708,7 +53100,7 @@ var manufacturingTools = [
52708
53100
  throw new Error("[erpnext_bom_get] 'name' is required");
52709
53101
  }
52710
53102
  const doc = await ctx.client.get("BOM", input.name);
52711
- return { data: doc };
53103
+ return { data: { ...doc, doctype: "BOM" } };
52712
53104
  }
52713
53105
  },
52714
53106
  // ── Work Orders ───────────────────────────────────────────────────────────
@@ -52786,6 +53178,7 @@ var manufacturingTools = [
52786
53178
  {
52787
53179
  name: "erpnext_work_order_get",
52788
53180
  annotations: { readOnlyHint: true },
53181
+ _meta: DOC_META,
52789
53182
  description: "Get a single Work Order by name (e.g. MFG-WO-00001). Returns full document with operations and materials.",
52790
53183
  category: "manufacturing",
52791
53184
  inputSchema: {
@@ -52803,7 +53196,7 @@ var manufacturingTools = [
52803
53196
  throw new Error("[erpnext_work_order_get] 'name' is required");
52804
53197
  }
52805
53198
  const doc = await ctx.client.get("Work Order", input.name);
52806
- return { data: doc };
53199
+ return { data: { ...doc, doctype: "Work Order" } };
52807
53200
  }
52808
53201
  },
52809
53202
  {
@@ -52919,6 +53312,7 @@ var manufacturingTools = [
52919
53312
  {
52920
53313
  name: "erpnext_job_card_get",
52921
53314
  annotations: { readOnlyHint: true },
53315
+ _meta: DOC_META,
52922
53316
  description: "Get a single Job Card by name. Returns full document with time logs and material transfers.",
52923
53317
  category: "manufacturing",
52924
53318
  inputSchema: {
@@ -52933,7 +53327,7 @@ var manufacturingTools = [
52933
53327
  throw new Error("[erpnext_job_card_get] 'name' is required");
52934
53328
  }
52935
53329
  const doc = await ctx.client.get("Job Card", input.name);
52936
- return { data: doc };
53330
+ return { data: { ...doc, doctype: "Job Card" } };
52937
53331
  }
52938
53332
  }
52939
53333
  ];
@@ -52995,6 +53389,7 @@ var crmTools = [
52995
53389
  {
52996
53390
  name: "erpnext_lead_get",
52997
53391
  annotations: { readOnlyHint: true },
53392
+ _meta: DOC_META,
52998
53393
  description: "Get a single CRM Lead by name. Returns all lead details including contact info.",
52999
53394
  category: "crm",
53000
53395
  inputSchema: {
@@ -53009,7 +53404,7 @@ var crmTools = [
53009
53404
  throw new Error("[erpnext_lead_get] 'name' is required");
53010
53405
  }
53011
53406
  const doc = await ctx.client.get("Lead", input.name);
53012
- return { data: doc };
53407
+ return { data: { ...doc, doctype: "Lead" } };
53013
53408
  }
53014
53409
  },
53015
53410
  {
@@ -53141,6 +53536,7 @@ var crmTools = [
53141
53536
  {
53142
53537
  name: "erpnext_opportunity_get",
53143
53538
  annotations: { readOnlyHint: true },
53539
+ _meta: DOC_META,
53144
53540
  description: "Get a single CRM Opportunity by name. Returns full details including items and competitors.",
53145
53541
  category: "crm",
53146
53542
  inputSchema: {
@@ -53155,7 +53551,7 @@ var crmTools = [
53155
53551
  throw new Error("[erpnext_opportunity_get] 'name' is required");
53156
53552
  }
53157
53553
  const doc = await ctx.client.get("Opportunity", input.name);
53158
- return { data: doc };
53554
+ return { data: { ...doc, doctype: "Opportunity" } };
53159
53555
  }
53160
53556
  },
53161
53557
  // ── Contacts ──────────────────────────────────────────────────────────────
@@ -53210,6 +53606,7 @@ var crmTools = [
53210
53606
  {
53211
53607
  name: "erpnext_contact_get",
53212
53608
  annotations: { readOnlyHint: true },
53609
+ _meta: DOC_META,
53213
53610
  description: "Get a single Contact by name. Returns all contact details.",
53214
53611
  category: "crm",
53215
53612
  inputSchema: {
@@ -53224,7 +53621,7 @@ var crmTools = [
53224
53621
  throw new Error("[erpnext_contact_get] 'name' is required");
53225
53622
  }
53226
53623
  const doc = await ctx.client.get("Contact", input.name);
53227
- return { data: doc };
53624
+ return { data: { ...doc, doctype: "Contact" } };
53228
53625
  }
53229
53626
  },
53230
53627
  // ── Campaigns ─────────────────────────────────────────────────────────────
@@ -53374,6 +53771,7 @@ var assetsTools = [
53374
53771
  {
53375
53772
  name: "erpnext_asset_get",
53376
53773
  annotations: { readOnlyHint: true },
53774
+ _meta: DOC_META,
53377
53775
  description: "Get a single Asset by name. Returns full details including depreciation schedule and maintenance logs.",
53378
53776
  category: "assets",
53379
53777
  inputSchema: {
@@ -53388,7 +53786,7 @@ var assetsTools = [
53388
53786
  throw new Error("[erpnext_asset_get] 'name' is required");
53389
53787
  }
53390
53788
  const doc = await ctx.client.get("Asset", input.name);
53391
- return { data: doc };
53789
+ return { data: { ...doc, doctype: "Asset" } };
53392
53790
  }
53393
53791
  },
53394
53792
  {
@@ -53524,6 +53922,7 @@ var assetsTools = [
53524
53922
  {
53525
53923
  name: "erpnext_asset_movement_get",
53526
53924
  annotations: { readOnlyHint: true },
53925
+ _meta: DOC_META,
53527
53926
  description: "Get a single Asset Movement by name. Returns full details including assets moved.",
53528
53927
  category: "assets",
53529
53928
  inputSchema: {
@@ -53538,7 +53937,7 @@ var assetsTools = [
53538
53937
  throw new Error("[erpnext_asset_movement_get] 'name' is required");
53539
53938
  }
53540
53939
  const doc = await ctx.client.get("Asset Movement", input.name);
53541
- return { data: doc };
53940
+ return { data: { ...doc, doctype: "Asset Movement" } };
53542
53941
  }
53543
53942
  },
53544
53943
  // ── Asset Maintenance ─────────────────────────────────────────────────────
@@ -53595,6 +53994,7 @@ var assetsTools = [
53595
53994
  {
53596
53995
  name: "erpnext_asset_maintenance_get",
53597
53996
  annotations: { readOnlyHint: true },
53997
+ _meta: DOC_META,
53598
53998
  description: "Get a single Asset Maintenance record by name. Returns full details including maintenance tasks.",
53599
53999
  category: "assets",
53600
54000
  inputSchema: {
@@ -53612,7 +54012,7 @@ var assetsTools = [
53612
54012
  "Asset Maintenance",
53613
54013
  input.name
53614
54014
  );
53615
- return { data: doc };
54015
+ return { data: { ...doc, doctype: "Asset Maintenance" } };
53616
54016
  }
53617
54017
  },
53618
54018
  // ── Asset Categories ──────────────────────────────────────────────────────
@@ -53729,11 +54129,166 @@ function isEmptyPartyAccountCurrencyNone(text) {
53729
54129
  }
53730
54130
 
53731
54131
  // src/tools/operations.ts
54132
+ var DOC_LIST_MAX_LIMIT = 5e3;
54133
+ var DOC_LIST_MAX_OFFSET = 1e5;
54134
+ function bytesToBase64(bytes) {
54135
+ const chunkSize = 32 * 1024;
54136
+ let binary = "";
54137
+ for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
54138
+ binary += String.fromCharCode(
54139
+ ...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.byteLength))
54140
+ );
54141
+ }
54142
+ return btoa(binary);
54143
+ }
53732
54144
  async function withPiAccountHintsIfNeeded(doctype, fn) {
53733
54145
  return isPurchaseInvoiceDoctype(doctype) ? await withPurchaseInvoiceAccountHints(fn) : await fn();
53734
54146
  }
53735
54147
  var operationsTools = [
53736
54148
  // ── File Attachments ───────────────────────────────────────────────────────
54149
+ {
54150
+ name: "erpnext_file_list",
54151
+ annotations: { readOnlyHint: true },
54152
+ description: "List the files attached to an ERPNext document. Returns name, size, privacy and URL for each attachment. Pairs with erpnext_file_upload: that one attaches, this one reads back.",
54153
+ category: "operations",
54154
+ inputSchema: {
54155
+ type: "object",
54156
+ properties: {
54157
+ attached_to_doctype: {
54158
+ type: "string",
54159
+ description: "DocType of the document whose attachments to list.",
54160
+ minLength: 1
54161
+ },
54162
+ attached_to_name: {
54163
+ type: "string",
54164
+ description: "Name/ID of the document whose attachments to list.",
54165
+ minLength: 1
54166
+ },
54167
+ limit: {
54168
+ type: "number",
54169
+ description: "Maximum number of files to return. Defaults to 50.",
54170
+ minimum: 1,
54171
+ maximum: 500
54172
+ }
54173
+ },
54174
+ required: ["attached_to_doctype", "attached_to_name"]
54175
+ },
54176
+ handler: async (input, ctx) => {
54177
+ for (const field of ["attached_to_doctype", "attached_to_name"]) {
54178
+ if (typeof input[field] !== "string" || !input[field].trim()) {
54179
+ throw new Error(
54180
+ `[erpnext_file_list] '${field}' must be a non-empty string`
54181
+ );
54182
+ }
54183
+ }
54184
+ if (input.limit !== void 0 && (typeof input.limit !== "number" || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > 500)) {
54185
+ throw new Error(
54186
+ "[erpnext_file_list] 'limit' must be an integer between 1 and 500"
54187
+ );
54188
+ }
54189
+ const files = await ctx.client.list("File", {
54190
+ fields: [
54191
+ "name",
54192
+ "file_name",
54193
+ "file_url",
54194
+ "file_size",
54195
+ "is_private",
54196
+ "attached_to_field",
54197
+ "creation",
54198
+ "modified",
54199
+ "owner"
54200
+ ],
54201
+ filters: [
54202
+ ["attached_to_doctype", "=", input.attached_to_doctype],
54203
+ ["attached_to_name", "=", input.attached_to_name]
54204
+ ],
54205
+ order_by: "creation desc",
54206
+ limit: input.limit ?? 50
54207
+ });
54208
+ return {
54209
+ count: files.length,
54210
+ data: files.map((file) => ({
54211
+ name: file.name,
54212
+ file_name: file.file_name,
54213
+ file_url: file.file_url,
54214
+ file_size: file.file_size ?? null,
54215
+ is_private: file.is_private === 1,
54216
+ attached_to_field: file.attached_to_field ?? null,
54217
+ creation: file.creation,
54218
+ modified: file.modified,
54219
+ owner: file.owner
54220
+ }))
54221
+ };
54222
+ }
54223
+ },
54224
+ {
54225
+ name: "erpnext_file_download",
54226
+ annotations: { readOnlyHint: true },
54227
+ _meta: {
54228
+ ui: {
54229
+ resourceUri: DOC_META.ui.resourceUri,
54230
+ visibility: ["app"]
54231
+ }
54232
+ },
54233
+ description: "Download one ERPNext attachment for the document viewer. The tool accepts a File ID, verifies its document attachment, and returns one embedded binary resource.",
54234
+ category: "operations",
54235
+ inputSchema: {
54236
+ type: "object",
54237
+ properties: {
54238
+ file_id: {
54239
+ type: "string",
54240
+ description: "Native ERPNext File.name identifier, never a URL.",
54241
+ minLength: 1
54242
+ },
54243
+ attached_to_doctype: {
54244
+ type: "string",
54245
+ description: "Expected parent document DocType.",
54246
+ minLength: 1
54247
+ },
54248
+ attached_to_name: {
54249
+ type: "string",
54250
+ description: "Expected parent document name/ID.",
54251
+ minLength: 1
54252
+ }
54253
+ },
54254
+ required: ["file_id", "attached_to_doctype", "attached_to_name"],
54255
+ additionalProperties: false
54256
+ },
54257
+ handler: async (input, ctx) => {
54258
+ for (const field of [
54259
+ "file_id",
54260
+ "attached_to_doctype",
54261
+ "attached_to_name"
54262
+ ]) {
54263
+ if (typeof input[field] !== "string" || !input[field].trim()) {
54264
+ throw new Error(
54265
+ `[erpnext_file_download] '${field}' must be a non-empty string`
54266
+ );
54267
+ }
54268
+ }
54269
+ const file = await ctx.client.downloadFile({
54270
+ fileId: input.file_id.trim(),
54271
+ attachedToDoctype: input.attached_to_doctype.trim(),
54272
+ attachedToName: input.attached_to_name.trim()
54273
+ });
54274
+ return {
54275
+ content: [
54276
+ {
54277
+ type: "text",
54278
+ text: `Prepared ${file.fileName} for download (${file.bytes.byteLength} bytes).`
54279
+ },
54280
+ {
54281
+ type: "resource",
54282
+ resource: {
54283
+ uri: `file:///${encodeURIComponent(file.fileName)}`,
54284
+ mimeType: file.mimeType,
54285
+ blob: bytesToBase64(file.bytes)
54286
+ }
54287
+ }
54288
+ ]
54289
+ };
54290
+ }
54291
+ },
53737
54292
  {
53738
54293
  name: "erpnext_file_upload",
53739
54294
  annotations: { destructiveHint: true },
@@ -54047,6 +54602,7 @@ var operationsTools = [
54047
54602
  {
54048
54603
  name: "erpnext_doc_get",
54049
54604
  annotations: { readOnlyHint: true },
54605
+ _meta: DOC_META,
54050
54606
  description: "Get any ERPNext document by DocType and name. Useful for DocTypes not covered by dedicated tools. Returns the full document with all fields.",
54051
54607
  category: "operations",
54052
54608
  inputSchema: {
@@ -54064,17 +54620,18 @@ var operationsTools = [
54064
54620
  required: ["doctype", "name"]
54065
54621
  },
54066
54622
  handler: async (input, ctx) => {
54067
- if (!input.doctype) {
54623
+ if (typeof input.doctype !== "string" || !input.doctype.trim()) {
54068
54624
  throw new Error("[erpnext_doc_get] 'doctype' is required");
54069
54625
  }
54070
- if (!input.name) {
54626
+ if (typeof input.name !== "string" || !input.name.trim()) {
54071
54627
  throw new Error("[erpnext_doc_get] 'name' is required");
54072
54628
  }
54629
+ const doctype = input.doctype.trim();
54073
54630
  const doc = await ctx.client.get(
54074
- input.doctype,
54075
- input.name
54631
+ doctype,
54632
+ input.name.trim()
54076
54633
  );
54077
- return { data: doc };
54634
+ return { data: { ...doc, doctype } };
54078
54635
  }
54079
54636
  },
54080
54637
  // ── Generic List ──────────────────────────────────────────────────────────
@@ -54082,7 +54639,7 @@ var operationsTools = [
54082
54639
  name: "erpnext_doc_list",
54083
54640
  annotations: { readOnlyHint: true },
54084
54641
  _meta: DOCLIST_META,
54085
- description: "List any ERPNext documents by DocType. Useful for DocTypes not covered by dedicated tools. Supports field selection, filters (as JSON array), and limit.",
54642
+ description: "List any ERPNext documents by DocType. Useful for DocTypes not covered by dedicated tools. Supports field selection, filters (as JSON array), bounded pagination, and an optional fresh read.",
54086
54643
  category: "operations",
54087
54644
  inputSchema: {
54088
54645
  type: "object",
@@ -54098,13 +54655,70 @@ var operationsTools = [
54098
54655
  },
54099
54656
  filters: {
54100
54657
  type: "array",
54101
- description: 'Frappe filters as array of [fieldname, operator, value] tuples. Example: [["status","=","Open"],["company","=","Acme"]]',
54658
+ description: 'Frappe filters as array of [fieldname, operator, value] tuples, or [child doctype, fieldname, operator, value] to filter on a child table. Values may be strings, numbers, booleans, null, or string/number arrays for in/not in. Example: [["status","=","Open"],["company","=","Acme"]]',
54102
54659
  items: {
54103
54660
  type: "array",
54104
- items: { type: "string" }
54661
+ anyOf: [
54662
+ {
54663
+ prefixItems: [
54664
+ { type: "string", minLength: 1 },
54665
+ { type: "string", minLength: 1 },
54666
+ {
54667
+ oneOf: [
54668
+ { type: ["string", "number", "boolean", "null"] },
54669
+ {
54670
+ type: "array",
54671
+ items: { type: ["string", "number"] }
54672
+ }
54673
+ ]
54674
+ }
54675
+ ],
54676
+ minItems: 3,
54677
+ maxItems: 3
54678
+ },
54679
+ {
54680
+ prefixItems: [
54681
+ { type: "string", minLength: 1 },
54682
+ { type: "string", minLength: 1 },
54683
+ { type: "string", minLength: 1 },
54684
+ {
54685
+ oneOf: [
54686
+ { type: ["string", "number", "boolean", "null"] },
54687
+ {
54688
+ type: "array",
54689
+ items: { type: ["string", "number"] }
54690
+ }
54691
+ ]
54692
+ }
54693
+ ],
54694
+ minItems: 4,
54695
+ maxItems: 4
54696
+ },
54697
+ {
54698
+ // 3.0.x advertised arbitrary string arrays. Keep accepting
54699
+ // that legacy surface while describing real 3/4-part Frappe
54700
+ // tuples precisely for modern clients.
54701
+ items: { type: "string" }
54702
+ }
54703
+ ]
54105
54704
  }
54106
54705
  },
54107
- limit: { type: "number", description: "Max results (default 20)" },
54706
+ limit: {
54707
+ type: "integer",
54708
+ minimum: 1,
54709
+ maximum: DOC_LIST_MAX_LIMIT,
54710
+ description: "Max results (default 20, maximum 5000)"
54711
+ },
54712
+ offset: {
54713
+ type: "integer",
54714
+ minimum: 0,
54715
+ maximum: DOC_LIST_MAX_OFFSET,
54716
+ description: "Pagination offset (default 0, maximum 100000)"
54717
+ },
54718
+ skip_cache: {
54719
+ type: "boolean",
54720
+ description: "Force a fresh ERPNext read instead of using the short-lived list cache"
54721
+ },
54108
54722
  order_by: {
54109
54723
  type: "string",
54110
54724
  description: "Order by clause (e.g. 'modified desc', 'name asc')"
@@ -54116,16 +54730,34 @@ var operationsTools = [
54116
54730
  if (!input.doctype) {
54117
54731
  throw new Error("[erpnext_doc_list] 'doctype' is required");
54118
54732
  }
54733
+ if (input.limit !== void 0 && (typeof input.limit !== "number" || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > DOC_LIST_MAX_LIMIT)) {
54734
+ throw new Error(
54735
+ `[erpnext_doc_list] 'limit' must be an integer between 1 and ${DOC_LIST_MAX_LIMIT}`
54736
+ );
54737
+ }
54738
+ if (input.offset !== void 0 && (typeof input.offset !== "number" || !Number.isInteger(input.offset) || input.offset < 0 || input.offset > DOC_LIST_MAX_OFFSET)) {
54739
+ throw new Error(
54740
+ `[erpnext_doc_list] 'offset' must be an integer between 0 and ${DOC_LIST_MAX_OFFSET}`
54741
+ );
54742
+ }
54743
+ if (input.skip_cache !== void 0 && typeof input.skip_cache !== "boolean") {
54744
+ throw new Error("[erpnext_doc_list] 'skip_cache' must be a boolean");
54745
+ }
54119
54746
  const limit = input.limit ?? 20;
54120
54747
  const fields = input.fields ?? ["name", "modified"];
54121
54748
  const filters = input.filters ?? [];
54122
54749
  const order_by = input.order_by ?? "modified desc";
54123
- const docs = await ctx.client.list(input.doctype, {
54750
+ const offset = input.offset;
54751
+ const listOptions = {
54124
54752
  fields,
54125
54753
  filters,
54126
54754
  limit,
54755
+ ...offset !== void 0 ? { limit_start: offset } : {},
54127
54756
  order_by
54128
- });
54757
+ };
54758
+ const docs = input.skip_cache === true ? await ctx.client.list(input.doctype, listOptions, {
54759
+ skipCache: true
54760
+ }) : await ctx.client.list(input.doctype, listOptions);
54129
54761
  return {
54130
54762
  doctype: input.doctype,
54131
54763
  count: docs.length,
@@ -54368,11 +55000,11 @@ var setupTools = [
54368
55000
  filters.push(["enabled", "=", 1]);
54369
55001
  }
54370
55002
  if (input.search) {
54371
- const literal2 = input.search.replace(
55003
+ const literal3 = input.search.replace(
54372
55004
  /[\\%_]/g,
54373
55005
  (match2) => `\\${match2}`
54374
55006
  );
54375
- filters.push(["full_name", "like", `%${literal2}%`]);
55007
+ filters.push(["full_name", "like", `%${literal3}%`]);
54376
55008
  }
54377
55009
  const docs = await ctx.client.list("User", {
54378
55010
  fields: ["name", "full_name", "enabled"],
@@ -54477,14 +55109,14 @@ var setupTools = [
54477
55109
  // src/tools/analytics.ts
54478
55110
  var MAX_RADAR_ITEMS = 8;
54479
55111
  function monthlyLabels(now, count) {
54480
- return Array.from({ length: count }, (_, index) => {
54481
- const month = new Date(
54482
- now.getFullYear(),
54483
- now.getMonth() - count + index + 1,
54484
- 1
55112
+ const labels = [];
55113
+ for (let offset = count - 1; offset >= 0; offset--) {
55114
+ const month = new Date(now.getFullYear(), now.getMonth() - offset, 1);
55115
+ labels.push(
55116
+ `${month.toLocaleString("en", { month: "short" })} ${month.getFullYear().toString().slice(2)}`
54485
55117
  );
54486
- return `${month.toLocaleString("en", { month: "short" })} ${String(month.getFullYear()).slice(-2)}`;
54487
- });
55118
+ }
55119
+ return labels;
54488
55120
  }
54489
55121
  function frappeCalendarMonth(value) {
54490
55122
  if (typeof value !== "string") return null;
@@ -54615,7 +55247,6 @@ var analyticsTools = [
54615
55247
  const invoices2 = await ctx.client.list("Sales Invoice", {
54616
55248
  fields: ["name", "status", "grand_total"],
54617
55249
  filters: [["docstatus", "!=", 2]],
54618
- // exclude cancelled
54619
55250
  limit: 500,
54620
55251
  order_by: "modified desc"
54621
55252
  });
@@ -54746,17 +55377,7 @@ var analyticsTools = [
54746
55377
  limit: 1e3,
54747
55378
  order_by: "transaction_date asc"
54748
55379
  });
54749
- const months = [];
54750
- for (let m = 0; m < monthsBack; m++) {
54751
- const d = new Date(
54752
- now.getFullYear(),
54753
- now.getMonth() - monthsBack + 1 + m,
54754
- 1
54755
- );
54756
- months.push(
54757
- `${d.toLocaleString("en", { month: "short" })} ${d.getFullYear().toString().slice(2)}`
54758
- );
54759
- }
55380
+ const months = monthlyLabels(now, monthsBack);
54760
55381
  if (groupBy === "customer") {
54761
55382
  const byCustomerMonth = {};
54762
55383
  for (const order2 of orders) {
@@ -54953,6 +55574,7 @@ var analyticsTools = [
54953
55574
  color: "#fbbf24",
54954
55575
  type: "line",
54955
55576
  yAxisId: "right",
55577
+ unit: "orders",
54956
55578
  showDots: true
54957
55579
  }
54958
55580
  ],
@@ -55236,6 +55858,7 @@ var analyticsTools = [
55236
55858
  limit: 5e3
55237
55859
  });
55238
55860
  const sparkline = [0, 0, 0, 0, 0, 0];
55861
+ const sparklineLabels = monthlyLabels(now, sparkline.length);
55239
55862
  for (const o of allOrders) {
55240
55863
  const calendarMonth = frappeCalendarMonth(o.transaction_date);
55241
55864
  if (!calendarMonth) continue;
@@ -55257,7 +55880,7 @@ var analyticsTools = [
55257
55880
  trend: delta > 0 ? "up" : delta < 0 ? "down" : "flat",
55258
55881
  trendIsGood: true,
55259
55882
  sparkline,
55260
- sparklineLabels: monthlyLabels(now, sparkline.length),
55883
+ sparklineLabels,
55261
55884
  color: "#60a5fa",
55262
55885
  _meta: KPI_META
55263
55886
  };
@@ -55410,7 +56033,9 @@ var analyticsTools = [
55410
56033
  category: "analytics",
55411
56034
  inputSchema: { type: "object", properties: {} },
55412
56035
  handler: async (_input, ctx) => {
55413
- const today = localCalendarDate(/* @__PURE__ */ new Date());
56036
+ const now = /* @__PURE__ */ new Date();
56037
+ const pad = (n) => String(n).padStart(2, "0");
56038
+ const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
55414
56039
  const invoices = await ctx.client.list("Sales Invoice", {
55415
56040
  fields: ["outstanding_amount", "due_date"],
55416
56041
  filters: [
@@ -56890,6 +57515,1350 @@ var kanbanTools = [
56890
57515
  }
56891
57516
  ];
56892
57517
 
57518
+ // src/buy/identities.ts
57519
+ var BUY_SOURCE_CAPTURE_SCHEMA = "io.casys.mcp-erpnext.buy-source-capture/1.0";
57520
+ var BUY_RECORDED_RESULT_SCHEMA = "io.casys.mcp-erpnext.buy-recorded-result/1.0";
57521
+ var BUY_RECORDED_RESULT_SCHEMA_V2 = "io.casys.mcp-erpnext.buy-recorded-result/2.0";
57522
+ var BUY_RECORDED_SESSION_SCHEMA = "io.casys.mcp-erpnext.buy-recorded-session/1.0";
57523
+ var BUY_EVIDENCE_VIEWER_URI = "ui://mcp-erpnext/buy-evidence-viewer";
57524
+ var BUY_VIEW_APP_MANIFEST_URI = "ui://mcp-erpnext/buy-evidence-manifest";
57525
+ var BUY_VIEW_APP_ID = "io.casys.mcp-erpnext.buy-evidence";
57526
+ var BUY_VIEW_APP_TITLE = "ERPNext Buy Evidence";
57527
+ var BUY_VIEW_APP_VERSION = "3.2.0-beta.1";
57528
+ var BUY_SOURCE_INSTANCE_KIND = "erpnext-site";
57529
+ var BUY_CAPTURE_DOCTYPE = [
57530
+ "Item",
57531
+ "BOM",
57532
+ "Item Price",
57533
+ "Supplier Quotation",
57534
+ "Supplier",
57535
+ "Price List",
57536
+ "UOM",
57537
+ "Currency Exchange"
57538
+ ];
57539
+ var BUY_CAPTURE_MAX_DOCUMENTS = 32;
57540
+ var BUY_CAPTURE_MAX_CHILD_ROWS = 200;
57541
+ var BUY_CAPTURE_MAX_CANONICAL_BYTES = 262144;
57542
+ var BUY_CAPTURE_MAX_NAME_LENGTH = 140;
57543
+ var BUY_DOCTYPE_SOURCE_CATEGORY = {
57544
+ Item: "item",
57545
+ BOM: "bom",
57546
+ "Item Price": "catalogue-price",
57547
+ "Supplier Quotation": "supplier-quotation",
57548
+ Supplier: "supplier",
57549
+ "Price List": "price-list",
57550
+ UOM: "uom",
57551
+ "Currency Exchange": "currency-exchange"
57552
+ };
57553
+ var BUY_CONSISTENCY_KIND = "repeated-read";
57554
+ var BUY_CONSISTENCY_READS = 2;
57555
+
57556
+ // src/buy/errors.ts
57557
+ var BuyCaptureError = class extends Error {
57558
+ code;
57559
+ retryable;
57560
+ context;
57561
+ recovery;
57562
+ constructor(code, message2, options) {
57563
+ super(message2);
57564
+ this.name = "BuyCaptureError";
57565
+ this.code = code;
57566
+ this.retryable = options.retryable === true;
57567
+ this.context = Object.freeze({ ...options.context ?? {} });
57568
+ this.recovery = options.recovery;
57569
+ }
57570
+ };
57571
+
57572
+ // src/buy/json.ts
57573
+ var FINGERPRINT = /^sha256:[a-f0-9]{64}$/;
57574
+ var ISO_MILLISECONDS = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/;
57575
+ var CALENDAR_DATE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
57576
+ var FRAPPE_DATETIME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?$/;
57577
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
57578
+ "api_key",
57579
+ "api_secret",
57580
+ "authorization",
57581
+ "baseUrl",
57582
+ "command",
57583
+ "commands",
57584
+ "cookie",
57585
+ "credentials",
57586
+ "endpoint",
57587
+ "password",
57588
+ "source_path",
57589
+ "sourcePath",
57590
+ "stagedPath"
57591
+ ]);
57592
+ function record2(value, name) {
57593
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
57594
+ throw new TypeError(`${name} must be an object.`);
57595
+ }
57596
+ return value;
57597
+ }
57598
+ function exactKeys(value, keys, name) {
57599
+ const ownKeys = Reflect.ownKeys(value);
57600
+ if (ownKeys.some((key) => typeof key !== "string")) {
57601
+ throw new TypeError(`${name} contains missing or unsupported fields.`);
57602
+ }
57603
+ for (const key of ownKeys) {
57604
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
57605
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
57606
+ throw new TypeError(`${name} contains missing or unsupported fields.`);
57607
+ }
57608
+ }
57609
+ const actual = ownKeys.toSorted();
57610
+ const expected = [...keys].toSorted();
57611
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
57612
+ throw new TypeError(`${name} contains missing or unsupported fields.`);
57613
+ }
57614
+ }
57615
+ function exactRecord(value, keys, name) {
57616
+ const root = record2(value, name);
57617
+ exactKeys(root, keys, name);
57618
+ rejectForbiddenKeys(root, name);
57619
+ return root;
57620
+ }
57621
+ function denseArray(value, name) {
57622
+ if (!Array.isArray(value)) throw new TypeError(`${name} must be an array.`);
57623
+ const allowed = /* @__PURE__ */ new Set(["length"]);
57624
+ for (let index = 0; index < value.length; index += 1) {
57625
+ allowed.add(String(index));
57626
+ }
57627
+ const ownKeys = Reflect.ownKeys(value);
57628
+ if (ownKeys.length !== allowed.size || ownKeys.some((key) => typeof key !== "string" || !allowed.has(key))) {
57629
+ throw new TypeError(`${name} must be a dense, unadorned array.`);
57630
+ }
57631
+ for (let index = 0; index < value.length; index += 1) {
57632
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
57633
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
57634
+ throw new TypeError(`${name} must be a dense, unadorned array.`);
57635
+ }
57636
+ }
57637
+ return value;
57638
+ }
57639
+ function literal2(value, expected, name) {
57640
+ if (value !== expected) throw new TypeError(`${name} must be ${expected}.`);
57641
+ return expected;
57642
+ }
57643
+ function oneOf(value, allowed, name) {
57644
+ if (typeof value !== "string" || !allowed.includes(value)) {
57645
+ throw new TypeError(`${name} must be one of ${allowed.join(", ")}.`);
57646
+ }
57647
+ return value;
57648
+ }
57649
+ function nonEmpty(value, name) {
57650
+ if (typeof value !== "string" || value.trim() === "") {
57651
+ throw new TypeError(`${name} must be a non-empty string.`);
57652
+ }
57653
+ if (value.trim() !== value) {
57654
+ throw new TypeError(`${name} must not contain whitespace padding.`);
57655
+ }
57656
+ return value;
57657
+ }
57658
+ function pattern(value, expected, name) {
57659
+ if (typeof value !== "string" || !expected.test(value)) {
57660
+ throw new TypeError(`${name} has an invalid format.`);
57661
+ }
57662
+ return value;
57663
+ }
57664
+ function fingerprint(value, name) {
57665
+ return pattern(value, FINGERPRINT, name);
57666
+ }
57667
+ function canonicalTimestamp(value, name) {
57668
+ const timestamp = pattern(value, ISO_MILLISECONDS, name);
57669
+ try {
57670
+ if (new Date(timestamp).toISOString() !== timestamp) {
57671
+ throw new TypeError(`${name} is not canonical UTC.`);
57672
+ }
57673
+ } catch {
57674
+ throw new TypeError(`${name} is not canonical UTC.`);
57675
+ }
57676
+ return timestamp;
57677
+ }
57678
+ function calendarDate(value, name) {
57679
+ const date4 = pattern(value, CALENDAR_DATE, name);
57680
+ const parsed = /* @__PURE__ */ new Date(`${date4}T00:00:00.000Z`);
57681
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== date4) {
57682
+ throw new TypeError(`${name} is not a real calendar date.`);
57683
+ }
57684
+ return date4;
57685
+ }
57686
+ function frappeDatetime(value, name) {
57687
+ const datetime3 = pattern(value, FRAPPE_DATETIME, name);
57688
+ calendarDate(datetime3.slice(0, 10), name);
57689
+ const [hour, minute, second] = datetime3.slice(11, 19).split(":").map(Number);
57690
+ if (hour > 23 || minute > 59 || second > 59) {
57691
+ throw new TypeError(`${name} is not a real Frappe datetime.`);
57692
+ }
57693
+ return datetime3;
57694
+ }
57695
+ function boundedArray(value, max, name) {
57696
+ const items = denseArray(value, name);
57697
+ if (items.length > max) {
57698
+ throw new TypeError(`${name} exceeds the ${max}-entry bound.`);
57699
+ }
57700
+ return items;
57701
+ }
57702
+ function rejectForbiddenKeys(value, name) {
57703
+ if (Array.isArray(value)) {
57704
+ const items = denseArray(value, name);
57705
+ for (let index = 0; index < items.length; index += 1) {
57706
+ rejectForbiddenKeys(items[index], `${name}[${index}]`);
57707
+ }
57708
+ return;
57709
+ }
57710
+ if (typeof value !== "object" || value === null) return;
57711
+ const root = record2(value, name);
57712
+ for (const key of Object.keys(root)) {
57713
+ if (FORBIDDEN_KEYS.has(key)) {
57714
+ throw new TypeError(
57715
+ `${name}.${key} is a private credential, path, or command field and must not appear in the Buy contract.`
57716
+ );
57717
+ }
57718
+ rejectForbiddenKeys(root[key], `${name}.${key}`);
57719
+ }
57720
+ }
57721
+ function canonicalJson2(value, name) {
57722
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
57723
+ return JSON.stringify(value);
57724
+ }
57725
+ if (typeof value === "number") {
57726
+ if (!Number.isFinite(value)) {
57727
+ throw new TypeError(`${name} must contain only finite JSON numbers.`);
57728
+ }
57729
+ return JSON.stringify(value);
57730
+ }
57731
+ if (Array.isArray(value)) {
57732
+ const items = denseArray(value, name);
57733
+ return `[${items.map((item, index) => canonicalJson2(item, `${name}[${index}]`)).join(",")}]`;
57734
+ }
57735
+ const root = record2(value, name);
57736
+ const prototype = Object.getPrototypeOf(root);
57737
+ if (prototype !== Object.prototype && prototype !== null) {
57738
+ throw new TypeError(`${name} must contain only plain JSON objects.`);
57739
+ }
57740
+ const ownKeys = Reflect.ownKeys(root);
57741
+ if (ownKeys.some((key) => typeof key !== "string")) {
57742
+ throw new TypeError(`${name} must contain only string-keyed JSON objects.`);
57743
+ }
57744
+ const keys = ownKeys.toSorted();
57745
+ const fields = keys.map((key) => {
57746
+ const descriptor = Object.getOwnPropertyDescriptor(root, key);
57747
+ if (!descriptor?.enumerable || !("value" in descriptor)) {
57748
+ throw new TypeError(`${name}.${key} must be an enumerable JSON value.`);
57749
+ }
57750
+ if (descriptor.value === void 0) {
57751
+ throw new TypeError(`${name}.${key} must not be undefined.`);
57752
+ }
57753
+ return `${JSON.stringify(key)}:${canonicalJson2(descriptor.value, `${name}.${key}`)}`;
57754
+ });
57755
+ return `{${fields.join(",")}}`;
57756
+ }
57757
+ function utf8ByteCount(text) {
57758
+ return new TextEncoder().encode(text).byteLength;
57759
+ }
57760
+ async function sha256HexOfUtf8(text) {
57761
+ const bytes = new TextEncoder().encode(text);
57762
+ const digestBytes = new Uint8Array(
57763
+ await crypto.subtle.digest("SHA-256", bytes)
57764
+ );
57765
+ return Array.from(digestBytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
57766
+ }
57767
+ async function sha256FingerprintOfUtf8(text) {
57768
+ return `sha256:${await sha256HexOfUtf8(text)}`;
57769
+ }
57770
+
57771
+ // src/buy/projection.ts
57772
+ var DECIMAL = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
57773
+ var CURRENCY = /^[A-Z]{3}$/;
57774
+ var SENSITIVE_FIELDS = /* @__PURE__ */ new Set([
57775
+ "api_key",
57776
+ "api_secret",
57777
+ "password",
57778
+ "email_id",
57779
+ "email",
57780
+ "mobile_no",
57781
+ "phone",
57782
+ "cell_number"
57783
+ ]);
57784
+ var ITEM_FIELDS = [
57785
+ { key: "item_code", kind: "string", required: true },
57786
+ { key: "item_name", kind: "string" },
57787
+ { key: "item_group", kind: "string" },
57788
+ { key: "stock_uom", kind: "string" },
57789
+ { key: "is_purchase_item", kind: "boolean" },
57790
+ { key: "is_stock_item", kind: "boolean" },
57791
+ { key: "disabled", kind: "boolean" }
57792
+ ];
57793
+ var BOM_FIELDS = [
57794
+ { key: "item", kind: "string", required: true },
57795
+ { key: "item_name", kind: "string" },
57796
+ { key: "quantity", kind: "decimal", required: true },
57797
+ { key: "uom", kind: "string", required: true },
57798
+ { key: "is_active", kind: "boolean" },
57799
+ { key: "is_default", kind: "boolean" },
57800
+ { key: "company", kind: "string" },
57801
+ { key: "currency", kind: "currency" },
57802
+ { key: "with_operations", kind: "boolean" }
57803
+ ];
57804
+ var BOM_ITEM_FIELDS = [
57805
+ { key: "item_code", kind: "string", required: true },
57806
+ { key: "item_name", kind: "string" },
57807
+ { key: "qty", kind: "decimal", required: true },
57808
+ { key: "uom", kind: "string", required: true },
57809
+ { key: "rate", kind: "decimal" },
57810
+ { key: "amount", kind: "decimal" },
57811
+ { key: "sourced_by_supplier", kind: "boolean" }
57812
+ ];
57813
+ var BOM_OPERATION_FIELDS = [
57814
+ { key: "operation", kind: "string", required: true },
57815
+ { key: "time_in_mins", kind: "decimal" },
57816
+ { key: "hour_rate", kind: "decimal" }
57817
+ ];
57818
+ var ITEM_PRICE_FIELDS = [
57819
+ { key: "item_code", kind: "string", required: true },
57820
+ { key: "item_name", kind: "string" },
57821
+ { key: "price_list", kind: "string" },
57822
+ { key: "price_list_rate", kind: "decimal", required: true },
57823
+ { key: "currency", kind: "currency", required: true },
57824
+ { key: "uom", kind: "string" },
57825
+ { key: "min_qty", kind: "decimal" },
57826
+ { key: "valid_from", kind: "date" },
57827
+ { key: "valid_upto", kind: "date" },
57828
+ { key: "supplier", kind: "string" },
57829
+ { key: "buying", kind: "boolean" },
57830
+ { key: "selling", kind: "boolean" }
57831
+ ];
57832
+ var SUPPLIER_QUOTATION_FIELDS = [
57833
+ { key: "supplier", kind: "string", required: true },
57834
+ { key: "supplier_name", kind: "string" },
57835
+ { key: "transaction_date", kind: "date", required: true },
57836
+ { key: "valid_till", kind: "date" },
57837
+ { key: "currency", kind: "currency", required: true },
57838
+ { key: "conversion_rate", kind: "decimal" },
57839
+ { key: "status", kind: "string" },
57840
+ { key: "company", kind: "string" },
57841
+ { key: "price_list", kind: "string" }
57842
+ ];
57843
+ var SUPPLIER_QUOTATION_ITEM_FIELDS = [
57844
+ { key: "item_code", kind: "string", required: true },
57845
+ { key: "item_name", kind: "string" },
57846
+ { key: "qty", kind: "decimal", required: true },
57847
+ { key: "uom", kind: "string", required: true },
57848
+ { key: "rate", kind: "decimal" },
57849
+ { key: "amount", kind: "decimal" },
57850
+ { key: "price_list_rate", kind: "decimal" },
57851
+ { key: "discount_percentage", kind: "decimal" },
57852
+ { key: "supplier_part_no", kind: "string" }
57853
+ ];
57854
+ var SUPPLIER_FIELDS = [
57855
+ { key: "supplier_name", kind: "string", required: true },
57856
+ { key: "supplier_group", kind: "string" },
57857
+ { key: "supplier_type", kind: "string" },
57858
+ { key: "country", kind: "string" },
57859
+ { key: "default_currency", kind: "currency" },
57860
+ { key: "disabled", kind: "boolean" }
57861
+ ];
57862
+ var PRICE_LIST_FIELDS = [
57863
+ { key: "currency", kind: "currency", required: true },
57864
+ { key: "enabled", kind: "boolean" },
57865
+ { key: "buying", kind: "boolean" },
57866
+ { key: "selling", kind: "boolean" },
57867
+ { key: "price_not_uom_dependent", kind: "boolean" }
57868
+ ];
57869
+ var UOM_FIELDS = [
57870
+ { key: "uom_name", kind: "string" },
57871
+ { key: "must_be_whole_number", kind: "boolean" }
57872
+ ];
57873
+ var CURRENCY_EXCHANGE_FIELDS = [
57874
+ { key: "from_currency", kind: "currency", required: true },
57875
+ { key: "to_currency", kind: "currency", required: true },
57876
+ { key: "exchange_rate", kind: "decimal", required: true },
57877
+ { key: "date", kind: "date", required: true }
57878
+ ];
57879
+ var BUY_DOCTYPE_CHILD_TABLES = {
57880
+ BOM: [
57881
+ { table: "items", fields: BOM_ITEM_FIELDS },
57882
+ { table: "operations", fields: BOM_OPERATION_FIELDS }
57883
+ ],
57884
+ "Supplier Quotation": [
57885
+ { table: "items", fields: SUPPLIER_QUOTATION_ITEM_FIELDS }
57886
+ ]
57887
+ };
57888
+ var BUY_DOCTYPE_FIELD_SPECS = {
57889
+ Item: ITEM_FIELDS,
57890
+ BOM: BOM_FIELDS,
57891
+ "Item Price": ITEM_PRICE_FIELDS,
57892
+ "Supplier Quotation": SUPPLIER_QUOTATION_FIELDS,
57893
+ Supplier: SUPPLIER_FIELDS,
57894
+ "Price List": PRICE_LIST_FIELDS,
57895
+ UOM: UOM_FIELDS,
57896
+ "Currency Exchange": CURRENCY_EXCHANGE_FIELDS
57897
+ };
57898
+ function isBuyCaptureDoctype(value) {
57899
+ return BUY_CAPTURE_DOCTYPE.includes(value);
57900
+ }
57901
+ async function projectBuyDocument(doctype, raw2) {
57902
+ const name = requiredName(raw2.name, `${doctype}.name`);
57903
+ const modified = requiredModified(raw2.modified, `${doctype}.modified`);
57904
+ const fields = projectFields(
57905
+ raw2,
57906
+ BUY_DOCTYPE_FIELD_SPECS[doctype],
57907
+ `${doctype}`
57908
+ );
57909
+ const children = projectChildren(doctype, raw2);
57910
+ const docstatus = optionalDocstatus(raw2.docstatus, `${doctype}.docstatus`);
57911
+ const status = optionalStatus(raw2.status, `${doctype}.status`);
57912
+ const identity = {
57913
+ doctype,
57914
+ name,
57915
+ modified,
57916
+ ...docstatus !== void 0 ? { docstatus } : {},
57917
+ ...status !== void 0 ? { status } : {},
57918
+ sourceCategory: BUY_DOCTYPE_SOURCE_CATEGORY[doctype],
57919
+ fields,
57920
+ ...children !== void 0 ? { children } : {}
57921
+ };
57922
+ const fingerprint2 = await sha256FingerprintOfUtf8(
57923
+ canonicalJson2(identity, `${doctype}:${name} projection`)
57924
+ );
57925
+ return { ...identity, fingerprint: fingerprint2 };
57926
+ }
57927
+ function projectChildren(doctype, raw2) {
57928
+ const specs = BUY_DOCTYPE_CHILD_TABLES[doctype];
57929
+ if (!specs) return void 0;
57930
+ const tables = [];
57931
+ for (const spec of specs) {
57932
+ const value = raw2[spec.table];
57933
+ if (value === void 0 || value === null) continue;
57934
+ if (!Array.isArray(value)) {
57935
+ throw new BuyCaptureError(
57936
+ "BUY_CAPTURE_INVALID_PROJECTION",
57937
+ `${doctype}.${spec.table} must be a child table array.`,
57938
+ {
57939
+ recovery: "Capture only closed child tables with exact row identities.",
57940
+ context: { doctype, table: spec.table }
57941
+ }
57942
+ );
57943
+ }
57944
+ if (value.length > BUY_CAPTURE_MAX_CHILD_ROWS) {
57945
+ throw new BuyCaptureError(
57946
+ "BUY_CAPTURE_BOUNDS_EXCEEDED",
57947
+ `${doctype}.${spec.table} exceeds the ${BUY_CAPTURE_MAX_CHILD_ROWS}-row bound.`,
57948
+ {
57949
+ recovery: "Narrow the capture to fewer child rows.",
57950
+ context: {
57951
+ doctype,
57952
+ table: spec.table,
57953
+ limit: BUY_CAPTURE_MAX_CHILD_ROWS,
57954
+ actual: value.length
57955
+ }
57956
+ }
57957
+ );
57958
+ }
57959
+ if (value.length === 0) continue;
57960
+ const rows = value.map(
57961
+ (row, index) => projectChildRow(
57962
+ row,
57963
+ spec.fields,
57964
+ `${doctype}.${spec.table}[${index}]`
57965
+ )
57966
+ );
57967
+ tables.push({ table: spec.table, rows });
57968
+ }
57969
+ return tables.length > 0 ? tables : void 0;
57970
+ }
57971
+ function projectChildRow(row, specs, path) {
57972
+ if (typeof row !== "object" || row === null || Array.isArray(row)) {
57973
+ throw new BuyCaptureError(
57974
+ "BUY_CAPTURE_INVALID_PROJECTION",
57975
+ `${path} must be a child row object.`,
57976
+ {
57977
+ recovery: "Child rows must keep their exact Frappe name identity.",
57978
+ context: { path }
57979
+ }
57980
+ );
57981
+ }
57982
+ const record3 = row;
57983
+ const name = requiredName(record3.name, `${path}.name`);
57984
+ const idx = optionalIdx(record3.idx, `${path}.idx`);
57985
+ return {
57986
+ name,
57987
+ ...idx !== void 0 ? { idx } : {},
57988
+ fields: projectFields(record3, specs, path)
57989
+ };
57990
+ }
57991
+ function projectFields(raw2, specs, path) {
57992
+ for (const key of Object.keys(raw2)) {
57993
+ if (SENSITIVE_FIELDS.has(key)) {
57994
+ continue;
57995
+ }
57996
+ }
57997
+ const fields = {};
57998
+ for (const spec of specs) {
57999
+ const value = projectField(raw2[spec.key], spec, `${path}.${spec.key}`);
58000
+ if (value !== void 0) fields[spec.key] = value;
58001
+ }
58002
+ return fields;
58003
+ }
58004
+ function projectField(value, spec, path) {
58005
+ if (value === void 0 || value === null || value === "") {
58006
+ if (spec.required) {
58007
+ throw new BuyCaptureError(
58008
+ "BUY_CAPTURE_INVALID_PROJECTION",
58009
+ `${path} is required on the closed commercial projection.`,
58010
+ {
58011
+ recovery: "The ERP document is missing a field later consumed as cost evidence.",
58012
+ context: { path, field: spec.key }
58013
+ }
58014
+ );
58015
+ }
58016
+ return void 0;
58017
+ }
58018
+ switch (spec.kind) {
58019
+ case "string":
58020
+ return requiredString(value, path);
58021
+ case "decimal":
58022
+ return requiredDecimal(value, path);
58023
+ case "boolean":
58024
+ return requiredBoolean(value, path);
58025
+ case "currency":
58026
+ return requiredCurrency(value, path);
58027
+ case "date":
58028
+ return requiredDate(value, path);
58029
+ case "integer":
58030
+ return requiredInteger(value, path);
58031
+ }
58032
+ }
58033
+ function requiredName(value, path) {
58034
+ const name = requiredString(value, path);
58035
+ if (name.length > BUY_CAPTURE_MAX_NAME_LENGTH) {
58036
+ throw new BuyCaptureError(
58037
+ "BUY_CAPTURE_BOUNDS_EXCEEDED",
58038
+ `${path} exceeds the ${BUY_CAPTURE_MAX_NAME_LENGTH}-character bound.`,
58039
+ {
58040
+ recovery: "Use the exact ERP document name; do not pass a description.",
58041
+ context: {
58042
+ path,
58043
+ limit: BUY_CAPTURE_MAX_NAME_LENGTH,
58044
+ actual: name.length
58045
+ }
58046
+ }
58047
+ );
58048
+ }
58049
+ return name;
58050
+ }
58051
+ function requiredModified(value, path) {
58052
+ try {
58053
+ return frappeDatetime(value, path);
58054
+ } catch {
58055
+ throw new BuyCaptureError(
58056
+ "BUY_CAPTURE_INVALID_PROJECTION",
58057
+ `${path} is not a Frappe datetime.`,
58058
+ {
58059
+ recovery: "Capture the document's exact modified timestamp.",
58060
+ context: { path }
58061
+ }
58062
+ );
58063
+ }
58064
+ }
58065
+ function requiredString(value, path) {
58066
+ if (typeof value !== "string" || value.trim() === "" || value.trim() !== value) {
58067
+ throw new BuyCaptureError(
58068
+ "BUY_CAPTURE_INVALID_PROJECTION",
58069
+ `${path} must be a non-empty unpadded string.`,
58070
+ {
58071
+ recovery: "Use the exact ERP string; do not invent a label.",
58072
+ context: { path }
58073
+ }
58074
+ );
58075
+ }
58076
+ return value;
58077
+ }
58078
+ function requiredDecimal(value, path) {
58079
+ if (typeof value === "number") {
58080
+ if (!Number.isFinite(value)) {
58081
+ throw new BuyCaptureError(
58082
+ "BUY_CAPTURE_INVALID_PROJECTION",
58083
+ `${path} must be a finite decimal.`,
58084
+ {
58085
+ recovery: "Refuse non-finite commercial amounts rather than coerce them.",
58086
+ context: { path }
58087
+ }
58088
+ );
58089
+ }
58090
+ return JSON.stringify(value);
58091
+ }
58092
+ if (typeof value === "string") {
58093
+ if (!DECIMAL.test(value)) {
58094
+ throw new BuyCaptureError(
58095
+ "BUY_CAPTURE_INVALID_PROJECTION",
58096
+ `${path} must be a canonical decimal string.`,
58097
+ {
58098
+ recovery: "Keep the ERP decimal; do not fill absence with zero.",
58099
+ context: { path }
58100
+ }
58101
+ );
58102
+ }
58103
+ return value;
58104
+ }
58105
+ throw new BuyCaptureError(
58106
+ "BUY_CAPTURE_INVALID_PROJECTION",
58107
+ `${path} must be a decimal.`,
58108
+ {
58109
+ recovery: "Keep the ERP decimal; do not fill absence with zero.",
58110
+ context: { path }
58111
+ }
58112
+ );
58113
+ }
58114
+ function requiredBoolean(value, path) {
58115
+ if (value === true || value === 1) return true;
58116
+ if (value === false || value === 0) return false;
58117
+ throw new BuyCaptureError(
58118
+ "BUY_CAPTURE_INVALID_PROJECTION",
58119
+ `${path} must be a boolean or Frappe 0/1.`,
58120
+ {
58121
+ recovery: "Do not coerce missing flags to false.",
58122
+ context: { path }
58123
+ }
58124
+ );
58125
+ }
58126
+ function requiredCurrency(value, path) {
58127
+ const code = requiredString(value, path);
58128
+ if (!CURRENCY.test(code)) {
58129
+ throw new BuyCaptureError(
58130
+ "BUY_CAPTURE_INVALID_PROJECTION",
58131
+ `${path} must be a 3-letter currency code.`,
58132
+ {
58133
+ recovery: "Capture the document currency; do not guess a conversion.",
58134
+ context: { path }
58135
+ }
58136
+ );
58137
+ }
58138
+ return code;
58139
+ }
58140
+ function requiredDate(value, path) {
58141
+ try {
58142
+ return calendarDate(value, path);
58143
+ } catch {
58144
+ throw new BuyCaptureError(
58145
+ "BUY_CAPTURE_INVALID_PROJECTION",
58146
+ `${path} must be a real YYYY-MM-DD date.`,
58147
+ {
58148
+ recovery: "Keep the ERP calendar date; do not localize it.",
58149
+ context: { path }
58150
+ }
58151
+ );
58152
+ }
58153
+ }
58154
+ function requiredInteger(value, path) {
58155
+ if (!Number.isSafeInteger(value)) {
58156
+ throw new BuyCaptureError(
58157
+ "BUY_CAPTURE_INVALID_PROJECTION",
58158
+ `${path} must be a safe integer.`,
58159
+ { recovery: "Keep the ERP integer identity.", context: { path } }
58160
+ );
58161
+ }
58162
+ return value;
58163
+ }
58164
+ function optionalDocstatus(value, path) {
58165
+ if (value === void 0 || value === null || value === "") return void 0;
58166
+ if (value !== 0 && value !== 1 && value !== 2) {
58167
+ throw new BuyCaptureError(
58168
+ "BUY_CAPTURE_INVALID_PROJECTION",
58169
+ `${path} must be 0, 1, or 2.`,
58170
+ { recovery: "Keep the ERP docstatus.", context: { path } }
58171
+ );
58172
+ }
58173
+ return value;
58174
+ }
58175
+ function optionalStatus(value, path) {
58176
+ if (value === void 0 || value === null || value === "") return void 0;
58177
+ return requiredString(value, path);
58178
+ }
58179
+ function optionalIdx(value, path) {
58180
+ if (value === void 0 || value === null || value === "") return void 0;
58181
+ return requiredInteger(value, path);
58182
+ }
58183
+ function parseClosedWireFields(value, specs, path) {
58184
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
58185
+ throw new TypeError(`${path} must be an object.`);
58186
+ }
58187
+ const root = value;
58188
+ const allowed = new Map(specs.map((spec) => [spec.key, spec]));
58189
+ for (const key of Object.keys(root)) {
58190
+ if (!allowed.has(key)) {
58191
+ throw new TypeError(`${path}.${key} is not a closed commercial field.`);
58192
+ }
58193
+ }
58194
+ const fields = {};
58195
+ for (const spec of specs) {
58196
+ const raw2 = root[spec.key];
58197
+ if (raw2 === void 0 || raw2 === null || raw2 === "") {
58198
+ if (spec.required) {
58199
+ throw new TypeError(`${path}.${spec.key} is required.`);
58200
+ }
58201
+ continue;
58202
+ }
58203
+ fields[spec.key] = parseWireValue(raw2, spec, `${path}.${spec.key}`);
58204
+ }
58205
+ return fields;
58206
+ }
58207
+ function parseClosedWireChildren(doctype, value, path) {
58208
+ if (value === void 0) return void 0;
58209
+ if (!Array.isArray(value)) {
58210
+ throw new TypeError(`${path} must be an array.`);
58211
+ }
58212
+ const specs = BUY_DOCTYPE_CHILD_TABLES[doctype] ?? [];
58213
+ if (specs.length === 0) {
58214
+ throw new TypeError(`${path} is not admitted for ${doctype}.`);
58215
+ }
58216
+ if (value.length === 0) {
58217
+ throw new TypeError(`${path} must be omitted when empty.`);
58218
+ }
58219
+ const known = new Map(specs.map((spec) => [spec.table, spec]));
58220
+ const seenTables = /* @__PURE__ */ new Set();
58221
+ return value.map((item, index) => {
58222
+ const tablePath = `${path}[${index}]`;
58223
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
58224
+ throw new TypeError(`${tablePath} must be an object.`);
58225
+ }
58226
+ const table2 = item;
58227
+ const keys = Object.keys(table2).toSorted();
58228
+ if (keys.length !== 2 || keys[0] !== "rows" || keys[1] !== "table") {
58229
+ throw new TypeError(
58230
+ `${tablePath} contains missing or unsupported fields.`
58231
+ );
58232
+ }
58233
+ if (typeof table2.table !== "string" || !known.has(table2.table)) {
58234
+ throw new TypeError(
58235
+ `${tablePath}.table is not a closed child table for ${doctype}.`
58236
+ );
58237
+ }
58238
+ if (seenTables.has(table2.table)) {
58239
+ throw new TypeError(`${tablePath}.table duplicates ${table2.table}.`);
58240
+ }
58241
+ seenTables.add(table2.table);
58242
+ const spec = known.get(table2.table);
58243
+ if (!Array.isArray(table2.rows)) {
58244
+ throw new TypeError(`${tablePath}.rows must be an array.`);
58245
+ }
58246
+ if (table2.rows.length > BUY_CAPTURE_MAX_CHILD_ROWS) {
58247
+ throw new TypeError(
58248
+ `${tablePath}.rows exceeds the ${BUY_CAPTURE_MAX_CHILD_ROWS}-row bound.`
58249
+ );
58250
+ }
58251
+ const seenRows = /* @__PURE__ */ new Set();
58252
+ const rows = table2.rows.map((row, rowIndex) => {
58253
+ const rowPath = `${tablePath}.rows[${rowIndex}]`;
58254
+ const parsed = parseClosedWireRow(row, spec.fields, rowPath);
58255
+ if (seenRows.has(parsed.name)) {
58256
+ throw new TypeError(`${rowPath} duplicates child row ${parsed.name}.`);
58257
+ }
58258
+ seenRows.add(parsed.name);
58259
+ return parsed;
58260
+ });
58261
+ return { table: spec.table, rows };
58262
+ });
58263
+ }
58264
+ function parseClosedWireRow(value, specs, path) {
58265
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
58266
+ throw new TypeError(`${path} must be an object.`);
58267
+ }
58268
+ const root = value;
58269
+ const keys = Object.keys(root).toSorted();
58270
+ const allowed = root.idx === void 0 ? ["fields", "name"] : ["fields", "idx", "name"];
58271
+ if (keys.length !== allowed.length || keys.some((key, index) => key !== allowed[index])) {
58272
+ throw new TypeError(`${path} contains missing or unsupported fields.`);
58273
+ }
58274
+ const name = requiredWireName(root.name, `${path}.name`);
58275
+ return {
58276
+ name,
58277
+ ...root.idx === void 0 ? {} : {
58278
+ idx: requiredWireIdx(root.idx, `${path}.idx`)
58279
+ },
58280
+ fields: parseClosedWireFields(root.fields, specs, `${path}.fields`)
58281
+ };
58282
+ }
58283
+ function parseWireValue(value, spec, path) {
58284
+ switch (spec.kind) {
58285
+ case "string":
58286
+ if (typeof value !== "string" || value.trim() === "" || value.trim() !== value) {
58287
+ throw new TypeError(`${path} must be a non-empty unpadded string.`);
58288
+ }
58289
+ return value;
58290
+ case "decimal":
58291
+ if (typeof value !== "string" || !DECIMAL.test(value)) {
58292
+ throw new TypeError(`${path} must be a canonical decimal string.`);
58293
+ }
58294
+ return value;
58295
+ case "boolean":
58296
+ if (value !== true && value !== false) {
58297
+ throw new TypeError(`${path} must be a boolean.`);
58298
+ }
58299
+ return value;
58300
+ case "currency":
58301
+ if (typeof value !== "string" || !CURRENCY.test(value)) {
58302
+ throw new TypeError(`${path} must be a 3-letter currency code.`);
58303
+ }
58304
+ return value;
58305
+ case "date":
58306
+ return calendarDate(value, path);
58307
+ case "integer":
58308
+ if (!Number.isSafeInteger(value)) {
58309
+ throw new TypeError(`${path} must be a safe integer.`);
58310
+ }
58311
+ return value;
58312
+ }
58313
+ }
58314
+ function requiredWireName(value, path) {
58315
+ if (typeof value !== "string" || value.trim() === "" || value.trim() !== value) {
58316
+ throw new TypeError(`${path} must be a non-empty unpadded string.`);
58317
+ }
58318
+ if (value.length > BUY_CAPTURE_MAX_NAME_LENGTH) {
58319
+ throw new TypeError(
58320
+ `${path} exceeds the ${BUY_CAPTURE_MAX_NAME_LENGTH}-character bound.`
58321
+ );
58322
+ }
58323
+ return value;
58324
+ }
58325
+ function requiredWireIdx(value, path) {
58326
+ if (!Number.isSafeInteger(value) || value < 0) {
58327
+ throw new TypeError(`${path} must be a non-negative integer.`);
58328
+ }
58329
+ return value;
58330
+ }
58331
+
58332
+ // src/buy/site.ts
58333
+ async function sourceInstanceFromClient(client) {
58334
+ const normalized = client.normalizedSiteUrl();
58335
+ return {
58336
+ kind: BUY_SOURCE_INSTANCE_KIND,
58337
+ siteId: await sha256FingerprintOfUtf8(normalized)
58338
+ };
58339
+ }
58340
+
58341
+ // src/buy/capture.ts
58342
+ var CALLER_AUTHORITY_KEYS = [
58343
+ "sourceInstance",
58344
+ "capturedAt",
58345
+ "fingerprint",
58346
+ "canonicalText",
58347
+ "byteCount",
58348
+ "endpoint",
58349
+ "uri",
58350
+ "credentials",
58351
+ "baseUrl",
58352
+ "siteId",
58353
+ "attestedTimestamp",
58354
+ "digest"
58355
+ ];
58356
+ function parseBuyCaptureInput(value) {
58357
+ const root = asRecord(value, "buy capture input");
58358
+ for (const key of CALLER_AUTHORITY_KEYS) {
58359
+ if (key in root) {
58360
+ throw new BuyCaptureError(
58361
+ "BUY_CAPTURE_CALLER_AUTHORITY_REJECTED",
58362
+ `buy capture input must not include caller-supplied ${key}.`,
58363
+ {
58364
+ recovery: "Omit sourceInstance, capturedAt, fingerprints, endpoints, and credentials; the configured FrappeClient and server clock own those fields.",
58365
+ context: { key }
58366
+ }
58367
+ );
58368
+ }
58369
+ }
58370
+ const keys = Object.keys(root);
58371
+ if (keys.length !== 1 || keys[0] !== "documents") {
58372
+ throw new BuyCaptureError(
58373
+ "BUY_CAPTURE_INVALID_INPUT",
58374
+ "buy capture input must contain only documents.",
58375
+ {
58376
+ recovery: "Pass a bounded list of exact {doctype, name, expectedModified?} references.",
58377
+ context: { keys }
58378
+ }
58379
+ );
58380
+ }
58381
+ if (!Array.isArray(root.documents)) {
58382
+ throw new BuyCaptureError(
58383
+ "BUY_CAPTURE_INVALID_INPUT",
58384
+ "buy capture input.documents must be an array.",
58385
+ {
58386
+ recovery: "Pass a dense list of exact document references."
58387
+ }
58388
+ );
58389
+ }
58390
+ if (root.documents.length === 0) {
58391
+ throw new BuyCaptureError(
58392
+ "BUY_CAPTURE_INVALID_INPUT",
58393
+ "buy capture input.documents must not be empty.",
58394
+ {
58395
+ recovery: "Name the exact documents to capture."
58396
+ }
58397
+ );
58398
+ }
58399
+ if (root.documents.length > BUY_CAPTURE_MAX_DOCUMENTS) {
58400
+ throw new BuyCaptureError(
58401
+ "BUY_CAPTURE_BOUNDS_EXCEEDED",
58402
+ `buy capture input.documents exceeds the ${BUY_CAPTURE_MAX_DOCUMENTS}-document bound.`,
58403
+ {
58404
+ recovery: "Narrow the capture; silent pagination is not allowed.",
58405
+ context: {
58406
+ limit: BUY_CAPTURE_MAX_DOCUMENTS,
58407
+ actual: root.documents.length
58408
+ }
58409
+ }
58410
+ );
58411
+ }
58412
+ const seen = /* @__PURE__ */ new Set();
58413
+ const documents = root.documents.map(
58414
+ (item, index) => parseDocumentRef(item, index, seen)
58415
+ );
58416
+ return { documents };
58417
+ }
58418
+ async function parseBuySourceCapture(value) {
58419
+ const root = exactRecord(value, [
58420
+ "schemaVersion",
58421
+ "sourceInstance",
58422
+ "capturedAt",
58423
+ "documents",
58424
+ "consistency"
58425
+ ], "buy source capture");
58426
+ literal2(
58427
+ root.schemaVersion,
58428
+ BUY_SOURCE_CAPTURE_SCHEMA,
58429
+ "buy source capture.schemaVersion"
58430
+ );
58431
+ const source = exactRecord(
58432
+ root.sourceInstance,
58433
+ ["kind", "siteId"],
58434
+ "buy source capture.sourceInstance"
58435
+ );
58436
+ literal2(
58437
+ source.kind,
58438
+ BUY_SOURCE_INSTANCE_KIND,
58439
+ "buy source capture.sourceInstance.kind"
58440
+ );
58441
+ const documents = [];
58442
+ const rawDocuments = boundedArray(
58443
+ root.documents,
58444
+ BUY_CAPTURE_MAX_DOCUMENTS,
58445
+ "buy source capture.documents"
58446
+ );
58447
+ if (rawDocuments.length === 0) {
58448
+ throw new TypeError("buy source capture.documents must not be empty.");
58449
+ }
58450
+ const seenDocuments = /* @__PURE__ */ new Set();
58451
+ for (let index = 0; index < rawDocuments.length; index += 1) {
58452
+ const parsed = await parseCapturedDocument(rawDocuments[index], index);
58453
+ const id = `${parsed.doctype}:${parsed.name}`;
58454
+ if (seenDocuments.has(id)) {
58455
+ throw new TypeError(
58456
+ `buy source capture.documents[${index}] duplicates ${id}.`
58457
+ );
58458
+ }
58459
+ seenDocuments.add(id);
58460
+ documents.push(parsed);
58461
+ }
58462
+ const consistency = exactRecord(
58463
+ root.consistency,
58464
+ ["kind", "reads", "consistent"],
58465
+ "buy source capture.consistency"
58466
+ );
58467
+ literal2(
58468
+ consistency.kind,
58469
+ BUY_CONSISTENCY_KIND,
58470
+ "buy source capture.consistency.kind"
58471
+ );
58472
+ if (consistency.reads !== BUY_CONSISTENCY_READS) {
58473
+ throw new TypeError(
58474
+ "buy source capture.consistency.reads must be 2."
58475
+ );
58476
+ }
58477
+ if (consistency.consistent !== true) {
58478
+ throw new TypeError(
58479
+ "buy source capture.consistency.consistent must be true; inconsistent captures are not a usable payload."
58480
+ );
58481
+ }
58482
+ return {
58483
+ schemaVersion: BUY_SOURCE_CAPTURE_SCHEMA,
58484
+ sourceInstance: {
58485
+ kind: BUY_SOURCE_INSTANCE_KIND,
58486
+ siteId: fingerprint(
58487
+ source.siteId,
58488
+ "buy source capture.sourceInstance.siteId"
58489
+ )
58490
+ },
58491
+ capturedAt: canonicalTimestamp(
58492
+ root.capturedAt,
58493
+ "buy source capture.capturedAt"
58494
+ ),
58495
+ documents,
58496
+ consistency: {
58497
+ kind: BUY_CONSISTENCY_KIND,
58498
+ reads: BUY_CONSISTENCY_READS,
58499
+ consistent: true
58500
+ }
58501
+ };
58502
+ }
58503
+ async function sealBuySourceCapture(capture) {
58504
+ const parsed = await parseBuySourceCapture(capture);
58505
+ const canonicalText = canonicalJson2(parsed, "buy source capture");
58506
+ const byteCount = utf8ByteCount(canonicalText);
58507
+ if (byteCount > BUY_CAPTURE_MAX_CANONICAL_BYTES) {
58508
+ throw new BuyCaptureError(
58509
+ "BUY_CAPTURE_BOUNDS_EXCEEDED",
58510
+ `buy source capture exceeds the ${BUY_CAPTURE_MAX_CANONICAL_BYTES}-byte bound.`,
58511
+ {
58512
+ recovery: "Narrow the capture; silent pagination is not allowed.",
58513
+ context: { limit: BUY_CAPTURE_MAX_CANONICAL_BYTES, actual: byteCount }
58514
+ }
58515
+ );
58516
+ }
58517
+ return {
58518
+ schemaVersion: BUY_SOURCE_CAPTURE_SCHEMA,
58519
+ capture: parsed,
58520
+ canonicalText,
58521
+ fingerprint: await sha256FingerprintOfUtf8(canonicalText),
58522
+ byteCount
58523
+ };
58524
+ }
58525
+ async function runBuyCapture(input, deps) {
58526
+ const parsedInput = parseBuyCaptureInput(input);
58527
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
58528
+ const capturedAt = canonicalTimestamp(
58529
+ now().toISOString(),
58530
+ "buy source capture.capturedAt"
58531
+ );
58532
+ const sourceInstance = await sourceInstanceFromClient(deps.client);
58533
+ const first = await readAll(parsedInput.documents, deps.client);
58534
+ const documents = await Promise.all(
58535
+ first.map((row) => projectBuyDocument(row.doctype, row.doc))
58536
+ );
58537
+ for (let index = 0; index < parsedInput.documents.length; index += 1) {
58538
+ const expected = parsedInput.documents[index].expectedModified;
58539
+ if (expected !== void 0 && documents[index].modified !== expected) {
58540
+ throw new BuyCaptureError(
58541
+ "BUY_CAPTURE_EXPECTED_MODIFIED_MISMATCH",
58542
+ `Document ${documents[index].doctype}:${documents[index].name} modified does not match expectedModified.`,
58543
+ {
58544
+ recovery: "Re-resolve the exact document revision before capturing.",
58545
+ context: {
58546
+ doctype: documents[index].doctype,
58547
+ name: documents[index].name,
58548
+ modified: documents[index].modified,
58549
+ expectedModified: expected
58550
+ }
58551
+ }
58552
+ );
58553
+ }
58554
+ }
58555
+ const second = await readAll(parsedInput.documents, deps.client);
58556
+ const reread = await Promise.all(
58557
+ second.map((row) => projectBuyDocument(row.doctype, row.doc))
58558
+ );
58559
+ for (let index = 0; index < documents.length; index += 1) {
58560
+ const a = documents[index];
58561
+ const b = reread[index];
58562
+ if (a.modified !== b.modified || a.fingerprint !== b.fingerprint) {
58563
+ throw new BuyCaptureError(
58564
+ "BUY_CAPTURE_INCONSISTENT",
58565
+ `Document ${a.doctype}:${a.name} changed between the two skipCache reads.`,
58566
+ {
58567
+ retryable: true,
58568
+ recovery: "Retry the capture. Repeated-read consistency failed; this is not a transactional snapshot.",
58569
+ context: {
58570
+ doctype: a.doctype,
58571
+ name: a.name,
58572
+ firstModified: a.modified,
58573
+ secondModified: b.modified,
58574
+ firstFingerprint: a.fingerprint,
58575
+ secondFingerprint: b.fingerprint
58576
+ }
58577
+ }
58578
+ );
58579
+ }
58580
+ }
58581
+ return await sealBuySourceCapture({
58582
+ schemaVersion: BUY_SOURCE_CAPTURE_SCHEMA,
58583
+ sourceInstance,
58584
+ capturedAt,
58585
+ documents,
58586
+ consistency: {
58587
+ kind: BUY_CONSISTENCY_KIND,
58588
+ reads: BUY_CONSISTENCY_READS,
58589
+ consistent: true
58590
+ }
58591
+ });
58592
+ }
58593
+ async function readAll(refs, client) {
58594
+ const docs = [];
58595
+ for (const ref of refs) {
58596
+ let doc;
58597
+ try {
58598
+ doc = await client.get(ref.doctype, ref.name, { skipCache: true });
58599
+ } catch (error2) {
58600
+ if (error2 instanceof FrappeAPIError && error2.status === 404) {
58601
+ throw new BuyCaptureError(
58602
+ "BUY_CAPTURE_MISSING_SOURCE",
58603
+ `Document ${ref.doctype}:${ref.name} was not found.`,
58604
+ {
58605
+ recovery: "Capture only documents that currently exist on the configured site.",
58606
+ context: { doctype: ref.doctype, name: ref.name }
58607
+ }
58608
+ );
58609
+ }
58610
+ throw error2;
58611
+ }
58612
+ assertExactReturnedIdentity(ref, doc);
58613
+ docs.push({ doctype: ref.doctype, doc });
58614
+ }
58615
+ return docs;
58616
+ }
58617
+ function assertExactReturnedIdentity(ref, doc) {
58618
+ if (doc.name !== ref.name) {
58619
+ throw new BuyCaptureError(
58620
+ "BUY_CAPTURE_IDENTITY_MISMATCH",
58621
+ `Requested ${ref.doctype}:${ref.name} but the client returned name ${String(doc.name)}.`,
58622
+ {
58623
+ recovery: "Refuse the response; do not relabel a different ERP document as the requested identity.",
58624
+ context: {
58625
+ requestedDoctype: ref.doctype,
58626
+ requestedName: ref.name,
58627
+ returnedName: doc.name
58628
+ }
58629
+ }
58630
+ );
58631
+ }
58632
+ if (doc.doctype !== void 0 && doc.doctype !== null && doc.doctype !== "" && doc.doctype !== ref.doctype) {
58633
+ throw new BuyCaptureError(
58634
+ "BUY_CAPTURE_IDENTITY_MISMATCH",
58635
+ `Requested ${ref.doctype}:${ref.name} but the client returned doctype ${String(doc.doctype)}.`,
58636
+ {
58637
+ recovery: "Refuse the response; do not relabel a different ERP document as the requested identity.",
58638
+ context: {
58639
+ requestedDoctype: ref.doctype,
58640
+ requestedName: ref.name,
58641
+ returnedDoctype: doc.doctype
58642
+ }
58643
+ }
58644
+ );
58645
+ }
58646
+ }
58647
+ function parseDocumentRef(value, index, seen) {
58648
+ const path = `buy capture input.documents[${index}]`;
58649
+ const root = asRecord(value, path);
58650
+ const keys = Object.keys(root).toSorted();
58651
+ const allowed = root.expectedModified === void 0 ? ["doctype", "name"] : ["doctype", "expectedModified", "name"];
58652
+ if (keys.length !== allowed.length || keys.some((key, keyIndex) => key !== allowed[keyIndex])) {
58653
+ throw new BuyCaptureError(
58654
+ "BUY_CAPTURE_INVALID_INPUT",
58655
+ `${path} must contain only doctype, name, and optional expectedModified.`,
58656
+ {
58657
+ recovery: "Do not pass endpoints, sourceInstance, or extra ERP fields.",
58658
+ context: { keys }
58659
+ }
58660
+ );
58661
+ }
58662
+ if (typeof root.doctype !== "string" || !isBuyCaptureDoctype(root.doctype)) {
58663
+ throw new BuyCaptureError(
58664
+ "BUY_CAPTURE_UNSUPPORTED_DOCTYPE",
58665
+ `${path}.doctype is not a closed Buy capture DocType.`,
58666
+ {
58667
+ recovery: `Use one of ${BUY_CAPTURE_DOCTYPE.join(", ")}. Generic doctypes cannot masquerade as a price.`,
58668
+ context: { doctype: root.doctype }
58669
+ }
58670
+ );
58671
+ }
58672
+ if (typeof root.name !== "string" || root.name.trim() === "" || root.name.trim() !== root.name) {
58673
+ throw new BuyCaptureError(
58674
+ "BUY_CAPTURE_INVALID_INPUT",
58675
+ `${path}.name must be the exact ERP document name.`,
58676
+ { recovery: "Pass the exact document name, not a label." }
58677
+ );
58678
+ }
58679
+ if (root.name.length > BUY_CAPTURE_MAX_NAME_LENGTH) {
58680
+ throw new BuyCaptureError(
58681
+ "BUY_CAPTURE_BOUNDS_EXCEEDED",
58682
+ `${path}.name exceeds the ${BUY_CAPTURE_MAX_NAME_LENGTH}-character bound.`,
58683
+ {
58684
+ recovery: "Use the exact ERP document name.",
58685
+ context: {
58686
+ limit: BUY_CAPTURE_MAX_NAME_LENGTH,
58687
+ actual: root.name.length
58688
+ }
58689
+ }
58690
+ );
58691
+ }
58692
+ const id = `${root.doctype}:${root.name}`;
58693
+ if (seen.has(id)) {
58694
+ throw new BuyCaptureError(
58695
+ "BUY_CAPTURE_INVALID_INPUT",
58696
+ `${path} duplicates ${id}.`,
58697
+ { recovery: "Each exact document reference may appear once." }
58698
+ );
58699
+ }
58700
+ seen.add(id);
58701
+ const expectedModified = root.expectedModified === void 0 ? void 0 : parseExpectedModified(root.expectedModified, `${path}.expectedModified`);
58702
+ return {
58703
+ doctype: root.doctype,
58704
+ name: root.name,
58705
+ ...expectedModified !== void 0 ? { expectedModified } : {}
58706
+ };
58707
+ }
58708
+ function parseExpectedModified(value, path) {
58709
+ if (typeof value !== "string" || value.trim() === "" || value.trim() !== value) {
58710
+ throw new BuyCaptureError(
58711
+ "BUY_CAPTURE_INVALID_INPUT",
58712
+ `${path} must be the exact Frappe modified timestamp.`,
58713
+ { recovery: "Pass the document's modified value, not a caller digest." }
58714
+ );
58715
+ }
58716
+ try {
58717
+ frappeDatetime(value, path);
58718
+ } catch {
58719
+ throw new BuyCaptureError(
58720
+ "BUY_CAPTURE_INVALID_INPUT",
58721
+ `${path} is not a Frappe datetime.`,
58722
+ { recovery: "Pass the document's modified value." }
58723
+ );
58724
+ }
58725
+ return value;
58726
+ }
58727
+ async function parseCapturedDocument(value, index) {
58728
+ const path = `buy source capture.documents[${index}]`;
58729
+ const root = record2(value, path);
58730
+ const keys = Object.keys(root).toSorted();
58731
+ const optional2 = /* @__PURE__ */ new Set(["docstatus", "status", "children"]);
58732
+ const required2 = [
58733
+ "doctype",
58734
+ "fields",
58735
+ "fingerprint",
58736
+ "modified",
58737
+ "name",
58738
+ "sourceCategory"
58739
+ ];
58740
+ const actualRequired = keys.filter((key) => !optional2.has(key)).toSorted();
58741
+ if (actualRequired.length !== required2.length || actualRequired.some((key, keyIndex) => key !== required2[keyIndex])) {
58742
+ throw new TypeError(`${path} contains missing or unsupported fields.`);
58743
+ }
58744
+ const extra = keys.filter(
58745
+ (key) => !required2.includes(key) && !optional2.has(key)
58746
+ );
58747
+ if (extra.length > 0) {
58748
+ throw new TypeError(`${path} contains missing or unsupported fields.`);
58749
+ }
58750
+ const doctype = oneOf(
58751
+ root.doctype,
58752
+ BUY_CAPTURE_DOCTYPE,
58753
+ `${path}.doctype`
58754
+ );
58755
+ const document = {
58756
+ doctype,
58757
+ name: nonEmpty(root.name, `${path}.name`),
58758
+ modified: frappeDatetime(root.modified, `${path}.modified`),
58759
+ ...root.docstatus === void 0 ? {} : {
58760
+ docstatus: parseDocstatus(root.docstatus, `${path}.docstatus`)
58761
+ },
58762
+ ...root.status === void 0 ? {} : { status: nonEmpty(root.status, `${path}.status`) },
58763
+ sourceCategory: nonEmpty(
58764
+ root.sourceCategory,
58765
+ `${path}.sourceCategory`
58766
+ ),
58767
+ fields: parseClosedWireFields(
58768
+ root.fields,
58769
+ BUY_DOCTYPE_FIELD_SPECS[doctype],
58770
+ `${path}.fields`
58771
+ ),
58772
+ ...root.children === void 0 ? {} : {
58773
+ children: parseClosedWireChildren(
58774
+ doctype,
58775
+ root.children,
58776
+ `${path}.children`
58777
+ )
58778
+ },
58779
+ fingerprint: fingerprint(root.fingerprint, `${path}.fingerprint`)
58780
+ };
58781
+ if (document.sourceCategory !== BUY_DOCTYPE_SOURCE_CATEGORY[doctype]) {
58782
+ throw new TypeError(
58783
+ `${path}.sourceCategory must match the closed DocType; a generic document cannot masquerade as a price.`
58784
+ );
58785
+ }
58786
+ const { fingerprint: _ignored, ...identity } = document;
58787
+ const expectedFingerprint = await sha256FingerprintOfUtf8(
58788
+ canonicalJson2(identity, `${path} identity`)
58789
+ );
58790
+ if (document.fingerprint !== expectedFingerprint) {
58791
+ throw new TypeError(
58792
+ `${path}.fingerprint must be SHA-256 of the document projection without fingerprint.`
58793
+ );
58794
+ }
58795
+ return document;
58796
+ }
58797
+ function parseDocstatus(value, path) {
58798
+ if (value !== 0 && value !== 1 && value !== 2) {
58799
+ throw new TypeError(`${path} must be 0, 1, or 2.`);
58800
+ }
58801
+ return value;
58802
+ }
58803
+ function asRecord(value, name) {
58804
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
58805
+ throw new BuyCaptureError(
58806
+ "BUY_CAPTURE_INVALID_INPUT",
58807
+ `${name} must be an object.`,
58808
+ { recovery: "Pass a JSON object with exact keys." }
58809
+ );
58810
+ }
58811
+ return value;
58812
+ }
58813
+
58814
+ // src/tools/buy.ts
58815
+ var buyTools = [
58816
+ {
58817
+ name: "erpnext_buy_capture",
58818
+ annotations: { readOnlyHint: true },
58819
+ description: "Read-only bounded capture of exact ERPNext commercial documents (Item, BOM, Item Price, Supplier Quotation, Supplier, Price List, UOM, Currency Exchange). Two skipCache reads must agree on modified and the closed projection fingerprint. Returns canonical JSON + SHA-256; does not store CAS, create BOM/RFQ/PO, compute a cost total, or qualify a live ERP. Not a registered purchase capability.",
58820
+ category: "buy",
58821
+ inputSchema: {
58822
+ type: "object",
58823
+ additionalProperties: false,
58824
+ required: ["documents"],
58825
+ properties: {
58826
+ documents: {
58827
+ type: "array",
58828
+ minItems: 1,
58829
+ maxItems: BUY_CAPTURE_MAX_DOCUMENTS,
58830
+ description: "Exact document references. Do not pass sourceInstance, URL, credentials, capturedAt, or a caller digest.",
58831
+ items: {
58832
+ type: "object",
58833
+ additionalProperties: false,
58834
+ required: ["doctype", "name"],
58835
+ properties: {
58836
+ doctype: {
58837
+ type: "string",
58838
+ enum: [...BUY_CAPTURE_DOCTYPE],
58839
+ description: "Closed Buy capture DocType"
58840
+ },
58841
+ name: {
58842
+ type: "string",
58843
+ minLength: 1,
58844
+ maxLength: 140,
58845
+ description: "Exact ERP document name"
58846
+ },
58847
+ expectedModified: {
58848
+ type: "string",
58849
+ description: "Optional exact Frappe modified timestamp to match"
58850
+ }
58851
+ }
58852
+ }
58853
+ }
58854
+ }
58855
+ },
58856
+ handler: async (input, ctx) => {
58857
+ return await runBuyCapture(input, { client: ctx.client });
58858
+ }
58859
+ }
58860
+ ];
58861
+
56893
58862
  // src/tools/mod.ts
56894
58863
  var toolsByCategory = {
56895
58864
  sales: salesTools,
@@ -56905,7 +58874,8 @@ var toolsByCategory = {
56905
58874
  operations: operationsTools,
56906
58875
  setup: setupTools,
56907
58876
  analytics: analyticsTools,
56908
- kanban: kanbanTools
58877
+ kanban: kanbanTools,
58878
+ buy: buyTools
56909
58879
  };
56910
58880
  var allTools = [
56911
58881
  ...salesTools,
@@ -56921,7 +58891,8 @@ var allTools = [
56921
58891
  ...operationsTools,
56922
58892
  ...setupTools,
56923
58893
  ...analyticsTools,
56924
- ...kanbanTools
58894
+ ...kanbanTools,
58895
+ ...buyTools
56925
58896
  ];
56926
58897
  function getToolsByCategory(category) {
56927
58898
  return toolsByCategory[category] ?? [];
@@ -57071,46 +59042,211 @@ var DOCTYPE_GET_TOOLS = {
57071
59042
  };
57072
59043
  var DOCTYPE_SEND_MESSAGE_HINTS = {
57073
59044
  "Customer": [
57074
- { label: "Orders", message: "Show sales orders for customer {id}" },
57075
- { label: "Invoices", message: "Show sales invoices for customer {id}" }
59045
+ {
59046
+ key: "orders",
59047
+ label: "Orders",
59048
+ message: "Show sales orders for customer {id}",
59049
+ tool: "erpnext_sales_order_list",
59050
+ args: { customer: "{id}", limit: 20 }
59051
+ },
59052
+ {
59053
+ key: "invoices",
59054
+ label: "Invoices",
59055
+ message: "Show sales invoices for customer {id}",
59056
+ tool: "erpnext_sales_invoice_list",
59057
+ args: { customer: "{id}", limit: 20 }
59058
+ }
57076
59059
  ],
57077
59060
  "Sales Order": [
57078
- { label: "Invoice", message: "Show invoices linked to sales order {id}" },
57079
- { label: "Delivery", message: "Show delivery notes for sales order {id}" }
59061
+ {
59062
+ key: "invoices",
59063
+ label: "Invoices",
59064
+ message: "Show invoices linked to sales order {id}",
59065
+ tool: "erpnext_doc_list",
59066
+ args: {
59067
+ doctype: "Sales Invoice",
59068
+ fields: ["name", "customer", "posting_date", "status", "grand_total"],
59069
+ filters: [["Sales Invoice Item", "sales_order", "=", "{id}"]],
59070
+ limit: 20
59071
+ }
59072
+ },
59073
+ {
59074
+ key: "deliveries",
59075
+ label: "Delivery notes",
59076
+ message: "Show delivery notes for sales order {id}",
59077
+ tool: "erpnext_doc_list",
59078
+ args: {
59079
+ doctype: "Delivery Note",
59080
+ fields: ["name", "customer", "posting_date", "status"],
59081
+ filters: [["Delivery Note Item", "against_sales_order", "=", "{id}"]],
59082
+ limit: 20
59083
+ }
59084
+ }
57080
59085
  ],
57081
59086
  "Sales Invoice": [
57082
- { label: "Payments", message: "Show payment entries for invoice {id}" }
59087
+ {
59088
+ key: "payments",
59089
+ label: "Payments",
59090
+ message: "Show payment entries for invoice {id}",
59091
+ tool: "erpnext_doc_list",
59092
+ args: {
59093
+ doctype: "Payment Entry",
59094
+ fields: [
59095
+ "name",
59096
+ "posting_date",
59097
+ "paid_amount",
59098
+ "mode_of_payment",
59099
+ "docstatus"
59100
+ ],
59101
+ filters: [["Payment Entry Reference", "reference_name", "=", "{id}"]],
59102
+ limit: 20
59103
+ }
59104
+ }
59105
+ ],
59106
+ "Purchase Invoice": [
59107
+ {
59108
+ key: "payments",
59109
+ label: "Payments",
59110
+ message: "Show payment entries for purchase invoice {id}",
59111
+ tool: "erpnext_doc_list",
59112
+ args: {
59113
+ doctype: "Payment Entry",
59114
+ fields: [
59115
+ "name",
59116
+ "posting_date",
59117
+ "paid_amount",
59118
+ "mode_of_payment",
59119
+ "docstatus"
59120
+ ],
59121
+ filters: [["Payment Entry Reference", "reference_name", "=", "{id}"]],
59122
+ limit: 20
59123
+ },
59124
+ kind: "list"
59125
+ }
57083
59126
  ],
57084
59127
  "Item": [
57085
- { label: "Stock", message: "Show stock balance for item {id}" },
57086
- { label: "Orders", message: "Show sales orders containing item {id}" }
59128
+ {
59129
+ key: "stock",
59130
+ label: "Stock",
59131
+ message: "Show stock balance for item {id}",
59132
+ tool: "erpnext_stock_balance",
59133
+ args: { item_code: "{id}" }
59134
+ },
59135
+ {
59136
+ key: "orders",
59137
+ label: "Orders",
59138
+ message: "Show sales orders containing item {id}",
59139
+ tool: "erpnext_doc_list",
59140
+ args: {
59141
+ doctype: "Sales Order",
59142
+ fields: ["name", "customer", "transaction_date", "status"],
59143
+ filters: [["Sales Order Item", "item_code", "=", "{id}"]],
59144
+ limit: 20
59145
+ }
59146
+ }
57087
59147
  ],
57088
59148
  "Supplier": [
57089
- { label: "PO", message: "Show purchase orders for supplier {id}" },
57090
- { label: "Invoices", message: "Show purchase invoices for supplier {id}" }
59149
+ {
59150
+ key: "po",
59151
+ label: "PO",
59152
+ message: "Show purchase orders for supplier {id}",
59153
+ tool: "erpnext_purchase_order_list",
59154
+ args: { supplier: "{id}", limit: 20 }
59155
+ },
59156
+ {
59157
+ key: "invoices",
59158
+ label: "Invoices",
59159
+ message: "Show purchase invoices for supplier {id}",
59160
+ tool: "erpnext_purchase_invoice_list",
59161
+ args: { supplier: "{id}", limit: 20 }
59162
+ }
57091
59163
  ],
57092
59164
  "Purchase Order": [
57093
- { label: "Receipt", message: "Show purchase receipts for order {id}" },
57094
- { label: "Invoice", message: "Show purchase invoices for order {id}" }
59165
+ {
59166
+ key: "receipt",
59167
+ label: "Receipt",
59168
+ message: "Show purchase receipts for order {id}",
59169
+ tool: "erpnext_doc_list",
59170
+ args: {
59171
+ doctype: "Purchase Receipt",
59172
+ fields: ["name", "supplier", "posting_date", "status"],
59173
+ filters: [["Purchase Receipt Item", "purchase_order", "=", "{id}"]],
59174
+ limit: 20
59175
+ }
59176
+ },
59177
+ {
59178
+ key: "invoice",
59179
+ label: "Invoice",
59180
+ message: "Show purchase invoices for order {id}",
59181
+ tool: "erpnext_doc_list",
59182
+ args: {
59183
+ doctype: "Purchase Invoice",
59184
+ fields: ["name", "supplier", "posting_date", "status", "grand_total"],
59185
+ filters: [["Purchase Invoice Item", "purchase_order", "=", "{id}"]],
59186
+ limit: 20
59187
+ }
59188
+ }
57095
59189
  ],
57096
59190
  "Employee": [
57097
- { label: "Attendance", message: "Show attendance for employee {id}" },
57098
- { label: "Leaves", message: "Show leave applications for employee {id}" }
59191
+ {
59192
+ key: "attendance",
59193
+ label: "Attendance",
59194
+ message: "Show attendance for employee {id}",
59195
+ tool: "erpnext_attendance_list",
59196
+ args: { employee: "{id}", limit: 20 }
59197
+ },
59198
+ {
59199
+ key: "leaves",
59200
+ label: "Leaves",
59201
+ message: "Show leave applications for employee {id}",
59202
+ tool: "erpnext_leave_application_list",
59203
+ args: { employee: "{id}", limit: 20 }
59204
+ }
57099
59205
  ],
57100
59206
  "Project": [
57101
- { label: "Tasks", message: "Show tasks for project {id}" },
57102
- { label: "Timesheets", message: "Show timesheets for project {id}" }
59207
+ {
59208
+ key: "tasks",
59209
+ label: "Tasks",
59210
+ message: "Show tasks for project {id}",
59211
+ tool: "erpnext_task_list",
59212
+ args: { project: "{id}", limit: 20 }
59213
+ },
59214
+ {
59215
+ key: "timesheets",
59216
+ label: "Timesheets",
59217
+ message: "Show timesheets for project {id}",
59218
+ tool: "erpnext_timesheet_list",
59219
+ args: { project: "{id}", limit: 20 }
59220
+ }
57103
59221
  ],
57104
59222
  "Task": [
57105
- { label: "Timesheets", message: "Show timesheets for task {id}" }
59223
+ {
59224
+ key: "timesheets",
59225
+ label: "Timesheets",
59226
+ message: "Show timesheets for task {id}",
59227
+ tool: "erpnext_doc_list",
59228
+ args: {
59229
+ doctype: "Timesheet",
59230
+ fields: ["name", "employee", "start_date", "total_hours", "status"],
59231
+ filters: [["Timesheet Detail", "task", "=", "{id}"]],
59232
+ limit: 20
59233
+ }
59234
+ }
57106
59235
  ],
57107
59236
  "Lead": [
57108
- { label: "Opportunities", message: "Show opportunities for lead {id}" }
59237
+ {
59238
+ key: "opportunities",
59239
+ label: "Opportunities",
59240
+ message: "Show opportunities for lead {id}",
59241
+ tool: "erpnext_opportunity_list",
59242
+ // Le handler exige opportunity_from avec party_name.
59243
+ args: { opportunity_from: "Lead", party_name: "{id}", limit: 20 }
59244
+ }
57109
59245
  ]
57110
59246
  };
57111
59247
  var KPI_DRILL_DOWN = {
57112
59248
  "erpnext_kpi_revenue": {
57113
- _drillDown: "Show all sales invoices for this month",
59249
+ _drillDown: "Show all sales orders for this month",
57114
59250
  _trendDrillDown: "Show revenue trend chart for the last 12 months"
57115
59251
  },
57116
59252
  "erpnext_kpi_outstanding": {
@@ -57122,7 +59258,7 @@ var KPI_DRILL_DOWN = {
57122
59258
  _trendDrillDown: "Show order breakdown chart for the last 6 months"
57123
59259
  },
57124
59260
  "erpnext_kpi_gross_margin": {
57125
- _drillDown: "Show gross profit breakdown by item",
59261
+ _drillDown: "Show the non-cancelled Sales Order Items and Bin valuation rates used to estimate this gross margin",
57126
59262
  _trendDrillDown: "Show profit and loss chart for the last 12 months"
57127
59263
  },
57128
59264
  "erpnext_kpi_overdue": {
@@ -57133,12 +59269,522 @@ var KPI_DRILL_DOWN = {
57133
59269
  var CHART_DRILL_DOWN = {
57134
59270
  "erpnext_sales_chart": "Show sales invoices for {label}",
57135
59271
  "erpnext_stock_chart": "Show stock balance for item {label}",
57136
- "erpnext_revenue_trend": "Show sales invoices for month {label}",
59272
+ "erpnext_revenue_trend": "Show sales orders for month {label}",
57137
59273
  "erpnext_order_breakdown": "Show sales orders for {label}",
57138
- "erpnext_ar_aging": "Show overdue sales invoices in the {label} aging bucket",
59274
+ "erpnext_ar_aging": "Show outstanding sales invoices for customer {label}",
57139
59275
  "erpnext_gross_profit": "Show gross profit details for {label}",
57140
- "erpnext_profit_loss": "Show accounting details for month {label}"
59276
+ "erpnext_profit_loss": "Show submitted sales and purchase orders for month {label}"
57141
59277
  };
59278
+ var INVOICE_ITEM_HINTS = [
59279
+ {
59280
+ key: "item",
59281
+ label: "Item",
59282
+ message: "Show item {item}",
59283
+ tool: "erpnext_item_get",
59284
+ args: { name: "{item}" },
59285
+ kind: "record"
59286
+ },
59287
+ {
59288
+ key: "stock",
59289
+ label: "Stock",
59290
+ message: "Show stock balance for item {item}",
59291
+ tool: "erpnext_stock_balance",
59292
+ args: { item_code: "{item}", limit: 50 },
59293
+ kind: "list"
59294
+ }
59295
+ ];
59296
+ function nonEmptyDocumentString(document, key) {
59297
+ const value = document?.[key];
59298
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
59299
+ }
59300
+ function documentPartyHint(doctype, document) {
59301
+ if (doctype === "Sales Order" || doctype === "Sales Invoice") {
59302
+ const customer = nonEmptyDocumentString(document, "customer");
59303
+ return customer ? {
59304
+ key: "customer",
59305
+ label: "Customer",
59306
+ message: `Show customer ${customer}`,
59307
+ tool: "erpnext_customer_get",
59308
+ args: { name: customer },
59309
+ kind: "record"
59310
+ } : null;
59311
+ }
59312
+ if (doctype === "Purchase Invoice") {
59313
+ const supplier = nonEmptyDocumentString(document, "supplier");
59314
+ return supplier ? {
59315
+ key: "supplier",
59316
+ label: "Supplier",
59317
+ message: `Show supplier ${supplier}`,
59318
+ tool: "erpnext_supplier_get",
59319
+ args: { name: supplier },
59320
+ kind: "record"
59321
+ } : null;
59322
+ }
59323
+ if (doctype !== "Quotation") return null;
59324
+ const party = nonEmptyDocumentString(document, "party_name");
59325
+ const target = nonEmptyDocumentString(document, "quotation_to");
59326
+ if (!party || target !== "Customer" && target !== "Lead") return null;
59327
+ return target === "Customer" ? {
59328
+ key: "customer",
59329
+ label: "Customer",
59330
+ message: `Show customer ${party}`,
59331
+ tool: "erpnext_customer_get",
59332
+ args: { name: party },
59333
+ kind: "record"
59334
+ } : {
59335
+ key: "lead",
59336
+ label: "Lead",
59337
+ message: `Show lead ${party}`,
59338
+ tool: "erpnext_lead_get",
59339
+ args: { name: party },
59340
+ kind: "record"
59341
+ };
59342
+ }
59343
+ function documentNavigationHints(doctype, document) {
59344
+ const base = DOCTYPE_SEND_MESSAGE_HINTS[doctype] ?? [];
59345
+ const party = documentPartyHint(doctype, document);
59346
+ if (!party) return [...base];
59347
+ return doctype === "Sales Order" ? [party, ...base] : [...base, party];
59348
+ }
59349
+ var INVOICE_LINE_ITEM_DOCTYPES = /* @__PURE__ */ new Set([
59350
+ "Sales Order",
59351
+ "Sales Invoice",
59352
+ "Purchase Invoice",
59353
+ "Quotation"
59354
+ ]);
59355
+ function invoiceNavigationHints(doctype, document) {
59356
+ const documentHints = documentNavigationHints(doctype, document);
59357
+ return INVOICE_LINE_ITEM_DOCTYPES.has(doctype) ? [...documentHints, ...INVOICE_ITEM_HINTS] : documentHints;
59358
+ }
59359
+ var STOCK_HINTS = [
59360
+ {
59361
+ key: "item",
59362
+ label: "Item",
59363
+ message: "Show item {id}",
59364
+ tool: "erpnext_item_get",
59365
+ args: { name: "{id}" },
59366
+ kind: "record"
59367
+ },
59368
+ {
59369
+ key: "movements",
59370
+ label: "Stock entries",
59371
+ message: "Show stock entries for item {id}",
59372
+ tool: "erpnext_doc_list",
59373
+ args: {
59374
+ doctype: "Stock Entry",
59375
+ fields: ["name", "posting_date", "stock_entry_type", "docstatus"],
59376
+ filters: [["Stock Entry Detail", "item_code", "=", "{id}"]],
59377
+ limit: 20
59378
+ },
59379
+ kind: "list"
59380
+ },
59381
+ {
59382
+ key: "warehouse",
59383
+ label: "Warehouse stock",
59384
+ message: "Show stock chart for warehouse {warehouse}",
59385
+ tool: "erpnext_stock_chart",
59386
+ args: { warehouse: "{warehouse}", limit: 10 },
59387
+ kind: "chart"
59388
+ }
59389
+ ];
59390
+ function monthRange(now) {
59391
+ const y = now.getFullYear();
59392
+ const m = now.getMonth();
59393
+ const pad = (n) => String(n).padStart(2, "0");
59394
+ const last = new Date(y, m + 1, 0).getDate();
59395
+ return {
59396
+ from: `${y}-${pad(m + 1)}-01`,
59397
+ to: `${y}-${pad(m + 1)}-${pad(last)}`,
59398
+ today: `${y}-${pad(m + 1)}-${pad(now.getDate())}`
59399
+ };
59400
+ }
59401
+ var INVOICE_LIST_FIELDS = [
59402
+ "name",
59403
+ "customer",
59404
+ "posting_date",
59405
+ "due_date",
59406
+ "status",
59407
+ "outstanding_amount"
59408
+ ];
59409
+ var KPI_JUMPS = {
59410
+ "erpnext_kpi_revenue": (r) => ({
59411
+ number: {
59412
+ key: "sales_orders_month",
59413
+ label: "Sales orders this month",
59414
+ tool: "erpnext_doc_list",
59415
+ args: {
59416
+ doctype: "Sales Order",
59417
+ fields: [
59418
+ "name",
59419
+ "customer",
59420
+ "transaction_date",
59421
+ "status",
59422
+ "grand_total"
59423
+ ],
59424
+ filters: [
59425
+ ["transaction_date", ">=", r.from],
59426
+ ["transaction_date", "<=", r.to],
59427
+ // comme le KPI : les commandes annulées ne comptent pas
59428
+ ["docstatus", "<", 2]
59429
+ ],
59430
+ limit: 20
59431
+ },
59432
+ kind: "list"
59433
+ },
59434
+ trend: {
59435
+ key: "revenue_trend",
59436
+ label: "Revenue trend",
59437
+ tool: "erpnext_revenue_trend",
59438
+ args: { months: 12 },
59439
+ kind: "chart"
59440
+ }
59441
+ }),
59442
+ "erpnext_kpi_outstanding": () => ({
59443
+ number: {
59444
+ key: "unpaid_invoices",
59445
+ label: "Unpaid invoices",
59446
+ tool: "erpnext_doc_list",
59447
+ args: {
59448
+ doctype: "Sales Invoice",
59449
+ fields: INVOICE_LIST_FIELDS,
59450
+ filters: [["outstanding_amount", ">", 0], ["docstatus", "=", 1]],
59451
+ limit: 20
59452
+ },
59453
+ kind: "list"
59454
+ },
59455
+ trend: {
59456
+ key: "receivables_aging",
59457
+ label: "Receivables aging",
59458
+ tool: "erpnext_ar_aging",
59459
+ args: {},
59460
+ kind: "chart"
59461
+ }
59462
+ }),
59463
+ "erpnext_kpi_orders": (r) => ({
59464
+ number: {
59465
+ key: "sales_orders_month",
59466
+ label: "Sales orders this month",
59467
+ tool: "erpnext_doc_list",
59468
+ args: {
59469
+ doctype: "Sales Order",
59470
+ fields: [
59471
+ "name",
59472
+ "customer",
59473
+ "transaction_date",
59474
+ "status",
59475
+ "grand_total"
59476
+ ],
59477
+ filters: [
59478
+ ["transaction_date", ">=", r.from],
59479
+ ["transaction_date", "<=", r.to],
59480
+ // comme le KPI : les commandes annulées ne comptent pas
59481
+ ["docstatus", "<", 2]
59482
+ ],
59483
+ limit: 20
59484
+ },
59485
+ kind: "list"
59486
+ },
59487
+ trend: {
59488
+ key: "order_breakdown",
59489
+ label: "Order breakdown",
59490
+ tool: "erpnext_order_breakdown",
59491
+ args: {},
59492
+ kind: "chart"
59493
+ }
59494
+ }),
59495
+ "erpnext_kpi_gross_margin": () => ({
59496
+ trend: {
59497
+ key: "profit_loss",
59498
+ label: "Profit and loss",
59499
+ tool: "erpnext_profit_loss",
59500
+ args: { months: 12 },
59501
+ kind: "chart"
59502
+ }
59503
+ }),
59504
+ "erpnext_kpi_overdue": (r) => ({
59505
+ number: {
59506
+ key: "overdue_invoices",
59507
+ label: "Overdue invoices",
59508
+ tool: "erpnext_doc_list",
59509
+ args: {
59510
+ doctype: "Sales Invoice",
59511
+ fields: INVOICE_LIST_FIELDS,
59512
+ filters: [
59513
+ ["due_date", "<", r.today],
59514
+ ["outstanding_amount", ">", 0],
59515
+ ["docstatus", "=", 1]
59516
+ ],
59517
+ limit: 20
59518
+ },
59519
+ kind: "list"
59520
+ },
59521
+ trend: {
59522
+ key: "receivables_aging",
59523
+ label: "Receivables aging",
59524
+ tool: "erpnext_ar_aging",
59525
+ args: {},
59526
+ kind: "chart"
59527
+ }
59528
+ })
59529
+ };
59530
+ var FUNNEL_LIST_FIELDS = {
59531
+ "Leads": [
59532
+ "name",
59533
+ "lead_name",
59534
+ "company_name",
59535
+ "status",
59536
+ "lead_owner",
59537
+ "creation"
59538
+ ],
59539
+ "Opportunities": [
59540
+ "name",
59541
+ "opportunity_from",
59542
+ "party_name",
59543
+ "status",
59544
+ "opportunity_amount",
59545
+ "currency",
59546
+ "probability",
59547
+ "opportunity_owner",
59548
+ "transaction_date"
59549
+ ],
59550
+ "Quotations": [
59551
+ "name",
59552
+ "party_name",
59553
+ "transaction_date",
59554
+ "status",
59555
+ "grand_total"
59556
+ ],
59557
+ "Orders": [
59558
+ "name",
59559
+ "customer",
59560
+ "transaction_date",
59561
+ "status",
59562
+ "grand_total"
59563
+ ]
59564
+ };
59565
+ var FUNNEL_STAGE_JUMPS = {
59566
+ "Leads": {
59567
+ key: "leads",
59568
+ label: "Leads",
59569
+ tool: "erpnext_lead_list",
59570
+ args: { limit: 20 },
59571
+ kind: "list"
59572
+ },
59573
+ "Opportunities": {
59574
+ key: "opportunities",
59575
+ label: "Opportunities",
59576
+ tool: "erpnext_opportunity_list",
59577
+ args: { limit: 20 },
59578
+ kind: "list"
59579
+ },
59580
+ "Quotations": {
59581
+ key: "quotations",
59582
+ label: "Quotations",
59583
+ tool: "erpnext_doc_list",
59584
+ args: {
59585
+ doctype: "Quotation",
59586
+ fields: FUNNEL_LIST_FIELDS.Quotations,
59587
+ filters: [["docstatus", "!=", 2]],
59588
+ limit: 20
59589
+ },
59590
+ kind: "list"
59591
+ },
59592
+ "Orders": {
59593
+ key: "sales_orders",
59594
+ label: "Sales orders",
59595
+ tool: "erpnext_doc_list",
59596
+ args: {
59597
+ doctype: "Sales Order",
59598
+ fields: FUNNEL_LIST_FIELDS.Orders,
59599
+ filters: [["docstatus", "!=", 2]],
59600
+ limit: 20
59601
+ },
59602
+ kind: "list"
59603
+ }
59604
+ };
59605
+ function funnelPeriodStart(period, now) {
59606
+ const year = now.getFullYear();
59607
+ const pad = (value) => String(value).padStart(2, "0");
59608
+ if (period === "this_month") {
59609
+ return `${year}-${pad(now.getMonth() + 1)}-01`;
59610
+ }
59611
+ if (period === "this_quarter") {
59612
+ const firstMonth = Math.floor(now.getMonth() / 3) * 3 + 1;
59613
+ return `${year}-${pad(firstMonth)}-01`;
59614
+ }
59615
+ if (period === "this_year") return `${year}-01-01`;
59616
+ return void 0;
59617
+ }
59618
+ function funnelStageJumps(args, now) {
59619
+ const since = funnelPeriodStart(args.period, now);
59620
+ if (!since) return FUNNEL_STAGE_JUMPS;
59621
+ const scopedJump = (stage, doctype, dateField) => {
59622
+ const base = FUNNEL_STAGE_JUMPS[stage];
59623
+ const baseFilters = Array.isArray(base.args.filters) ? base.args.filters : [];
59624
+ return {
59625
+ ...base,
59626
+ tool: "erpnext_doc_list",
59627
+ args: {
59628
+ doctype,
59629
+ fields: FUNNEL_LIST_FIELDS[stage],
59630
+ filters: [[dateField, ">=", since], ...baseFilters],
59631
+ limit: 20
59632
+ }
59633
+ };
59634
+ };
59635
+ return {
59636
+ "Leads": scopedJump("Leads", "Lead", "creation"),
59637
+ "Opportunities": scopedJump(
59638
+ "Opportunities",
59639
+ "Opportunity",
59640
+ "transaction_date"
59641
+ ),
59642
+ "Quotations": scopedJump(
59643
+ "Quotations",
59644
+ "Quotation",
59645
+ "transaction_date"
59646
+ ),
59647
+ "Orders": scopedJump("Orders", "Sales Order", "transaction_date")
59648
+ };
59649
+ }
59650
+ var SALES_ORDER_LIST_FIELDS = [
59651
+ "name",
59652
+ "customer",
59653
+ "transaction_date",
59654
+ "status",
59655
+ "grand_total"
59656
+ ];
59657
+ var PURCHASE_ORDER_LIST_FIELDS = [
59658
+ "name",
59659
+ "supplier",
59660
+ "transaction_date",
59661
+ "status",
59662
+ "grand_total"
59663
+ ];
59664
+ function monthLabel(d) {
59665
+ return `${d.toLocaleString("en", { month: "short" })} ${d.getFullYear().toString().slice(2)}`;
59666
+ }
59667
+ function monthBuckets(monthsBack, now) {
59668
+ const buckets = [];
59669
+ for (let m = 0; m < monthsBack; m++) {
59670
+ const d = new Date(
59671
+ now.getFullYear(),
59672
+ now.getMonth() - monthsBack + 1 + m,
59673
+ 1
59674
+ );
59675
+ buckets.push({ label: monthLabel(d), range: monthRange(d) });
59676
+ }
59677
+ return buckets;
59678
+ }
59679
+ var docList = (label, doctype, fields, filters) => ({
59680
+ label,
59681
+ tool: "erpnext_doc_list",
59682
+ args: { doctype, fields, filters, limit: 20 },
59683
+ kind: "list"
59684
+ });
59685
+ function chartPointJumps(toolName, args, labels, now) {
59686
+ const jumps = {};
59687
+ const byLabel = (make) => {
59688
+ for (const label of labels) {
59689
+ if (label === "Unknown") continue;
59690
+ jumps[label] = make(label);
59691
+ }
59692
+ };
59693
+ switch (toolName) {
59694
+ case "erpnext_revenue_trend": {
59695
+ const months = Number(args.months ?? 6);
59696
+ for (const { label, range } of monthBuckets(months, now)) {
59697
+ if (!labels.includes(label)) continue;
59698
+ jumps[label] = docList(label, "Sales Order", SALES_ORDER_LIST_FIELDS, [
59699
+ ["transaction_date", ">=", range.from],
59700
+ ["transaction_date", "<=", range.to],
59701
+ ["docstatus", "<", 2]
59702
+ ]);
59703
+ }
59704
+ break;
59705
+ }
59706
+ case "erpnext_sales_chart": {
59707
+ const groupBy = String(args.group_by ?? "customer");
59708
+ byLabel((label) => {
59709
+ const filters = [
59710
+ groupBy === "item" ? ["Sales Invoice Item", "item_name", "=", label] : groupBy === "status" ? ["status", "=", label] : ["customer_name", "=", label]
59711
+ ];
59712
+ if (groupBy === "status") {
59713
+ filters.push(["docstatus", "!=", 2]);
59714
+ } else if (groupBy === "item" || args.include_drafts !== true) {
59715
+ filters.push(["docstatus", "=", 1]);
59716
+ }
59717
+ return docList(label, "Sales Invoice", INVOICE_LIST_FIELDS, filters);
59718
+ });
59719
+ break;
59720
+ }
59721
+ case "erpnext_order_breakdown":
59722
+ case "erpnext_revenue_vs_orders":
59723
+ byLabel(
59724
+ (label) => docList(label, "Sales Order", SALES_ORDER_LIST_FIELDS, [
59725
+ ["customer_name", "=", label],
59726
+ ["docstatus", "<", 2]
59727
+ ])
59728
+ );
59729
+ break;
59730
+ case "erpnext_ar_aging":
59731
+ case "erpnext_profit_loss":
59732
+ return void 0;
59733
+ case "erpnext_gross_profit": {
59734
+ const groupBy = String(args.group_by ?? "item");
59735
+ byLabel(
59736
+ (label) => docList(label, "Sales Invoice", INVOICE_LIST_FIELDS, [
59737
+ groupBy === "customer" ? ["customer_name", "=", label] : ["Sales Invoice Item", "item_name", "=", label],
59738
+ ["docstatus", "=", 1]
59739
+ ])
59740
+ );
59741
+ break;
59742
+ }
59743
+ case "erpnext_stock_chart":
59744
+ byLabel((label) => ({
59745
+ label,
59746
+ tool: "erpnext_stock_balance",
59747
+ args: {
59748
+ item_code: label,
59749
+ ...typeof args.warehouse === "string" ? { warehouse: args.warehouse } : {},
59750
+ limit: 50
59751
+ },
59752
+ kind: "list"
59753
+ }));
59754
+ break;
59755
+ default:
59756
+ return void 0;
59757
+ }
59758
+ return Object.keys(jumps).length > 0 ? jumps : void 0;
59759
+ }
59760
+ function chartSeriesPointJumps(toolName, args, labels, now) {
59761
+ if (toolName !== "erpnext_profit_loss") return void 0;
59762
+ const jumps = {};
59763
+ const months = Number(args.months ?? 6);
59764
+ for (const { label, range } of monthBuckets(months, now)) {
59765
+ if (!labels.includes(label)) continue;
59766
+ const filters = [
59767
+ ["transaction_date", ">=", range.from],
59768
+ ["transaction_date", "<=", range.to],
59769
+ ["docstatus", "=", 1]
59770
+ ];
59771
+ jumps[label] = {
59772
+ "Income": docList(
59773
+ `${label} \xB7 Income`,
59774
+ "Sales Order",
59775
+ SALES_ORDER_LIST_FIELDS,
59776
+ filters
59777
+ ),
59778
+ "Expenses": docList(
59779
+ `${label} \xB7 Expenses`,
59780
+ "Purchase Order",
59781
+ PURCHASE_ORDER_LIST_FIELDS,
59782
+ filters
59783
+ )
59784
+ };
59785
+ }
59786
+ return Object.keys(jumps).length > 0 ? jumps : void 0;
59787
+ }
57142
59788
  function isChartViewer(result) {
57143
59789
  const uri = result._meta?.ui?.resourceUri;
57144
59790
  return uri === "ui://mcp-erpnext/chart-viewer";
@@ -57147,6 +59793,31 @@ function isKpiViewer(result) {
57147
59793
  const uri = result._meta?.ui?.resourceUri;
57148
59794
  return uri === "ui://mcp-erpnext/kpi-viewer";
57149
59795
  }
59796
+ function isFunnelViewer(result) {
59797
+ const uri = result._meta?.ui?.resourceUri;
59798
+ return uri === "ui://mcp-erpnext/funnel-viewer";
59799
+ }
59800
+ function resultDoctype(result) {
59801
+ if (typeof result.doctype === "string") return result.doctype;
59802
+ const data = result.data;
59803
+ if (isRecord8(data) && typeof data.doctype === "string") return data.doctype;
59804
+ return void 0;
59805
+ }
59806
+ function resultDocument(result) {
59807
+ return isRecord8(result.data) ? result.data : void 0;
59808
+ }
59809
+ function isInvoiceViewer(result) {
59810
+ const uri = result._meta?.ui?.resourceUri;
59811
+ return uri === "ui://mcp-erpnext/invoice-viewer";
59812
+ }
59813
+ function isStockViewer(result) {
59814
+ const uri = result._meta?.ui?.resourceUri;
59815
+ return uri === "ui://mcp-erpnext/stock-viewer";
59816
+ }
59817
+ function isKanbanViewer(result) {
59818
+ const uri = result._meta?.ui?.resourceUri;
59819
+ return uri === "ui://mcp-erpnext/kanban-viewer";
59820
+ }
57150
59821
  function isDoclistResult(result) {
57151
59822
  return typeof result.doctype === "string" && Array.isArray(result.data);
57152
59823
  }
@@ -57154,7 +59825,193 @@ function isDoclistViewer(result) {
57154
59825
  const uri = result._meta?.ui?.resourceUri;
57155
59826
  return uri === "ui://mcp-erpnext/doclist-viewer";
57156
59827
  }
57157
- function withUiRefreshRequest(result, toolName, args) {
59828
+ function isDocViewer(result) {
59829
+ const uri = result._meta?.ui?.resourceUri;
59830
+ return uri === "ui://mcp-erpnext/doc-viewer";
59831
+ }
59832
+ var VIEWER_TOOL_CANDIDATES = {
59833
+ "ui://mcp-erpnext/invoice-viewer": [
59834
+ "erpnext_item_get",
59835
+ "erpnext_stock_balance",
59836
+ "erpnext_file_list",
59837
+ "erpnext_file_upload",
59838
+ "erpnext_file_download"
59839
+ ],
59840
+ "ui://mcp-erpnext/doclist-viewer": [],
59841
+ "ui://mcp-erpnext/doc-viewer": [
59842
+ "erpnext_file_list",
59843
+ "erpnext_file_upload",
59844
+ "erpnext_file_download"
59845
+ ],
59846
+ "ui://mcp-erpnext/kanban-viewer": [
59847
+ "erpnext_doc_get",
59848
+ "erpnext_doc_update",
59849
+ "erpnext_user_list",
59850
+ "erpnext_doc_assign",
59851
+ "erpnext_doc_unassign"
59852
+ ],
59853
+ "ui://mcp-erpnext/stock-viewer": [
59854
+ "erpnext_item_get",
59855
+ "erpnext_stock_entry_list"
59856
+ ]
59857
+ };
59858
+ var SUBMITTABLE_VIEWER_DOCTYPES = /* @__PURE__ */ new Set([
59859
+ "Quotation",
59860
+ "Sales Order",
59861
+ "Delivery Note",
59862
+ "Sales Invoice",
59863
+ "Purchase Order",
59864
+ "Purchase Receipt",
59865
+ "Purchase Invoice",
59866
+ "Stock Entry",
59867
+ "Journal Entry",
59868
+ "Payment Entry",
59869
+ "Timesheet",
59870
+ "Leave Application",
59871
+ "Salary Slip",
59872
+ "BOM",
59873
+ "Work Order",
59874
+ "Job Card",
59875
+ "Asset",
59876
+ "Asset Movement",
59877
+ "Shipment"
59878
+ ]);
59879
+ var INVOICE_DEDICATED_MUTATION_TOOLS = {
59880
+ "Sales Order": [
59881
+ "erpnext_sales_order_submit",
59882
+ "erpnext_sales_order_cancel"
59883
+ ],
59884
+ "Sales Invoice": ["erpnext_sales_invoice_submit"]
59885
+ };
59886
+ function addDoctypeMutationCandidates(target, uri, doctype) {
59887
+ if (!doctype || !SUBMITTABLE_VIEWER_DOCTYPES.has(doctype)) return;
59888
+ if (uri === "ui://mcp-erpnext/invoice-viewer" || uri === "ui://mcp-erpnext/doclist-viewer" || uri === "ui://mcp-erpnext/doc-viewer") {
59889
+ target.add("erpnext_doc_submit");
59890
+ target.add("erpnext_doc_cancel");
59891
+ }
59892
+ if (uri === "ui://mcp-erpnext/invoice-viewer") {
59893
+ for (const tool of INVOICE_DEDICATED_MUTATION_TOOLS[doctype] ?? []) {
59894
+ target.add(tool);
59895
+ }
59896
+ }
59897
+ }
59898
+ function addJumpTools(target, jumps) {
59899
+ if (!jumps) return;
59900
+ for (const jump of Object.values(jumps)) target.add(jump.tool);
59901
+ }
59902
+ function addSeriesJumpTools(target, jumps) {
59903
+ if (!jumps) return;
59904
+ for (const seriesJumps of Object.values(jumps)) {
59905
+ addJumpTools(target, seriesJumps);
59906
+ }
59907
+ }
59908
+ function availableViewerToolNames(result, availableToolNames) {
59909
+ const candidates = /* @__PURE__ */ new Set();
59910
+ const uri = result._meta?.ui?.resourceUri;
59911
+ for (const name of uri ? VIEWER_TOOL_CANDIDATES[uri] ?? [] : []) {
59912
+ candidates.add(name);
59913
+ }
59914
+ addDoctypeMutationCandidates(candidates, uri, resultDoctype(result));
59915
+ if (result.refreshRequest) candidates.add(result.refreshRequest.toolName);
59916
+ if (result._rowAction) candidates.add(result._rowAction.toolName);
59917
+ for (const hint of result._sendMessageHints ?? []) {
59918
+ if (hint.tool) candidates.add(hint.tool);
59919
+ }
59920
+ addJumpTools(
59921
+ candidates,
59922
+ result._jumps ? Object.fromEntries(
59923
+ Object.entries(result._jumps).filter(
59924
+ (entry) => entry[1] !== void 0
59925
+ )
59926
+ ) : void 0
59927
+ );
59928
+ addJumpTools(candidates, result._stageJumps);
59929
+ addJumpTools(candidates, result._pointJumps);
59930
+ addSeriesJumpTools(candidates, result._seriesPointJumps);
59931
+ if (typeof result.moveToolName === "string") {
59932
+ candidates.add(result.moveToolName);
59933
+ }
59934
+ return [...candidates].filter((name) => availableToolNames.has(name)).sort();
59935
+ }
59936
+ function filterJumpMap(jumps, availableToolNames) {
59937
+ const available = Object.fromEntries(
59938
+ Object.entries(jumps).filter(
59939
+ ([, jump]) => availableToolNames.has(jump.tool)
59940
+ )
59941
+ );
59942
+ return Object.keys(available).length > 0 ? available : void 0;
59943
+ }
59944
+ function filterSeriesJumpMap(jumps, availableToolNames) {
59945
+ const available = {};
59946
+ for (const [label, seriesJumps] of Object.entries(jumps)) {
59947
+ const filtered = filterJumpMap(seriesJumps, availableToolNames);
59948
+ if (filtered) available[label] = filtered;
59949
+ }
59950
+ return Object.keys(available).length > 0 ? available : void 0;
59951
+ }
59952
+ function filterNavJumpsByAvailableTools(result, availableToolNames) {
59953
+ if (!availableToolNames) return result;
59954
+ const filtered = { ...result };
59955
+ if (result._jumps) {
59956
+ const jumps = filterJumpMap(
59957
+ Object.fromEntries(
59958
+ Object.entries(result._jumps).filter(
59959
+ (entry) => entry[1] !== void 0
59960
+ )
59961
+ ),
59962
+ availableToolNames
59963
+ );
59964
+ if (jumps) filtered._jumps = jumps;
59965
+ else delete filtered._jumps;
59966
+ }
59967
+ if (result._stageJumps) {
59968
+ const jumps = filterJumpMap(result._stageJumps, availableToolNames);
59969
+ if (jumps) filtered._stageJumps = jumps;
59970
+ else delete filtered._stageJumps;
59971
+ }
59972
+ if (result._pointJumps) {
59973
+ const jumps = filterJumpMap(result._pointJumps, availableToolNames);
59974
+ if (jumps) filtered._pointJumps = jumps;
59975
+ else delete filtered._pointJumps;
59976
+ }
59977
+ if (result._seriesPointJumps) {
59978
+ const jumps = filterSeriesJumpMap(
59979
+ result._seriesPointJumps,
59980
+ availableToolNames
59981
+ );
59982
+ if (jumps) filtered._seriesPointJumps = jumps;
59983
+ else delete filtered._seriesPointJumps;
59984
+ }
59985
+ if (result._rowAction && !availableToolNames.has(result._rowAction.toolName)) {
59986
+ const doctype = resultDoctype(result);
59987
+ if (doctype && availableToolNames.has("erpnext_doc_get")) {
59988
+ filtered._rowAction = {
59989
+ toolName: "erpnext_doc_get",
59990
+ idField: "name",
59991
+ argName: "name",
59992
+ extraArgs: { doctype }
59993
+ };
59994
+ } else {
59995
+ delete filtered._rowAction;
59996
+ }
59997
+ }
59998
+ if (result._sendMessageHints) {
59999
+ filtered._sendMessageHints = result._sendMessageHints.map((hint) => {
60000
+ if (!hint.tool || availableToolNames.has(hint.tool)) return hint;
60001
+ return { key: hint.key, label: hint.label, message: hint.message };
60002
+ });
60003
+ }
60004
+ filtered._availableTools = availableViewerToolNames(
60005
+ filtered,
60006
+ availableToolNames
60007
+ );
60008
+ return filtered;
60009
+ }
60010
+ function withViewerToolCapabilities(result, availableToolNames) {
60011
+ if (!isRecord8(result) || !hasUiResource(result)) return result;
60012
+ return filterNavJumpsByAvailableTools(result, availableToolNames);
60013
+ }
60014
+ function withUiRefreshRequest(result, toolName, args, now = /* @__PURE__ */ new Date(), availableToolNames) {
57158
60015
  if (!isRecord8(result) || !hasUiResource(result)) {
57159
60016
  return result;
57160
60017
  }
@@ -57184,8 +60041,8 @@ function withUiRefreshRequest(result, toolName, args) {
57184
60041
  }
57185
60042
  }
57186
60043
  if (isDoclistResult(enriched) && isDoclistViewer(enriched) && !enriched._sendMessageHints) {
57187
- const hints = DOCTYPE_SEND_MESSAGE_HINTS[enriched.doctype];
57188
- if (hints) {
60044
+ const hints = documentNavigationHints(enriched.doctype);
60045
+ if (hints.length > 0) {
57189
60046
  enriched._sendMessageHints = hints;
57190
60047
  }
57191
60048
  }
@@ -57204,10 +60061,92 @@ function withUiRefreshRequest(result, toolName, args) {
57204
60061
  }
57205
60062
  }
57206
60063
  }
57207
- return enriched;
60064
+ if (isKpiViewer(enriched) && !enriched._jumps) {
60065
+ const jumps = KPI_JUMPS[toolName];
60066
+ if (jumps) enriched._jumps = jumps(monthRange(now));
60067
+ }
60068
+ if (isFunnelViewer(enriched) && !enriched._stageJumps) {
60069
+ enriched._stageJumps = funnelStageJumps(args, now);
60070
+ }
60071
+ if (isChartViewer(enriched) && Array.isArray(enriched.labels)) {
60072
+ const labels = enriched.labels.map(String);
60073
+ if (!enriched._pointJumps) {
60074
+ const jumps = chartPointJumps(toolName, args, labels, now);
60075
+ if (jumps) enriched._pointJumps = jumps;
60076
+ }
60077
+ if (!enriched._seriesPointJumps) {
60078
+ const jumps = chartSeriesPointJumps(toolName, args, labels, now);
60079
+ if (jumps) enriched._seriesPointJumps = jumps;
60080
+ }
60081
+ }
60082
+ if (!enriched._sendMessageHints) {
60083
+ const doctype = resultDoctype(enriched);
60084
+ if (isInvoiceViewer(enriched) && doctype) {
60085
+ const hints = invoiceNavigationHints(doctype, resultDocument(enriched));
60086
+ if (hints.length > 0) enriched._sendMessageHints = hints;
60087
+ } else if (isStockViewer(enriched)) {
60088
+ enriched._sendMessageHints = STOCK_HINTS;
60089
+ } else if ((isKanbanViewer(enriched) || isDocViewer(enriched)) && doctype) {
60090
+ const hints = isDocViewer(enriched) ? invoiceNavigationHints(doctype, resultDocument(enriched)) : documentNavigationHints(doctype, resultDocument(enriched));
60091
+ if (hints.length > 0) enriched._sendMessageHints = hints;
60092
+ }
60093
+ }
60094
+ return filterNavJumpsByAvailableTools(enriched, availableToolNames);
57208
60095
  }
57209
60096
 
57210
60097
  // src/client.ts
60098
+ function isRecord9(value) {
60099
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60100
+ }
60101
+ function isSingleEmbeddedBlobResult(value) {
60102
+ if (!isRecord9(value) || !Array.isArray(value.content)) return false;
60103
+ if (Object.keys(value).some((key) => key !== "content")) return false;
60104
+ if (value.content.length !== 2) return false;
60105
+ const [summary, embedded] = value.content;
60106
+ if (!isRecord9(summary) || summary.type !== "text" || typeof summary.text !== "string") {
60107
+ return false;
60108
+ }
60109
+ if (!isRecord9(embedded) || embedded.type !== "resource") return false;
60110
+ if (!isRecord9(embedded.resource)) return false;
60111
+ const resource = embedded.resource;
60112
+ return typeof resource.uri === "string" && resource.uri.startsWith("file:///") && typeof resource.mimeType === "string" && typeof resource.blob === "string";
60113
+ }
60114
+ function withSafeUiRefresh(result, tool, args, availableToolNames) {
60115
+ const readOnly = tool.annotations?.readOnlyHint === true;
60116
+ if (!isRecord9(result)) {
60117
+ return result;
60118
+ }
60119
+ const resultMeta = isRecord9(result._meta) ? result._meta : {};
60120
+ const declaredUi = tool._meta?.ui;
60121
+ const viewerResult = declaredUi && !isRecord9(resultMeta.ui) ? {
60122
+ ...result,
60123
+ _meta: { ...resultMeta, ...tool._meta, ui: declaredUi }
60124
+ } : result;
60125
+ const request = isRecord9(viewerResult.refreshRequest) ? viewerResult.refreshRequest : null;
60126
+ const target = request && typeof request.toolName === "string" ? getToolByName(request.toolName) : void 0;
60127
+ const hasSafeExplicitRefresh = request !== null && typeof request.toolName === "string" && availableToolNames.has(request.toolName) && isRecord9(request.arguments) && (request.toolName === tool.name ? readOnly : target?.annotations?.readOnlyHint === true);
60128
+ if (hasSafeExplicitRefresh) {
60129
+ return withUiRefreshRequest(
60130
+ viewerResult,
60131
+ tool.name,
60132
+ args,
60133
+ /* @__PURE__ */ new Date(),
60134
+ availableToolNames
60135
+ );
60136
+ }
60137
+ let sanitized = viewerResult;
60138
+ if ("refreshRequest" in viewerResult) {
60139
+ sanitized = { ...viewerResult };
60140
+ delete sanitized.refreshRequest;
60141
+ }
60142
+ return readOnly ? withUiRefreshRequest(
60143
+ sanitized,
60144
+ tool.name,
60145
+ args,
60146
+ /* @__PURE__ */ new Date(),
60147
+ availableToolNames
60148
+ ) : withViewerToolCapabilities(sanitized, availableToolNames);
60149
+ }
57211
60150
  var ErpNextToolsClient = class {
57212
60151
  tools;
57213
60152
  enableLinkDisambiguation;
@@ -57248,6 +60187,7 @@ var ErpNextToolsClient = class {
57248
60187
  */
57249
60188
  buildHandlersMap() {
57250
60189
  const handlers = /* @__PURE__ */ new Map();
60190
+ const availableToolNames = new Set(this.tools.map((tool) => tool.name));
57251
60191
  for (const tool of this.tools) {
57252
60192
  const toolMeta = tool._meta;
57253
60193
  handlers.set(tool.name, async (args, mcpContext) => {
@@ -57270,10 +60210,14 @@ var ErpNextToolsClient = class {
57270
60210
  if (execution.result !== null && typeof execution.result === "object" && !Array.isArray(execution.result) && execution.result.resultType === "input_required") {
57271
60211
  return execution.result;
57272
60212
  }
57273
- const result = withUiRefreshRequest(
60213
+ if (tool.name === "erpnext_file_download" && isSingleEmbeddedBlobResult(execution.result)) {
60214
+ return execution.result;
60215
+ }
60216
+ const result = withSafeUiRefresh(
57274
60217
  execution.result,
57275
- tool.name,
57276
- execution.args
60218
+ tool,
60219
+ execution.args,
60220
+ availableToolNames
57277
60221
  );
57278
60222
  const r = result !== null && typeof result === "object" && !Array.isArray(result) ? result : null;
57279
60223
  const resultUi = r?._meta && typeof r._meta === "object" && r._meta.ui;
@@ -57300,7 +60244,15 @@ var ErpNextToolsClient = class {
57300
60244
  }
57301
60245
  const client = getFrappeClient();
57302
60246
  const result = await tool.handler(args, { client });
57303
- return withUiRefreshRequest(result, tool.name, args);
60247
+ if (tool.name === "erpnext_file_download" && isSingleEmbeddedBlobResult(result)) {
60248
+ return result;
60249
+ }
60250
+ return withSafeUiRefresh(
60251
+ result,
60252
+ tool,
60253
+ args,
60254
+ new Set(this.tools.map((candidate) => candidate.name))
60255
+ );
57304
60256
  }
57305
60257
  /** Get tool count */
57306
60258
  get count() {
@@ -57314,11 +60266,128 @@ var UI_VIEWERS = [
57314
60266
  "invoice-viewer",
57315
60267
  "stock-viewer",
57316
60268
  "doclist-viewer",
60269
+ "doc-viewer",
57317
60270
  "chart-viewer",
57318
60271
  "kpi-viewer",
57319
60272
  "funnel-viewer",
57320
- "kanban-viewer"
60273
+ "kanban-viewer",
60274
+ "buy-evidence-viewer"
57321
60275
  ];
60276
+ var VIEWER_RESOURCE_META = {
60277
+ ui: {
60278
+ csp: {
60279
+ resourceDomains: ["blob:"],
60280
+ frameDomains: ["blob:"]
60281
+ }
60282
+ }
60283
+ };
60284
+ function withViewerResourceMeta(resource) {
60285
+ return { ...resource, _meta: VIEWER_RESOURCE_META };
60286
+ }
60287
+
60288
+ // src/buy/manifest.ts
60289
+ var VIEW_APP_MANIFEST_SCHEMA = "io.casys.mcp.view-app-manifest/1.0";
60290
+ var VIEWER_SESSION_APPLY_ACTION = "viewer.session.apply";
60291
+ var BUY_VIEW_APP_MANIFEST = {
60292
+ schemaVersion: VIEW_APP_MANIFEST_SCHEMA,
60293
+ app: {
60294
+ id: BUY_VIEW_APP_ID,
60295
+ title: BUY_VIEW_APP_TITLE,
60296
+ version: BUY_VIEW_APP_VERSION
60297
+ },
60298
+ resources: [{
60299
+ uri: BUY_EVIDENCE_VIEWER_URI,
60300
+ ownership: "whole-view",
60301
+ resultSchemas: [BUY_RECORDED_RESULT_SCHEMA, BUY_RECORDED_RESULT_SCHEMA_V2],
60302
+ acceptedActions: [VIEWER_SESSION_APPLY_ACTION],
60303
+ sessionSchemas: [BUY_RECORDED_SESSION_SCHEMA]
60304
+ }]
60305
+ };
60306
+ var BUY_VIEW_APP_MANIFEST_JSON = `${JSON.stringify(BUY_VIEW_APP_MANIFEST)}
60307
+ `;
60308
+ function parseBuyViewAppManifest(value) {
60309
+ const root = exactRecord(
60310
+ value,
60311
+ ["schemaVersion", "app", "resources"],
60312
+ "Buy View App manifest"
60313
+ );
60314
+ literal2(
60315
+ root.schemaVersion,
60316
+ VIEW_APP_MANIFEST_SCHEMA,
60317
+ "Buy View App manifest.schemaVersion"
60318
+ );
60319
+ const app = exactRecord(
60320
+ root.app,
60321
+ ["id", "title", "version"],
60322
+ "Buy View App manifest.app"
60323
+ );
60324
+ literal2(app.id, BUY_VIEW_APP_ID, "Buy View App manifest.app.id");
60325
+ literal2(app.title, BUY_VIEW_APP_TITLE, "Buy View App manifest.app.title");
60326
+ literal2(
60327
+ app.version,
60328
+ BUY_VIEW_APP_VERSION,
60329
+ "Buy View App manifest.app.version"
60330
+ );
60331
+ const resources = denseArray(
60332
+ root.resources,
60333
+ "Buy View App manifest.resources"
60334
+ );
60335
+ if (resources.length !== 1) {
60336
+ throw new TypeError(
60337
+ "Buy View App manifest.resources must contain exactly one whole view."
60338
+ );
60339
+ }
60340
+ const resource = exactRecord(
60341
+ resources[0],
60342
+ [
60343
+ "uri",
60344
+ "ownership",
60345
+ "resultSchemas",
60346
+ "acceptedActions",
60347
+ "sessionSchemas"
60348
+ ],
60349
+ "Buy View App manifest.resources[0]"
60350
+ );
60351
+ literal2(
60352
+ resource.uri,
60353
+ BUY_EVIDENCE_VIEWER_URI,
60354
+ "Buy View App manifest.resources[0].uri"
60355
+ );
60356
+ literal2(
60357
+ resource.ownership,
60358
+ "whole-view",
60359
+ "Buy View App manifest.resources[0].ownership"
60360
+ );
60361
+ const resultSchemas = denseArray(
60362
+ resource.resultSchemas,
60363
+ "Buy View App manifest.resources[0].resultSchemas"
60364
+ );
60365
+ if (resultSchemas.length !== 2 || resultSchemas[0] !== BUY_RECORDED_RESULT_SCHEMA || resultSchemas[1] !== BUY_RECORDED_RESULT_SCHEMA_V2) {
60366
+ throw new TypeError(
60367
+ "Buy View App manifest result schemas must list buy-recorded-result/1.0 then /2.0 exactly."
60368
+ );
60369
+ }
60370
+ const acceptedActions = denseArray(
60371
+ resource.acceptedActions,
60372
+ "Buy View App manifest.resources[0].acceptedActions"
60373
+ );
60374
+ if (acceptedActions.length !== 1 || acceptedActions[0] !== VIEWER_SESSION_APPLY_ACTION) {
60375
+ throw new TypeError(
60376
+ "Buy View App manifest must accept viewer.session.apply exactly."
60377
+ );
60378
+ }
60379
+ const sessionSchemas = denseArray(
60380
+ resource.sessionSchemas,
60381
+ "Buy View App manifest.resources[0].sessionSchemas"
60382
+ );
60383
+ if (sessionSchemas.length !== 1 || sessionSchemas[0] !== BUY_RECORDED_SESSION_SCHEMA) {
60384
+ throw new TypeError(
60385
+ "Buy View App manifest must bind buy-recorded-session/1.0 exactly."
60386
+ );
60387
+ }
60388
+ return BUY_VIEW_APP_MANIFEST;
60389
+ }
60390
+ parseBuyViewAppManifest(BUY_VIEW_APP_MANIFEST);
57322
60391
 
57323
60392
  // src/ui/viewer-errors.ts
57324
60393
  var ViewerResourceError = class extends Error {
@@ -57667,6 +60736,28 @@ function loadMrtrConfig() {
57667
60736
 
57668
60737
  // server.ts
57669
60738
  var DEFAULT_HTTP_PORT = 3012;
60739
+ function registerBuyViewAppManifest(server) {
60740
+ const bytes = new TextEncoder().encode(BUY_VIEW_APP_MANIFEST_JSON);
60741
+ server.registerResource(
60742
+ {
60743
+ uri: BUY_VIEW_APP_MANIFEST_URI,
60744
+ name: "ERPNext Buy Evidence App manifest",
60745
+ description: `Exact ${BUY_VIEW_APP_MANIFEST.app.id}@${BUY_VIEW_APP_MANIFEST.app.version} whole-view and recorded-session contract.`,
60746
+ mimeType: "application/json",
60747
+ size: bytes.byteLength
60748
+ },
60749
+ (requested) => {
60750
+ if (requested.toString() !== BUY_VIEW_APP_MANIFEST_URI) {
60751
+ throw new Error("Requested URI does not match the Buy App manifest.");
60752
+ }
60753
+ return {
60754
+ uri: BUY_VIEW_APP_MANIFEST_URI,
60755
+ mimeType: "application/json",
60756
+ text: BUY_VIEW_APP_MANIFEST_JSON
60757
+ };
60758
+ }
60759
+ );
60760
+ }
57670
60761
  async function main() {
57671
60762
  const args = getArgs3();
57672
60763
  if (args.includes("--inspect")) {
@@ -57696,7 +60787,7 @@ async function main() {
57696
60787
  );
57697
60788
  const server = new McpApp({
57698
60789
  name: "mcp-erpnext",
57699
- version: "3.1.0",
60790
+ version: "3.2.0-beta.1",
57700
60791
  transport: "stateless",
57701
60792
  cache: {
57702
60793
  ttlMs: 36e5,
@@ -57717,6 +60808,7 @@ async function main() {
57717
60808
  const mcpTools = toolsClient.toMCPFormat();
57718
60809
  const handlers = toolsClient.buildHandlersMap();
57719
60810
  server.registerTools(mcpTools, handlers);
60811
+ registerBuyViewAppManifest(server);
57720
60812
  const skippedRemoteViewers = [];
57721
60813
  let skippedRemoteHost = "";
57722
60814
  for (const viewerName of UI_VIEWERS) {
@@ -57733,15 +60825,19 @@ async function main() {
57733
60825
  skippedRemoteHost = new URL(distPath).hostname;
57734
60826
  } else if (distPath) {
57735
60827
  server.registerResource(
57736
- {
60828
+ withViewerResourceMeta({
57737
60829
  uri: resourceUri,
57738
60830
  name: `ERPNext ${humanName}`,
57739
60831
  description: `ERPNext UI: ${viewerName}`,
57740
60832
  mimeType: MCP_APP_MIME_TYPE
57741
- },
60833
+ }),
57742
60834
  async () => {
57743
60835
  const html = await readViewerDist(distPath, readTextFile6);
57744
- return { uri: resourceUri, mimeType: MCP_APP_MIME_TYPE, text: html };
60836
+ return withViewerResourceMeta({
60837
+ uri: resourceUri,
60838
+ mimeType: MCP_APP_MIME_TYPE,
60839
+ text: html
60840
+ });
57745
60841
  }
57746
60842
  );
57747
60843
  console.error(`[mcp-erpnext] Registered UI resource: ${resourceUri}`);