ed-mcp 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,7 +8,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
8
8
  var __getProtoOf = Object.getPrototypeOf;
9
9
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
10
  var __commonJS = (cb, mod) => function __require() {
11
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ try {
12
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
13
+ } catch (e) {
14
+ throw mod = 0, e;
15
+ }
12
16
  };
13
17
  var __export = (target, all) => {
14
18
  for (var name in all)
@@ -408,11 +412,11 @@ var require_codegen = __commonJS({
408
412
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
409
413
  return `${varKind} ${this.name}${rhs};` + _n;
410
414
  }
411
- optimizeNames(names, constants) {
415
+ optimizeNames(names, constants3) {
412
416
  if (!names[this.name.str])
413
417
  return;
414
418
  if (this.rhs)
415
- this.rhs = optimizeExpr(this.rhs, names, constants);
419
+ this.rhs = optimizeExpr(this.rhs, names, constants3);
416
420
  return this;
417
421
  }
418
422
  get names() {
@@ -429,10 +433,10 @@ var require_codegen = __commonJS({
429
433
  render({ _n }) {
430
434
  return `${this.lhs} = ${this.rhs};` + _n;
431
435
  }
432
- optimizeNames(names, constants) {
436
+ optimizeNames(names, constants3) {
433
437
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
434
438
  return;
435
- this.rhs = optimizeExpr(this.rhs, names, constants);
439
+ this.rhs = optimizeExpr(this.rhs, names, constants3);
436
440
  return this;
437
441
  }
438
442
  get names() {
@@ -493,8 +497,8 @@ var require_codegen = __commonJS({
493
497
  optimizeNodes() {
494
498
  return `${this.code}` ? this : void 0;
495
499
  }
496
- optimizeNames(names, constants) {
497
- this.code = optimizeExpr(this.code, names, constants);
500
+ optimizeNames(names, constants3) {
501
+ this.code = optimizeExpr(this.code, names, constants3);
498
502
  return this;
499
503
  }
500
504
  get names() {
@@ -523,12 +527,12 @@ var require_codegen = __commonJS({
523
527
  }
524
528
  return nodes.length > 0 ? this : void 0;
525
529
  }
526
- optimizeNames(names, constants) {
530
+ optimizeNames(names, constants3) {
527
531
  const { nodes } = this;
528
532
  let i = nodes.length;
529
533
  while (i--) {
530
534
  const n = nodes[i];
531
- if (n.optimizeNames(names, constants))
535
+ if (n.optimizeNames(names, constants3))
532
536
  continue;
533
537
  subtractNames(names, n.names);
534
538
  nodes.splice(i, 1);
@@ -581,12 +585,12 @@ var require_codegen = __commonJS({
581
585
  return void 0;
582
586
  return this;
583
587
  }
584
- optimizeNames(names, constants) {
588
+ optimizeNames(names, constants3) {
585
589
  var _a3;
586
- this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
587
- if (!(super.optimizeNames(names, constants) || this.else))
590
+ this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants3);
591
+ if (!(super.optimizeNames(names, constants3) || this.else))
588
592
  return;
589
- this.condition = optimizeExpr(this.condition, names, constants);
593
+ this.condition = optimizeExpr(this.condition, names, constants3);
590
594
  return this;
591
595
  }
592
596
  get names() {
@@ -609,10 +613,10 @@ var require_codegen = __commonJS({
609
613
  render(opts) {
610
614
  return `for(${this.iteration})` + super.render(opts);
611
615
  }
612
- optimizeNames(names, constants) {
613
- if (!super.optimizeNames(names, constants))
616
+ optimizeNames(names, constants3) {
617
+ if (!super.optimizeNames(names, constants3))
614
618
  return;
615
- this.iteration = optimizeExpr(this.iteration, names, constants);
619
+ this.iteration = optimizeExpr(this.iteration, names, constants3);
616
620
  return this;
617
621
  }
618
622
  get names() {
@@ -648,10 +652,10 @@ var require_codegen = __commonJS({
648
652
  render(opts) {
649
653
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
650
654
  }
651
- optimizeNames(names, constants) {
652
- if (!super.optimizeNames(names, constants))
655
+ optimizeNames(names, constants3) {
656
+ if (!super.optimizeNames(names, constants3))
653
657
  return;
654
- this.iterable = optimizeExpr(this.iterable, names, constants);
658
+ this.iterable = optimizeExpr(this.iterable, names, constants3);
655
659
  return this;
656
660
  }
657
661
  get names() {
@@ -693,11 +697,11 @@ var require_codegen = __commonJS({
693
697
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
694
698
  return this;
695
699
  }
696
- optimizeNames(names, constants) {
700
+ optimizeNames(names, constants3) {
697
701
  var _a3, _b;
698
- super.optimizeNames(names, constants);
699
- (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
700
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants);
702
+ super.optimizeNames(names, constants3);
703
+ (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants3);
704
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants3);
701
705
  return this;
702
706
  }
703
707
  get names() {
@@ -998,7 +1002,7 @@ var require_codegen = __commonJS({
998
1002
  function addExprNames(names, from) {
999
1003
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
1000
1004
  }
1001
- function optimizeExpr(expr, names, constants) {
1005
+ function optimizeExpr(expr, names, constants3) {
1002
1006
  if (expr instanceof code_1.Name)
1003
1007
  return replaceName(expr);
1004
1008
  if (!canOptimize(expr))
@@ -1013,14 +1017,14 @@ var require_codegen = __commonJS({
1013
1017
  return items;
1014
1018
  }, []));
1015
1019
  function replaceName(n) {
1016
- const c = constants[n.str];
1020
+ const c = constants3[n.str];
1017
1021
  if (c === void 0 || names[n.str] !== 1)
1018
1022
  return n;
1019
1023
  delete names[n.str];
1020
1024
  return c;
1021
1025
  }
1022
1026
  function canOptimize(e) {
1023
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== void 0);
1027
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants3[c.str] !== void 0);
1024
1028
  }
1025
1029
  }
1026
1030
  function subtractNames(names, from) {
@@ -3260,8 +3264,8 @@ var require_utils = __commonJS({
3260
3264
  }
3261
3265
  return ind;
3262
3266
  }
3263
- function removeDotSegments(path12) {
3264
- let input2 = path12;
3267
+ function removeDotSegments(path15) {
3268
+ let input2 = path15;
3265
3269
  const output3 = [];
3266
3270
  let nextSlash = -1;
3267
3271
  let len = 0;
@@ -3335,6 +3339,37 @@ var require_utils = __commonJS({
3335
3339
  }
3336
3340
  return output3.join("");
3337
3341
  }
3342
+ function removeDotSegmentsRelative(path15) {
3343
+ if (path15 === "" || path15[0] === "/") {
3344
+ return removeDotSegments(path15);
3345
+ }
3346
+ const segments = path15.split("/");
3347
+ const last = segments.length - 1;
3348
+ const output3 = [];
3349
+ let trailingDirectory = false;
3350
+ for (let i = 0; i <= last; i++) {
3351
+ const segment = segments[i];
3352
+ if (segment === "." || segment === "..") {
3353
+ if (segment === "..") {
3354
+ if (output3.length !== 0 && output3[output3.length - 1] !== "..") {
3355
+ output3.pop();
3356
+ } else {
3357
+ output3.push("..");
3358
+ }
3359
+ }
3360
+ trailingDirectory = i === last;
3361
+ } else {
3362
+ output3.push(segment);
3363
+ }
3364
+ }
3365
+ if (trailingDirectory && output3[output3.length - 1] !== "..") {
3366
+ output3.push("");
3367
+ }
3368
+ if (output3.length === 0 || output3[0] === "" || output3[0].includes(":")) {
3369
+ output3.unshift(".");
3370
+ }
3371
+ return output3.join("/");
3372
+ }
3338
3373
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3339
3374
  var HOST_DELIM_RE = /[@/?#:]/g;
3340
3375
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
@@ -3595,6 +3630,7 @@ var require_utils = __commonJS({
3595
3630
  encodeFragment,
3596
3631
  escapePreservingEscapes,
3597
3632
  removeDotSegments,
3633
+ removeDotSegmentsRelative,
3598
3634
  isIPv4,
3599
3635
  isUUID,
3600
3636
  normalizeIPv6,
@@ -3670,8 +3706,8 @@ var require_schemes = __commonJS({
3670
3706
  }
3671
3707
  if (wsComponent.resourceName) {
3672
3708
  const queryIndex = wsComponent.resourceName.indexOf("?");
3673
- const path12 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3674
- wsComponent.path = path12 && path12 !== "/" ? path12 : void 0;
3709
+ const path15 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3710
+ wsComponent.path = path15 && path15 !== "/" ? path15 : void 0;
3675
3711
  wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3676
3712
  wsComponent.resourceName = void 0;
3677
3713
  }
@@ -3818,16 +3854,18 @@ var require_schemes = __commonJS({
3818
3854
  var require_fast_uri = __commonJS({
3819
3855
  "node_modules/fast-uri/index.js"(exports, module) {
3820
3856
  "use strict";
3821
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3857
+ var { normalizeIPv6, removeDotSegments, removeDotSegmentsRelative, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3822
3858
  var { SCHEMES, getSchemeHandler } = require_schemes();
3823
3859
  var VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
3824
3860
  var MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
3825
- function decodeValidScheme(scheme) {
3826
- const decodedScheme = unescape(String(scheme));
3827
- if (!VALID_SCHEME.test(decodedScheme)) {
3861
+ var MALFORMED_HOST_ERROR = "URI host is malformed.";
3862
+ var FORBIDDEN_DOMAIN_HOST_CHARACTER = /[\0-\x20"#/:<>?@[\\\]^`{|}\x7F]/u;
3863
+ function validateScheme(scheme) {
3864
+ const stringScheme = String(scheme);
3865
+ if (!VALID_SCHEME.test(stringScheme)) {
3828
3866
  throw new TypeError(MALFORMED_SCHEME_ERROR);
3829
3867
  }
3830
- return decodedScheme;
3868
+ return stringScheme;
3831
3869
  }
3832
3870
  function normalize(uri, options) {
3833
3871
  if (typeof uri === "string") {
@@ -3835,7 +3873,7 @@ var require_fast_uri = __commonJS({
3835
3873
  normalizeString(uri, options);
3836
3874
  } else if (typeof uri === "object") {
3837
3875
  uri = /** @type {T} */
3838
- parse3(serialize(uri, options), options);
3876
+ parse4(serialize(uri, options), options);
3839
3877
  }
3840
3878
  return uri;
3841
3879
  }
@@ -3864,6 +3902,9 @@ var require_fast_uri = __commonJS({
3864
3902
  const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
3865
3903
  const resolvedHost = resolved.host;
3866
3904
  const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
3905
+ if (hasForbiddenDomainHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)) {
3906
+ throw new Error(MALFORMED_HOST_ERROR);
3907
+ }
3867
3908
  canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
3868
3909
  const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !new RegExp("\\P{ASCII}", "u").test(resolvedHost);
3869
3910
  if (resolved.error && !encodedASCIIHost) {
@@ -3875,8 +3916,8 @@ var require_fast_uri = __commonJS({
3875
3916
  function resolveComponent(base2, relative, options, skipNormalization) {
3876
3917
  const target = {};
3877
3918
  if (!skipNormalization) {
3878
- base2 = parse3(serialize(base2, options), options);
3879
- relative = parse3(serialize(relative, options), options);
3919
+ base2 = parse4(serialize(base2, options), options);
3920
+ relative = parse4(serialize(relative, options), options);
3880
3921
  }
3881
3922
  options = options || {};
3882
3923
  if (!options.tolerant && relative.scheme) {
@@ -3905,14 +3946,15 @@ var require_fast_uri = __commonJS({
3905
3946
  if (relative.path[0] === "/") {
3906
3947
  target.path = removeDotSegments(relative.path);
3907
3948
  } else {
3908
- if ((base2.userinfo !== void 0 || base2.host !== void 0 || base2.port !== void 0) && !base2.path) {
3949
+ const baseHasAuthority = base2.userinfo !== void 0 || base2.host !== void 0 || base2.port !== void 0;
3950
+ if (baseHasAuthority && !base2.path) {
3909
3951
  target.path = "/" + relative.path;
3910
3952
  } else if (!base2.path) {
3911
3953
  target.path = relative.path;
3912
3954
  } else {
3913
3955
  target.path = base2.path.slice(0, base2.path.lastIndexOf("/") + 1) + relative.path;
3914
3956
  }
3915
- target.path = removeDotSegments(target.path);
3957
+ target.path = base2.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
3916
3958
  }
3917
3959
  target.query = relative.query;
3918
3960
  }
@@ -3950,12 +3992,13 @@ var require_fast_uri = __commonJS({
3950
3992
  const options = Object.assign({}, opts);
3951
3993
  const uriTokens = [];
3952
3994
  if (component.scheme) {
3953
- component.scheme = decodeValidScheme(component.scheme);
3995
+ component.scheme = validateScheme(component.scheme);
3954
3996
  }
3955
3997
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
3956
3998
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3957
3999
  const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
3958
- const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
4000
+ const schemelessReference = (component.scheme === void 0 || options.reference === "suffix") && !hasAuthority;
4001
+ const pathNoScheme = !options.skipEscape && schemelessReference;
3959
4002
  if (component.path !== void 0) {
3960
4003
  if (!options.skipEscape) {
3961
4004
  component.path = serializePathEncoding(component.path, pathNoScheme);
@@ -3964,7 +4007,7 @@ var require_fast_uri = __commonJS({
3964
4007
  }
3965
4008
  }
3966
4009
  if (options.reference !== "suffix" && component.scheme) {
3967
- component.scheme = decodeValidScheme(component.scheme);
4010
+ component.scheme = validateScheme(component.scheme);
3968
4011
  uriTokens.push(component.scheme, ":");
3969
4012
  }
3970
4013
  const authority = recomposeAuthority(component);
@@ -3980,9 +4023,9 @@ var require_fast_uri = __commonJS({
3980
4023
  if (component.path !== void 0) {
3981
4024
  let s = component.path;
3982
4025
  if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
3983
- s = removeDotSegments(s);
4026
+ s = schemelessReference && s[0] !== "/" ? removeDotSegmentsRelative(s) : removeDotSegments(s);
3984
4027
  }
3985
- if (pathNoScheme) {
4028
+ if (schemelessReference) {
3986
4029
  s = serializePathEncoding(s, true);
3987
4030
  }
3988
4031
  if (authority === void 0 && s[0] === "/" && s[1] === "/") {
@@ -4029,16 +4072,34 @@ var require_fast_uri = __commonJS({
4029
4072
  return hasMalformedPercentEncoding(matches2[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches2[6]) || hasMalformedPercentEncoding(matches2[7]) || hasMalformedPercentEncoding(matches2[8]);
4030
4073
  }
4031
4074
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
4075
+ if (hasForbiddenDomainHost(parsed, options, schemeHandler, isIP)) {
4076
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4077
+ return true;
4078
+ }
4032
4079
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4080
+ let url2;
4033
4081
  try {
4034
- parsed.host = new URL("http://" + parsed.host).hostname;
4082
+ url2 = new URL("http://" + parsed.host);
4035
4083
  } catch (e) {
4036
4084
  parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
4037
4085
  return true;
4038
4086
  }
4087
+ if (!isHostOnlyURL(url2)) {
4088
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4089
+ return true;
4090
+ }
4091
+ parsed.host = url2.hostname;
4039
4092
  }
4040
4093
  return false;
4041
4094
  }
4095
+ function hasForbiddenDomainHost(parsed, options, schemeHandler, isIP) {
4096
+ return Boolean(
4097
+ parsed.host && isIP === false && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && FORBIDDEN_DOMAIN_HOST_CHARACTER.test(parsed.host)
4098
+ );
4099
+ }
4100
+ function isHostOnlyURL(url2) {
4101
+ return url2.username === "" && url2.password === "" && url2.port === "" && url2.pathname === "/" && url2.search === "" && url2.hash === "" && url2.hostname !== "";
4102
+ }
4042
4103
  function parseWithStatus(uri, opts) {
4043
4104
  const options = Object.assign({}, opts);
4044
4105
  const parsed = {
@@ -4093,9 +4154,8 @@ var require_fast_uri = __commonJS({
4093
4154
  parsed.query = matches2[7];
4094
4155
  parsed.fragment = matches2[8];
4095
4156
  if (parsed.scheme !== void 0) {
4096
- const decodedScheme = unescape(parsed.scheme);
4097
- if (VALID_SCHEME.test(decodedScheme)) {
4098
- parsed.scheme = decodedScheme.toLowerCase();
4157
+ if (VALID_SCHEME.test(parsed.scheme)) {
4158
+ parsed.scheme = parsed.scheme.toLowerCase();
4099
4159
  } else {
4100
4160
  parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
4101
4161
  malformedScheme = true;
@@ -4123,7 +4183,7 @@ var require_fast_uri = __commonJS({
4123
4183
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4124
4184
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4125
4185
  if (malformedIPLiteral) {
4126
- parsed.error = parsed.error || "URI host is malformed.";
4186
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
4127
4187
  malformedAuthorityOrPort = true;
4128
4188
  }
4129
4189
  } else {
@@ -4151,7 +4211,7 @@ var require_fast_uri = __commonJS({
4151
4211
  if (!isIP) {
4152
4212
  host = normalizePercentEncoding(host.toLowerCase());
4153
4213
  }
4154
- parsed.host = reescapeHostDelimiters(host, isIP);
4214
+ parsed.host = isIP || options.scheme !== "null" ? reescapeHostDelimiters(host, isIP) : host;
4155
4215
  }
4156
4216
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4157
4217
  if (parsed.path) {
@@ -4175,7 +4235,7 @@ var require_fast_uri = __commonJS({
4175
4235
  }
4176
4236
  return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
4177
4237
  }
4178
- function parse3(uri, opts) {
4238
+ function parse4(uri, opts) {
4179
4239
  return parseWithStatus(uri, opts).parsed;
4180
4240
  }
4181
4241
  function normalizeString(uri, opts) {
@@ -4212,7 +4272,7 @@ var require_fast_uri = __commonJS({
4212
4272
  resolveComponent,
4213
4273
  equal,
4214
4274
  serialize,
4215
- parse: parse3
4275
+ parse: parse4
4216
4276
  };
4217
4277
  module.exports = fastUri;
4218
4278
  module.exports.default = fastUri;
@@ -8017,10 +8077,10 @@ function mergeDefs(...defs) {
8017
8077
  function cloneDef(schema) {
8018
8078
  return mergeDefs(schema._zod.def);
8019
8079
  }
8020
- function getElementAtPath(obj, path12) {
8021
- if (!path12)
8080
+ function getElementAtPath(obj, path15) {
8081
+ if (!path15)
8022
8082
  return obj;
8023
- return path12.reduce((acc, key) => acc?.[key], obj);
8083
+ return path15.reduce((acc, key) => acc?.[key], obj);
8024
8084
  }
8025
8085
  function promiseAllObject(promisesObj) {
8026
8086
  const keys = Object.keys(promisesObj);
@@ -8237,7 +8297,7 @@ var BIGINT_FORMAT_RANGES = {
8237
8297
  int64: [/* @__PURE__ */ BigInt("-9223372036854775808"), /* @__PURE__ */ BigInt("9223372036854775807")],
8238
8298
  uint64: [/* @__PURE__ */ BigInt(0), /* @__PURE__ */ BigInt("18446744073709551615")]
8239
8299
  };
8240
- function pick(schema, mask) {
8300
+ function pick(schema, mask2) {
8241
8301
  const currDef = schema._zod.def;
8242
8302
  const checks = currDef.checks;
8243
8303
  const hasChecks = checks && checks.length > 0;
@@ -8245,29 +8305,29 @@ function pick(schema, mask) {
8245
8305
  throw new Error(".pick() cannot be used on object schemas containing refinements");
8246
8306
  }
8247
8307
  const newShape = {};
8248
- mirrorShape(newShape, schema, maskedKeys(schema, mask));
8308
+ mirrorShape(newShape, schema, maskedKeys(schema, mask2));
8249
8309
  return clone(schema, mergeDefs(currDef, { shape: newShape, checks: [] }));
8250
8310
  }
8251
- function maskedKeys(schema, mask) {
8311
+ function maskedKeys(schema, mask2) {
8252
8312
  const raw = sourceShape(schema);
8253
8313
  const keys = [];
8254
- for (const key of Reflect.ownKeys(mask)) {
8314
+ for (const key of Reflect.ownKeys(mask2)) {
8255
8315
  if (!Object.getOwnPropertyDescriptor(raw, key)?.enumerable) {
8256
8316
  throw new Error(`Unrecognized key: "${String(key)}"`);
8257
8317
  }
8258
- if (mask[key])
8318
+ if (mask2[key])
8259
8319
  keys.push(key);
8260
8320
  }
8261
8321
  return keys;
8262
8322
  }
8263
- function omit(schema, mask) {
8323
+ function omit(schema, mask2) {
8264
8324
  const currDef = schema._zod.def;
8265
8325
  const checks = currDef.checks;
8266
8326
  const hasChecks = checks && checks.length > 0;
8267
8327
  if (hasChecks) {
8268
8328
  throw new Error(".omit() cannot be used on object schemas containing refinements");
8269
8329
  }
8270
- const omitted = new Set(maskedKeys(schema, mask));
8330
+ const omitted = new Set(maskedKeys(schema, mask2));
8271
8331
  const newShape = {};
8272
8332
  mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)).filter((key) => !omitted.has(key)));
8273
8333
  return clone(schema, mergeDefs(currDef, { shape: newShape, checks: [] }));
@@ -8319,20 +8379,20 @@ function merge(a, b) {
8319
8379
  });
8320
8380
  return clone(a, def);
8321
8381
  }
8322
- function partial(Class2, schema, mask, name = "partial") {
8382
+ function partial(Class2, schema, mask2, name = "partial") {
8323
8383
  const currDef = schema._zod.def;
8324
8384
  const checks = currDef.checks;
8325
8385
  const hasChecks = checks && checks.length > 0;
8326
8386
  if (hasChecks) {
8327
8387
  throw new Error(`.${name}() cannot be used on object schemas containing refinements`);
8328
8388
  }
8329
- const selected = mask ? new Set(maskedKeys(schema, mask)) : void 0;
8389
+ const selected = mask2 ? new Set(maskedKeys(schema, mask2)) : void 0;
8330
8390
  const newShape = {};
8331
8391
  mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)), Class2 && ((value, key) => selected && !selected.has(key) ? value : new Class2({ type: "optional", innerType: value })));
8332
8392
  return clone(schema, mergeDefs(schema._zod.def, { shape: newShape, checks: [] }));
8333
8393
  }
8334
- function required(Class2, schema, mask) {
8335
- const selected = mask ? new Set(maskedKeys(schema, mask)) : void 0;
8394
+ function required(Class2, schema, mask2) {
8395
+ const selected = mask2 ? new Set(maskedKeys(schema, mask2)) : void 0;
8336
8396
  const newShape = {};
8337
8397
  mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)), (value, key) => (
8338
8398
  // overwrite with non-optional
@@ -8360,11 +8420,11 @@ function explicitlyAborted(x, startIndex = 0) {
8360
8420
  }
8361
8421
  return false;
8362
8422
  }
8363
- function prefixIssues(path12, issues) {
8423
+ function prefixIssues(path15, issues) {
8364
8424
  return issues.map((iss) => {
8365
8425
  var _a3;
8366
8426
  (_a3 = iss).path ?? (_a3.path = []);
8367
- iss.path.unshift(path12);
8427
+ iss.path.unshift(path15);
8368
8428
  return iss;
8369
8429
  });
8370
8430
  }
@@ -8826,16 +8886,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
8826
8886
  }
8827
8887
  function formatError(error62, mapper = (issue2) => issue2.message) {
8828
8888
  const fieldErrors = { _errors: [] };
8829
- const processError = (error63, path12 = []) => {
8889
+ const processError = (error63, path15 = []) => {
8830
8890
  for (const issue2 of error63.issues) {
8831
8891
  if (issue2.code === "invalid_union" && issue2.errors.length) {
8832
- issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
8892
+ issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
8833
8893
  } else if (issue2.code === "invalid_key") {
8834
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8894
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8835
8895
  } else if (issue2.code === "invalid_element") {
8836
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8896
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8837
8897
  } else {
8838
- const fullpath = [...path12, ...issue2.path];
8898
+ const fullpath = [...path15, ...issue2.path];
8839
8899
  if (fullpath.length === 0) {
8840
8900
  fieldErrors._errors.push(mapper(issue2));
8841
8901
  } else {
@@ -8874,17 +8934,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
8874
8934
  }
8875
8935
  function treeifyError(error62, mapper = (issue2) => issue2.message) {
8876
8936
  const result = { errors: [] };
8877
- const processError = (error63, path12 = []) => {
8937
+ const processError = (error63, path15 = []) => {
8878
8938
  var _a3;
8879
8939
  for (const issue2 of error63.issues) {
8880
8940
  if (issue2.code === "invalid_union" && issue2.errors.length) {
8881
- issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
8941
+ issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
8882
8942
  } else if (issue2.code === "invalid_key") {
8883
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8943
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8884
8944
  } else if (issue2.code === "invalid_element") {
8885
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8945
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8886
8946
  } else {
8887
- const fullpath = [...path12, ...issue2.path];
8947
+ const fullpath = [...path15, ...issue2.path];
8888
8948
  if (fullpath.length === 0) {
8889
8949
  result.errors.push(mapper(issue2));
8890
8950
  continue;
@@ -8923,8 +8983,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
8923
8983
  }
8924
8984
  function toDotPath(_path) {
8925
8985
  const segs = [];
8926
- const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8927
- for (const seg of path12) {
8986
+ const path15 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8987
+ for (const seg of path15) {
8928
8988
  if (typeof seg === "number")
8929
8989
  segs.push(`[${seg}]`);
8930
8990
  else if (typeof seg === "symbol")
@@ -9058,18 +9118,18 @@ var validateAsync = async (schema, value, _ctx) => {
9058
9118
  return result.issues.length === 0;
9059
9119
  };
9060
9120
  var _encode = (_Err) => {
9061
- const parse3 = _parse(_Err);
9121
+ const parse4 = _parse(_Err);
9062
9122
  const fn = (schema, value, _ctx, _params) => {
9063
9123
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
9064
- return parse3(schema, value, ctx, finalizeParams(fn, _params));
9124
+ return parse4(schema, value, ctx, finalizeParams(fn, _params));
9065
9125
  };
9066
9126
  return fn;
9067
9127
  };
9068
9128
  var encode = /* @__PURE__ */ _encode($ZodRealError);
9069
9129
  var _decode = (_Err) => {
9070
- const parse3 = _parse(_Err);
9130
+ const parse4 = _parse(_Err);
9071
9131
  const fn = (schema, value, _ctx, _params) => {
9072
- return parse3(schema, value, _ctx, finalizeParams(fn, _params));
9132
+ return parse4(schema, value, _ctx, finalizeParams(fn, _params));
9073
9133
  };
9074
9134
  return fn;
9075
9135
  };
@@ -9803,7 +9863,7 @@ ${content.join("\n")}
9803
9863
  var version = {
9804
9864
  major: 4,
9805
9865
  minor: 6,
9806
- patch: 6
9866
+ patch: 7
9807
9867
  };
9808
9868
 
9809
9869
  // node_modules/zod/v4/core/schemas.js
@@ -10564,7 +10624,7 @@ function capSize(inst, size) {
10564
10624
  if (!caps.length)
10565
10625
  return;
10566
10626
  const cap2 = Math.min(...caps.map((c) => c[0]));
10567
- const parse3 = inst._zod.parse;
10627
+ const parse4 = inst._zod.parse;
10568
10628
  inst._zod.parse = (payload, ctx) => {
10569
10629
  const n = size(payload.value);
10570
10630
  if (n > cap2 && !ctx?.skipChecks && ctx?.direction !== "backward") {
@@ -10578,7 +10638,7 @@ function capSize(inst, size) {
10578
10638
  }
10579
10639
  }
10580
10640
  }
10581
- return parse3(payload, ctx);
10641
+ return parse4(payload, ctx);
10582
10642
  };
10583
10643
  }
10584
10644
  var arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
@@ -12423,9 +12483,9 @@ var memo = {
12423
12483
  if (!bucket)
12424
12484
  return empty;
12425
12485
  handoff = void 0;
12426
- const entry = { value: empty, issues: null };
12427
- bucket.set(payload.value, entry);
12428
- open.push(entry);
12486
+ const entry2 = { value: empty, issues: null };
12487
+ bucket.set(payload.value, entry2);
12488
+ open.push(entry2);
12429
12489
  return empty;
12430
12490
  },
12431
12491
  guard(inst) {
@@ -12499,16 +12559,16 @@ var memo = {
12499
12559
  const depth = open.length;
12500
12560
  const result = base2(payload, ctx);
12501
12561
  handoff = void 0;
12502
- const entry = open.length > depth ? open.pop() : void 0;
12562
+ const entry2 = open.length > depth ? open.pop() : void 0;
12503
12563
  if (result instanceof Promise) {
12504
12564
  return result.then((r) => {
12505
- if (entry)
12506
- entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
12565
+ if (entry2)
12566
+ entry2.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
12507
12567
  return r;
12508
12568
  });
12509
12569
  }
12510
- if (entry)
12511
- entry.issues = result.issues.length ? cloneIssues(result.issues) : NO_ISSUES;
12570
+ if (entry2)
12571
+ entry2.issues = result.issues.length ? cloneIssues(result.issues) : NO_ISSUES;
12512
12572
  return result;
12513
12573
  };
12514
12574
  inst._zod.parse = wrapped;
@@ -22719,8 +22779,8 @@ function describe(description) {
22719
22779
  const ch = new $ZodCheck({ check: "describe" });
22720
22780
  ch._zod.onattach = [
22721
22781
  (inst) => {
22722
- const existing = globalRegistry.get(inst) ?? {};
22723
- globalRegistry.add(inst, { ...existing, description });
22782
+ const existing2 = globalRegistry.get(inst) ?? {};
22783
+ globalRegistry.add(inst, { ...existing2, description });
22724
22784
  }
22725
22785
  ];
22726
22786
  ch._zod.check = () => {
@@ -22732,8 +22792,8 @@ function meta(metadata) {
22732
22792
  const ch = new $ZodCheck({ check: "meta" });
22733
22793
  ch._zod.onattach = [
22734
22794
  (inst) => {
22735
- const existing = globalRegistry.get(inst) ?? {};
22736
- globalRegistry.add(inst, { ...existing, ...metadata });
22795
+ const existing2 = globalRegistry.get(inst) ?? {};
22796
+ globalRegistry.add(inst, { ...existing2, ...metadata });
22737
22797
  }
22738
22798
  ];
22739
22799
  ch._zod.check = () => {
@@ -22922,42 +22982,42 @@ function extractDefs(ctx, schema) {
22922
22982
  if (ctx.external && ctx.sharedDefsExtractedFor === ctx.external)
22923
22983
  return;
22924
22984
  const idToSchema = /* @__PURE__ */ new Map();
22925
- for (const entry of ctx.seen.entries()) {
22926
- const id = ctx.metadataRegistry.get(entry[0])?.id;
22985
+ for (const entry2 of ctx.seen.entries()) {
22986
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
22927
22987
  if (id) {
22928
- const existing = idToSchema.get(id);
22929
- if (existing && existing !== entry[0]) {
22988
+ const existing2 = idToSchema.get(id);
22989
+ if (existing2 && existing2 !== entry2[0]) {
22930
22990
  throw new Error(`Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);
22931
22991
  }
22932
- idToSchema.set(id, entry[0]);
22992
+ idToSchema.set(id, entry2[0]);
22933
22993
  }
22934
22994
  }
22935
- const makeURI = (entry) => {
22995
+ const makeURI = (entry2) => {
22936
22996
  const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
22937
22997
  if (ctx.external) {
22938
- const externalId = ctx.external.registry.get(entry[0])?.id;
22998
+ const externalId = ctx.external.registry.get(entry2[0])?.id;
22939
22999
  const uriGenerator = ctx.external.uri ?? ((id2) => id2);
22940
23000
  if (externalId) {
22941
23001
  return { ref: uriGenerator(externalId) };
22942
23002
  }
22943
- const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
22944
- entry[1].defId = id;
23003
+ const id = entry2[1].defId ?? entry2[1].schema.id ?? `schema${ctx.counter++}`;
23004
+ entry2[1].defId = id;
22945
23005
  return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${encodeJSONPointerSegment(id)}` };
22946
23006
  }
22947
23007
  const uriPrefix = `#`;
22948
23008
  const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
22949
- if (entry[1] === root && !entry[1].schema.id) {
23009
+ if (entry2[1] === root && !entry2[1].schema.id) {
22950
23010
  return { ref: uriPrefix };
22951
23011
  }
22952
- const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
23012
+ const defId = entry2[1].schema.id ?? `__schema${ctx.counter++}`;
22953
23013
  return { defId, ref: defUriPrefix + encodeJSONPointerSegment(defId) };
22954
23014
  };
22955
- const extractToDef = (entry) => {
22956
- if (entry[1].schema.$ref) {
23015
+ const extractToDef = (entry2) => {
23016
+ if (entry2[1].schema.$ref) {
22957
23017
  return;
22958
23018
  }
22959
- const seen = entry[1];
22960
- const { ref, defId } = makeURI(entry);
23019
+ const seen = entry2[1];
23020
+ const { ref, defId } = makeURI(entry2);
22961
23021
  seen.def = { ...seen.schema };
22962
23022
  if (defId)
22963
23023
  seen.defId = defId;
@@ -22968,8 +23028,8 @@ function extractDefs(ctx, schema) {
22968
23028
  schema2.$ref = ref;
22969
23029
  };
22970
23030
  if (ctx.cycles === "throw") {
22971
- for (const entry of ctx.seen.entries()) {
22972
- const seen = entry[1];
23031
+ for (const entry2 of ctx.seen.entries()) {
23032
+ const seen = entry2[1];
22973
23033
  if (seen.cycle) {
22974
23034
  throw new Error(`Cycle detected: #/${seen.cycle?.join("/")}/<root>
22975
23035
 
@@ -22977,31 +23037,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
22977
23037
  }
22978
23038
  }
22979
23039
  }
22980
- for (const entry of ctx.seen.entries()) {
22981
- const seen = entry[1];
22982
- if (schema === entry[0]) {
22983
- extractToDef(entry);
23040
+ for (const entry2 of ctx.seen.entries()) {
23041
+ const seen = entry2[1];
23042
+ if (schema === entry2[0]) {
23043
+ extractToDef(entry2);
22984
23044
  continue;
22985
23045
  }
22986
23046
  if (ctx.external) {
22987
- const ext = ctx.external.registry.get(entry[0])?.id;
22988
- if (schema !== entry[0] && ext) {
22989
- extractToDef(entry);
23047
+ const ext = ctx.external.registry.get(entry2[0])?.id;
23048
+ if (schema !== entry2[0] && ext) {
23049
+ extractToDef(entry2);
22990
23050
  continue;
22991
23051
  }
22992
23052
  }
22993
- const id = ctx.metadataRegistry.get(entry[0])?.id;
23053
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
22994
23054
  if (id) {
22995
- extractToDef(entry);
23055
+ extractToDef(entry2);
22996
23056
  continue;
22997
23057
  }
22998
23058
  if (seen.cycle) {
22999
- extractToDef(entry);
23059
+ extractToDef(entry2);
23000
23060
  continue;
23001
23061
  }
23002
23062
  if (seen.count > 1) {
23003
23063
  if (ctx.reused === "ref") {
23004
- extractToDef(entry);
23064
+ extractToDef(entry2);
23005
23065
  }
23006
23066
  }
23007
23067
  }
@@ -23183,12 +23243,12 @@ function finalize(ctx, schema) {
23183
23243
  });
23184
23244
  };
23185
23245
  if (!ctx.external || ctx.sharedEmitDoneFor !== ctx.external) {
23186
- for (const entry of [...ctx.seen.entries()].reverse()) {
23187
- flattenRef(entry[0]);
23246
+ for (const entry2 of [...ctx.seen.entries()].reverse()) {
23247
+ flattenRef(entry2[0]);
23188
23248
  }
23189
23249
  if (ctx.target !== "openapi-3.0") {
23190
- for (const entry of ctx.seen.entries()) {
23191
- compactTypeUnion(entry[1].def ?? entry[1].schema);
23250
+ for (const entry2 of ctx.seen.entries()) {
23251
+ compactTypeUnion(entry2[1].def ?? entry2[1].schema);
23192
23252
  }
23193
23253
  }
23194
23254
  for (const rewrite of ctx.deferred)
@@ -23200,9 +23260,9 @@ function finalize(ctx, schema) {
23200
23260
  const allOf = json3?.allOf;
23201
23261
  if (!Array.isArray(allOf))
23202
23262
  continue;
23203
- const existing = carriers.get(allOf);
23204
- if (existing)
23205
- existing.push(json3);
23263
+ const existing2 = carriers.get(allOf);
23264
+ if (existing2)
23265
+ existing2.push(json3);
23206
23266
  else
23207
23267
  carriers.set(allOf, [json3]);
23208
23268
  }
@@ -23235,8 +23295,8 @@ function finalize(ctx, schema) {
23235
23295
  delete result.id;
23236
23296
  const defs = ctx.external?.defs ?? {};
23237
23297
  if (!ctx.external || ctx.sharedEmitDoneFor !== ctx.external) {
23238
- for (const entry of ctx.seen.entries()) {
23239
- const seen = entry[1];
23298
+ for (const entry2 of ctx.seen.entries()) {
23299
+ const seen = entry2[1];
23240
23300
  if (seen.def && seen.defId) {
23241
23301
  if (seen.def.id === seen.defId)
23242
23302
  delete seen.def.id;
@@ -23848,9 +23908,9 @@ function stringifyKeyNames(bySchema, json3, visited) {
23848
23908
  var pendingRecords = /* @__PURE__ */ new WeakMap();
23849
23909
  function rewriteKeyNames(ctx) {
23850
23910
  const bySchema = /* @__PURE__ */ new Map();
23851
- for (const entry of ctx.seen.values()) {
23852
- if (entry.def && !bySchema.has(entry.schema))
23853
- bySchema.set(entry.schema, entry);
23911
+ for (const entry2 of ctx.seen.values()) {
23912
+ if (entry2.def && !bySchema.has(entry2.schema))
23913
+ bySchema.set(entry2.schema, entry2);
23854
23914
  }
23855
23915
  const rewrites = /* @__PURE__ */ new Map();
23856
23916
  for (const record2 of pendingRecords.get(ctx) ?? []) {
@@ -23864,8 +23924,8 @@ function rewriteKeyNames(ctx) {
23864
23924
  }
23865
23925
  if (!rewrites.size)
23866
23926
  return;
23867
- for (const entry of ctx.seen.values()) {
23868
- for (const carrier of [entry.schema, entry.def]) {
23927
+ for (const entry2 of ctx.seen.values()) {
23928
+ for (const carrier of [entry2.schema, entry2.def]) {
23869
23929
  const rewritten = carrier && rewrites.get(carrier.propertyNames);
23870
23930
  if (rewritten)
23871
23931
  carrier.propertyNames = rewritten;
@@ -24059,8 +24119,8 @@ function toJSONSchema(input2, params) {
24059
24119
  const registry2 = input2;
24060
24120
  const ctx2 = initializeContext({ ...params, processors: allProcessors });
24061
24121
  const defs = {};
24062
- for (const entry of registry2._idmap.entries()) {
24063
- const [_, schema] = entry;
24122
+ for (const entry2 of registry2._idmap.entries()) {
24123
+ const [_, schema] = entry2;
24064
24124
  processSchema(schema, ctx2);
24065
24125
  }
24066
24126
  const schemas = {};
@@ -24070,8 +24130,8 @@ function toJSONSchema(input2, params) {
24070
24130
  defs
24071
24131
  };
24072
24132
  ctx2.external = external;
24073
- for (const entry of registry2._idmap.entries()) {
24074
- const [key, schema] = entry;
24133
+ for (const entry2 of registry2._idmap.entries()) {
24134
+ const [key, schema] = entry2;
24075
24135
  extractDefs(ctx2, schema);
24076
24136
  assignProp(schemas, key, finalize(ctx2, schema));
24077
24137
  }
@@ -25292,11 +25352,11 @@ var ZodObject = /* @__PURE__ */ $constructor("ZodObject", (inst, def) => {
25292
25352
  merge(other) {
25293
25353
  return util_exports.merge(this, other);
25294
25354
  },
25295
- pick(mask) {
25296
- return util_exports.pick(this, mask);
25355
+ pick(mask2) {
25356
+ return util_exports.pick(this, mask2);
25297
25357
  },
25298
- omit(mask) {
25299
- return util_exports.omit(this, mask);
25358
+ omit(mask2) {
25359
+ return util_exports.omit(this, mask2);
25300
25360
  },
25301
25361
  partial(...args) {
25302
25362
  return util_exports.partial(ZodOptional, this, args[0]);
@@ -26062,13 +26122,13 @@ function resolveRef(ref, ctx) {
26062
26122
  if (!ref.startsWith("#")) {
26063
26123
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
26064
26124
  }
26065
- const path12 = ref.slice(1).split("/").filter(Boolean);
26066
- if (path12.length === 0) {
26125
+ const path15 = ref.slice(1).split("/").filter(Boolean);
26126
+ if (path15.length === 0) {
26067
26127
  return ctx.rootSchema;
26068
26128
  }
26069
26129
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
26070
- if (path12[0] === defsKey) {
26071
- const key = path12[1] === void 0 ? void 0 : decodeJSONPointerSegment(path12[1]);
26130
+ if (path15[0] === defsKey) {
26131
+ const key = path15[1] === void 0 ? void 0 : decodeJSONPointerSegment(path15[1]);
26072
26132
  if (!key || !ctx.defs[key]) {
26073
26133
  throw new Error(`Reference not found: ${ref}`);
26074
26134
  }
@@ -28692,7 +28752,7 @@ async function canonical(p) {
28692
28752
  }
28693
28753
 
28694
28754
  // src/version.ts
28695
- var VERSION = "0.3.1";
28755
+ var VERSION = "0.5.0";
28696
28756
 
28697
28757
  // src/config.ts
28698
28758
  var ConfigError = class extends Error {
@@ -28724,6 +28784,7 @@ function userPath(env2, key) {
28724
28784
  }
28725
28785
  }
28726
28786
  var ED_MCP_CAPI_CLIENT_ID = "320100b9-b71e-4b8a-ab80-210f7192d330";
28787
+ var MIN_PASSWORD = 12;
28727
28788
  function loadConfig(env2, dirs, platform = process.platform) {
28728
28789
  const devMode = bool(env2, "INARA_DEV_MODE", false);
28729
28790
  const inaraKey = get(env2, "INARA_API_KEY");
@@ -28731,26 +28792,39 @@ function loadConfig(env2, dirs, platform = process.platform) {
28731
28792
  const edsmKey = get(env2, "EDSM_API_KEY");
28732
28793
  const edsmCmdr = get(env2, "EDSM_CMDR");
28733
28794
  const capiClientId = get(env2, "CAPI_CLIENT_ID") ?? ED_MCP_CAPI_CLIENT_ID;
28795
+ const syncDir = userPath(env2, "ED_MCP_SYNC_DIR");
28796
+ const rawPassword = env2.ED_MCP_SYNC_PASSWORD;
28797
+ const syncPassword = rawPassword && !UNSET_PLACEHOLDER.test(rawPassword.trim()) ? rawPassword : void 0;
28798
+ if (syncPassword !== void 0 && syncPassword.length < MIN_PASSWORD) {
28799
+ throw new ConfigError(`ED_MCP_SYNC_PASSWORD must be at least ${MIN_PASSWORD} characters.`);
28800
+ }
28801
+ const dataDir = syncDir ?? dirs.config;
28802
+ const appName = get(env2, "INARA_APP_NAME") ?? "ed-mcp";
28734
28803
  return {
28735
28804
  inara: inaraKey && inaraCmdr ? {
28736
28805
  apiKey: inaraKey,
28737
28806
  cmdr: inaraCmdr,
28738
- appName: get(env2, "INARA_APP_NAME") ?? "ed-mcp",
28807
+ appName,
28739
28808
  devMode
28740
28809
  } : void 0,
28741
28810
  edsm: edsmKey && edsmCmdr ? { apiKey: edsmKey, cmdr: edsmCmdr } : void 0,
28742
28811
  capi: capiClientId ? { clientId: capiClientId } : void 0,
28743
28812
  journalDir: userPath(env2, "ED_JOURNAL_DIR") ?? (platform === "win32" ? path3.join(os3.homedir(), "Saved Games", "Frontier Developments", "Elite Dangerous") : void 0),
28744
- plansFile: userPath(env2, "ED_MCP_PLANS") ?? path3.join(dirs.config, "plans.json"),
28813
+ inaraApp: { appName, devMode },
28814
+ syncDir,
28815
+ syncPassword,
28816
+ plansFile: path3.join(dataDir, "plans.json"),
28817
+ legacyPlansEnv: get(env2, "ED_MCP_PLANS"),
28818
+ plansMarkdownDir: path3.join(dataDir, "plans"),
28819
+ prefsFile: path3.join(dataDir, "preferences.json"),
28820
+ secretsFile: syncPassword ? path3.join(dataDir, "secrets.enc") : path3.join(dirs.config, "secrets.json"),
28821
+ locksDir: path3.join(dirs.config, "locks"),
28745
28822
  cacheDir: dirs.cache,
28746
28823
  configDir: dirs.config,
28747
28824
  userAgent: `ed-mcp/${VERSION} (+https://github.com/pilotso11/ed-mcp)`
28748
28825
  };
28749
28826
  }
28750
28827
 
28751
- // src/deps.ts
28752
- import path10 from "path";
28753
-
28754
28828
  // src/capi/auth.ts
28755
28829
  import { createServer } from "http";
28756
28830
 
@@ -28844,10 +28918,41 @@ var HttpClient = class {
28844
28918
 
28845
28919
  // src/capi/browser.ts
28846
28920
  import { spawn } from "child_process";
28921
+ var KEEP = {
28922
+ all: ["PATH", "HOME", "USER", "LOGNAME"],
28923
+ win32: [
28924
+ "SYSTEMROOT",
28925
+ "COMSPEC",
28926
+ "USERPROFILE",
28927
+ "APPDATA",
28928
+ "LOCALAPPDATA",
28929
+ "TEMP",
28930
+ "TMP",
28931
+ "PATHEXT"
28932
+ ],
28933
+ linux: ["DISPLAY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS"]
28934
+ };
28935
+ function browserEnv(env2 = process.env, platform = process.platform) {
28936
+ const names = [
28937
+ ...KEEP.all,
28938
+ ...platform === "win32" ? KEEP.win32 : [],
28939
+ ...platform === "linux" ? KEEP.linux : []
28940
+ ];
28941
+ const out = {};
28942
+ for (const n of names) {
28943
+ const key = Object.keys(env2).find((k) => k.toUpperCase() === n);
28944
+ if (key && env2[key] !== void 0) out[key] = env2[key];
28945
+ }
28946
+ return out;
28947
+ }
28847
28948
  async function openInBrowser(url2) {
28848
28949
  const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url2]] : ["xdg-open", [url2]];
28849
28950
  await new Promise((resolve, reject) => {
28850
- const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
28951
+ const child = spawn(cmd, args, {
28952
+ detached: true,
28953
+ stdio: "ignore",
28954
+ env: browserEnv()
28955
+ });
28851
28956
  child.once("error", reject);
28852
28957
  child.once("spawn", () => {
28853
28958
  child.unref();
@@ -28887,6 +28992,8 @@ var CapiAuth = class {
28887
28992
  pending = null;
28888
28993
  failure = null;
28889
28994
  refreshing = null;
28995
+ // Logout bumps this, so a refresh that began before it cannot write tokens back.
28996
+ session = 0;
28890
28997
  authBase;
28891
28998
  openUrl;
28892
28999
  now;
@@ -29025,6 +29132,7 @@ var CapiAuth = class {
29025
29132
  }
29026
29133
  async accessToken() {
29027
29134
  const cfg = this.requireConfig();
29135
+ const session = this.session;
29028
29136
  const t = await this.tokens.load();
29029
29137
  if (!t) throw new UserError("Not logged in to Frontier. Call capi_login first.");
29030
29138
  if (this.now() - t.authorized_at > REFRESH_WINDOW) {
@@ -29034,7 +29142,7 @@ var CapiAuth = class {
29034
29142
  );
29035
29143
  }
29036
29144
  if (t.access_token && t.expires_at - 6e4 > this.now()) return t.access_token;
29037
- this.refreshing ??= this.refresh(cfg, t).finally(() => this.refreshing = null);
29145
+ this.refreshing ??= this.refresh(cfg, t, session).finally(() => this.refreshing = null);
29038
29146
  return this.refreshing;
29039
29147
  }
29040
29148
  /** Forces the next accessToken() call to refresh (after a 401 from CAPI). */
@@ -29047,6 +29155,8 @@ var CapiAuth = class {
29047
29155
  }
29048
29156
  async logout() {
29049
29157
  this.pending?.cancel();
29158
+ this.session++;
29159
+ await this.refreshing?.catch(() => void 0);
29050
29160
  await this.tokens.clear();
29051
29161
  this.failure = null;
29052
29162
  this.onSessionChange();
@@ -29073,7 +29183,7 @@ var CapiAuth = class {
29073
29183
  this.onSessionChange();
29074
29184
  return true;
29075
29185
  }
29076
- async refresh(cfg, t, retried = false) {
29186
+ async refresh(cfg, t, session, retried = false) {
29077
29187
  let res;
29078
29188
  try {
29079
29189
  res = await this.token({
@@ -29089,15 +29199,18 @@ var CapiAuth = class {
29089
29199
  }
29090
29200
  const latest = await this.tokens.load();
29091
29201
  if (!retried && latest && latest.refresh_token !== t.refresh_token) {
29092
- if (latest.access_token && latest.expires_at - 6e4 > this.now())
29202
+ if (latest.access_token && latest.expires_at - 6e4 > this.now()) {
29203
+ if (session !== this.session)
29204
+ throw new UserError("Logged out while the login was refreshing.");
29093
29205
  return latest.access_token;
29094
- return this.refresh(cfg, latest, true);
29206
+ }
29207
+ return this.refresh(cfg, latest, session, true);
29095
29208
  }
29096
- await this.tokens.clear();
29097
29209
  throw new UserError(
29098
- `Frontier refused to refresh the login (${err.message}). Call capi_login to log in again.`
29210
+ `Frontier refused to refresh the login (${err.message}). If you use ed-mcp on another computer, wait for your sync app to catch up and try again; otherwise call capi_login.`
29099
29211
  );
29100
29212
  }
29213
+ if (session !== this.session) throw new UserError("Logged out while the login was refreshing.");
29101
29214
  await this.tokens.save({
29102
29215
  refresh_token: res.refresh_token,
29103
29216
  access_token: res.access_token,
@@ -29201,8 +29314,8 @@ function parseModifiers(list2) {
29201
29314
  return label && value !== null ? [{ label, value, original_value: num(o.OriginalValue) }] : [];
29202
29315
  });
29203
29316
  }
29204
- function parseEntry(entry) {
29205
- const o = entry;
29317
+ function parseEntry(entry2) {
29318
+ const o = entry2;
29206
29319
  const data = o?.data ?? o;
29207
29320
  const header = o?.data ? o.header ?? {} : {};
29208
29321
  const ship = str(data?.Ship);
@@ -29289,10 +29402,10 @@ var CapiClient = class {
29289
29402
  }))
29290
29403
  };
29291
29404
  }
29292
- async get(path12) {
29405
+ async get(path15) {
29293
29406
  for (let attempt = 0; attempt < 2; attempt++) {
29294
29407
  const token = await this.auth.accessToken();
29295
- const url2 = `${this.base}${path12}`;
29408
+ const url2 = `${this.base}${path15}`;
29296
29409
  const res = await this.http.request(url2, {
29297
29410
  headers: { authorization: `Bearer ${token}` }
29298
29411
  });
@@ -29347,52 +29460,30 @@ function mapShip(s) {
29347
29460
  }
29348
29461
 
29349
29462
  // src/capi/tokens.ts
29350
- import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
29351
- import path4 from "path";
29352
- var StoredTokensSchema = external_exports.object({
29353
- refresh_token: external_exports.string(),
29354
- access_token: external_exports.string().nullable(),
29355
- expires_at: external_exports.number(),
29356
- authorized_at: external_exports.number()
29357
- });
29358
29463
  var TokenStore = class {
29359
- constructor(file2) {
29360
- this.file = file2;
29464
+ constructor(secrets) {
29465
+ this.secrets = secrets;
29361
29466
  }
29362
- file;
29467
+ secrets;
29363
29468
  async load() {
29364
- let json3;
29365
- try {
29366
- json3 = JSON.parse(await readFile(this.file, "utf8"));
29367
- } catch (err) {
29368
- if (err.code === "ENOENT") return null;
29369
- if (err instanceof SyntaxError) return null;
29370
- throw err;
29371
- }
29372
- const parsed = StoredTokensSchema.safeParse(json3);
29373
- return parsed.success ? parsed.data : null;
29469
+ return (await this.secrets.get()).capi ?? null;
29374
29470
  }
29375
- // The refresh token grants account access, so only the user may read the file.
29376
29471
  async save(t) {
29377
- await mkdir(path4.dirname(this.file), { recursive: true });
29378
- const tmp = `${this.file}.${process.pid}.tmp`;
29379
- await writeFile(tmp, JSON.stringify(t), { mode: 384 });
29380
- await chmod(tmp, 384);
29381
- try {
29382
- await rename(tmp, this.file);
29383
- } catch (err) {
29384
- await rm(tmp, { force: true });
29385
- throw err;
29386
- }
29472
+ await this.secrets.update((s) => {
29473
+ s.capi = t;
29474
+ });
29387
29475
  }
29388
29476
  async clear() {
29389
- await rm(this.file, { force: true });
29477
+ if (!(await this.secrets.get()).capi) return;
29478
+ await this.secrets.update((s) => {
29479
+ delete s.capi;
29480
+ });
29390
29481
  }
29391
29482
  };
29392
29483
 
29393
29484
  // src/catalog/service.ts
29394
- import { mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
29395
- import path5 from "path";
29485
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
29486
+ import path4 from "path";
29396
29487
 
29397
29488
  // node_modules/fastest-levenshtein/esm/mod.js
29398
29489
  var peq = new Uint32Array(65536);
@@ -30942,7 +31033,7 @@ var DEFAULT_STATICS = {
30942
31033
  var CatalogService = class {
30943
31034
  constructor(http, cacheDir, opts = {}) {
30944
31035
  this.http = http;
30945
- this.dir = path5.join(cacheDir, "catalog");
31036
+ this.dir = path4.join(cacheDir, "catalog");
30946
31037
  this.statics = opts.statics ?? DEFAULT_STATICS;
30947
31038
  this.now = opts.now ?? Date.now;
30948
31039
  this.maxAgeMs = opts.maxAgeMs ?? 7 * 864e5;
@@ -31004,11 +31095,11 @@ var CatalogService = class {
31004
31095
  }
31005
31096
  async loadDisk() {
31006
31097
  try {
31007
- const meta3 = JSON.parse(await readFile2(path5.join(this.dir, "meta.json"), "utf8"));
31098
+ const meta3 = JSON.parse(await readFile(path4.join(this.dir, "meta.json"), "utf8"));
31008
31099
  const texts = {};
31009
31100
  for (const k of KEYS) {
31010
- const file2 = path5.join(this.dir, `${k}.raw`);
31011
- const text = await readFile2(file2, "utf8").catch((err) => {
31101
+ const file2 = path4.join(this.dir, `${k}.raw`);
31102
+ const text = await readFile(file2, "utf8").catch((err) => {
31012
31103
  if (OPTIONAL.has(k) && err.code === "ENOENT") return void 0;
31013
31104
  throw err;
31014
31105
  });
@@ -31073,11 +31164,11 @@ var CatalogService = class {
31073
31164
  changed.push(k);
31074
31165
  }
31075
31166
  const built = buildCatalog(next, this.statics, this.minimums);
31076
- await mkdir2(this.dir, { recursive: true });
31167
+ await mkdir(this.dir, { recursive: true });
31077
31168
  for (const k of changed)
31078
- await atomicWrite(path5.join(this.dir, `${k}.raw`), next[k]);
31169
+ await atomicWrite(path4.join(this.dir, `${k}.raw`), next[k]);
31079
31170
  const meta3 = { version: 1, checked_at: stamp, files };
31080
- await atomicWrite(path5.join(this.dir, "meta.json"), JSON.stringify(meta3, null, 2));
31171
+ await atomicWrite(path4.join(this.dir, "meta.json"), JSON.stringify(meta3, null, 2));
31081
31172
  this.catalog = built.catalog;
31082
31173
  this.warnings = [...missing, ...built.warnings];
31083
31174
  this.texts = next;
@@ -31092,8 +31183,8 @@ var CatalogService = class {
31092
31183
  };
31093
31184
  async function atomicWrite(file2, text) {
31094
31185
  const tmp = `${file2}.${process.pid}.tmp`;
31095
- await writeFile2(tmp, text);
31096
- await rename2(tmp, file2);
31186
+ await writeFile(tmp, text);
31187
+ await rename(tmp, file2);
31097
31188
  }
31098
31189
 
31099
31190
  // src/http/cache.ts
@@ -31196,12 +31287,12 @@ var CacheRegistry = class {
31196
31287
  };
31197
31288
 
31198
31289
  // src/journal/service.ts
31199
- import { readFile as readFile4, stat as stat4 } from "fs/promises";
31200
- import path7 from "path";
31290
+ import { readFile as readFile3, stat as stat4 } from "fs/promises";
31291
+ import path6 from "path";
31201
31292
 
31202
31293
  // src/journal/files.ts
31203
31294
  import { readdir, stat as stat2 } from "fs/promises";
31204
- import path6 from "path";
31295
+ import path5 from "path";
31205
31296
  var NEW = /^Journal\.(\d{4})-(\d{2})-(\d{2})T(\d{2})(\d{2})(\d{2})\.(\d{2})\.log$/;
31206
31297
  var OLD = /^Journal\.(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{2})\.log$/;
31207
31298
  function parseJournalName(name) {
@@ -31225,7 +31316,7 @@ async function listJournals(dir, opts = {}) {
31225
31316
  }
31226
31317
  const files = await Promise.all(
31227
31318
  candidates.map(async (n) => {
31228
- const file2 = path6.join(dir, n.name);
31319
+ const file2 = path5.join(dir, n.name);
31229
31320
  const mtimeMs = await stat2(file2).then(
31230
31321
  (st) => st.mtimeMs,
31231
31322
  () => null
@@ -31242,7 +31333,7 @@ async function listJournals(dir, opts = {}) {
31242
31333
  }
31243
31334
 
31244
31335
  // src/journal/reader.ts
31245
- import { readFile as readFile3, stat as stat3 } from "fs/promises";
31336
+ import { readFile as readFile2, stat as stat3 } from "fs/promises";
31246
31337
  var JournalReader = class {
31247
31338
  keep;
31248
31339
  cache = /* @__PURE__ */ new Map();
@@ -31256,7 +31347,7 @@ var JournalReader = class {
31256
31347
  if (hit && hit.size === st.size && hit.mtimeMs === st.mtimeMs) return hit.events;
31257
31348
  const events = [];
31258
31349
  let last;
31259
- for (const line of (await readFile3(file2.path, "utf8")).split(/\r?\n/)) {
31350
+ for (const line of (await readFile2(file2.path, "utf8")).split(/\r?\n/)) {
31260
31351
  if (!line.trim()) continue;
31261
31352
  try {
31262
31353
  const e = JSON.parse(line);
@@ -31355,12 +31446,12 @@ function buildState(events) {
31355
31446
  const symbol2 = name.toLowerCase();
31356
31447
  const target = kind2 ? materials.get(kind2) : KINDS.map((k) => materials.get(k)).find((m) => m.has(symbol2));
31357
31448
  if (!target) return;
31358
- const entry = target.get(symbol2) ?? { symbol: symbol2, name: symbol2, count: 0 };
31449
+ const entry2 = target.get(symbol2) ?? { symbol: symbol2, name: symbol2, count: 0 };
31359
31450
  const label = str2(localised);
31360
- if (label) entry.name = label;
31361
- entry.count = Math.max(0, entry.count + delta);
31362
- if (entry.count === 0) target.delete(symbol2);
31363
- else target.set(symbol2, entry);
31451
+ if (label) entry2.name = label;
31452
+ entry2.count = Math.max(0, entry2.count + delta);
31453
+ if (entry2.count === 0) target.delete(symbol2);
31454
+ else target.set(symbol2, entry2);
31364
31455
  };
31365
31456
  const setLocation = (e, docked) => {
31366
31457
  location.system = str2(e.StarSystem) ?? location.system;
@@ -31765,7 +31856,7 @@ var JournalService = class {
31765
31856
  // deliver the logs before this file, so a file older than the newest Cargo event is ignored.
31766
31857
  async readCargoFile(dir, newestCargoEvent) {
31767
31858
  try {
31768
- const j = JSON.parse(await readFile4(path7.join(dir, "Cargo.json"), "utf8"));
31859
+ const j = JSON.parse(await readFile3(path6.join(dir, "Cargo.json"), "utf8"));
31769
31860
  if (!Array.isArray(j.Inventory) || j.Vessel !== void 0 && j.Vessel !== "Ship") {
31770
31861
  return null;
31771
31862
  }
@@ -31781,7 +31872,7 @@ var JournalService = class {
31781
31872
  async stationFile(kind2) {
31782
31873
  let j;
31783
31874
  try {
31784
- j = JSON.parse(await readFile4(path7.join(this.requireDir(), STATION_FILES[kind2]), "utf8"));
31875
+ j = JSON.parse(await readFile3(path6.join(this.requireDir(), STATION_FILES[kind2]), "utf8"));
31785
31876
  } catch (err) {
31786
31877
  if (err instanceof NotConfiguredError) throw err;
31787
31878
  return null;
@@ -31845,7 +31936,7 @@ function marketDemand(item) {
31845
31936
  return demand > 1 ? demand : 0;
31846
31937
  }
31847
31938
  async function isSettingsFolder(dir) {
31848
- return stat4(path7.join(dir, "Options")).then(
31939
+ return stat4(path6.join(dir, "Options")).then(
31849
31940
  (s) => s.isDirectory(),
31850
31941
  () => false
31851
31942
  );
@@ -31867,12 +31958,14 @@ var InaraClient = class {
31867
31958
  endpoint;
31868
31959
  now;
31869
31960
  async profile(name) {
31961
+ const cfg = await this.requireConfig();
31962
+ const who = name ?? cfg.cmdr;
31870
31963
  const ev = await this.cache.getOrLoad(
31871
- `profile:${name ?? ""}`,
31872
- () => this.send("getCommanderProfile", name ? { searchName: name } : {})
31964
+ `profile:${cfg.cmdr}:${name ?? ""}`,
31965
+ () => this.send(cfg, "getCommanderProfile", name ? { searchName: name } : {})
31873
31966
  );
31874
31967
  if (ev.eventStatus === 204) {
31875
- throw new UserError(`Inara: no commander found for "${name ?? this.cfg?.cmdr}"`);
31968
+ throw new UserError(`Inara: no commander found for "${who}"`);
31876
31969
  }
31877
31970
  const d = ev.eventData ?? {};
31878
31971
  const ship = d.commanderMainShip;
@@ -31906,7 +31999,7 @@ var InaraClient = class {
31906
31999
  async communityGoals() {
31907
32000
  const ev = await this.cache.getOrLoad(
31908
32001
  "cgs",
31909
- () => this.send("getCommunityGoalsRecent", [])
32002
+ async () => this.send(await this.requireConfig(), "getCommunityGoalsRecent", [])
31910
32003
  );
31911
32004
  if (ev.eventStatus === 204) return [];
31912
32005
  const goals = ev.eventData ?? [];
@@ -31925,12 +32018,15 @@ var InaraClient = class {
31925
32018
  description: String(g.goalDescriptionText ?? "").slice(0, 300)
31926
32019
  }));
31927
32020
  }
32021
+ // One call resolves the config once and passes it down, so a key change mid-call cannot
32022
+ // mix the old and new values.
32023
+ async requireConfig() {
32024
+ const cfg = typeof this.cfg === "function" ? await this.cfg() : this.cfg;
32025
+ if (!cfg) throw new NotConfiguredError("Inara", ["INARA_API_KEY", "INARA_CMDR"]);
32026
+ return cfg;
32027
+ }
31928
32028
  // Inara asks clients not to hammer the API, so requests run one at a time.
31929
- send(eventName, eventData) {
31930
- const cfg = this.cfg;
31931
- if (!cfg) {
31932
- return Promise.reject(new NotConfiguredError("Inara", ["INARA_API_KEY", "INARA_CMDR"]));
31933
- }
32029
+ send(cfg, eventName, eventData) {
31934
32030
  const run = this.queue.then(async () => {
31935
32031
  const body = {
31936
32032
  header: {
@@ -31956,7 +32052,7 @@ var InaraClient = class {
31956
32052
  });
31957
32053
  if (res.header?.eventStatus === 400) {
31958
32054
  throw new UserError(
31959
- `Inara rejected the request: ${String(res.header.eventStatusText ?? "error").replace(/\.+$/, "")}. Check INARA_API_KEY, and that the app name "${cfg.appName}" has been whitelisted by Inara.`
32055
+ `Inara rejected the request: ${String(res.header.eventStatusText ?? "error").replace(/\.+$/, "")}. Check INARA_API_KEY or the key saved on the settings page (settings_open), and that the app name "${cfg.appName}" has been whitelisted by Inara.`
31960
32056
  );
31961
32057
  }
31962
32058
  const ev = res.events?.[0];
@@ -32006,8 +32102,11 @@ var EdsmClient = class {
32006
32102
  }
32007
32103
  return out;
32008
32104
  }
32105
+ config() {
32106
+ return typeof this.cfg === "function" ? this.cfg() : Promise.resolve(this.cfg);
32107
+ }
32009
32108
  async call(endpoint, params) {
32010
- const cfg = this.cfg;
32109
+ const cfg = await this.config();
32011
32110
  if (!cfg) throw new NotConfiguredError("EDSM", ["EDSM_API_KEY", "EDSM_CMDR"]);
32012
32111
  const url2 = new URL(`${this.baseUrl}/${endpoint}`);
32013
32112
  url2.search = new URLSearchParams({
@@ -32029,7 +32128,7 @@ function check2(b) {
32029
32128
  if (b.msgnum === 100) return;
32030
32129
  if (b.msgnum === 203) {
32031
32130
  throw new UserError(
32032
- "EDSM: commander name or API key not found. Check EDSM_CMDR and EDSM_API_KEY, and that EDSM sync is on in EDMC."
32131
+ "EDSM: commander name or API key not found. Check EDSM_CMDR and EDSM_API_KEY, or the values saved on the settings page (settings_open), and that EDSM sync is on in EDMC."
32033
32132
  );
32034
32133
  }
32035
32134
  throw new UserError(`EDSM error ${b.msgnum}: ${b.msg}`);
@@ -32041,25 +32140,139 @@ function parseSpanshJson(text) {
32041
32140
  return JSON.parse(text.replace(BIG_ID, '$1"$2"'));
32042
32141
  }
32043
32142
 
32143
+ // src/systems/sweep.ts
32144
+ function stationPad(s) {
32145
+ if (s.has_large_pad || (s.large_pads ?? 0) > 0) return "L";
32146
+ if ((s.medium_pads ?? 0) > 0) return "M";
32147
+ if ((s.small_pads ?? 0) > 0) return "S";
32148
+ return null;
32149
+ }
32150
+ var skipStation = (type) => type === "Drake-Class Carrier" || /Construction Depot/.test(type ?? "");
32151
+ function mapSystem(raw) {
32152
+ const { x, y, z: z2 } = raw;
32153
+ if (x == null || y == null || z2 == null) return null;
32154
+ return {
32155
+ name: raw.name,
32156
+ id64: String(raw.id64),
32157
+ coords: { x, y, z: z2 },
32158
+ government: raw.government ?? null,
32159
+ allegiance: raw.allegiance ?? null,
32160
+ security: raw.security ?? null,
32161
+ controlling_power: raw.controlling_power ?? null,
32162
+ powers: raw.power ?? [],
32163
+ power_state: raw.power_state ?? null,
32164
+ updated_at: raw.updated_at ?? null,
32165
+ factions: (raw.minor_faction_presences ?? []).map((f) => ({
32166
+ name: f.name,
32167
+ government: f.government ?? null,
32168
+ allegiance: f.allegiance ?? null,
32169
+ influence: f.influence ?? null,
32170
+ controlling: f.name === raw.controlling_minor_faction,
32171
+ // Spansh leaves out active_states when a faction has one state.
32172
+ active_states: f.active_states ?? (f.state && f.state !== "None" ? [f.state] : []),
32173
+ pending_states: f.pending_states ?? []
32174
+ })),
32175
+ stations: (raw.stations ?? []).filter((s) => !skipStation(s.type)).map((s) => ({
32176
+ name: s.name,
32177
+ type: s.type ?? null,
32178
+ pad: stationPad(s),
32179
+ arrival_ls: s.distance_to_arrival ?? null
32180
+ })),
32181
+ stars: (raw.bodies ?? []).filter((b) => b.type === "Star").map((b) => ({ name: b.name, arrival_ls: b.distance_to_arrival ?? 0 }))
32182
+ };
32183
+ }
32184
+ function distanceLy(a, b) {
32185
+ return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
32186
+ }
32187
+ var isAnarchy = (f) => f.government === "Anarchy";
32188
+
32189
+ // src/systems/sweep-cache.ts
32190
+ var SweepCache = class {
32191
+ constructor(ttlMs = 36e5, now = Date.now, maxSweeps = 4) {
32192
+ this.ttlMs = ttlMs;
32193
+ this.now = now;
32194
+ this.maxSweeps = maxSweeps;
32195
+ }
32196
+ ttlMs;
32197
+ now;
32198
+ maxSweeps;
32199
+ name = "spansh-sweeps";
32200
+ sweeps = [];
32201
+ find(centre, radiusLy) {
32202
+ this.dropExpired();
32203
+ const hit = this.sweeps.find(
32204
+ (s) => distanceLy(s.centre, centre) + radiusLy <= s.radiusLy + 1e-9
32205
+ );
32206
+ return hit?.systems.filter((s) => distanceLy(s.coords, centre) <= radiusLy);
32207
+ }
32208
+ add(centre, radiusLy, systems) {
32209
+ this.sweeps.push({ centre, radiusLy, systems, storedAt: this.now() });
32210
+ while (this.sweeps.length > this.maxSweeps) this.sweeps.shift();
32211
+ }
32212
+ clear() {
32213
+ const n = this.sweeps.length;
32214
+ this.sweeps = [];
32215
+ return n;
32216
+ }
32217
+ stats() {
32218
+ this.dropExpired();
32219
+ const oldest = this.sweeps[0]?.storedAt;
32220
+ return {
32221
+ name: this.name,
32222
+ entries: this.sweeps.length,
32223
+ oldestAgeSeconds: oldest === void 0 ? null : Math.round((this.now() - oldest) / 1e3)
32224
+ };
32225
+ }
32226
+ dropExpired() {
32227
+ const cutoff = this.now() - this.ttlMs;
32228
+ this.sweeps = this.sweeps.filter((s) => s.storedAt > cutoff);
32229
+ }
32230
+ };
32231
+
32044
32232
  // src/spansh/client.ts
32045
- var SERVICE_FILTERS = {
32233
+ var TRADER_FILTERS = {
32046
32234
  material_trader_raw: { material_trader: { value: ["Raw"] } },
32047
32235
  material_trader_manufactured: { material_trader: { value: ["Manufactured"] } },
32048
32236
  material_trader_encoded: { material_trader: { value: ["Encoded"] } },
32049
32237
  tech_broker_human: { technology_broker: { value: ["Human"] } },
32050
- tech_broker_guardian: { technology_broker: { value: ["Guardian"] } },
32051
- interstellar_factors: { services: [{ name: { value: ["Interstellar Factors Contact"] } }] }
32052
- };
32238
+ tech_broker_guardian: { technology_broker: { value: ["Guardian"] } }
32239
+ };
32240
+ var NAMED_SERVICES = {
32241
+ interstellar_factors: "Interstellar Factors Contact",
32242
+ black_market: "Black Market",
32243
+ universal_cartographics: "Universal Cartographics",
32244
+ vista_genomics: "Vista Genomics",
32245
+ search_and_rescue: "Search and Rescue",
32246
+ refuel: "Refuel",
32247
+ repair: "Repair",
32248
+ restock: "Restock",
32249
+ shipyard: "Shipyard",
32250
+ outfitting: "Outfitting",
32251
+ market: "Market",
32252
+ apex_interstellar: "Apex Interstellar",
32253
+ frontline_solutions: "Frontline Solutions",
32254
+ pioneer_supplies: "Pioneer Supplies",
32255
+ bartender: "Bartender",
32256
+ refinery_contact: "Refinery Contact",
32257
+ redemption_office: "Redemption Office",
32258
+ crew_lounge: "Crew Lounge",
32259
+ squadron_bank: "Squadron Bank",
32260
+ construction_services: "Construction Services",
32261
+ powerplay: "Powerplay"
32262
+ };
32263
+ var STATION_SERVICES = [
32264
+ ...Object.keys(TRADER_FILTERS),
32265
+ ...Object.keys(NAMED_SERVICES)
32266
+ ];
32267
+ function serviceFilter(s) {
32268
+ if (Object.hasOwn(TRADER_FILTERS, s)) return TRADER_FILTERS[s];
32269
+ return { services: [{ name: { value: [NAMED_SERVICES[s]] } }] };
32270
+ }
32053
32271
  var COUNT_CAP = 1e4;
32054
32272
  var shipKey = (s) => s.toLowerCase().replace(/[\s-]/g, "");
32055
- function pad(s) {
32056
- if (s.has_large_pad || (s.large_pads ?? 0) > 0) return "L";
32057
- if ((s.medium_pads ?? 0) > 0) return "M";
32058
- if ((s.small_pads ?? 0) > 0) return "S";
32059
- return null;
32060
- }
32061
32273
  var MAX_JOBS = 100;
32062
32274
  var SEARCH_PAGE_SIZE = 50;
32275
+ var SWEEP_PAGE_SIZE = 500;
32063
32276
  var CARRIER_TYPE = "Drake-Class Carrier";
32064
32277
  var NON_CARRIER_TYPES = [
32065
32278
  "Asteroid base",
@@ -32092,6 +32305,7 @@ var SpanshClient = class {
32092
32305
  this.now = opts.now ?? Date.now;
32093
32306
  this.systemCache = opts.systemCache ?? cache;
32094
32307
  this.marketCache = opts.marketCache ?? cache;
32308
+ this.sweepCache = opts.sweepCache ?? new SweepCache();
32095
32309
  }
32096
32310
  http;
32097
32311
  cache;
@@ -32102,10 +32316,12 @@ var SpanshClient = class {
32102
32316
  now;
32103
32317
  systemCache;
32104
32318
  marketCache;
32319
+ sweepCache;
32105
32320
  // Job ids by route form, so a call after a timeout collects the job instead of resubmitting.
32106
32321
  routeJobs = /* @__PURE__ */ new Map();
32107
32322
  tradeJobs = /* @__PURE__ */ new Map();
32108
32323
  tradeSubmits = /* @__PURE__ */ new Map();
32324
+ sweeps = /* @__PURE__ */ new Map();
32109
32325
  gate = Promise.resolve();
32110
32326
  lastStart = 0;
32111
32327
  async resolveShip(name) {
@@ -32130,7 +32346,7 @@ var SpanshClient = class {
32130
32346
  shipName = await this.resolveShip(q.ship);
32131
32347
  filters.ships = [{ name: { value: [shipName] } }];
32132
32348
  } else if (q.service) {
32133
- Object.assign(filters, SERVICE_FILTERS[q.service]);
32349
+ Object.assign(filters, serviceFilter(q.service));
32134
32350
  }
32135
32351
  if (q.largePad) filters.has_large_pad = { value: true };
32136
32352
  if (q.maxDistanceLy !== void 0) filters.distance = { min: 0, max: q.maxDistanceLy };
@@ -32197,7 +32413,7 @@ var SpanshClient = class {
32197
32413
  name: s.name,
32198
32414
  type: s.type ?? null,
32199
32415
  arrival_ls: s.distance_to_arrival ?? null,
32200
- pad: pad(s),
32416
+ pad: stationPad(s),
32201
32417
  market: s.has_market ?? false,
32202
32418
  market_id: s.market_id ?? null,
32203
32419
  outfitting: s.has_outfitting ?? false,
@@ -32214,6 +32430,66 @@ var SpanshClient = class {
32214
32430
  );
32215
32431
  return detail.record.needs_permit ?? null;
32216
32432
  }
32433
+ /** Populated systems within radiusLy of near. Stops paging at opts.deadline; a partial result is not cached. */
32434
+ async populatedSystems(near, radiusLy, opts = {}) {
32435
+ const centre = (await this.system(near, 0)).coords;
32436
+ const cached2 = this.sweepCache.find(centre, radiusLy);
32437
+ if (cached2) return { centre, systems: cached2, total: cached2.length, partial: false };
32438
+ const key = `${near.toLowerCase()}:${radiusLy}`;
32439
+ let running = this.sweeps.get(key);
32440
+ if (!running) {
32441
+ running = this.sweepPages(near, radiusLy, centre, opts).finally(
32442
+ () => this.sweeps.delete(key)
32443
+ );
32444
+ this.sweeps.set(key, running);
32445
+ }
32446
+ return running;
32447
+ }
32448
+ async sweepPages(near, radiusLy, centre, opts) {
32449
+ const systems = [];
32450
+ const seen = /* @__PURE__ */ new Set();
32451
+ let total = 0;
32452
+ let partial2 = false;
32453
+ for (let page2 = 0; ; page2++) {
32454
+ if (page2 > 0 && opts.deadline !== void 0 && this.now() >= opts.deadline) {
32455
+ partial2 = true;
32456
+ break;
32457
+ }
32458
+ const body = {
32459
+ filters: {
32460
+ distance: { min: 0, max: radiusLy },
32461
+ population: { comparison: "<=>", value: [1, 1e11] }
32462
+ },
32463
+ sort: [{ distance: { direction: "asc" } }],
32464
+ size: SWEEP_PAGE_SIZE,
32465
+ page: page2,
32466
+ reference_system: near
32467
+ };
32468
+ let res;
32469
+ try {
32470
+ res = await this.post("/systems/search", JSON.stringify(body), "application/json");
32471
+ } catch (err) {
32472
+ if (err instanceof SpanshError && err.status === 400) {
32473
+ throw new UserError(
32474
+ `Spansh rejected the search (${err.detail}). Check that "${near}" is an exact system name (spansh_system).`
32475
+ );
32476
+ }
32477
+ throw err;
32478
+ }
32479
+ total = res.count;
32480
+ const results = res.results ?? [];
32481
+ for (const raw of results) {
32482
+ const s = mapSystem(raw);
32483
+ if (s && !seen.has(s.id64)) {
32484
+ seen.add(s.id64);
32485
+ systems.push(s);
32486
+ }
32487
+ }
32488
+ if (results.length < SWEEP_PAGE_SIZE) break;
32489
+ }
32490
+ if (!partial2) this.sweepCache.add(centre, radiusLy, systems);
32491
+ return { centre, systems, total, partial: partial2 };
32492
+ }
32217
32493
  async route(q, opts = {}) {
32218
32494
  const form = new URLSearchParams({
32219
32495
  from: q.from,
@@ -32439,8 +32715,8 @@ var SpanshClient = class {
32439
32715
  let droppedOld = 0;
32440
32716
  let loaded = 0;
32441
32717
  let partial2 = false;
32442
- for (let page = 0; loaded < q.maxStations; page++) {
32443
- if (page > 0 && deadline !== void 0 && this.now() >= deadline) {
32718
+ for (let page2 = 0; loaded < q.maxStations; page2++) {
32719
+ if (page2 > 0 && deadline !== void 0 && this.now() >= deadline) {
32444
32720
  partial2 = true;
32445
32721
  break;
32446
32722
  }
@@ -32448,7 +32724,7 @@ var SpanshClient = class {
32448
32724
  filters,
32449
32725
  sort: [{ distance: { direction: "asc" } }],
32450
32726
  size: SEARCH_PAGE_SIZE,
32451
- page,
32727
+ page: page2,
32452
32728
  reference_system: q.near
32453
32729
  };
32454
32730
  let res;
@@ -32509,7 +32785,7 @@ var SpanshClient = class {
32509
32785
  distance_ly: s.distance ?? null,
32510
32786
  arrival_ls: s.distance_to_arrival ?? null,
32511
32787
  coords: { x, y, z: z2 },
32512
- pad: pad(s),
32788
+ pad: stationPad(s),
32513
32789
  type: s.type ?? null,
32514
32790
  planetary: s.is_planetary ?? false,
32515
32791
  market_age_hours: Number.isNaN(updated) ? null : Math.round((now - updated) / 36e5 * 10) / 10,
@@ -32540,7 +32816,7 @@ var SpanshClient = class {
32540
32816
  distance_ly: s.distance ?? 0,
32541
32817
  arrival_ls: s.distance_to_arrival ?? null,
32542
32818
  type: s.type ?? null,
32543
- pad: pad(s),
32819
+ pad: stationPad(s),
32544
32820
  planetary: s.is_planetary ?? null,
32545
32821
  price,
32546
32822
  market_updated_at: s.market_updated_at ?? null,
@@ -32551,14 +32827,14 @@ var SpanshClient = class {
32551
32827
  cached(key, load) {
32552
32828
  return this.cache.getOrLoad(key, load);
32553
32829
  }
32554
- async get(path12) {
32555
- const { status, body } = await this.raw(path12);
32830
+ async get(path15) {
32831
+ const { status, body } = await this.raw(path15);
32556
32832
  if (status >= 400) throw new SpanshError(status, body);
32557
32833
  return body;
32558
32834
  }
32559
- async post(path12, body, contentType, opts) {
32835
+ async post(path15, body, contentType, opts) {
32560
32836
  const res = await this.raw(
32561
- path12,
32837
+ path15,
32562
32838
  { method: "POST", body, headers: { "content-type": contentType } },
32563
32839
  opts
32564
32840
  );
@@ -32566,7 +32842,7 @@ var SpanshClient = class {
32566
32842
  return res.body;
32567
32843
  }
32568
32844
  // Spansh publishes no quota; spacing requests keeps us a polite client.
32569
- async raw(path12, init = {}, opts) {
32845
+ async raw(path15, init = {}, opts) {
32570
32846
  const turn = this.gate.then(async () => {
32571
32847
  const wait = this.lastStart + this.minIntervalMs - Date.now();
32572
32848
  if (wait > 0) await this.sleep(wait);
@@ -32574,7 +32850,7 @@ var SpanshClient = class {
32574
32850
  });
32575
32851
  this.gate = turn.catch(() => void 0);
32576
32852
  await turn;
32577
- const { status, text } = await this.http.text(`${this.baseUrl}${path12}`, init, opts);
32853
+ const { status, text } = await this.http.text(`${this.baseUrl}${path15}`, init, opts);
32578
32854
  let body;
32579
32855
  try {
32580
32856
  body = text ? parseSpanshJson(text) : void 0;
@@ -32616,17 +32892,34 @@ function mapRoute(r) {
32616
32892
  }
32617
32893
 
32618
32894
  // src/plans/store.ts
32619
- import { readFile as readFile6 } from "fs/promises";
32895
+ import { randomBytes as randomBytes2 } from "crypto";
32896
+ import { lstat as lstat2, mkdir as mkdir3, rename as rename3, rm, writeFile as writeFile3 } from "fs/promises";
32897
+ import path8 from "path";
32620
32898
 
32621
32899
  // src/fs/lock.ts
32622
- import { randomUUID } from "crypto";
32623
- import { link, mkdir as mkdir3, open as open2, readFile as readFile5, rename as rename3, stat as stat5, unlink, writeFile as writeFile3 } from "fs/promises";
32624
- import path8 from "path";
32900
+ import { createHash as createHash2, randomUUID } from "crypto";
32901
+ import {
32902
+ chmod,
32903
+ link,
32904
+ mkdir as mkdir2,
32905
+ open as open2,
32906
+ readFile as readFile4,
32907
+ rename as rename2,
32908
+ stat as stat5,
32909
+ unlink,
32910
+ writeFile as writeFile2
32911
+ } from "fs/promises";
32912
+ import path7 from "path";
32625
32913
  var LOCK_RETRY_MS = 50;
32626
32914
  var LOCK_STALE_MS = 3e4;
32627
32915
  var PENDING_DELETE_MS = 1e3;
32628
- async function acquireLock(file2, timeoutMs, lockedMessage) {
32629
- const lock = `${file2}.lock`;
32916
+ function lockPath(file2, lockDir) {
32917
+ if (!lockDir) return `${file2}.lock`;
32918
+ const hash2 = createHash2("sha256").update(path7.resolve(file2)).digest("hex").slice(0, 16);
32919
+ return path7.join(lockDir, `${hash2}.lock`);
32920
+ }
32921
+ async function acquireLock(file2, timeoutMs, lockedMessage, lockDir) {
32922
+ const lock = lockPath(file2, lockDir);
32630
32923
  const deadline = Date.now() + timeoutMs;
32631
32924
  let pendingSince;
32632
32925
  const waitForDelete = async (err) => {
@@ -32663,7 +32956,7 @@ async function acquireLock(file2, timeoutMs, lockedMessage) {
32663
32956
  }
32664
32957
  const token = randomUUID();
32665
32958
  const release = async () => {
32666
- const held = await readFile5(lock, "utf8").catch(() => void 0);
32959
+ const held = await readFile4(lock, "utf8").catch(() => void 0);
32667
32960
  if (held === token) await unlink(lock).catch(() => void 0);
32668
32961
  };
32669
32962
  try {
@@ -32682,18 +32975,21 @@ function pendingDelete(err) {
32682
32975
  return process.platform === "win32" && (code === "EPERM" || code === "EACCES");
32683
32976
  }
32684
32977
  async function lockedWrite(file2, opts, update) {
32685
- await mkdir3(path8.dirname(file2), { recursive: true });
32686
- const release = await acquireLock(file2, opts.timeoutMs, opts.lockedMessage);
32978
+ await mkdir2(path7.dirname(file2), { recursive: true });
32979
+ if (opts.lockDir) await mkdir2(opts.lockDir, { recursive: true });
32980
+ const release = await acquireLock(file2, opts.timeoutMs, opts.lockedMessage, opts.lockDir);
32687
32981
  try {
32688
32982
  const { content, result } = await update();
32689
32983
  const tmp = `${file2}.${process.pid}.${randomUUID()}.tmp`;
32690
32984
  try {
32691
- await writeFile3(tmp, content);
32692
- await rename3(tmp, file2);
32985
+ await writeFile2(tmp, content, opts.mode === void 0 ? void 0 : { mode: opts.mode });
32986
+ if (opts.mode !== void 0) await chmod(tmp, opts.mode);
32987
+ await rename2(tmp, file2);
32693
32988
  } catch (err) {
32694
32989
  await unlink(tmp).catch(() => void 0);
32695
32990
  throw err;
32696
32991
  }
32992
+ await opts.afterWrite?.();
32697
32993
  return result;
32698
32994
  } finally {
32699
32995
  await release();
@@ -32702,7 +32998,7 @@ async function lockedWrite(file2, opts, update) {
32702
32998
  async function breakStaleLock(lock) {
32703
32999
  const aside = `${lock}.${process.pid}.${Date.now()}.stale`;
32704
33000
  try {
32705
- await rename3(lock, aside);
33001
+ await rename2(lock, aside);
32706
33002
  } catch (err) {
32707
33003
  if (err.code === "ENOENT") return true;
32708
33004
  throw err;
@@ -32717,6 +33013,105 @@ async function breakStaleLock(lock) {
32717
33013
  return true;
32718
33014
  }
32719
33015
 
33016
+ // src/fs/regular.ts
33017
+ import { constants } from "fs";
33018
+ import { lstat, open as open3 } from "fs/promises";
33019
+ var MAX_SECRETS_FILE = 1024 * 1024;
33020
+ var MAX_DATA_FILE = 8 * 1024 * 1024;
33021
+ var NOFOLLOW = constants.O_NOFOLLOW;
33022
+ async function readRegularText(file2, max) {
33023
+ if (NOFOLLOW === void 0) {
33024
+ try {
33025
+ if (!(await lstat(file2)).isFile()) return { kind: "unsafe", reason: "not a regular file" };
33026
+ } catch (err) {
33027
+ return failure2(err, file2);
33028
+ }
33029
+ }
33030
+ let handle;
33031
+ try {
33032
+ handle = await open3(
33033
+ file2,
33034
+ constants.O_RDONLY | (NOFOLLOW ?? 0) | (NOFOLLOW ? constants.O_NONBLOCK : 0)
33035
+ );
33036
+ } catch (err) {
33037
+ if (err.code === "ELOOP") {
33038
+ return { kind: "unsafe", reason: "not a regular file" };
33039
+ }
33040
+ return failure2(err, file2);
33041
+ }
33042
+ try {
33043
+ const st = await handle.stat();
33044
+ if (!st.isFile()) return { kind: "unsafe", reason: "not a regular file" };
33045
+ if (st.size > max) return { kind: "unsafe", reason: "too large" };
33046
+ return { kind: "ok", text: await handle.readFile("utf8") };
33047
+ } catch (err) {
33048
+ return failure2(err, file2);
33049
+ } finally {
33050
+ await handle.close().catch(() => void 0);
33051
+ }
33052
+ }
33053
+ async function failure2(err, file2) {
33054
+ const code = err.code;
33055
+ if (code === "ENOENT") return { kind: "missing" };
33056
+ const st = await lstat(file2).catch(() => null);
33057
+ if (st && !st.isFile()) return { kind: "unsafe", reason: "not a regular file" };
33058
+ return { kind: "unsafe", reason: `not readable (${code ?? "unknown error"})` };
33059
+ }
33060
+
33061
+ // src/plans/markdown.ts
33062
+ var box = {
33063
+ todo: "[ ]",
33064
+ in_progress: "[~]",
33065
+ done: "[x]",
33066
+ skipped: "[-]"
33067
+ };
33068
+ function planToMarkdown(plan) {
33069
+ const done = plan.items.filter((i) => i.status === "done" || i.status === "skipped").length;
33070
+ const out = [
33071
+ `# ${plan.name}`,
33072
+ "",
33073
+ `Plan \`${plan.id}\` \xB7 updated ${plan.updated_at} \xB7 ${done}/${plan.items.length} done`,
33074
+ ""
33075
+ ];
33076
+ if (plan.notes) out.push(plan.notes, "");
33077
+ if (plan.items.length) {
33078
+ out.push("## Items", "");
33079
+ for (const i of plan.items) {
33080
+ const where = [
33081
+ i.station ? `at ${i.station}` : void 0,
33082
+ i.engineer ? `engineer ${i.engineer}` : void 0
33083
+ ].filter(Boolean);
33084
+ out.push(
33085
+ `- ${box[i.status]} **${i.kind}** ${i.description}${where.length ? ` \u2014 ${where.join(", ")}` : ""}`
33086
+ );
33087
+ if (i.materials?.length) {
33088
+ out.push(` - Materials: ${i.materials.map((m) => `${m.count}\xD7 ${m.name}`).join(", ")}`);
33089
+ }
33090
+ if (i.notes) out.push(` - Notes: ${i.notes}`);
33091
+ }
33092
+ out.push("");
33093
+ }
33094
+ if (plan.stops.length) {
33095
+ out.push("## Stops", "");
33096
+ plan.stops.forEach((s, n) => {
33097
+ out.push(`${n + 1}. ${s.system}${s.station ? ` \u2014 ${s.station}` : ""}: ${s.purpose}`);
33098
+ });
33099
+ out.push("");
33100
+ }
33101
+ if (plan.route) {
33102
+ out.push("## Route", "");
33103
+ plan.route.stops.forEach((s, n) => out.push(`${n + 1}. ${s}`));
33104
+ const r = plan.route;
33105
+ const kind2 = r.plotted ? "plotted" : "estimate";
33106
+ out.push(
33107
+ "",
33108
+ `From ${r.from}; ${kind2}. Total ${r.total_min} min, computed ${r.computed_at}`,
33109
+ ""
33110
+ );
33111
+ }
33112
+ return out.join("\n");
33113
+ }
33114
+
32720
33115
  // src/plans/schema.ts
32721
33116
  var ItemKind = external_exports.enum([
32722
33117
  "buy",
@@ -32774,16 +33169,28 @@ var PlanFileSchema = external_exports.object({
32774
33169
 
32775
33170
  // src/plans/store.ts
32776
33171
  var LOCK_TIMEOUT_MS = 5e3;
33172
+ var PlanExistsError = class extends UserError {
33173
+ constructor(id) {
33174
+ super(`Plan ${id} exists; pass overwrite: true to replace it`);
33175
+ }
33176
+ };
32777
33177
  var PlanStore = class {
32778
33178
  constructor(file2, now = () => /* @__PURE__ */ new Date(), opts = {}) {
32779
33179
  this.file = file2;
32780
33180
  this.now = now;
32781
33181
  this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
33182
+ this.lockDir = opts.lockDir;
33183
+ this.guard = opts.guard;
33184
+ this.markdownDir = opts.markdownDir;
32782
33185
  }
32783
33186
  file;
32784
33187
  now;
33188
+ lastCopyError = null;
32785
33189
  queue = Promise.resolve();
32786
33190
  lockTimeoutMs;
33191
+ lockDir;
33192
+ guard;
33193
+ markdownDir;
32787
33194
  async list() {
32788
33195
  const { plans } = await this.read();
32789
33196
  return plans.map((p) => ({
@@ -32806,7 +33213,7 @@ var PlanStore = class {
32806
33213
  const stamp = this.now().toISOString();
32807
33214
  const idx = data.plans.findIndex((p) => p.id === parsed.data.id);
32808
33215
  if (idx >= 0 && opts.overwrite === false) {
32809
- throw new UserError(`Plan ${parsed.data.id} exists; pass overwrite: true to replace it`);
33216
+ throw new PlanExistsError(parsed.data.id);
32810
33217
  }
32811
33218
  const plan = {
32812
33219
  ...parsed.data,
@@ -32847,13 +33254,13 @@ var PlanStore = class {
32847
33254
  });
32848
33255
  }
32849
33256
  async read() {
32850
- let raw;
32851
- try {
32852
- raw = await readFile6(this.file, "utf8");
32853
- } catch (err) {
32854
- if (err.code === "ENOENT") return { version: 1, plans: [] };
32855
- throw err;
33257
+ await this.guard?.();
33258
+ const read2 = await readRegularText(this.file, MAX_DATA_FILE);
33259
+ if (read2.kind === "missing") return { version: 1, plans: [] };
33260
+ if (read2.kind === "unsafe") {
33261
+ throw new UserError(`Plan file ${this.file} is invalid: ${read2.reason}`);
32856
33262
  }
33263
+ const raw = read2.text;
32857
33264
  let json3;
32858
33265
  try {
32859
33266
  json3 = JSON.parse(raw);
@@ -32869,27 +33276,99 @@ var PlanStore = class {
32869
33276
  // Read-modify-write runs one at a time so concurrent tool calls cannot lose updates.
32870
33277
  // The lock file extends that across processes (e.g. Claude Desktop and Claude Code).
32871
33278
  mutate(fn) {
32872
- const run = this.queue.then(
32873
- () => lockedWrite(
33279
+ const run = this.queue.then(async () => {
33280
+ await this.guard?.();
33281
+ let latest = null;
33282
+ let before = [];
33283
+ return lockedWrite(
32874
33284
  this.file,
32875
33285
  {
32876
33286
  timeoutMs: this.lockTimeoutMs,
32877
- lockedMessage: "Plan file is locked by another process; try again"
33287
+ lockDir: this.lockDir,
33288
+ lockedMessage: "Plan file is locked by another process; try again",
33289
+ // Under the lock, so the copies always match the last saved file.
33290
+ afterWrite: async () => {
33291
+ if (latest && this.markdownDir) await this.writeMarkdown(latest, before);
33292
+ }
32878
33293
  },
32879
33294
  async () => {
32880
33295
  const data = await this.read();
33296
+ before = data.plans.map((p) => p.id);
32881
33297
  const result = fn(data);
32882
- return { content: JSON.stringify(data, null, 2), result };
33298
+ latest = data;
33299
+ const content = JSON.stringify(data, null, 2);
33300
+ if (Buffer.byteLength(content) > MAX_DATA_FILE) {
33301
+ throw new UserError(
33302
+ "Plans file would be larger than 8 MB; delete plans or shorten notes first."
33303
+ );
33304
+ }
33305
+ return { content, result };
32883
33306
  }
32884
- )
32885
- );
33307
+ );
33308
+ });
32886
33309
  this.queue = run.catch(() => void 0);
32887
33310
  return run;
32888
33311
  }
33312
+ /** Why the Markdown copies are not being written, or null when they are fine. */
33313
+ async markdownProblem() {
33314
+ const dir = this.markdownDir;
33315
+ if (!dir) return null;
33316
+ const fixed = await this.directoryProblem();
33317
+ if (fixed) return fixed;
33318
+ return this.lastCopyError === null ? null : `Plan copies could not be written to ${dir}: ${this.lastCopyError}.`;
33319
+ }
33320
+ async directoryProblem() {
33321
+ const dir = this.markdownDir;
33322
+ if (!dir) return null;
33323
+ let st;
33324
+ try {
33325
+ st = await lstat2(dir);
33326
+ } catch {
33327
+ return null;
33328
+ }
33329
+ if (st.isSymbolicLink()) return `Plan copies were not written: ${dir} is a symbolic link.`;
33330
+ if (!st.isDirectory()) return `Plan copies were not written: ${dir} is not a folder.`;
33331
+ return null;
33332
+ }
33333
+ // Claude on mobile can read these through the Google Drive connector; they are copies only.
33334
+ // The sync folder may be shared, so nothing here follows a link or deletes a file we did not write.
33335
+ async writeMarkdown(data, before) {
33336
+ const dir = this.markdownDir;
33337
+ try {
33338
+ await mkdir3(dir, { recursive: true });
33339
+ const problem = await this.directoryProblem();
33340
+ if (problem) {
33341
+ console.error(problem);
33342
+ return;
33343
+ }
33344
+ const keep = /* @__PURE__ */ new Set();
33345
+ for (const p of data.plans) {
33346
+ const name = `${p.id}.md`;
33347
+ keep.add(name);
33348
+ const tmp = path8.join(dir, `${name}.${randomBytes2(6).toString("hex")}.tmp`);
33349
+ try {
33350
+ await writeFile3(tmp, planToMarkdown(p), { flag: "wx" });
33351
+ await rename3(tmp, path8.join(dir, name));
33352
+ } catch (err) {
33353
+ await rm(tmp, { force: true });
33354
+ throw err;
33355
+ }
33356
+ }
33357
+ for (const id of before) {
33358
+ const n = `${id}.md`;
33359
+ if (keep.has(n) || !PRUNABLE.test(n)) continue;
33360
+ const file2 = path8.join(dir, n);
33361
+ const st = await lstat2(file2).catch(() => null);
33362
+ if (st?.isFile()) await rm(file2, { force: true });
33363
+ }
33364
+ this.lastCopyError = null;
33365
+ } catch (err) {
33366
+ this.lastCopyError = err.message;
33367
+ console.error(`Could not write Markdown plan copies to ${dir}: ${err.message}`);
33368
+ }
33369
+ }
32889
33370
  };
32890
-
32891
- // src/prefs/store.ts
32892
- import { readFile as readFile7 } from "fs/promises";
33371
+ var PRUNABLE = /^[a-z0-9][a-z0-9-]{0,63}\.md$/;
32893
33372
 
32894
33373
  // src/prefs/schema.ts
32895
33374
  var Mode = external_exports.enum(["soft", "hard", "off"]);
@@ -33016,18 +33495,22 @@ var PreferencesStore = class {
33016
33495
  constructor(file2, opts = {}) {
33017
33496
  this.file = file2;
33018
33497
  this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS2;
33498
+ this.lockDir = opts.lockDir;
33499
+ this.guard = opts.guard;
33019
33500
  }
33020
33501
  file;
33021
33502
  queue = Promise.resolve();
33022
33503
  lockTimeoutMs;
33504
+ lockDir;
33505
+ guard;
33023
33506
  async get() {
33024
- let raw;
33025
- try {
33026
- raw = await readFile7(this.file, "utf8");
33027
- } catch (err) {
33028
- if (err.code === "ENOENT") return DEFAULT_PREFS;
33029
- throw err;
33507
+ await this.guard?.();
33508
+ const read2 = await readRegularText(this.file, MAX_DATA_FILE);
33509
+ if (read2.kind === "missing") return DEFAULT_PREFS;
33510
+ if (read2.kind === "unsafe") {
33511
+ throw new UserError(`Preferences file ${this.file} is invalid: ${read2.reason}`);
33030
33512
  }
33513
+ const raw = read2.text;
33031
33514
  let json3;
33032
33515
  try {
33033
33516
  json3 = JSON.parse(raw);
@@ -33049,22 +33532,30 @@ var PreferencesStore = class {
33049
33532
  }
33050
33533
  async write(patch) {
33051
33534
  const parsed = parsePatch(patch);
33535
+ await this.guard?.();
33052
33536
  return lockedWrite(
33053
33537
  this.file,
33054
33538
  {
33055
33539
  timeoutMs: this.lockTimeoutMs,
33540
+ lockDir: this.lockDir,
33056
33541
  lockedMessage: "Preferences file is locked by another process; try again"
33057
33542
  },
33058
33543
  async () => {
33059
33544
  const next = applyPatch(await this.get(), parsed);
33060
- return { content: JSON.stringify(next, null, 2), result: next };
33545
+ const content = JSON.stringify(next, null, 2);
33546
+ if (Buffer.byteLength(content) > MAX_DATA_FILE) {
33547
+ throw new UserError(
33548
+ "Preferences file would be larger than 8 MB; shorten the preferences first."
33549
+ );
33550
+ }
33551
+ return { content, result: next };
33061
33552
  }
33062
33553
  );
33063
33554
  }
33064
33555
  };
33065
33556
 
33066
33557
  // src/ships/data.ts
33067
- import { mkdir as mkdir4, readFile as readFile8, rename as rename4, writeFile as writeFile4 } from "fs/promises";
33558
+ import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
33068
33559
  import path9 from "path";
33069
33560
  import { gunzipSync } from "zlib";
33070
33561
 
@@ -33308,14 +33799,14 @@ function readTar(buf) {
33308
33799
  comment = paxFields(data).comment ?? comment;
33309
33800
  continue;
33310
33801
  }
33311
- let path12 = str3(h, 0, 100);
33802
+ let path15 = str3(h, 0, 100);
33312
33803
  if (str3(h, 257, 6) === "ustar") {
33313
33804
  const prefix = str3(h, 345, 155);
33314
- if (prefix) path12 = `${prefix}/${path12}`;
33805
+ if (prefix) path15 = `${prefix}/${path15}`;
33315
33806
  }
33316
- if (nextPath !== null) path12 = nextPath;
33807
+ if (nextPath !== null) path15 = nextPath;
33317
33808
  nextPath = null;
33318
- if (type === "0" || type === "\0") files.push({ path: path12, data });
33809
+ if (type === "0" || type === "\0") files.push({ path: path15, data });
33319
33810
  }
33320
33811
  return { files, comment };
33321
33812
  }
@@ -33581,7 +34072,7 @@ var ShipDataService = class {
33581
34072
  }
33582
34073
  async loadDisk() {
33583
34074
  try {
33584
- const snap = JSON.parse(await readFile8(this.file, "utf8"));
34075
+ const snap = JSON.parse(await readFile5(this.file, "utf8"));
33585
34076
  if (snap.version !== 1 || typeof snap.commit !== "string") return;
33586
34077
  const data = buildShipData(snap.commit, snap.files);
33587
34078
  if (this.data) return;
@@ -33600,37 +34091,775 @@ var ShipDataService = class {
33600
34091
  };
33601
34092
  var message = (err) => err instanceof Error ? err.message : String(err);
33602
34093
 
34094
+ // src/settings/secrets.ts
34095
+ import { lstat as lstat3, mkdir as mkdir5, rm as rm2 } from "fs/promises";
34096
+ import path10 from "path";
34097
+
34098
+ // src/settings/crypto.ts
34099
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, scryptSync } from "crypto";
34100
+ var SCRYPT = { N: 2 ** 17, r: 8, p: 1 };
34101
+ var SCRYPT_MAXMEM = 256 * 1024 * 1024;
34102
+ var SecretsError = class extends UserError {
34103
+ name = "SecretsError";
34104
+ };
34105
+ var EnvelopeSchema = external_exports.object({
34106
+ format: external_exports.literal("ed-mcp-secrets"),
34107
+ version: external_exports.number().int(),
34108
+ kdf: external_exports.literal("scrypt"),
34109
+ // With the memory check in open(), these bounds stop a crafted file from making scrypt use gigabytes.
34110
+ N: external_exports.number().int().min(2).max(2 ** 17).refine((n) => (n & n - 1) === 0),
34111
+ r: external_exports.number().int().min(1).max(8),
34112
+ p: external_exports.number().int().min(1).max(2),
34113
+ salt: external_exports.string(),
34114
+ iv: external_exports.string(),
34115
+ tag: external_exports.string(),
34116
+ data: external_exports.string()
34117
+ });
34118
+ var SecretCipher = class {
34119
+ constructor(password, params = SCRYPT) {
34120
+ this.password = password;
34121
+ this.params = params;
34122
+ }
34123
+ password;
34124
+ params;
34125
+ // Deriving the key is slow on purpose, so each salt's key is derived once per process.
34126
+ keys = /* @__PURE__ */ new Map();
34127
+ seal(plaintext, salt = randomBytes3(16).toString("base64")) {
34128
+ const key = this.key(salt, this.params);
34129
+ const iv = randomBytes3(12);
34130
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
34131
+ const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
34132
+ return {
34133
+ format: "ed-mcp-secrets",
34134
+ version: 1,
34135
+ kdf: "scrypt",
34136
+ ...this.params,
34137
+ salt,
34138
+ iv: iv.toString("base64"),
34139
+ tag: cipher.getAuthTag().toString("base64"),
34140
+ data: data.toString("base64")
34141
+ };
34142
+ }
34143
+ open(raw) {
34144
+ const env2 = EnvelopeSchema.safeParse(raw);
34145
+ if (!env2.success) throw new SecretsError("not an ed-mcp secrets file");
34146
+ const e = env2.data;
34147
+ if (e.version !== 1) {
34148
+ throw new SecretsError(
34149
+ e.version > 1 ? `made by a newer ed-mcp (version ${e.version})` : `unknown version ${e.version}`
34150
+ );
34151
+ }
34152
+ if (128 * e.N * e.r >= SCRYPT_MAXMEM) throw new SecretsError("not an ed-mcp secrets file");
34153
+ const key = this.key(e.salt, { N: e.N, r: e.r, p: e.p });
34154
+ try {
34155
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(e.iv, "base64"), {
34156
+ authTagLength: 16
34157
+ });
34158
+ decipher.setAuthTag(Buffer.from(e.tag, "base64"));
34159
+ const plaintext = Buffer.concat([
34160
+ decipher.update(Buffer.from(e.data, "base64")),
34161
+ decipher.final()
34162
+ ]).toString("utf8");
34163
+ return { plaintext, salt: e.salt };
34164
+ } catch {
34165
+ throw new SecretsError("wrong password");
34166
+ }
34167
+ }
34168
+ key(salt, params) {
34169
+ const id = `${salt}:${params.N}:${params.r}:${params.p}`;
34170
+ let key = this.keys.get(id);
34171
+ if (!key) {
34172
+ key = scryptSync(this.password, Buffer.from(salt, "base64"), 32, {
34173
+ ...params,
34174
+ maxmem: SCRYPT_MAXMEM
34175
+ });
34176
+ this.keys.set(id, key);
34177
+ }
34178
+ return key;
34179
+ }
34180
+ };
34181
+
34182
+ // src/settings/secrets.ts
34183
+ var StoredTokensSchema = external_exports.object({
34184
+ refresh_token: external_exports.string(),
34185
+ access_token: external_exports.string().nullable(),
34186
+ expires_at: external_exports.number(),
34187
+ authorized_at: external_exports.number()
34188
+ });
34189
+ var ServiceKeySchema = external_exports.strictObject({ apiKey: external_exports.string().min(1), cmdr: external_exports.string().min(1) });
34190
+ var SecretsSchema = external_exports.strictObject({
34191
+ version: external_exports.literal(1),
34192
+ inara: ServiceKeySchema.optional(),
34193
+ edsm: ServiceKeySchema.optional(),
34194
+ capi: StoredTokensSchema.optional()
34195
+ });
34196
+ var WrongPasswordError = class extends SecretsError {
34197
+ };
34198
+ var LOCK_TIMEOUT_MS3 = 5e3;
34199
+ var SecretStore = class _SecretStore {
34200
+ constructor(file2, opts = {}) {
34201
+ this.file = file2;
34202
+ this.opts = opts;
34203
+ this.encrypted = opts.password !== void 0;
34204
+ if (opts.password !== void 0) this.cipher = new SecretCipher(opts.password, opts.scrypt);
34205
+ }
34206
+ file;
34207
+ opts;
34208
+ encrypted;
34209
+ cipher;
34210
+ cached = null;
34211
+ salt;
34212
+ queue = Promise.resolve();
34213
+ async get() {
34214
+ return this.read();
34215
+ }
34216
+ update(fn) {
34217
+ const run = this.queue.then(() => this.write(fn));
34218
+ this.queue = run.catch(() => void 0);
34219
+ return run;
34220
+ }
34221
+ /** Decrypts with the old password and writes the same secrets with the current one. */
34222
+ reencrypt(oldPassword) {
34223
+ const cipher = this.cipher;
34224
+ if (!cipher) {
34225
+ return Promise.reject(new SecretsError("Secrets are not encrypted; nothing to re-encrypt."));
34226
+ }
34227
+ const run = this.queue.then(async () => {
34228
+ await this.opts.guard?.();
34229
+ await lockedWrite(
34230
+ this.file,
34231
+ {
34232
+ timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
34233
+ lockedMessage: "Secrets file is locked by another process; try again",
34234
+ lockDir: this.opts.lockDir,
34235
+ mode: 384
34236
+ },
34237
+ async () => {
34238
+ const st = await this.checkFile();
34239
+ if (!st) throw new SecretsError(`${this.file} does not exist; nothing to re-encrypt.`);
34240
+ const raw = await this.readText();
34241
+ const old = new _SecretStore(this.file, {
34242
+ password: oldPassword,
34243
+ scrypt: this.opts.scrypt
34244
+ });
34245
+ const { value } = old.parse(raw);
34246
+ const content = JSON.stringify(cipher.seal(JSON.stringify(value, null, 2)), null, 2);
34247
+ return { content, result: void 0 };
34248
+ }
34249
+ );
34250
+ });
34251
+ this.queue = run.catch(() => void 0);
34252
+ return run.finally(() => {
34253
+ this.cached = null;
34254
+ this.salt = void 0;
34255
+ });
34256
+ }
34257
+ /** Removes the secrets file: keys and the Frontier login are forgotten. */
34258
+ reset() {
34259
+ const run = this.queue.then(async () => {
34260
+ await this.opts.guard?.();
34261
+ await mkdir5(path10.dirname(this.file), { recursive: true });
34262
+ if (this.opts.lockDir) await mkdir5(this.opts.lockDir, { recursive: true });
34263
+ const release = await acquireLock(
34264
+ this.file,
34265
+ this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
34266
+ "Secrets file is locked by another process; try again",
34267
+ this.opts.lockDir
34268
+ );
34269
+ try {
34270
+ await rm2(this.file, { force: true });
34271
+ } finally {
34272
+ await release();
34273
+ }
34274
+ });
34275
+ this.queue = run.catch(() => void 0);
34276
+ return run.finally(() => {
34277
+ this.cached = null;
34278
+ this.salt = void 0;
34279
+ });
34280
+ }
34281
+ async read() {
34282
+ await this.opts.guard?.();
34283
+ const st = await this.checkFile();
34284
+ if (!st) {
34285
+ this.cached = null;
34286
+ return { version: 1 };
34287
+ }
34288
+ if (this.cached && this.cached.mtimeMs === st.mtimeMs && this.cached.size === st.size) {
34289
+ return structuredClone(this.cached.value);
34290
+ }
34291
+ const raw = await this.readText();
34292
+ const { value, salt } = this.parse(raw);
34293
+ this.cached = { mtimeMs: st.mtimeMs, size: st.size, value };
34294
+ this.salt = salt;
34295
+ return structuredClone(value);
34296
+ }
34297
+ // Returns null when the file is missing. Refuses anything that is not a small regular file.
34298
+ async checkFile() {
34299
+ let st;
34300
+ try {
34301
+ st = await lstat3(this.file);
34302
+ } catch (err) {
34303
+ const code = err.code;
34304
+ if (code === "ENOENT") return null;
34305
+ throw new SecretsError(`${this.file} is not readable (${code ?? "unknown error"}).`);
34306
+ }
34307
+ if (!st.isFile()) throw new SecretsError(`${this.file} is not readable (not a regular file).`);
34308
+ if (st.size > MAX_SECRETS_FILE)
34309
+ throw new SecretsError(`${this.file} is not readable (too large).`);
34310
+ return st;
34311
+ }
34312
+ async readText() {
34313
+ const read2 = await readRegularText(this.file, MAX_SECRETS_FILE);
34314
+ if (read2.kind === "ok") return read2.text;
34315
+ const reason2 = read2.kind === "missing" ? "not readable (ENOENT)" : read2.reason;
34316
+ throw new SecretsError(
34317
+ reason2.startsWith("not readable") ? `${this.file} is ${reason2}.` : `${this.file} is not readable (${reason2}).`
34318
+ );
34319
+ }
34320
+ parse(raw) {
34321
+ let json3;
34322
+ try {
34323
+ json3 = JSON.parse(raw);
34324
+ } catch {
34325
+ throw new SecretsError(`${this.file} is not readable (not JSON).`);
34326
+ }
34327
+ let salt;
34328
+ if (!this.cipher && json3?.format === "ed-mcp-secrets") {
34329
+ throw new SecretsError(`${this.file} is not readable (encrypted; set ED_MCP_SYNC_PASSWORD).`);
34330
+ }
34331
+ if (this.cipher) {
34332
+ try {
34333
+ const opened = this.cipher.open(json3);
34334
+ salt = opened.salt;
34335
+ json3 = JSON.parse(opened.plaintext);
34336
+ } catch (err) {
34337
+ if (err instanceof SecretsError && err.message === "wrong password") {
34338
+ throw new WrongPasswordError(`Can't decrypt ${this.file}: wrong ED_MCP_SYNC_PASSWORD.`);
34339
+ }
34340
+ const reason2 = err instanceof SecretsError ? err.message : "not JSON";
34341
+ throw new SecretsError(`${this.file} is not readable (${reason2}).`);
34342
+ }
34343
+ }
34344
+ const parsed = SecretsSchema.safeParse(json3);
34345
+ if (!parsed.success) {
34346
+ const issue2 = parsed.error.issues[0];
34347
+ const where = issue2?.path.length ? `${issue2.path.join(".")}: ` : "";
34348
+ throw new SecretsError(
34349
+ `${this.file} is not readable (${where}${issue2?.message ?? "invalid"}).`
34350
+ );
34351
+ }
34352
+ return { value: parsed.data, salt };
34353
+ }
34354
+ async write(fn) {
34355
+ await this.opts.guard?.();
34356
+ return lockedWrite(
34357
+ this.file,
34358
+ {
34359
+ timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
34360
+ lockedMessage: "Secrets file is locked by another process; try again",
34361
+ lockDir: this.opts.lockDir,
34362
+ mode: 384
34363
+ },
34364
+ async () => {
34365
+ this.cached = null;
34366
+ const next = await this.read();
34367
+ fn(next);
34368
+ const result = SecretsSchema.safeParse(next);
34369
+ if (!result.success) {
34370
+ const issue2 = result.error.issues[0]?.message ?? "invalid";
34371
+ throw new SecretsError(`${this.file} would be invalid (${issue2}).`);
34372
+ }
34373
+ const checked = result.data;
34374
+ const body = JSON.stringify(checked, null, 2);
34375
+ const content = this.cipher ? JSON.stringify(this.cipher.seal(body, this.salt), null, 2) : body;
34376
+ return { content, result: checked };
34377
+ }
34378
+ ).finally(() => {
34379
+ this.cached = null;
34380
+ });
34381
+ }
34382
+ };
34383
+
34384
+ // src/settings/sync.ts
34385
+ import { constants as constants2 } from "fs";
34386
+ import { access, readdir as readdir2, stat as stat6 } from "fs/promises";
34387
+ import path11 from "path";
34388
+ var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
34389
+ function conflictPattern(file2) {
34390
+ const ext = path11.extname(file2);
34391
+ const base2 = escape2(path11.basename(file2, ext));
34392
+ const e = escape2(ext);
34393
+ return new RegExp(
34394
+ [
34395
+ `^${base2} \\(\\d+\\)${e}$`,
34396
+ // Google Drive
34397
+ `^${base2} \\d+${e}$`,
34398
+ // iCloud
34399
+ `^${base2}-[^.\\s]+${e}$`,
34400
+ // OneDrive: name-COMPUTERNAME
34401
+ `^${base2}\\.sync-conflict-\\d{8}-\\d{6}-[A-Z0-9]{7}${e}$`
34402
+ // Syncthing
34403
+ ].join("|")
34404
+ );
34405
+ }
34406
+ var SyncFolder = class {
34407
+ dir;
34408
+ constructor(dir) {
34409
+ this.dir = path11.resolve(dir);
34410
+ }
34411
+ contains(file2) {
34412
+ const rel = path11.relative(this.dir, path11.resolve(file2));
34413
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${path11.sep}`) && !path11.isAbsolute(rel);
34414
+ }
34415
+ // No fallback to the local folder: writing there while the sync app is down would split
34416
+ // the plans into two copies that never merge.
34417
+ async check() {
34418
+ const fail2 = (reason2) => new UserError(
34419
+ `Sync folder ${this.dir} is not available: ${reason2}. Start your sync app, or fix ED_MCP_SYNC_DIR.`
34420
+ );
34421
+ let st;
34422
+ try {
34423
+ st = await stat6(this.dir);
34424
+ } catch (err) {
34425
+ const code = err.code;
34426
+ throw fail2(code === "ENOENT" ? "it does not exist" : String(code ?? err));
34427
+ }
34428
+ if (!st.isDirectory()) throw fail2("it is not a folder");
34429
+ await access(this.dir, constants2.W_OK).catch(() => {
34430
+ throw fail2("it is not writable");
34431
+ });
34432
+ }
34433
+ async conflicts(file2) {
34434
+ const names = await readdir2(path11.dirname(file2)).catch(() => []);
34435
+ const re = conflictPattern(file2);
34436
+ const own2 = path11.basename(file2);
34437
+ return names.filter((n) => n !== own2 && re.test(n));
34438
+ }
34439
+ async warnings(files) {
34440
+ const out = [];
34441
+ for (const f of files.filter((f2) => this.contains(f2))) {
34442
+ for (const copy of await this.conflicts(f)) {
34443
+ out.push(
34444
+ `Sync conflict: ${copy} exists next to ${path11.basename(f)}. Compare them and delete the copy you don't want.`
34445
+ );
34446
+ }
34447
+ }
34448
+ return out;
34449
+ }
34450
+ };
34451
+
34452
+ // src/settings/settings.ts
34453
+ var Settings = class {
34454
+ constructor(env2, inaraApp, secrets) {
34455
+ this.env = env2;
34456
+ this.inaraApp = inaraApp;
34457
+ this.secrets = secrets;
34458
+ }
34459
+ env;
34460
+ inaraApp;
34461
+ secrets;
34462
+ seeded = null;
34463
+ async inara() {
34464
+ const k = await this.key("inara");
34465
+ return k ? { ...k, ...this.inaraApp } : void 0;
34466
+ }
34467
+ async edsm() {
34468
+ return this.key("edsm");
34469
+ }
34470
+ async sources() {
34471
+ let stored = {};
34472
+ try {
34473
+ await this.seed();
34474
+ stored = await this.secrets.get();
34475
+ } catch (err) {
34476
+ if (!(err instanceof UserError)) throw err;
34477
+ }
34478
+ const info = (s) => {
34479
+ const env2 = this.env[s];
34480
+ const saved = stored[s];
34481
+ if (env2) {
34482
+ const differs = !!saved && (saved.apiKey !== env2.apiKey || saved.cmdr !== env2.cmdr);
34483
+ return { source: "environment", cmdr: env2.cmdr, differs_from_stored: differs };
34484
+ }
34485
+ if (saved) return { source: "stored", cmdr: saved.cmdr, differs_from_stored: false };
34486
+ return { source: "not set", cmdr: null, differs_from_stored: false };
34487
+ };
34488
+ return { inara: info("inara"), edsm: info("edsm") };
34489
+ }
34490
+ // The environment wins on this computer, so the store is read only when it has no value.
34491
+ async key(s) {
34492
+ await this.seed().catch(() => void 0);
34493
+ if (this.env[s]) return this.env[s];
34494
+ return (await this.secrets.get())[s];
34495
+ }
34496
+ // Copies a key set in the environment into an empty slot once, so other computers that
34497
+ // share the sync folder get it. A stored value is never overwritten from the environment.
34498
+ seed() {
34499
+ this.seeded ??= (async () => {
34500
+ const current = await this.secrets.get();
34501
+ const missing = ["inara", "edsm"].filter((s) => this.env[s] && !current[s]);
34502
+ if (!missing.length) return;
34503
+ await this.secrets.update((d) => {
34504
+ for (const s of missing) d[s] ??= this.env[s];
34505
+ });
34506
+ })().catch((err) => {
34507
+ this.seeded = null;
34508
+ throw err;
34509
+ });
34510
+ return this.seeded;
34511
+ }
34512
+ };
34513
+
34514
+ // src/settings/page.ts
34515
+ import { randomBytes as randomBytes4, timingSafeEqual } from "crypto";
34516
+ import { createServer as createServer2 } from "http";
34517
+ var TIMEOUT_MS = 10 * 6e4;
34518
+ var MAX_BODY = 16 * 1024;
34519
+ var BodyTooLarge = class extends Error {
34520
+ };
34521
+ var esc2 = (v) => v.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
34522
+ var mask = (k) => !k ? "not set" : k.length <= 8 ? "set" : `\u2026${esc2(k.slice(-4))}`;
34523
+ var page = (body) => `<!doctype html><meta charset="utf-8"><title>ed-mcp settings</title><style>body{font:15px system-ui;max-width:640px;margin:2em auto}label{display:block;margin:.6em 0}input[type=text],input[type=password]{width:100%}fieldset{margin:1em 0}</style>${body}`;
34524
+ var SettingsPage = class {
34525
+ constructor(deps, opts = {}) {
34526
+ this.deps = deps;
34527
+ this.inSyncFolder = opts.inSyncFolder ?? false;
34528
+ this.openUrl = opts.openUrl ?? openInBrowser;
34529
+ this.timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
34530
+ }
34531
+ deps;
34532
+ server = null;
34533
+ closed = Promise.resolve();
34534
+ opening = Promise.resolve();
34535
+ openUrl;
34536
+ timeoutMs;
34537
+ inSyncFolder;
34538
+ isOpen() {
34539
+ return this.server !== null;
34540
+ }
34541
+ waitForClose() {
34542
+ return this.closed;
34543
+ }
34544
+ // Calls run one after another, so two overlapping calls cannot leave a server nobody tracks.
34545
+ open() {
34546
+ const run = this.opening.then(() => this.start());
34547
+ this.opening = run.catch(() => void 0);
34548
+ return run;
34549
+ }
34550
+ async start() {
34551
+ this.close();
34552
+ const key = randomBytes4(24).toString("hex");
34553
+ let done;
34554
+ this.closed = new Promise((r) => done = r);
34555
+ const server = createServer2((req, res) => {
34556
+ this.handle(req, res, key, server).catch((err) => {
34557
+ if (!(err instanceof UserError)) console.error(err instanceof Error ? err.message : err);
34558
+ const text = err instanceof UserError ? esc2(err.message) : "Something went wrong.";
34559
+ if (!res.headersSent) res.writeHead(500).end(page(`<p>${text}</p>`));
34560
+ });
34561
+ });
34562
+ server.on("close", done);
34563
+ server.unref();
34564
+ await new Promise((resolve, reject) => {
34565
+ server.once("error", reject);
34566
+ server.listen(0, "127.0.0.1", resolve);
34567
+ });
34568
+ this.server = server;
34569
+ const timer = setTimeout(() => this.stop(server), this.timeoutMs);
34570
+ timer.unref();
34571
+ server.on("close", () => clearTimeout(timer));
34572
+ const url2 = `http://127.0.0.1:${server.address().port}/?key=${key}`;
34573
+ const opened = await this.openUrl(url2).then(
34574
+ () => true,
34575
+ () => false
34576
+ );
34577
+ return { url: url2, expires_at: new Date(Date.now() + this.timeoutMs).toISOString(), opened };
34578
+ }
34579
+ close() {
34580
+ if (this.server) this.stop(this.server);
34581
+ }
34582
+ stop(server) {
34583
+ if (this.server === server) this.server = null;
34584
+ server.close();
34585
+ server.closeAllConnections();
34586
+ }
34587
+ async handle(req, res, key, server) {
34588
+ const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
34589
+ const html = (status, body) => res.writeHead(status, {
34590
+ "content-type": "text/html; charset=utf-8",
34591
+ "cache-control": "no-store",
34592
+ "referrer-policy": "no-referrer"
34593
+ }).end(page(body));
34594
+ const denied = () => html(403, "<p>Use the link from Claude.</p>");
34595
+ if (req.method === "GET" && url2.pathname === "/") {
34596
+ if (!sameKey(url2.searchParams.get("key"), key)) return void denied();
34597
+ return void html(200, await this.form(key));
34598
+ }
34599
+ if (req.method !== "POST") return void res.writeHead(404).end();
34600
+ let form;
34601
+ try {
34602
+ form = new URLSearchParams(await readBody(req));
34603
+ } catch (err) {
34604
+ if (!(err instanceof BodyTooLarge)) throw err;
34605
+ return void res.writeHead(413, { connection: "close" }).end();
34606
+ }
34607
+ if (!sameKey(form.get("key"), key)) return void denied();
34608
+ try {
34609
+ if (url2.pathname === "/save") {
34610
+ await this.deps.secrets.update((d) => {
34611
+ for (const s of ["inara", "edsm"]) {
34612
+ if (form.get(`clear_${s}`) === "on") {
34613
+ delete d[s];
34614
+ continue;
34615
+ }
34616
+ const next = entry(form, s, d[s]);
34617
+ if (next) d[s] = next;
34618
+ }
34619
+ });
34620
+ } else if (url2.pathname === "/reencrypt") {
34621
+ await this.deps.secrets.reencrypt(form.get("old_password") ?? "");
34622
+ } else if (url2.pathname === "/reset") {
34623
+ await this.deps.secrets.reset();
34624
+ } else {
34625
+ return void res.writeHead(404).end();
34626
+ }
34627
+ } catch (err) {
34628
+ if (!(err instanceof UserError)) throw err;
34629
+ const text = err instanceof WrongPasswordError && url2.pathname === "/reencrypt" ? "The old password is wrong." : err.message;
34630
+ return void html(400, `<p>${esc2(text)}</p><p>Go back and try again.</p>`);
34631
+ }
34632
+ res.once("finish", () => this.stop(server));
34633
+ html(200, "<p>Saved. You can close this tab and go back to Claude.</p>");
34634
+ }
34635
+ async form(key) {
34636
+ const k = `<input type="hidden" name="key" value="${key}">`;
34637
+ const shared = this.inSyncFolder ? "<p><strong>This changes the secrets for every computer that shares the sync folder.</strong></p>" : "";
34638
+ let stored;
34639
+ try {
34640
+ stored = await this.deps.secrets.get();
34641
+ } catch (err) {
34642
+ if (!(err instanceof SecretsError)) throw err;
34643
+ return `<h1>ed-mcp settings</h1><p>${esc2(err.message)}</p><form method="post" action="/reencrypt">${k}<fieldset><legend>Re-encrypt with the current password</legend>${shared}<label>Old password <input type="password" name="old_password" autocomplete="off"></label><button>Re-encrypt</button></fieldset></form><form method="post" action="/reset">${k}<fieldset><legend>Start fresh</legend>${shared}<p>Removes the saved keys and the Frontier login. Plans and preferences are not affected.</p><button>Start fresh</button></fieldset></form>`;
34644
+ }
34645
+ const sources = await this.deps.settings.sources();
34646
+ const help = {
34647
+ inara: `Get your key: log in at <a href="https://inara.cz/elite/inara-api/" target="_blank" rel="noopener noreferrer">inara.cz</a>, click the cog next to "Personal content", open API settings and choose "Generate new key". The commander name is your in-game name.`,
34648
+ edsm: `Get your key: log in at <a href="https://www.edsm.net/" target="_blank" rel="noopener noreferrer">edsm.net</a>, open the user menu and choose "My API Key". Use your EDSM commander name, which may differ from your in-game name.`
34649
+ };
34650
+ const section = (s, title, info) => `<fieldset><legend>${title}</legend><p><small>${help[s]}</small></p><p>In use: ${info.source === "environment" ? "from the environment (it wins on this computer)" : info.source}. Saved key: ${mask(stored[s]?.apiKey)}</p><label>${title} API key <input type="password" name="${s}_key" autocomplete="off" placeholder="leave empty to keep"></label><label>Commander name <input type="text" name="${s}_cmdr" value="${esc2(stored[s]?.cmdr ?? "")}"></label><label><input type="checkbox" name="clear_${s}"> Forget the saved ${title} key</label></fieldset>`;
34651
+ return `<h1>ed-mcp settings</h1><form method="post" action="/save">${k}${shared}` + section("inara", "Inara", sources.inara) + section("edsm", "EDSM", sources.edsm) + `<button>Save</button></form>`;
34652
+ }
34653
+ };
34654
+ function sameKey(given, key) {
34655
+ const a = Buffer.from(given ?? "");
34656
+ const b = Buffer.from(key);
34657
+ return a.length === b.length && timingSafeEqual(a, b);
34658
+ }
34659
+ function entry(form, s, current) {
34660
+ const apiKey = form.get(`${s}_key`)?.trim() || current?.apiKey;
34661
+ const cmdr = form.get(`${s}_cmdr`)?.trim() || current?.cmdr;
34662
+ if (!!apiKey !== !!cmdr) {
34663
+ const name = s === "inara" ? "Inara" : "EDSM";
34664
+ throw new UserError(`Give both the API key and the commander name for ${name}.`);
34665
+ }
34666
+ return apiKey && cmdr ? { apiKey, cmdr } : void 0;
34667
+ }
34668
+ function readBody(req) {
34669
+ return new Promise((resolve, reject) => {
34670
+ let size = 0;
34671
+ let failed = false;
34672
+ const chunks = [];
34673
+ req.on("data", (c) => {
34674
+ if (failed) return;
34675
+ size += c.length;
34676
+ if (size > MAX_BODY) {
34677
+ failed = true;
34678
+ reject(new BodyTooLarge());
34679
+ } else chunks.push(c);
34680
+ });
34681
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
34682
+ req.on("error", reject);
34683
+ });
34684
+ }
34685
+
34686
+ // src/version/check.ts
34687
+ import { readFile as readFile6 } from "fs/promises";
34688
+ import path12 from "path";
34689
+ var RELEASE_URL = "https://api.github.com/repos/pilotso11/ed-mcp/releases/latest";
34690
+ var DAY_MS = 24 * 60 * 60 * 1e3;
34691
+ var RETRY_MS = 60 * 60 * 1e3;
34692
+ var RELEASE_PAGE = "https://github.com/pilotso11/ed-mcp/releases/tag";
34693
+ var STRICT_VERSION = /^\d+\.\d+\.\d+$/;
34694
+ var releaseSchema = external_exports.object({
34695
+ tag_name: external_exports.string().transform((t) => t.replace(/^v/, "")).pipe(external_exports.string().regex(STRICT_VERSION))
34696
+ });
34697
+ var cacheSchema = external_exports.object({
34698
+ checked_at: external_exports.number(),
34699
+ latest: external_exports.string().regex(STRICT_VERSION)
34700
+ });
34701
+ function parse3(version2) {
34702
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version2.trim());
34703
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
34704
+ }
34705
+ function isNewer(latest, current) {
34706
+ const a = parse3(latest);
34707
+ const b = parse3(current);
34708
+ if (!a || !b) return false;
34709
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
34710
+ return false;
34711
+ }
34712
+ var VersionCheck = class {
34713
+ constructor(opts) {
34714
+ this.opts = opts;
34715
+ this.file = path12.join(opts.cacheDir, "version-check.json");
34716
+ this.now = opts.now ?? Date.now;
34717
+ }
34718
+ opts;
34719
+ file;
34720
+ now;
34721
+ running = Promise.resolve();
34722
+ inFlight = false;
34723
+ stopped = false;
34724
+ timer;
34725
+ /**
34726
+ * Checks in the background, then again every 24 hours (every hour after a failure), so a
34727
+ * server that runs for days still learns of a new release. It never throws.
34728
+ */
34729
+ start() {
34730
+ this.stopped = false;
34731
+ if (this.timer || this.inFlight) return;
34732
+ this.run();
34733
+ }
34734
+ /** Cancels the next scheduled check. */
34735
+ stop() {
34736
+ this.stopped = true;
34737
+ clearTimeout(this.timer);
34738
+ this.timer = void 0;
34739
+ }
34740
+ /** Resolves when the check in progress is done. */
34741
+ whenIdle() {
34742
+ return this.running;
34743
+ }
34744
+ /** Reads the saved result only. It never makes a request or writes. */
34745
+ async notice() {
34746
+ try {
34747
+ const cached2 = await this.read();
34748
+ if (!cached2 || !isNewer(cached2.latest, this.opts.current)) return null;
34749
+ return `ed-mcp ${cached2.latest} is available (you have ${this.opts.current}): ${RELEASE_PAGE}/v${cached2.latest}`;
34750
+ } catch {
34751
+ return null;
34752
+ }
34753
+ }
34754
+ run() {
34755
+ clearTimeout(this.timer);
34756
+ this.timer = void 0;
34757
+ if (this.inFlight) return;
34758
+ this.inFlight = true;
34759
+ this.running = this.check().then(
34760
+ () => true,
34761
+ (err) => {
34762
+ if (!(err instanceof UserError || err instanceof external_exports.ZodError || err instanceof SyntaxError)) {
34763
+ console.error(
34764
+ `ed-mcp version check failed: ${err instanceof Error ? err.message : err}`
34765
+ );
34766
+ }
34767
+ return false;
34768
+ }
34769
+ ).then((ok2) => {
34770
+ this.inFlight = false;
34771
+ if (this.stopped) return;
34772
+ clearTimeout(this.timer);
34773
+ this.timer = setTimeout(
34774
+ () => {
34775
+ this.timer = void 0;
34776
+ this.run();
34777
+ },
34778
+ ok2 ? DAY_MS : RETRY_MS
34779
+ );
34780
+ this.timer.unref();
34781
+ });
34782
+ }
34783
+ async read() {
34784
+ try {
34785
+ return cacheSchema.parse(JSON.parse(await readFile6(this.file, "utf8")));
34786
+ } catch {
34787
+ return null;
34788
+ }
34789
+ }
34790
+ // A clock set back would keep a stale entry fresh for too long, so a future time counts as old.
34791
+ isStale(cached2) {
34792
+ if (!cached2) return true;
34793
+ const age = this.now() - cached2.checked_at;
34794
+ return age < 0 || age >= DAY_MS;
34795
+ }
34796
+ async check() {
34797
+ if (!this.isStale(await this.read())) return;
34798
+ const release = releaseSchema.parse(await this.opts.http.json(RELEASE_URL));
34799
+ const entry2 = {
34800
+ checked_at: this.now(),
34801
+ latest: release.tag_name
34802
+ };
34803
+ await lockedWrite(
34804
+ this.file,
34805
+ { timeoutMs: 2e3, lockedMessage: "version-check.json is locked" },
34806
+ async () => ({ content: JSON.stringify(entry2), result: void 0 })
34807
+ );
34808
+ }
34809
+ };
34810
+
33603
34811
  // src/deps.ts
34812
+ var envKey = (c) => c ? { apiKey: c.apiKey, cmdr: c.cmdr } : void 0;
33604
34813
  function buildDeps(config2, opts = {}) {
33605
34814
  const caches = new CacheRegistry();
33606
34815
  const http = new HttpClient({ userAgent: config2.userAgent, fetchFn: opts.fetchFn });
33607
34816
  const catalog = new CatalogService(http, config2.cacheDir);
33608
34817
  const ships2 = new ShipDataService({ cacheDir: config2.cacheDir, http });
34818
+ const sync = config2.syncDir ? new SyncFolder(config2.syncDir) : void 0;
34819
+ const guardFor = (file2) => sync?.contains(file2) ? () => sync.check() : void 0;
34820
+ const lockFor = (file2) => sync?.contains(file2) ? config2.locksDir : void 0;
34821
+ const secrets = new SecretStore(config2.secretsFile, {
34822
+ password: config2.syncPassword,
34823
+ lockDir: lockFor(config2.secretsFile),
34824
+ guard: guardFor(config2.secretsFile)
34825
+ });
34826
+ const settings = new Settings(
34827
+ { inara: envKey(config2.inara), edsm: envKey(config2.edsm) },
34828
+ config2.inaraApp,
34829
+ secrets
34830
+ );
34831
+ const plans = new PlanStore(config2.plansFile, void 0, {
34832
+ lockDir: lockFor(config2.plansFile),
34833
+ guard: guardFor(config2.plansFile),
34834
+ markdownDir: config2.plansMarkdownDir
34835
+ });
34836
+ const legacyPlans = config2.legacyPlansEnv ? `ED_MCP_PLANS is no longer used. Plans are now in ${config2.plansFile}. Move your old plans file there, or use plan_import, then remove ED_MCP_PLANS.` : void 0;
34837
+ const syncWarnings = sync ? () => sync.warnings([config2.plansFile, config2.prefsFile, config2.secretsFile]) : void 0;
33609
34838
  const inaraCache = new TtlCache("inara", 6e4);
33610
34839
  caches.register(inaraCache);
33611
- const inara = new InaraClient(http, config2.inara, inaraCache);
34840
+ const inara = new InaraClient(http, () => settings.inara(), inaraCache);
33612
34841
  const edsmCache = new TtlCache("edsm", 6e4);
33613
34842
  caches.register(edsmCache);
33614
- const edsm = new EdsmClient(http, config2.edsm, edsmCache);
34843
+ const edsm = new EdsmClient(http, () => settings.edsm(), edsmCache);
33615
34844
  const spanshCache = new TtlCache("spansh", 3e5);
33616
34845
  caches.register(spanshCache);
33617
34846
  const spanshSystemCache = new TtlCache("spansh-systems", 864e5);
33618
34847
  caches.register(spanshSystemCache);
33619
34848
  const spanshMarketCache = new TtlCache("spansh-markets", 6e5);
33620
34849
  caches.register(spanshMarketCache);
34850
+ const spanshSweepCache = new SweepCache();
34851
+ caches.register(spanshSweepCache);
33621
34852
  const spansh = new SpanshClient(http, spanshCache, {
33622
34853
  systemCache: spanshSystemCache,
33623
- marketCache: spanshMarketCache
34854
+ marketCache: spanshMarketCache,
34855
+ sweepCache: spanshSweepCache
33624
34856
  });
33625
34857
  const capiHttp = new HttpClient({ userAgent: `EDCD-EdMcp-${VERSION}`, fetchFn: opts.fetchFn });
33626
34858
  const capiCache = new TtlCache("capi", 6e4);
33627
34859
  caches.register(capiCache);
33628
- const capiAuth = new CapiAuth(
33629
- capiHttp,
33630
- config2.capi,
33631
- new TokenStore(path10.join(config2.configDir, "capi-token.json")),
33632
- { onSessionChange: () => void capiCache.clear() }
33633
- );
34860
+ const capiAuth = new CapiAuth(capiHttp, config2.capi, new TokenStore(secrets), {
34861
+ onSessionChange: () => void capiCache.clear()
34862
+ });
33634
34863
  const capi = new CapiClient(capiHttp, capiAuth, capiCache);
33635
34864
  const journal = opts.journal ?? new JournalService(config2.journalDir);
33636
34865
  return {
@@ -33650,8 +34879,30 @@ function buildDeps(config2, opts = {}) {
33650
34879
  },
33651
34880
  jumpRange: async () => (await journal.freshShip())?.max_jump_range ?? null,
33652
34881
  journal,
33653
- plans: new PlanStore(config2.plansFile),
33654
- prefs: new PreferencesStore(path10.join(config2.configDir, "preferences.json"))
34882
+ settings,
34883
+ secrets,
34884
+ settingsPage: new SettingsPage(
34885
+ { secrets, settings },
34886
+ { inSyncFolder: !!sync?.contains(config2.secretsFile) }
34887
+ ),
34888
+ plans,
34889
+ planWarnings: async () => {
34890
+ const problem = await plans.markdownProblem();
34891
+ return [
34892
+ ...await syncWarnings?.() ?? [],
34893
+ ...problem ? [problem] : [],
34894
+ ...legacyPlans ? [legacyPlans] : []
34895
+ ];
34896
+ },
34897
+ prefs: new PreferencesStore(config2.prefsFile, {
34898
+ lockDir: lockFor(config2.prefsFile),
34899
+ guard: guardFor(config2.prefsFile)
34900
+ }),
34901
+ sync,
34902
+ syncWarnings,
34903
+ versionCheck: new VersionCheck({ http, cacheDir: config2.cacheDir, current: VERSION }),
34904
+ notes: legacyPlans ? [legacyPlans] : [],
34905
+ localDir: config2.configDir
33655
34906
  };
33656
34907
  }
33657
34908
 
@@ -34029,8 +35280,8 @@ function getErrorMap2() {
34029
35280
 
34030
35281
  // node_modules/zod/v3/helpers/parseUtil.js
34031
35282
  var makeIssue = (params) => {
34032
- const { data, path: path12, errorMaps, issueData } = params;
34033
- const fullPath = [...path12, ...issueData.path || []];
35283
+ const { data, path: path15, errorMaps, issueData } = params;
35284
+ const fullPath = [...path15, ...issueData.path || []];
34034
35285
  const fullIssue = {
34035
35286
  ...issueData,
34036
35287
  path: fullPath
@@ -34145,11 +35396,11 @@ var errorUtil;
34145
35396
 
34146
35397
  // node_modules/zod/v3/types.js
34147
35398
  var ParseInputLazyPath = class {
34148
- constructor(parent, value, path12, key) {
35399
+ constructor(parent, value, path15, key) {
34149
35400
  this._cachedPath = [];
34150
35401
  this.parent = parent;
34151
35402
  this.data = value;
34152
- this._path = path12;
35403
+ this._path = path15;
34153
35404
  this._key = key;
34154
35405
  }
34155
35406
  get path() {
@@ -36161,10 +37412,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
36161
37412
  catchall: index
36162
37413
  });
36163
37414
  }
36164
- pick(mask) {
37415
+ pick(mask2) {
36165
37416
  const shape = {};
36166
- for (const key of util.objectKeys(mask)) {
36167
- if (mask[key] && this.shape[key]) {
37417
+ for (const key of util.objectKeys(mask2)) {
37418
+ if (mask2[key] && this.shape[key]) {
36168
37419
  shape[key] = this.shape[key];
36169
37420
  }
36170
37421
  }
@@ -36173,10 +37424,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
36173
37424
  shape: () => shape
36174
37425
  });
36175
37426
  }
36176
- omit(mask) {
37427
+ omit(mask2) {
36177
37428
  const shape = {};
36178
37429
  for (const key of util.objectKeys(this.shape)) {
36179
- if (!mask[key]) {
37430
+ if (!mask2[key]) {
36180
37431
  shape[key] = this.shape[key];
36181
37432
  }
36182
37433
  }
@@ -36191,11 +37442,11 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
36191
37442
  deepPartial() {
36192
37443
  return deepPartialify(this);
36193
37444
  }
36194
- partial(mask) {
37445
+ partial(mask2) {
36195
37446
  const newShape = {};
36196
37447
  for (const key of util.objectKeys(this.shape)) {
36197
37448
  const fieldSchema = this.shape[key];
36198
- if (mask && !mask[key]) {
37449
+ if (mask2 && !mask2[key]) {
36199
37450
  newShape[key] = fieldSchema;
36200
37451
  } else {
36201
37452
  newShape[key] = fieldSchema.optional();
@@ -36206,10 +37457,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
36206
37457
  shape: () => newShape
36207
37458
  });
36208
37459
  }
36209
- required(mask) {
37460
+ required(mask2) {
36210
37461
  const newShape = {};
36211
37462
  for (const key of util.objectKeys(this.shape)) {
36212
- if (mask && !mask[key]) {
37463
+ if (mask2 && !mask2[key]) {
36213
37464
  newShape[key] = this.shape[key];
36214
37465
  } else {
36215
37466
  const fieldSchema = this.shape[key];
@@ -37700,11 +38951,11 @@ function normalizeObjectSchema(schema) {
37700
38951
  }
37701
38952
  return void 0;
37702
38953
  }
37703
- function getDotPath(path12) {
37704
- if (path12.length === 0) {
38954
+ function getDotPath(path15) {
38955
+ if (path15.length === 0) {
37705
38956
  return "object root";
37706
38957
  }
37707
- return path12.reduce((acc, seg, index) => {
38958
+ return path15.reduce((acc, seg, index) => {
37708
38959
  if (index === 0) {
37709
38960
  return String(seg);
37710
38961
  }
@@ -39697,7 +40948,7 @@ var Protocol = class {
39697
40948
  }
39698
40949
  };
39699
40950
  }
39700
- const cancel = (reason) => {
40951
+ const cancel = (reason2) => {
39701
40952
  this._responseHandlers.delete(messageId);
39702
40953
  this._progressHandlers.delete(messageId);
39703
40954
  this._cleanupTimeout(messageId);
@@ -39706,10 +40957,10 @@ var Protocol = class {
39706
40957
  method: "notifications/cancelled",
39707
40958
  params: {
39708
40959
  requestId: messageId,
39709
- reason: String(reason)
40960
+ reason: String(reason2)
39710
40961
  }
39711
40962
  }, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error63) => this._onerror(new Error(`Failed to send cancellation: ${error63}`)));
39712
- const error62 = reason instanceof McpError ? reason : new McpError(ErrorCode.RequestTimeout, String(reason));
40963
+ const error62 = reason2 instanceof McpError ? reason2 : new McpError(ErrorCode.RequestTimeout, String(reason2));
39713
40964
  reject(error62);
39714
40965
  };
39715
40966
  this._responseHandlers.set(messageId, (response) => {
@@ -40907,6 +42158,15 @@ var McpServer = class {
40907
42158
  this._promptHandlersInitialized = false;
40908
42159
  this.server = new Server(serverInfo, options);
40909
42160
  this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
42161
+ if (options?.capabilities?.tools) {
42162
+ this.setToolRequestHandlers();
42163
+ }
42164
+ if (options?.capabilities?.resources) {
42165
+ this.setResourceRequestHandlers();
42166
+ }
42167
+ if (options?.capabilities?.prompts) {
42168
+ this.setPromptRequestHandlers();
42169
+ }
40910
42170
  }
40911
42171
  /**
40912
42172
  * Access experimental features.
@@ -42234,10 +43494,10 @@ function slotInfo(hull, slot, item) {
42234
43494
  return { slot, kind: kind2, size };
42235
43495
  }
42236
43496
  function bulkheadFromItem(hull, item) {
42237
- const lower = item.toLowerCase();
43497
+ const lower2 = item.toLowerCase();
42238
43498
  const offset = hull.bulkheads.length > 5 ? 1 : 0;
42239
- if (lower.endsWith("_armour_grade1_default")) return hull.bulkheads[0] ?? null;
42240
- const i = BULKHEAD_SUFFIXES.findIndex((s) => lower.endsWith(`_armour_${s}`));
43499
+ if (lower2.endsWith("_armour_grade1_default")) return hull.bulkheads[0] ?? null;
43500
+ const i = BULKHEAD_SUFFIXES.findIndex((s) => lower2.endsWith(`_armour_${s}`));
42241
43501
  return i < 0 ? null : hull.bulkheads[i + offset] ?? null;
42242
43502
  }
42243
43503
  function bulkheadItem(hull, module) {
@@ -43002,6 +44262,27 @@ function wrap(fn) {
43002
44262
  }
43003
44263
  };
43004
44264
  }
44265
+ function wrapWarn(fn, warnings) {
44266
+ const inner = wrap(fn);
44267
+ return async (args) => {
44268
+ const result = await inner(args);
44269
+ const list2 = warnings ? await warnings().catch(() => []) : [];
44270
+ if (!list2.length) return result;
44271
+ return {
44272
+ ...result,
44273
+ content: [
44274
+ ...result.content,
44275
+ { type: "text", text: `Warnings:
44276
+ ${list2.map((w) => `- ${w}`).join("\n")}` }
44277
+ ]
44278
+ };
44279
+ };
44280
+ }
44281
+ function reason(err) {
44282
+ if (err instanceof UserError) return err.message;
44283
+ console.error(err);
44284
+ return err instanceof Error ? err.message : String(err);
44285
+ }
43005
44286
 
43006
44287
  // src/tools/build.ts
43007
44288
  var SCO_NOTE = "Jump range uses the drive's normal values, not supercruise overcharge.";
@@ -43028,6 +44309,13 @@ function registerBuildStatsTool(server, deps) {
43028
44309
  server.registerTool(
43029
44310
  "build_stats",
43030
44311
  {
44312
+ annotations: {
44313
+ title: "Build Stats",
44314
+ readOnlyHint: true,
44315
+ destructiveHint: false,
44316
+ idempotentHint: true,
44317
+ openWorldHint: true
44318
+ },
43031
44319
  description: "Model a ship build: mass, fuel, jump range, power, speed, shields, armour and weapon damage. The build comes from the latest journal Loadout (the default), the CAPI ship, a SLEF build or a plan's target_build. `changes` apply what-if changes in order: swap, engineer or remove modules. With changes, `diff` lists only the values that changed, and `build` is the changed build as SLEF. Save it with plan_upsert (target_build), in a file or in an artifact. Real builds use their real engineering values; engineering from changes assumes the best roll for the blueprint and grade.",
43032
44320
  inputSchema: {
43033
44321
  build: BuildInput.optional().describe("Where to load the build. Default: the journal."),
@@ -43166,12 +44454,12 @@ function diff(before, after) {
43166
44454
  const a = flatten(before, "", /* @__PURE__ */ new Map());
43167
44455
  const b = flatten(after, "", /* @__PURE__ */ new Map());
43168
44456
  const out = {};
43169
- for (const path12 of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) {
43170
- const x = a.get(path12) ?? null;
43171
- const y = b.get(path12) ?? null;
44457
+ for (const path15 of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) {
44458
+ const x = a.get(path15) ?? null;
44459
+ const y = b.get(path15) ?? null;
43172
44460
  if (x === y) continue;
43173
44461
  const change = typeof x === "number" && typeof y === "number" ? Math.round((y - x) * 1e4) / 1e4 : null;
43174
- out[path12] = { before: x, after: y, change };
44462
+ out[path15] = { before: x, after: y, change };
43175
44463
  }
43176
44464
  return out;
43177
44465
  }
@@ -43181,6 +44469,13 @@ function registerBuildTools(server, deps) {
43181
44469
  server.registerTool(
43182
44470
  "build_parse_slef",
43183
44471
  {
44472
+ annotations: {
44473
+ title: "Parse SLEF Build",
44474
+ readOnlyHint: true,
44475
+ destructiveHint: false,
44476
+ idempotentHint: true,
44477
+ openWorldHint: true
44478
+ },
43184
44479
  description: "Parse a SLEF ship build (export from EDSY, Coriolis or Inara) into slots, modules, blueprints, grades and experimental effects. Module names, class and rating are resolved from the game data catalog when available. Accepts SLEF JSON only. An EDSY link (edsy.org/#/L=...) is not SLEF: open it in a browser, click Export..., read the export_slef text input and pass that value, or ask the user to paste the SLEF.",
43185
44480
  inputSchema: { slef: external_exports.string().describe("SLEF JSON text") }
43186
44481
  },
@@ -43230,6 +44525,13 @@ function registerCacheTools(server, deps) {
43230
44525
  server.registerTool(
43231
44526
  "cache_refresh",
43232
44527
  {
44528
+ annotations: {
44529
+ title: "Refresh Cache",
44530
+ readOnlyHint: false,
44531
+ destructiveHint: false,
44532
+ idempotentHint: true,
44533
+ openWorldHint: true
44534
+ },
43233
44535
  description: "Clear cached data so the next call fetches fresh data. source: a source name or 'all' (default). For 'catalog' and 'ships' this also checks upstream for new game data.",
43234
44536
  inputSchema: { source: external_exports.string().optional() }
43235
44537
  },
@@ -43265,6 +44567,13 @@ function registerCacheTools(server, deps) {
43265
44567
  server.registerTool(
43266
44568
  "cache_status",
43267
44569
  {
44570
+ annotations: {
44571
+ title: "Cache Status",
44572
+ readOnlyHint: true,
44573
+ destructiveHint: false,
44574
+ idempotentHint: true,
44575
+ openWorldHint: false
44576
+ },
43268
44577
  description: "Show cached entries and their age per source, and the catalog and ship data versions."
43269
44578
  },
43270
44579
  wrap(async () => ({
@@ -43279,6 +44588,13 @@ function registerCapiTools(server, deps) {
43279
44588
  server.registerTool(
43280
44589
  "capi_login",
43281
44590
  {
44591
+ annotations: {
44592
+ title: "Log In to Frontier",
44593
+ readOnlyHint: false,
44594
+ destructiveHint: false,
44595
+ idempotentHint: false,
44596
+ openWorldHint: true
44597
+ },
43282
44598
  description: "Log in to Frontier's Companion API in the browser. Returns immediately with the login URL; the user finishes in the browser. Call again to check the state; pass force: true to log in again. Logins last 25 days.",
43283
44599
  inputSchema: { force: external_exports.boolean().optional() }
43284
44600
  },
@@ -43294,13 +44610,29 @@ function registerCapiTools(server, deps) {
43294
44610
  server.registerTool(
43295
44611
  "capi_profile",
43296
44612
  {
44613
+ annotations: {
44614
+ title: "Frontier Profile",
44615
+ readOnlyHint: true,
44616
+ destructiveHint: false,
44617
+ idempotentHint: true,
44618
+ openWorldHint: true
44619
+ },
43297
44620
  description: "Live commander data from Frontier: current system and docked station, current ship with every module and its engineering (blueprint, grade, engineer, experimental), and all owned ships with locations. Does not include materials (use edsm_commander_materials). Cached 60 s."
43298
44621
  },
43299
44622
  wrap(async () => deps.capi.profile())
43300
44623
  );
43301
44624
  server.registerTool(
43302
44625
  "capi_logout",
43303
- { description: "Forget the stored Frontier login on this machine." },
44626
+ {
44627
+ annotations: {
44628
+ title: "Log Out of Frontier",
44629
+ readOnlyHint: false,
44630
+ destructiveHint: true,
44631
+ idempotentHint: true,
44632
+ openWorldHint: false
44633
+ },
44634
+ description: "Forget the stored Frontier login. With a sync password, this logs out every computer that shares the sync folder."
44635
+ },
43304
44636
  wrap(async () => {
43305
44637
  await deps.capiAuth.logout();
43306
44638
  return { state: "logged_out" };
@@ -43314,6 +44646,13 @@ function registerCatalogTools(server, deps) {
43314
44646
  server.registerTool(
43315
44647
  "catalog_blueprint",
43316
44648
  {
44649
+ annotations: {
44650
+ title: "Look Up Blueprint",
44651
+ readOnlyHint: true,
44652
+ destructiveHint: false,
44653
+ idempotentHint: true,
44654
+ openWorldHint: true
44655
+ },
43317
44656
  description: "Engineering blueprint by journal id (e.g. FSD_LongRange) or name (e.g. 'Increased range'): materials per grade 1-5 with material symbol, grade and trade group, and the experimental effects available for the module. Give module when the name is shared by several modules.",
43318
44657
  inputSchema: { name: external_exports.string(), module: external_exports.string().optional() }
43319
44658
  },
@@ -43322,6 +44661,13 @@ function registerCatalogTools(server, deps) {
43322
44661
  server.registerTool(
43323
44662
  "catalog_engineers",
43324
44663
  {
44664
+ annotations: {
44665
+ title: "Look Up Engineers",
44666
+ readOnlyHint: true,
44667
+ destructiveHint: false,
44668
+ idempotentHint: true,
44669
+ openWorldHint: true
44670
+ },
43325
44671
  description: "With blueprint (and optional module): engineers who apply it, their max grade, location and invite/unlock requirements, best first. With engineer: that engineer's location, requirements and every blueprint they offer.",
43326
44672
  inputSchema: {
43327
44673
  blueprint: external_exports.string().optional(),
@@ -43342,6 +44688,13 @@ function registerCatalogTools(server, deps) {
43342
44688
  server.registerTool(
43343
44689
  "catalog_material",
43344
44690
  {
44691
+ annotations: {
44692
+ title: "Look Up Material",
44693
+ readOnlyHint: true,
44694
+ destructiveHint: false,
44695
+ idempotentHint: true,
44696
+ openWorldHint: true
44697
+ },
43345
44698
  description: "Engineering material by name or symbol: kind, grade, trade group, other materials in the same trade group, and material trader exchange rules.",
43346
44699
  inputSchema: { name: external_exports.string() }
43347
44700
  },
@@ -43350,6 +44703,13 @@ function registerCatalogTools(server, deps) {
43350
44703
  server.registerTool(
43351
44704
  "catalog_module",
43352
44705
  {
44706
+ annotations: {
44707
+ title: "Look Up Module",
44708
+ readOnlyHint: true,
44709
+ destructiveHint: false,
44710
+ idempotentHint: true,
44711
+ openWorldHint: true
44712
+ },
43353
44713
  description: "Outfitting module by journal symbol (e.g. Int_Hyperdrive_Size5_Class5) or name: class, rating, mount and engineering group. Returns total matches and at most 20 items.",
43354
44714
  inputSchema: { query: external_exports.string() }
43355
44715
  },
@@ -43601,13 +44961,13 @@ function resolveRules(stored, override, usePreferences, auto, jumpAuto = { ly: n
43601
44961
  const prefs = applyPatch(usePreferences ? stored : OFF_PREFS, override ?? {});
43602
44962
  const p = prefs.stations.min_pad;
43603
44963
  const fromRequest = override?.stations?.min_pad !== void 0;
43604
- const pad2 = p === "auto" ? auto : p === "none" ? { size: null, source: fromRequest ? "request" : "none" } : { size: p, source: fromRequest ? "request" : "preferences" };
44964
+ const pad = p === "auto" ? auto : p === "none" ? { size: null, source: fromRequest ? "request" : "none" } : { size: p, source: fromRequest ? "request" : "preferences" };
43605
44965
  const range = prefs.travel.jump_range_ly;
43606
44966
  const fromRequestRange = override?.travel?.jump_range_ly !== void 0;
43607
44967
  const { ly, source } = range !== "auto" ? { ly: range, source: fromRequestRange ? "request" : "preferences" } : jumpAuto.ly ? { ly: jumpAuto.ly, source: jumpAuto.source } : { ly: 30, source: "auto: default 30 LY" };
43608
44968
  return {
43609
44969
  prefs,
43610
- pad: pad2,
44970
+ pad,
43611
44971
  travel: { ...prefs.travel, jump_range_ly: ly },
43612
44972
  jumpRangeSource: source
43613
44973
  };
@@ -43760,23 +45120,23 @@ async function screenPermits(a) {
43760
45120
  if (kept.length >= a.limit) break;
43761
45121
  const refs = a.systemsOf(item).filter((r) => !exempt.has(r.name.toLowerCase()));
43762
45122
  let needsPermit = false;
43763
- let open3 = true;
45123
+ let open4 = true;
43764
45124
  let missed = false;
43765
45125
  for (const ref of refs) {
43766
45126
  if (!lookup2.has(ref.name)) stop ??= stopReason();
43767
45127
  if (stop && !lookup2.has(ref.name)) {
43768
45128
  missed = true;
43769
- open3 = false;
45129
+ open4 = false;
43770
45130
  continue;
43771
45131
  }
43772
45132
  const permit = await within(lookup2.check(ref), deadline - now());
43773
45133
  if (permit === TIMED_OUT) {
43774
45134
  stop = "time limit";
43775
45135
  missed = true;
43776
- open3 = false;
45136
+ open4 = false;
43777
45137
  continue;
43778
45138
  }
43779
- if (permit === null) open3 = false;
45139
+ if (permit === null) open4 = false;
43780
45140
  if (permit === true) {
43781
45141
  needsPermit = true;
43782
45142
  if (!locked.has(ref.name.toLowerCase())) locked.set(ref.name.toLowerCase(), ref.name);
@@ -43793,7 +45153,7 @@ async function screenPermits(a) {
43793
45153
  }
43794
45154
  } else {
43795
45155
  kept.push(item);
43796
- if (open3) checkedOpen.push(item);
45156
+ if (open4) checkedOpen.push(item);
43797
45157
  }
43798
45158
  }
43799
45159
  const notes = [];
@@ -43868,20 +45228,20 @@ function credits(n) {
43868
45228
  function recommendStation(ranked, t) {
43869
45229
  const best = ranked[0];
43870
45230
  if (!best) return null;
43871
- let reason;
45231
+ let reason2;
43872
45232
  if (best.effective_cost === null) {
43873
- reason = `fastest, about ${best.travel_min} min`;
45233
+ reason2 = `fastest, about ${best.travel_min} min`;
43874
45234
  } else {
43875
45235
  const fastest = [...ranked].sort((a, b) => a.travel_s - b.travel_s)[0];
43876
45236
  if (fastest !== best && fastest.price && best.price && fastest.price > best.price) {
43877
45237
  const extra = best.travel_s - fastest.travel_s;
43878
45238
  const more = extra < 60 ? `${extra} more seconds` : `${(extra / 60).toFixed(1)} more minutes`;
43879
- reason = `${credits(fastest.price - best.price)} cr cheaper than ${fastest.name} (${fastest.system}) for ${more} at ${credits(t.credits_per_hour)} cr/h`;
45239
+ reason2 = `${credits(fastest.price - best.price)} cr cheaper than ${fastest.name} (${fastest.system}) for ${more} at ${credits(t.credits_per_hour)} cr/h`;
43880
45240
  } else {
43881
- reason = `lowest cost including travel time, about ${best.travel_min} min`;
45241
+ reason2 = `lowest cost including travel time, about ${best.travel_min} min`;
43882
45242
  }
43883
45243
  }
43884
- return { name: best.name, system: best.system, reason };
45244
+ return { name: best.name, system: best.system, reason: reason2 };
43885
45245
  }
43886
45246
 
43887
45247
  // src/stations/search.ts
@@ -43952,20 +45312,20 @@ async function searchStations(spansh, query, rules, limit, permits = {}) {
43952
45312
  spansh,
43953
45313
  ...permits
43954
45314
  });
43955
- const page = screen.kept.map(
45315
+ const page2 = screen.kept.map(
43956
45316
  (h) => screen.locked(h.system) ? { ...h, needs_permit: true } : h
43957
45317
  );
43958
- const open3 = mode === "exclude" ? ranked.filter((h) => !screen.locked(h.system)) : ranked;
45318
+ const open4 = mode === "exclude" ? ranked.filter((h) => !screen.locked(h.system)) : ranked;
43959
45319
  const recommended = screen.checkedOpen.length ? recommendStation(screen.checkedOpen, rules.travel) : null;
43960
45320
  return {
43961
45321
  near: source.near,
43962
45322
  total: source.total,
43963
- matched: open3.length,
45323
+ matched: open4.length,
43964
45324
  total_capped: source.total_capped,
43965
- stations: page,
45325
+ stations: page2,
43966
45326
  recommended,
43967
45327
  // Only the returned page counts, so every listed rule is broken by a station shown.
43968
- relaxed: relaxAttempted ? brokenRules(page, rules) : [],
45328
+ relaxed: relaxAttempted ? brokenRules(page2, rules) : [],
43969
45329
  relax_attempted: relaxAttempted,
43970
45330
  applied_preferences: describeRules(rules),
43971
45331
  notes: screen.notes
@@ -43981,11 +45341,11 @@ function union2(a, b) {
43981
45341
  async function resolveSearchRules(deps, preferences, usePreferences, largePad) {
43982
45342
  const stored = deps.prefs && usePreferences ? await deps.prefs.get() : DEFAULT_PREFS;
43983
45343
  const override = largePad ? { ...preferences, stations: { ...preferences?.stations, min_pad: "L" } } : preferences ?? {};
43984
- let pad2 = { size: null, source: "none" };
45344
+ let pad = { size: null, source: "none" };
43985
45345
  let jump2;
43986
- const first = resolveRules(stored, override, usePreferences, pad2);
45346
+ const first = resolveRules(stored, override, usePreferences, pad);
43987
45347
  if (first.prefs.stations.min_pad === "auto") {
43988
- pad2 = await autoPad(deps.currentShip ?? (async () => null));
45348
+ pad = await autoPad(deps.currentShip ?? (async () => null));
43989
45349
  }
43990
45350
  if (first.prefs.travel.jump_range_ly === "auto") {
43991
45351
  const max = await deps.jumpRange?.().catch(() => null);
@@ -43997,7 +45357,7 @@ async function resolveSearchRules(deps, preferences, usePreferences, largePad) {
43997
45357
  };
43998
45358
  }
43999
45359
  }
44000
- return resolveRules(stored, override, usePreferences, pad2, jump2);
45360
+ return resolveRules(stored, override, usePreferences, pad, jump2);
44001
45361
  }
44002
45362
 
44003
45363
  // src/tools/materials.ts
@@ -44006,6 +45366,13 @@ function registerMaterialsTools(server, deps) {
44006
45366
  server.registerTool(
44007
45367
  "materials_plan",
44008
45368
  {
45369
+ annotations: {
45370
+ title: "Plan Materials",
45371
+ readOnlyHint: true,
45372
+ destructiveHint: false,
45373
+ idempotentHint: true,
45374
+ openWorldHint: true
45375
+ },
44009
45376
  description: "Plan engineering materials: needs for blueprints (grades, rolls, experimentals) and extra materials, compared with the commander's inventory (journal or EDSM), trades from surplus at exact material trader rates, what is still short, and the best trader of each type near the commander (travel preferences apply).",
44010
45377
  inputSchema: {
44011
45378
  blueprints: external_exports.array(
@@ -44181,7 +45548,7 @@ function legBetween(from, to, travel) {
44181
45548
  if (same(from.system, to.system) && same(from.station, to.station)) {
44182
45549
  return { jumps: 0, seconds: 0, distanceLy: 0 };
44183
45550
  }
44184
- const distanceLy = same(from.system, to.system) ? 0 : Math.hypot(
45551
+ const distanceLy2 = same(from.system, to.system) ? 0 : Math.hypot(
44185
45552
  from.coords.x - to.coords.x,
44186
45553
  from.coords.y - to.coords.y,
44187
45554
  from.coords.z - to.coords.z
@@ -44189,14 +45556,14 @@ function legBetween(from, to, travel) {
44189
45556
  return {
44190
45557
  ...legSeconds(
44191
45558
  {
44192
- distanceLy,
45559
+ distanceLy: distanceLy2,
44193
45560
  arrivalLs: to.arrivalLs,
44194
45561
  planetary: to.planetary,
44195
45562
  stationKnown: to.station !== null
44196
45563
  },
44197
45564
  travel
44198
45565
  ),
44199
- distanceLy
45566
+ distanceLy: distanceLy2
44200
45567
  };
44201
45568
  }
44202
45569
 
@@ -44207,17 +45574,17 @@ var better = (l1, t1, l2, t2) => l1 < l2 || l1 === l2 && t1 < t2;
44207
45574
  var lateAt = (deadlines, t) => deadlines.filter((d) => t > d).length;
44208
45575
  function findCycle(after) {
44209
45576
  const state = [];
44210
- const path12 = [];
45577
+ const path15 = [];
44211
45578
  const visit2 = (i) => {
44212
45579
  if (state[i] === 2) return null;
44213
- if (state[i] === 1) return path12.slice(path12.indexOf(i));
45580
+ if (state[i] === 1) return path15.slice(path15.indexOf(i));
44214
45581
  state[i] = 1;
44215
- path12.push(i);
45582
+ path15.push(i);
44216
45583
  for (const j of after[i] ?? []) {
44217
45584
  const cycle = visit2(j);
44218
45585
  if (cycle) return cycle;
44219
45586
  }
44220
- path12.pop();
45587
+ path15.pop();
44221
45588
  state[i] = 2;
44222
45589
  return null;
44223
45590
  };
@@ -44296,14 +45663,14 @@ function exactOrder(p) {
44296
45663
  parent: null
44297
45664
  });
44298
45665
  }
44299
- for (let mask = 1; mask < 1 << n; mask++) {
45666
+ for (let mask2 = 1; mask2 < 1 << n; mask2++) {
44300
45667
  for (let last = 0; last < n; last++) {
44301
- for (const label of fronts[mask][last]) {
45668
+ for (const label of fronts[mask2][last]) {
44302
45669
  for (let next = 0; next < n; next++) {
44303
- if (mask & 1 << next || (needs[next] & mask) !== needs[next]) continue;
45670
+ if (mask2 & 1 << next || (needs[next] & mask2) !== needs[next]) continue;
44304
45671
  const time3 = label.time + between[last][next];
44305
45672
  const late2 = label.late + lateAt(p.deadlines[next] ?? [], time3);
44306
- insert(fronts[mask | 1 << next][next], { late: late2, time: time3, last: next, parent: label });
45673
+ insert(fronts[mask2 | 1 << next][next], { late: late2, time: time3, last: next, parent: label });
44307
45674
  }
44308
45675
  }
44309
45676
  }
@@ -44427,6 +45794,13 @@ function registerMissionTools(server, deps) {
44427
45794
  server.registerTool(
44428
45795
  "missions_route",
44429
45796
  {
45797
+ annotations: {
45798
+ title: "Plan Mission Route",
45799
+ readOnlyHint: true,
45800
+ destructiveHint: false,
45801
+ idempotentHint: true,
45802
+ openWorldHint: true
45803
+ },
44430
45804
  description: "Order the stops for accepted missions (from the game journal): passengers, deliveries and hand-ins for completed missions. Uses Spansh coordinates and the travel-time model to minimise time while meeting deadlines; lists late and unroutable missions. The order is exact up to 12 stops and 24 missions with an expiry; above that it is a nearest-first heuristic and deadline handling is best-effort. Planetary stations are inferred from the Spansh station type. `from` and `return_to` must be systems Spansh knows. The first call for systems not seen in the last day can take tens of seconds, because Spansh lookups are rate limited.",
44431
45805
  inputSchema: {
44432
45806
  from: external_exports.string().optional().describe("Start system; default the journal location"),
@@ -44479,8 +45853,8 @@ function registerMissionTools(server, deps) {
44479
45853
  places.push(await place2(stop.system, stop.station));
44480
45854
  stops.push(stop);
44481
45855
  } catch (e) {
44482
- const reason = e instanceof UserError ? e.message : `Spansh lookup failed: ${e instanceof Error ? e.message : String(e)}`;
44483
- for (const m of stop.missions) unroutable.push({ id: m.id, name: m.name, reason });
45856
+ const reason2 = e instanceof UserError ? e.message : `Spansh lookup failed: ${e instanceof Error ? e.message : String(e)}`;
45857
+ for (const m of stop.missions) unroutable.push({ id: m.id, name: m.name, reason: reason2 });
44484
45858
  }
44485
45859
  }
44486
45860
  const returnPlace = a.return_to ? await endpoint("return_to", a.return_to.system, a.return_to.station ?? null) : void 0;
@@ -44570,13 +45944,31 @@ function registerJournalTools(server, deps) {
44570
45944
  server.registerTool(
44571
45945
  "journal_status",
44572
45946
  {
44573
- description: "Check the Elite Dangerous journal folder: whether it exists, the newest journal and how old its last event is, the commander and game version. Use it first when journal data looks wrong or out of date; warnings explain missing folders and stalled Syncthing sync."
45947
+ annotations: {
45948
+ title: "Journal Status",
45949
+ readOnlyHint: true,
45950
+ destructiveHint: false,
45951
+ idempotentHint: true,
45952
+ openWorldHint: false
45953
+ },
45954
+ description: "Check the Elite Dangerous journal folder: whether it exists, the newest journal and how old its last event is, the commander and game version. Use it first when journal data looks wrong or out of date; warnings explain missing folders and stalled Syncthing sync, and also mention a newer ed-mcp release when one is available."
44574
45955
  },
44575
- wrap(async () => deps.journal.status())
45956
+ wrap(async () => {
45957
+ const status = await deps.journal.status();
45958
+ const update = await deps.update?.();
45959
+ return update ? { ...status, warnings: [...status.warnings, update] } : status;
45960
+ })
44576
45961
  );
44577
45962
  server.registerTool(
44578
45963
  "journal_commander",
44579
45964
  {
45965
+ annotations: {
45966
+ title: "Journal Commander",
45967
+ readOnlyHint: true,
45968
+ destructiveHint: false,
45969
+ idempotentHint: true,
45970
+ openWorldHint: false
45971
+ },
44580
45972
  description: "The commander's current state from the local game journal: name, credits, location (system, station, docked), ship (type, name, jump range, cargo and fuel capacity) and cargo. " + FRESHNESS_TEXT
44581
45973
  },
44582
45974
  wrap(async () => {
@@ -44594,6 +45986,13 @@ function registerJournalTools(server, deps) {
44594
45986
  server.registerTool(
44595
45987
  "journal_materials",
44596
45988
  {
45989
+ annotations: {
45990
+ title: "Journal Materials",
45991
+ readOnlyHint: true,
45992
+ destructiveHint: false,
45993
+ idempotentHint: true,
45994
+ openWorldHint: false
45995
+ },
44597
45996
  description: "Engineering materials the commander holds, from the local game journal, by kind (raw, manufactured, encoded) with counts. Pass kinds to limit the result. " + FRESHNESS_TEXT,
44598
45997
  inputSchema: { kinds: external_exports.array(external_exports.enum(["raw", "manufactured", "encoded"])).optional() }
44599
45998
  },
@@ -44619,6 +46018,13 @@ function registerJournalTools(server, deps) {
44619
46018
  server.registerTool(
44620
46019
  "journal_missions",
44621
46020
  {
46021
+ annotations: {
46022
+ title: "Journal Missions",
46023
+ readOnlyHint: true,
46024
+ destructiveHint: false,
46025
+ idempotentHint: true,
46026
+ openWorldHint: false
46027
+ },
44622
46028
  description: "Accepted missions (active and complete) from the local game journal: destination, expiry, reward and passenger or cargo details, soonest expiry first. status is active or complete (objectives met, not handed in). origin_system and origin_station are where the mission was accepted (station is null when not docked). expired is true once the expiry has passed. details_missing marks missions accepted before the 14-day journal window (name and expiry only). " + FRESHNESS_TEXT
44623
46029
  },
44624
46030
  wrap(async () => {
@@ -44634,6 +46040,13 @@ function registerJournalTools(server, deps) {
44634
46040
  server.registerTool(
44635
46041
  "journal_engineers",
44636
46042
  {
46043
+ annotations: {
46044
+ title: "Journal Engineers",
46045
+ readOnlyHint: true,
46046
+ destructiveHint: false,
46047
+ idempotentHint: true,
46048
+ openWorldHint: false
46049
+ },
44637
46050
  description: "Engineer progress from the local game journal: invited, unlocked or known, with rank and rank progress. " + FRESHNESS_TEXT
44638
46051
  },
44639
46052
  wrap(async () => {
@@ -44853,6 +46266,13 @@ function registerLoopTools(server, deps) {
44853
46266
  server.registerTool(
44854
46267
  "trade_loops",
44855
46268
  {
46269
+ annotations: {
46270
+ title: "Find Trade Loops",
46271
+ readOnlyHint: true,
46272
+ destructiveHint: false,
46273
+ idempotentHint: true,
46274
+ openWorldHint: true
46275
+ },
44856
46276
  description: `Find repeatable trade loops: two or three stations, where a leg can run empty. start "here" (default) finds loops through your docked station; start "any" finds the best loops anywhere within radius_ly of near (default: where you are). Ranked by sustained profit per hour: thin markets that cannot refill a full hold every lap rank low. Uses Spansh market data (check each price's age). Cargo and capital default to the journal. Loops through permit-locked systems are left out unless permit_systems is allow. Legality is not checked.`,
44857
46277
  inputSchema: {
44858
46278
  start: external_exports.enum(["here", "any"]).default("here"),
@@ -45000,25 +46420,25 @@ function registerLoopTools(server, deps) {
45000
46420
  );
45001
46421
  if (!startMarket) {
45002
46422
  const info = await spansh.system(centre.system, STATION_LIMIT);
45003
- const entry = info.stations.find((st) => same2(st.name, name));
45004
- if (entry?.market_id == null) {
46423
+ const entry2 = info.stations.find((st) => same2(st.name, name));
46424
+ if (entry2?.market_id == null) {
45005
46425
  throw new UserError(`Spansh has no market for ${name} in ${info.name}.`);
45006
46426
  }
45007
- const id2 = entry.market_id;
46427
+ const id2 = entry2.market_id;
45008
46428
  const fresh = journalMarket?.market_id === id2 && localHours !== null && localHours <= a.max_price_age_days * 24;
45009
46429
  const rows = fresh ? renamedLocalRows() : null;
45010
46430
  startMarket = rows ? fromJournal2(
45011
46431
  {
45012
- name: entry.name,
46432
+ name: entry2.name,
45013
46433
  system: info.name,
45014
46434
  system_id64: info.id64,
45015
46435
  market_id: id2,
45016
46436
  distance_ly: null,
45017
- arrival_ls: entry.arrival_ls,
46437
+ arrival_ls: entry2.arrival_ls,
45018
46438
  coords: info.coords,
45019
- pad: entry.pad,
45020
- type: entry.type,
45021
- planetary: PLANETARY.test(entry.type ?? ""),
46439
+ pad: entry2.pad,
46440
+ type: entry2.type,
46441
+ planetary: PLANETARY.test(entry2.type ?? ""),
45022
46442
  market_age_hours: null,
45023
46443
  market: []
45024
46444
  },
@@ -45044,15 +46464,15 @@ function registerLoopTools(server, deps) {
45044
46464
  break;
45045
46465
  }
45046
46466
  let market = null;
45047
- let failure2 = "Spansh has no market for it.";
46467
+ let failure3 = "Spansh has no market for it.";
45048
46468
  try {
45049
46469
  market = await includeMarket(inc);
45050
46470
  } catch (err) {
45051
46471
  if (!(err instanceof UserError)) throw err;
45052
- failure2 = err.message;
46472
+ failure3 = err.message;
45053
46473
  }
45054
46474
  if (!market) {
45055
- notes.push(`Cannot include ${inc.station}: ${failure2}`);
46475
+ notes.push(`Cannot include ${inc.station}: ${failure3}`);
45056
46476
  } else if (!candidates.some((c) => c.market_id === market.market_id)) {
45057
46477
  candidates.push(market);
45058
46478
  }
@@ -45198,11 +46618,347 @@ function registerLoopTools(server, deps) {
45198
46618
  }
45199
46619
  }
45200
46620
 
46621
+ // src/systems/massacre.ts
46622
+ var SOURCE_RANGE_LY = 10;
46623
+ var NO_MASSACRES = /* @__PURE__ */ new Set(["war", "civil war", "election"]);
46624
+ function massacreTargets(systems, centre, opts) {
46625
+ const pirates = systems.flatMap(
46626
+ (s) => s.factions.filter(isAnarchy).map((f) => ({ faction: f.name, system: s }))
46627
+ );
46628
+ const piratesNear = /* @__PURE__ */ new Map();
46629
+ const piratesInRange = (s) => {
46630
+ let list2 = piratesNear.get(s.id64);
46631
+ if (!list2) {
46632
+ list2 = pirates.filter((p) => p.system.id64 !== s.id64).filter((p) => distanceLy(p.system.coords, s.coords) <= SOURCE_RANGE_LY).map((p) => ({ faction: p.faction, system: p.system.name }));
46633
+ piratesNear.set(s.id64, list2);
46634
+ }
46635
+ return list2;
46636
+ };
46637
+ const candidates = systems.filter((s) => s.stations.length > 0);
46638
+ const targets = [];
46639
+ let leftOutSpread = 0;
46640
+ for (const p of pirates) {
46641
+ const distance2 = distanceLy(p.system.coords, centre);
46642
+ if (distance2 > opts.radiusLy) continue;
46643
+ const spreadLs = Math.max(0, ...p.system.stars.map((s) => s.arrival_ls));
46644
+ if (spreadLs > opts.maxStarLs) {
46645
+ leftOutSpread++;
46646
+ continue;
46647
+ }
46648
+ const sources = [];
46649
+ const excluded = [];
46650
+ for (const s of candidates) {
46651
+ if (s.id64 === p.system.id64) continue;
46652
+ const toTarget = distanceLy(s.coords, p.system.coords);
46653
+ if (toTarget > SOURCE_RANGE_LY) continue;
46654
+ const blocked = s.factions.flatMap(
46655
+ (f) => f.active_states.filter((x) => NO_MASSACRES.has(x.toLowerCase())).map((state) => ({ system: s.name, faction: f.name, state, updated_at: s.updated_at }))
46656
+ );
46657
+ if (blocked.length) {
46658
+ excluded.push(...blocked);
46659
+ continue;
46660
+ }
46661
+ const others = piratesInRange(s).filter((x) => x.faction !== p.faction);
46662
+ sources.push({
46663
+ system: s,
46664
+ distanceToTargetLy: toTarget,
46665
+ primary: others.length === 0,
46666
+ otherPirates: others
46667
+ });
46668
+ }
46669
+ const primary = sources.filter((s) => s.primary);
46670
+ if (!primary.length) continue;
46671
+ const givers = new Set(
46672
+ primary.flatMap((s) => s.system.factions.filter((f) => !isAnarchy(f)).map((f) => f.name))
46673
+ );
46674
+ if (givers.size === 0) continue;
46675
+ sources.sort(
46676
+ (a, b) => Number(b.primary) - Number(a.primary) || a.distanceToTargetLy - b.distanceToTargetLy
46677
+ );
46678
+ targets.push({
46679
+ faction: p.faction,
46680
+ system: p.system,
46681
+ distanceLy: distance2,
46682
+ spreadLs,
46683
+ stackSize: givers.size,
46684
+ sources,
46685
+ excluded
46686
+ });
46687
+ }
46688
+ targets.sort(
46689
+ (a, b) => b.stackSize - a.stackSize || a.spreadLs - b.spreadLs || a.distanceLy - b.distanceLy
46690
+ );
46691
+ return { targets, leftOutSpread };
46692
+ }
46693
+
46694
+ // src/systems/state.ts
46695
+ var lower = (xs) => xs?.length ? new Set(xs.map((x) => x.toLowerCase())) : null;
46696
+ var has = (set2, v) => !set2 || v !== null && set2.has(v.toLowerCase());
46697
+ function hasFilter(q) {
46698
+ return [q.states, q.government, q.allegiance, q.power, q.security].some((x) => x?.length);
46699
+ }
46700
+ function systemsByState(systems, centre, radiusLy, q) {
46701
+ const states = lower(q.states);
46702
+ const government = lower(q.government);
46703
+ const allegiance = lower(q.allegiance);
46704
+ const power2 = lower(q.power);
46705
+ const security = lower(q.security);
46706
+ const out = [];
46707
+ for (const s of systems) {
46708
+ const d = distanceLy(s.coords, centre);
46709
+ if (d > radiusLy) continue;
46710
+ if (!has(government, s.government) || !has(allegiance, s.allegiance)) continue;
46711
+ if (!has(power2, s.controlling_power) || !has(security, s.security)) continue;
46712
+ let factions = [];
46713
+ if (states) {
46714
+ factions = s.factions.filter(
46715
+ (f) => (!q.controllingOnly || f.controlling) && [...f.active_states, ...q.includePending ? f.pending_states : []].some(
46716
+ (x) => states.has(x.toLowerCase())
46717
+ )
46718
+ );
46719
+ if (!factions.length) continue;
46720
+ }
46721
+ out.push({ system: s, distanceLy: d, factions });
46722
+ }
46723
+ return out.sort((a, b) => a.distanceLy - b.distanceLy);
46724
+ }
46725
+
46726
+ // src/tools/systems.ts
46727
+ var TIME_LIMIT_MS2 = 2e4;
46728
+ var MASSACRE_MARGIN_LY = 2 * SOURCE_RANGE_LY;
46729
+ var STALE_HOURS = 72;
46730
+ var FAR_STAR_LS = 1e3;
46731
+ var Radius = external_exports.number().min(1).max(50).default(50);
46732
+ var Limit = external_exports.number().int().min(1).max(100).default(50);
46733
+ var Values = external_exports.array(external_exports.string()).optional();
46734
+ function registerSystemTools(server, deps) {
46735
+ const now = deps.now ?? Date.now;
46736
+ const resolveNear = async (near, notes) => {
46737
+ if (near) return near;
46738
+ if (deps.journal) {
46739
+ try {
46740
+ const system = (await deps.journal.state())?.location.system;
46741
+ if (system) return system;
46742
+ } catch (err) {
46743
+ notes.push(`Journal unavailable: ${err instanceof Error ? err.message : String(err)}`);
46744
+ }
46745
+ }
46746
+ const why = notes.length ? ` (${notes.join(" ")})` : "";
46747
+ throw new UserError(`Give near, or set up the journal to use the current system${why}.`);
46748
+ };
46749
+ const ageHours2 = (s) => s.updated_at ? Math.round((now() - Date.parse(s.updated_at)) / 36e5 * 10) / 10 : null;
46750
+ const leg = (ly, t) => legSeconds({ distanceLy: ly, arrivalLs: null, planetary: false, stationKnown: false }, t);
46751
+ const nearestWith = (s, ok2) => {
46752
+ const ls = s.stations.filter((x) => ok2(x.pad)).map((x) => x.arrival_ls ?? Infinity);
46753
+ const min3 = Math.min(...ls);
46754
+ return Number.isFinite(min3) ? Math.round(min3) : null;
46755
+ };
46756
+ const pads = (s) => ({
46757
+ L: nearestWith(s, (p) => p === "L"),
46758
+ M: nearestWith(s, (p) => p === "L" || p === "M"),
46759
+ S: nearestWith(s, (p) => p !== null)
46760
+ });
46761
+ server.registerTool(
46762
+ "systems_by_state",
46763
+ {
46764
+ annotations: {
46765
+ title: "Systems by Faction State",
46766
+ readOnlyHint: true,
46767
+ destructiveHint: false,
46768
+ idempotentHint: true,
46769
+ openWorldHint: true
46770
+ },
46771
+ description: "Find the nearest populated systems with a faction in a given state (e.g. War, Civil War, Election, Boom, Outbreak), or by government, allegiance, controlling power or security. near defaults to the journal location. War and Civil War results say conflict_zones: likely (settlements are not checked). Faction states come from player visits: check age_hours. Uses one Spansh search of all populated systems in radius_ly, cached for an hour; the first call can take 10 s.",
46772
+ inputSchema: {
46773
+ near: external_exports.string().optional().describe("Exact reference system name"),
46774
+ radius_ly: Radius,
46775
+ state: Values.describe("Faction states to match, e.g. ['War', 'Civil War']"),
46776
+ include_pending: external_exports.boolean().default(false),
46777
+ controlling_only: external_exports.boolean().default(false),
46778
+ government: Values,
46779
+ allegiance: Values,
46780
+ power: Values.describe("Controlling power"),
46781
+ security: Values,
46782
+ limit: Limit
46783
+ }
46784
+ },
46785
+ wrap(
46786
+ async (a) => {
46787
+ const q = {
46788
+ states: a.state,
46789
+ includePending: a.include_pending,
46790
+ controllingOnly: a.controlling_only,
46791
+ government: a.government,
46792
+ allegiance: a.allegiance,
46793
+ power: a.power,
46794
+ security: a.security
46795
+ };
46796
+ if (!hasFilter(q)) {
46797
+ throw new UserError(
46798
+ "Give at least one of state, government, allegiance, power or security."
46799
+ );
46800
+ }
46801
+ if (a.controlling_only && !a.state?.length) {
46802
+ throw new UserError(
46803
+ "controlling_only needs state: it limits the state match to the controlling faction."
46804
+ );
46805
+ }
46806
+ const notes = [];
46807
+ const near = await resolveNear(a.near, notes);
46808
+ const rules = await resolveSearchRules(deps, void 0, true);
46809
+ const sweep = await deps.spansh.populatedSystems(near, a.radius_ly, {
46810
+ deadline: now() + TIME_LIMIT_MS2
46811
+ });
46812
+ const matches2 = systemsByState(sweep.systems, sweep.centre, a.radius_ly, q);
46813
+ const shown = matches2.slice(0, a.limit);
46814
+ const stale = shown.filter((m) => (ageHours2(m.system) ?? 0) > STALE_HOURS).length;
46815
+ if (stale) notes.push(`${stale} results have data more than ${STALE_HOURS / 24} days old.`);
46816
+ if (sweep.partial) {
46817
+ notes.push("Spansh did not return every system in time; results may be missing.");
46818
+ }
46819
+ return {
46820
+ near,
46821
+ searched: sweep.systems.length,
46822
+ travel: describeRules(rules).travel,
46823
+ total_matches: matches2.length,
46824
+ partial: sweep.partial,
46825
+ systems: shown.map((m) => {
46826
+ const s = m.system;
46827
+ const war = m.factions.some(
46828
+ (f) => f.active_states.some((x) => /^(war|civil war)$/i.test(x))
46829
+ );
46830
+ const nearest3 = pads(s);
46831
+ const bestPad = nearest3.L !== null ? "L" : nearest3.M !== null ? "M" : nearest3.S !== null ? "S" : null;
46832
+ return {
46833
+ system: s.name,
46834
+ distance_ly: Math.round(m.distanceLy * 100) / 100,
46835
+ jumps: leg(m.distanceLy, rules.travel).jumps,
46836
+ matching_factions: m.factions.map((f) => ({
46837
+ name: f.name,
46838
+ controlling: f.controlling,
46839
+ influence: f.influence,
46840
+ active_states: f.active_states,
46841
+ pending_states: f.pending_states
46842
+ })),
46843
+ government: s.government,
46844
+ allegiance: s.allegiance,
46845
+ security: s.security,
46846
+ controlling_power: s.controlling_power,
46847
+ power_state: s.power_state,
46848
+ best_pad: bestPad,
46849
+ nearest_station_ls: bestPad ? nearest3[bestPad] : null,
46850
+ updated_at: s.updated_at,
46851
+ age_hours: ageHours2(s),
46852
+ ...war ? { conflict_zones: "likely" } : {}
46853
+ };
46854
+ }),
46855
+ notes
46856
+ };
46857
+ }
46858
+ )
46859
+ );
46860
+ server.registerTool(
46861
+ "massacre_targets",
46862
+ {
46863
+ annotations: {
46864
+ title: "Find Massacre Targets",
46865
+ readOnlyHint: true,
46866
+ destructiveHint: false,
46867
+ idempotentHint: true,
46868
+ openWorldHint: true
46869
+ },
46870
+ description: "Find pirate (Anarchy) factions near you with good sources of stackable pirate massacre missions, as Inara's 'nearest massacre missions' does. Sources are populated systems within 10 ly of the target; a primary source has no other pirate faction in other systems within 10 ly (its own pirate faction does not count, as missions send you to another system), so all its massacre missions are for this target. Sources with a faction in War, Civil War or Election are excluded and listed. stack_size is the number of distinct giving factions in the primary sources. Targets whose farthest star is beyond max_star_ls (default 10,000) are left out, because fights there can be far from the arrival star. Ranked by stack_size, then spread_ls, then distance. Results are candidates: mission boards are not published. Pads are listed, not filtered. near defaults to the journal location.",
46871
+ inputSchema: {
46872
+ near: external_exports.string().optional().describe("Exact reference system name"),
46873
+ radius_ly: Radius.describe("Targets within this distance"),
46874
+ max_star_ls: external_exports.number().min(0).default(1e4),
46875
+ limit: Limit,
46876
+ preferences: PrefsPatchSchema.optional(),
46877
+ use_preferences: external_exports.boolean().default(true)
46878
+ }
46879
+ },
46880
+ wrap(
46881
+ async (a) => {
46882
+ const notes = [];
46883
+ const near = await resolveNear(a.near, notes);
46884
+ const rules = await resolveSearchRules(deps, a.preferences, a.use_preferences);
46885
+ const sweep = await deps.spansh.populatedSystems(near, a.radius_ly + MASSACRE_MARGIN_LY, {
46886
+ deadline: now() + TIME_LIMIT_MS2
46887
+ });
46888
+ const { targets, leftOutSpread } = massacreTargets(sweep.systems, sweep.centre, {
46889
+ radiusLy: a.radius_ly,
46890
+ maxStarLs: a.max_star_ls
46891
+ });
46892
+ notes.push(
46893
+ "These are candidates: whether the mission boards have massacre missions now depends on recent player activity."
46894
+ );
46895
+ if (leftOutSpread) {
46896
+ notes.push(
46897
+ `${leftOutSpread} targets left out: a star beyond ${a.max_star_ls} ls. Raise max_star_ls to see them.`
46898
+ );
46899
+ }
46900
+ if (sweep.partial) {
46901
+ notes.push(
46902
+ "Spansh did not return every system in time: a source can look primary when a pirate faction near the edge was missed."
46903
+ );
46904
+ }
46905
+ return {
46906
+ near,
46907
+ searched: sweep.systems.length,
46908
+ left_out_spread: leftOutSpread,
46909
+ partial: sweep.partial,
46910
+ travel: describeRules(rules).travel,
46911
+ targets: targets.slice(0, a.limit).map((t) => ({
46912
+ target_faction: t.faction,
46913
+ target_system: t.system.name,
46914
+ distance_ly: Math.round(t.distanceLy * 100) / 100,
46915
+ travel_min: Math.round(leg(t.distanceLy, rules.travel).seconds / 6) / 10,
46916
+ stack_size: t.stackSize,
46917
+ spread_ls: Math.round(t.spreadLs),
46918
+ far_stars: t.system.stars.filter((s) => s.arrival_ls > FAR_STAR_LS).map((s) => ({ name: s.name, arrival_ls: Math.round(s.arrival_ls) })),
46919
+ sources: t.sources.map((s) => {
46920
+ const counts2 = { federation: 0, empire: 0, alliance: 0, independent: 0 };
46921
+ for (const f of s.system.factions) {
46922
+ const k = f.allegiance?.toLowerCase();
46923
+ if (!isAnarchy(f) && k && k in counts2) counts2[k]++;
46924
+ }
46925
+ return {
46926
+ system: s.system.name,
46927
+ primary: s.primary,
46928
+ other_pirates: s.otherPirates,
46929
+ distance_to_target_ly: Math.round(s.distanceToTargetLy * 100) / 100,
46930
+ allegiance_counts: counts2,
46931
+ pads: pads(s.system),
46932
+ factions: s.system.factions.map((f) => ({
46933
+ name: f.name,
46934
+ allegiance: f.allegiance,
46935
+ active_states: f.active_states
46936
+ })),
46937
+ updated_at: s.system.updated_at,
46938
+ age_hours: ageHours2(s.system)
46939
+ };
46940
+ }),
46941
+ excluded: t.excluded
46942
+ })),
46943
+ notes
46944
+ };
46945
+ }
46946
+ )
46947
+ );
46948
+ }
46949
+
45201
46950
  // src/tools/inara.ts
45202
46951
  function registerInaraTools(server, deps) {
45203
46952
  server.registerTool(
45204
46953
  "inara_commander_profile",
45205
46954
  {
46955
+ annotations: {
46956
+ title: "Inara Commander Profile",
46957
+ readOnlyHint: true,
46958
+ destructiveHint: false,
46959
+ idempotentHint: true,
46960
+ openWorldHint: true
46961
+ },
45206
46962
  description: "Commander profile from Inara: ranks with progress, allegiance, power, main ship type, squadron and Inara URL. Omit name for your own commander. Inara does not expose location, loadout or materials; use EDSM or CAPI tools for those. Cached 60 s.",
45207
46963
  inputSchema: { name: external_exports.string().optional() }
45208
46964
  },
@@ -45210,7 +46966,16 @@ function registerInaraTools(server, deps) {
45210
46966
  );
45211
46967
  server.registerTool(
45212
46968
  "inara_community_goals",
45213
- { description: "Recent and active community goals from Inara. Cached 60 s." },
46969
+ {
46970
+ annotations: {
46971
+ title: "Community Goals",
46972
+ readOnlyHint: true,
46973
+ destructiveHint: false,
46974
+ idempotentHint: true,
46975
+ openWorldHint: true
46976
+ },
46977
+ description: "Recent and active community goals from Inara. Cached 60 s."
46978
+ },
45214
46979
  wrap(async () => deps.inara.communityGoals())
45215
46980
  );
45216
46981
  }
@@ -45221,6 +46986,13 @@ function registerEdsmTools(server, deps) {
45221
46986
  server.registerTool(
45222
46987
  "edsm_commander_location",
45223
46988
  {
46989
+ annotations: {
46990
+ title: "Commander Location",
46991
+ readOnlyHint: true,
46992
+ destructiveHint: false,
46993
+ idempotentHint: true,
46994
+ openWorldHint: true
46995
+ },
45224
46996
  description: "Commander's last known system, coordinates, docked station and ship, from EDSM. Cached 60 s."
45225
46997
  },
45226
46998
  wrap(async () => deps.edsm.position())
@@ -45228,6 +47000,13 @@ function registerEdsmTools(server, deps) {
45228
47000
  server.registerTool(
45229
47001
  "edsm_commander_materials",
45230
47002
  {
47003
+ annotations: {
47004
+ title: "EDSM Commander Materials",
47005
+ readOnlyHint: true,
47006
+ destructiveHint: false,
47007
+ idempotentHint: true,
47008
+ openWorldHint: true
47009
+ },
45231
47010
  description: "Commander's engineering materials (raw and manufactured), encoded data, and cargo, from EDSM. kinds defaults to all three. Cached 60 s.",
45232
47011
  inputSchema: { kinds: external_exports.array(Kind).min(1).optional() }
45233
47012
  },
@@ -45306,13 +47085,13 @@ function buildPool(a, catalog) {
45306
47085
  const byEngineer = /* @__PURE__ */ new Map();
45307
47086
  for (const item of a.plan.items) {
45308
47087
  if (!item.engineer || item.status === "done" || item.status === "skipped") continue;
45309
- const entry = byEngineer.get(lc(item.engineer)) ?? {
47088
+ const entry2 = byEngineer.get(lc(item.engineer)) ?? {
45310
47089
  id: item.id,
45311
47090
  name: item.engineer,
45312
47091
  descriptions: []
45313
47092
  };
45314
- entry.descriptions.push(item.description);
45315
- byEngineer.set(lc(item.engineer), entry);
47093
+ entry2.descriptions.push(item.description);
47094
+ byEngineer.set(lc(item.engineer), entry2);
45316
47095
  }
45317
47096
  const baseFor = /* @__PURE__ */ new Map();
45318
47097
  if (catalog) {
@@ -45548,6 +47327,13 @@ function registerRouteTools(server, deps) {
45548
47327
  server.registerTool(
45549
47328
  "route_stops",
45550
47329
  {
47330
+ annotations: {
47331
+ title: "Plan Route Stops",
47332
+ readOnlyHint: false,
47333
+ destructiveHint: false,
47334
+ idempotentHint: true,
47335
+ openWorldHint: true
47336
+ },
45551
47337
  description: "Order the stops for a build: shops, material traders and engineers, from a plan (plan_id) or a list (stops). Uses Spansh coordinates and the travel-time model. For each engineer stop, set `after` to the shops and traders that supply what that engineer needs (the module to buy, the materials to trade). Give `after: []` to an engineer who needs nothing from the other stops, so they can come first. Without your rules, a plan puts every engineer after every shop and trader. Use `one_of` for alternatives, for example two engineers who both do the work; the tool tries every combination (at most 16) and keeps the fastest. `only` and `skip` choose stops from the plan, so one plan can serve several trips. One place (the same engineer, or the same system and station) can be an option in several groups; it is visited once, and each stop's `meets` lists the groups it covers. Leg times are estimates from straight-line jumps; pass plot: true to plot the chosen route with Spansh.",
45552
47338
  inputSchema: {
45553
47339
  plan_id: external_exports.string().optional(),
@@ -45589,8 +47375,8 @@ function registerRouteTools(server, deps) {
45589
47375
  );
45590
47376
  const cycle = findCycle(graph);
45591
47377
  if (cycle) {
45592
- const path12 = [...cycle, cycle[0]].map((i) => names(all[i]).join("+"));
45593
- throw new UserError(`The after rules form a cycle: ${path12.join(" -> ")}`);
47378
+ const path15 = [...cycle, cycle[0]].map((i) => names(all[i]).join("+"));
47379
+ throw new UserError(`The after rules form a cycle: ${path15.join(" -> ")}`);
45594
47380
  }
45595
47381
  const rules = await resolveSearchRules(deps, a.preferences, a.use_preferences);
45596
47382
  const travel = rules.travel;
@@ -45835,19 +47621,23 @@ function registerRouteTools(server, deps) {
45835
47621
  const total = min2(totalSeconds);
45836
47622
  let saved = false;
45837
47623
  if (a.save_to_plan && a.plan_id) {
45838
- await deps.plans.setRoute(a.plan_id, {
45839
- computed_at: new Date(now()).toISOString(),
45840
- from: startPlace.system,
45841
- stops: [
45842
- ...solved.order.map(optionLabel),
45843
- ...returnPlace ? [
45844
- `return to ${returnPlace.system}${returnPlace.station ? ` \u2014 ${returnPlace.station}` : ""}`
45845
- ] : []
45846
- ],
45847
- total_min: total,
45848
- plotted: a.plot && allPlotted
45849
- });
45850
- saved = true;
47624
+ try {
47625
+ await deps.plans.setRoute(a.plan_id, {
47626
+ computed_at: new Date(now()).toISOString(),
47627
+ from: startPlace.system,
47628
+ stops: [
47629
+ ...solved.order.map(optionLabel),
47630
+ ...returnPlace ? [
47631
+ `return to ${returnPlace.system}${returnPlace.station ? ` \u2014 ${returnPlace.station}` : ""}`
47632
+ ] : []
47633
+ ],
47634
+ total_min: total,
47635
+ plotted: a.plot && allPlotted
47636
+ });
47637
+ saved = true;
47638
+ } catch (err) {
47639
+ notes.add(`The route was not saved to plan "${a.plan_id}": ${reason(err)}`);
47640
+ }
45851
47641
  }
45852
47642
  return {
45853
47643
  start: { system: startPlace.system, station: startPlace.station, source },
@@ -45873,66 +47663,26 @@ function registerRouteTools(server, deps) {
45873
47663
 
45874
47664
  // src/tools/plans.ts
45875
47665
  import { randomUUID as randomUUID2 } from "crypto";
45876
- import { mkdir as mkdir5, readFile as readFile9, rename as rename5, stat as stat6, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
45877
- import path11 from "path";
47666
+ import { mkdir as mkdir6, readFile as readFile7, rename as rename5, stat as stat7, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
47667
+ import path13 from "path";
47668
+ var MAX_ECHOED_PLAN = 100 * 1024;
47669
+ function registerPlanTools(server, deps) {
47670
+ const { plans, warnings } = deps;
47671
+ const saveOrReturnJson = async (input2, tooLargeAdvice, opts = {}) => {
47672
+ const parsed = PlanInputSchema.safeParse(input2);
47673
+ if (!parsed.success) throw new UserError(`Invalid plan: ${external_exports.prettifyError(parsed.error)}`);
47674
+ try {
47675
+ return await plans.upsert(parsed.data, opts);
47676
+ } catch (err) {
47677
+ if (err instanceof PlanExistsError) throw err;
47678
+ const json3 = JSON.stringify(parsed.data, null, 2);
47679
+ const tail = json3.length > MAX_ECHOED_PLAN ? `The plan was not saved, and it is too large to include here. ${tooLargeAdvice}` : `The plan was not saved. Save this JSON and load it later with plan_import:
47680
+ ${json3}`;
47681
+ throw new UserError(`${reason(err)}
45878
47682
 
45879
- // src/plans/markdown.ts
45880
- var box = {
45881
- todo: "[ ]",
45882
- in_progress: "[~]",
45883
- done: "[x]",
45884
- skipped: "[-]"
45885
- };
45886
- function planToMarkdown(plan) {
45887
- const done = plan.items.filter((i) => i.status === "done" || i.status === "skipped").length;
45888
- const out = [
45889
- `# ${plan.name}`,
45890
- "",
45891
- `Plan \`${plan.id}\` \xB7 updated ${plan.updated_at} \xB7 ${done}/${plan.items.length} done`,
45892
- ""
45893
- ];
45894
- if (plan.notes) out.push(plan.notes, "");
45895
- if (plan.items.length) {
45896
- out.push("## Items", "");
45897
- for (const i of plan.items) {
45898
- const where = [
45899
- i.station ? `at ${i.station}` : void 0,
45900
- i.engineer ? `engineer ${i.engineer}` : void 0
45901
- ].filter(Boolean);
45902
- out.push(
45903
- `- ${box[i.status]} **${i.kind}** ${i.description}${where.length ? ` \u2014 ${where.join(", ")}` : ""}`
45904
- );
45905
- if (i.materials?.length) {
45906
- out.push(` - Materials: ${i.materials.map((m) => `${m.count}\xD7 ${m.name}`).join(", ")}`);
45907
- }
45908
- if (i.notes) out.push(` - Notes: ${i.notes}`);
47683
+ ${tail}`);
45909
47684
  }
45910
- out.push("");
45911
- }
45912
- if (plan.stops.length) {
45913
- out.push("## Stops", "");
45914
- plan.stops.forEach((s, n) => {
45915
- out.push(`${n + 1}. ${s.system}${s.station ? ` \u2014 ${s.station}` : ""}: ${s.purpose}`);
45916
- });
45917
- out.push("");
45918
- }
45919
- if (plan.route) {
45920
- out.push("## Route", "");
45921
- plan.route.stops.forEach((s, n) => out.push(`${n + 1}. ${s}`));
45922
- const r = plan.route;
45923
- const kind2 = r.plotted ? "plotted" : "estimate";
45924
- out.push(
45925
- "",
45926
- `From ${r.from}; ${kind2}. Total ${r.total_min} min, computed ${r.computed_at}`,
45927
- ""
45928
- );
45929
- }
45930
- return out.join("\n");
45931
- }
45932
-
45933
- // src/tools/plans.ts
45934
- function registerPlanTools(server, deps) {
45935
- const { plans } = deps;
47685
+ };
45936
47686
  const exports = /* @__PURE__ */ new Map();
45937
47687
  const serialise = (target, fn) => {
45938
47688
  const key = process.platform === "linux" ? target : target.toLowerCase();
@@ -45945,25 +47695,61 @@ function registerPlanTools(server, deps) {
45945
47695
  };
45946
47696
  server.registerTool(
45947
47697
  "plan_list",
45948
- { description: "List saved build plans with item counts and progress." },
45949
- wrap(async () => plans.list())
47698
+ {
47699
+ annotations: {
47700
+ title: "List Plans",
47701
+ readOnlyHint: true,
47702
+ destructiveHint: false,
47703
+ idempotentHint: true,
47704
+ openWorldHint: false
47705
+ },
47706
+ description: "List saved build plans with item counts and progress."
47707
+ },
47708
+ wrapWarn(async () => plans.list(), warnings)
45950
47709
  );
45951
47710
  server.registerTool(
45952
47711
  "plan_get",
45953
- { description: "Get a saved plan by id.", inputSchema: { id: external_exports.string() } },
45954
- wrap(async ({ id }) => plans.get(id))
47712
+ {
47713
+ annotations: {
47714
+ title: "Get Plan",
47715
+ readOnlyHint: true,
47716
+ destructiveHint: false,
47717
+ idempotentHint: true,
47718
+ openWorldHint: false
47719
+ },
47720
+ description: "Get a saved plan by id.",
47721
+ inputSchema: { id: external_exports.string() }
47722
+ },
47723
+ wrapWarn(async ({ id }) => plans.get(id), warnings)
45955
47724
  );
45956
47725
  server.registerTool(
45957
47726
  "plan_upsert",
45958
47727
  {
47728
+ annotations: {
47729
+ title: "Save Plan",
47730
+ readOnlyHint: false,
47731
+ destructiveHint: true,
47732
+ idempotentHint: true,
47733
+ openWorldHint: false
47734
+ },
45959
47735
  description: "Create or replace a build plan. Items are things to buy, engineer, unlock, gather or trade, plus refits (moving modules between slots, storing modules, or a fitting decision); stops are the places to visit (optionally with label, kind and after rules); route is the saved visiting order. Replacing keeps created_at.",
45960
47736
  inputSchema: { plan: PlanInputSchema }
45961
47737
  },
45962
- wrap(async ({ plan }) => plans.upsert(plan))
47738
+ wrapWarn(
47739
+ async ({ plan }) => saveOrReturnJson(plan, "Ask Claude to save it again later."),
47740
+ warnings
47741
+ )
45963
47742
  );
45964
47743
  server.registerTool(
45965
47744
  "plan_update_item",
45966
47745
  {
47746
+ annotations: {
47747
+ title: "Update Plan Item",
47748
+ readOnlyHint: false,
47749
+ destructiveHint: false,
47750
+ idempotentHint: true,
47751
+ openWorldHint: false
47752
+ },
45967
47753
  description: "Set the status and/or notes of one plan item.",
45968
47754
  inputSchema: {
45969
47755
  plan_id: external_exports.string(),
@@ -45972,26 +47758,57 @@ function registerPlanTools(server, deps) {
45972
47758
  notes: external_exports.string().optional()
45973
47759
  }
45974
47760
  },
45975
- wrap(
47761
+ wrapWarn(
45976
47762
  async (a) => {
45977
47763
  if (a.status === void 0 && a.notes === void 0) {
45978
47764
  throw new UserError("Give status or notes to update");
45979
47765
  }
45980
- return plans.updateItem(a.plan_id, a.item_id, { status: a.status, notes: a.notes });
45981
- }
47766
+ try {
47767
+ return await plans.updateItem(a.plan_id, a.item_id, {
47768
+ status: a.status,
47769
+ notes: a.notes
47770
+ });
47771
+ } catch (err) {
47772
+ const changes = [
47773
+ ...a.status !== void 0 ? [`status: ${a.status}`] : [],
47774
+ ...a.notes !== void 0 ? ["notes changed"] : []
47775
+ ].join(", ");
47776
+ throw new UserError(
47777
+ `Item "${a.item_id}" in plan "${a.plan_id}" was not updated (${changes}). ${reason(err)}`
47778
+ );
47779
+ }
47780
+ },
47781
+ warnings
45982
47782
  )
45983
47783
  );
45984
47784
  server.registerTool(
45985
47785
  "plan_delete",
45986
- { description: "Delete a saved plan.", inputSchema: { id: external_exports.string() } },
45987
- wrap(async ({ id }) => {
47786
+ {
47787
+ annotations: {
47788
+ title: "Delete Plan",
47789
+ readOnlyHint: false,
47790
+ destructiveHint: true,
47791
+ idempotentHint: true,
47792
+ openWorldHint: false
47793
+ },
47794
+ description: "Delete a saved plan.",
47795
+ inputSchema: { id: external_exports.string() }
47796
+ },
47797
+ wrapWarn(async ({ id }) => {
45988
47798
  await plans.delete(id);
45989
47799
  return { deleted: id };
45990
- })
47800
+ }, warnings)
45991
47801
  );
45992
47802
  server.registerTool(
45993
47803
  "plan_export",
45994
47804
  {
47805
+ annotations: {
47806
+ title: "Export Plan",
47807
+ readOnlyHint: false,
47808
+ destructiveHint: true,
47809
+ idempotentHint: true,
47810
+ openWorldHint: false
47811
+ },
45995
47812
  description: "Export a plan as Markdown (default) or JSON. Without path, returns the text (markdown) or the plan (json). With path (absolute or ~/...), writes the file. A JSON file can be loaded again with plan_import.",
45996
47813
  inputSchema: {
45997
47814
  id: external_exports.string(),
@@ -46000,7 +47817,7 @@ function registerPlanTools(server, deps) {
46000
47817
  overwrite: external_exports.boolean().optional()
46001
47818
  }
46002
47819
  },
46003
- wrap(
47820
+ wrapWarn(
46004
47821
  async (a) => {
46005
47822
  const plan = await plans.get(a.id);
46006
47823
  const json3 = a.format === "json";
@@ -46014,7 +47831,7 @@ function registerPlanTools(server, deps) {
46014
47831
  if (!a.overwrite && await exists(target)) {
46015
47832
  throw new UserError(`${target} exists; pass overwrite: true to replace it`);
46016
47833
  }
46017
- await mkdir5(path11.dirname(target), { recursive: true });
47834
+ await mkdir6(path13.dirname(target), { recursive: true });
46018
47835
  const tmp = `${target}.${process.pid}.${randomUUID2()}.tmp`;
46019
47836
  try {
46020
47837
  await writeFile5(tmp, text);
@@ -46025,12 +47842,20 @@ function registerPlanTools(server, deps) {
46025
47842
  }
46026
47843
  });
46027
47844
  return { path: target, bytes: Buffer.byteLength(text) };
46028
- }
47845
+ },
47846
+ warnings
46029
47847
  )
46030
47848
  );
46031
47849
  server.registerTool(
46032
47850
  "plan_import",
46033
47851
  {
47852
+ annotations: {
47853
+ title: "Import Plan",
47854
+ readOnlyHint: false,
47855
+ destructiveHint: true,
47856
+ idempotentHint: true,
47857
+ openWorldHint: false
47858
+ },
46034
47859
  description: "Load a plan from a JSON file written by plan_export (absolute or ~/... path). id replaces the id in the file. Refuses to replace an existing plan unless overwrite is true.",
46035
47860
  inputSchema: {
46036
47861
  path: external_exports.string(),
@@ -46038,14 +47863,14 @@ function registerPlanTools(server, deps) {
46038
47863
  overwrite: external_exports.boolean().optional()
46039
47864
  }
46040
47865
  },
46041
- wrap(async (a) => {
47866
+ wrapWarn(async (a) => {
46042
47867
  const source = resolveUserPath(a.path);
46043
47868
  let raw;
46044
47869
  try {
46045
- if ((await stat6(source)).isDirectory()) {
47870
+ if ((await stat7(source)).isDirectory()) {
46046
47871
  throw new UserError(`${source} is a directory, not a plan file`);
46047
47872
  }
46048
- raw = await readFile9(source, "utf8");
47873
+ raw = await readFile7(source, "utf8");
46049
47874
  } catch (err) {
46050
47875
  if (err instanceof UserError) throw err;
46051
47876
  if (err.code === "ENOENT") {
@@ -46064,13 +47889,15 @@ function registerPlanTools(server, deps) {
46064
47889
  }
46065
47890
  const parsed = PlanInputSchema.safeParse(json3);
46066
47891
  if (!parsed.success) throw new UserError(`Invalid plan: ${external_exports.prettifyError(parsed.error)}`);
46067
- return plans.upsert(parsed.data, { overwrite: a.overwrite === true });
46068
- })
47892
+ return saveOrReturnJson(parsed.data, "Keep your original file and try again later.", {
47893
+ overwrite: a.overwrite === true
47894
+ });
47895
+ }, warnings)
46069
47896
  );
46070
47897
  }
46071
47898
  async function exists(p) {
46072
47899
  try {
46073
- await stat6(p);
47900
+ await stat7(p);
46074
47901
  return true;
46075
47902
  } catch (err) {
46076
47903
  if (err.code === "ENOENT") return false;
@@ -46083,17 +47910,31 @@ function registerPrefsTools(server, deps) {
46083
47910
  server.registerTool(
46084
47911
  "prefs_get",
46085
47912
  {
47913
+ annotations: {
47914
+ title: "Get Preferences",
47915
+ readOnlyHint: true,
47916
+ destructiveHint: false,
47917
+ idempotentHint: true,
47918
+ openWorldHint: false
47919
+ },
46086
47920
  description: "Get the commander's stored travel preferences used by station searches: maximum arrival distance (soft/hard/off), planetary, fleet carrier and megaship rules, minimum pad (auto = current ship), permit-locked systems (allow/exclude), and the travel-time model (jump range and the share of the journal's unladen range that auto uses, seconds per jump, supercruise curve, docking, planetary time, value of time in cr/h)."
46087
47921
  },
46088
- wrap(async () => deps.prefs.get())
47922
+ wrapWarn(async () => deps.prefs.get(), deps.warnings)
46089
47923
  );
46090
47924
  server.registerTool(
46091
47925
  "prefs_set",
46092
47926
  {
47927
+ annotations: {
47928
+ title: "Set Preferences",
47929
+ readOnlyHint: false,
47930
+ destructiveHint: false,
47931
+ idempotentHint: true,
47932
+ openWorldHint: false
47933
+ },
46093
47934
  description: 'Change stored travel preferences with a partial patch, for example {"stations":{"max_arrival_ls":{"value":2000}}}. Saved for future sessions; returns the full preferences. For a one-off change pass `preferences` to the search tool instead.',
46094
47935
  inputSchema: { patch: PrefsPatchSchema }
46095
47936
  },
46096
- wrap(async ({ patch }) => deps.prefs.set(patch))
47937
+ wrapWarn(async ({ patch }) => deps.prefs.set(patch), deps.warnings)
46097
47938
  );
46098
47939
  }
46099
47940
 
@@ -46191,6 +48032,13 @@ function registerStationTools(server, deps) {
46191
48032
  server.registerTool(
46192
48033
  "station_info",
46193
48034
  {
48035
+ annotations: {
48036
+ title: "Station Info",
48037
+ readOnlyHint: true,
48038
+ destructiveHint: false,
48039
+ idempotentHint: true,
48040
+ openWorldHint: false
48041
+ },
46194
48042
  description: "What the station you last opened a screen at sells, from the game's own files in the journal folder (Market.json, Outfitting.json, Shipyard.json). Complete and current, but only for the latest station of each kind. part: market (commodities sold and bought, each with price, stock or demand and vs_mean_pct), outfitting (modules by group with price), shipyard (ships by price) or all (a short summary of each, the default). commodity filters the market by name; search filters modules by name, group, symbol or class and rating (for example '5A'). Notes flag data that is old or from a station other than the docked one.",
46195
48043
  inputSchema: {
46196
48044
  part: external_exports.enum(["all", "market", "outfitting", "shipyard"]).optional(),
@@ -46271,22 +48119,22 @@ function registerStationTools(server, deps) {
46271
48119
  }
46272
48120
 
46273
48121
  // src/tools/spansh.ts
46274
- var Service = external_exports.enum([
46275
- "material_trader_raw",
46276
- "material_trader_manufactured",
46277
- "material_trader_encoded",
46278
- "tech_broker_human",
46279
- "tech_broker_guardian",
46280
- "interstellar_factors"
46281
- ]);
46282
- var Limit = external_exports.number().int().min(1).max(50).default(10);
48122
+ var Service = external_exports.enum(STATION_SERVICES);
48123
+ var Limit2 = external_exports.number().int().min(1).max(50).default(10);
46283
48124
  function registerSpanshTools(server, deps) {
46284
48125
  const { spansh } = deps;
46285
48126
  const now = deps.now ?? Date.now;
46286
48127
  server.registerTool(
46287
48128
  "spansh_find_stations",
46288
48129
  {
46289
- description: "Find the best stations near a system, ranked by travel time and cost, that sell a module or ship, or offer a service (material trader, tech broker, interstellar factors). Give exactly one of module, ship, service. Module names are generic (e.g. 'Frame Shift Drive') with class 1-8 and rating A-I; give class and rating to compare prices. Applies the commander's stored travel preferences (arrival distance, station types, pad size from the current ship, travel-time model); override any of them for this call with `preferences` (same shape as prefs_set's patch) or ignore them with use_preferences=false. Returns total (Spansh matches before preferences; 10000 means 10000 or more), matched (matches after preferences among the stations fetched: the nearest 50, or up to 100 when the arrival limit was relaxed). Stations are ranked by estimated travel time (jumps plus supercruise, docking and planetary time) and, for purchases, by effective cost = price + travel time \xD7 the commander's credits_per_hour. Each station has jumps, travel_s, travel_min, arrival_assumed, effective_cost and discount_pct (versus the median price); recommended explains the trade-off. Stations in permit-locked systems are left out unless permit_systems is allow, which keeps them with needs_permit: true. Also returns any relaxed rules, the applied preferences and notes. Cached 5 min.",
48130
+ annotations: {
48131
+ title: "Find Stations",
48132
+ readOnlyHint: true,
48133
+ destructiveHint: false,
48134
+ idempotentHint: true,
48135
+ openWorldHint: true
48136
+ },
48137
+ description: "Find the best stations near a system, ranked by travel time and cost, that sell a module or ship, or offer a service: material traders, tech brokers, interstellar factors, black market, Universal Cartographics, Vista Genomics, Search and Rescue, refuel, repair, restock, shipyard, outfitting, market and more (see the service enum). Give exactly one of module, ship, service. Module names are generic (e.g. 'Frame Shift Drive') with class 1-8 and rating A-I; give class and rating to compare prices. Applies the commander's stored travel preferences (arrival distance, station types, pad size from the current ship, travel-time model); override any of them for this call with `preferences` (same shape as prefs_set's patch) or ignore them with use_preferences=false. Returns total (Spansh matches before preferences; 10000 means 10000 or more), matched (matches after preferences among the stations fetched: the nearest 50, or up to 100 when the arrival limit was relaxed). Stations are ranked by estimated travel time (jumps plus supercruise, docking and planetary time) and, for purchases, by effective cost = price + travel time \xD7 the commander's credits_per_hour. Each station has jumps, travel_s, travel_min, arrival_assumed, effective_cost and discount_pct (versus the median price); recommended explains the trade-off. Stations in permit-locked systems are left out unless permit_systems is allow, which keeps them with needs_permit: true. Also returns any relaxed rules, the applied preferences and notes. Cached 5 min.",
46290
48138
  inputSchema: {
46291
48139
  near: external_exports.string().describe("Exact reference system name"),
46292
48140
  module: external_exports.object({
@@ -46300,7 +48148,7 @@ function registerSpanshTools(server, deps) {
46300
48148
  max_distance_ly: external_exports.number().positive().optional(),
46301
48149
  preferences: PrefsPatchSchema.optional(),
46302
48150
  use_preferences: external_exports.boolean().default(true),
46303
- limit: Limit
48151
+ limit: Limit2
46304
48152
  }
46305
48153
  },
46306
48154
  wrap(
@@ -46326,14 +48174,28 @@ function registerSpanshTools(server, deps) {
46326
48174
  server.registerTool(
46327
48175
  "spansh_system",
46328
48176
  {
48177
+ annotations: {
48178
+ title: "Look Up System",
48179
+ readOnlyHint: true,
48180
+ destructiveHint: false,
48181
+ idempotentHint: true,
48182
+ openWorldHint: true
48183
+ },
46329
48184
  description: "Look up a system by exact name: coordinates, allegiance, permit, and its stations sorted by distance from arrival (limit applies to stations). Cached 24 h.",
46330
- inputSchema: { name: external_exports.string(), limit: Limit }
48185
+ inputSchema: { name: external_exports.string(), limit: Limit2 }
46331
48186
  },
46332
48187
  wrap(async (a) => spansh.system(a.name, a.limit))
46333
48188
  );
46334
48189
  server.registerTool(
46335
48190
  "spansh_route",
46336
48191
  {
48192
+ annotations: {
48193
+ title: "Plot Route",
48194
+ readOnlyHint: true,
48195
+ destructiveHint: false,
48196
+ idempotentHint: true,
48197
+ openWorldHint: true
48198
+ },
46337
48199
  description: "Plot a neutron-boosted route between two systems for a laden jump range in LY. Returns waypoints with jumps needed to reach each. May take a while; if it says still plotting, call again with the same arguments. Cached 5 min.",
46338
48200
  inputSchema: {
46339
48201
  from: external_exports.string(),
@@ -46444,7 +48306,7 @@ function recommendCommodityStation(stations, mode, maxAgeHours = Infinity) {
46444
48306
  const best = stations.find(
46445
48307
  (h) => h.market_age_hours === null || h.market_age_hours <= maxAgeHours
46446
48308
  );
46447
- const reason = (b) => {
48309
+ const reason2 = (b) => {
46448
48310
  const trip = `about ${b.travel_min} min`;
46449
48311
  if (b.value === null) {
46450
48312
  return `${mode === "sell" ? "highest" : "lowest"} price, ${credits2(b.price ?? 0)} cr, ${trip}`;
@@ -46454,7 +48316,7 @@ function recommendCommodityStation(stations, mode, maxAgeHours = Infinity) {
46454
48316
  }
46455
48317
  return `best ${mode === "sell" ? "net return" : "total cost"} after travel time: ${credits2(b.value)} cr, ${trip}`;
46456
48318
  };
46457
- return best ? { name: best.name, system: best.system, reason: reason(best) } : null;
48319
+ return best ? { name: best.name, system: best.system, reason: reason2(best) } : null;
46458
48320
  }
46459
48321
 
46460
48322
  // src/tools/trade.ts
@@ -46512,6 +48374,13 @@ function registerTradeTools(server, deps) {
46512
48374
  server.registerTool(
46513
48375
  "trade_route",
46514
48376
  {
48377
+ annotations: {
48378
+ title: "Find Trade Route",
48379
+ readOnlyHint: true,
48380
+ destructiveHint: false,
48381
+ idempotentHint: true,
48382
+ openWorldHint: true
48383
+ },
46515
48384
  description: "Plan a multi-hop trade run with the Spansh trade planner and rank it by profit per hour. Starts at from_system/from_station, or the docked station from the game journal. Cargo and capital default to the journal's ship and credits (else 7 t and 1000 cr). Pad size, planetary stations and fleet carriers and a hard arrival-distance limit come from the commander's travel preferences. Returns hops with commodities to buy and sell, profit, travel_min, total_profit, total_travel_min and profit_per_hour (from the travel-time model), plus the resolved inputs. Spansh takes one to several minutes: if the result is status pending, call again with the inputs it shows to resume. Finished routes are cached 5 min.",
46516
48385
  inputSchema: {
46517
48386
  from_system: external_exports.string().optional(),
@@ -46653,6 +48522,13 @@ function registerTradeTools(server, deps) {
46653
48522
  server.registerTool(
46654
48523
  "commodity_prices",
46655
48524
  {
48525
+ annotations: {
48526
+ title: "Commodity Prices",
48527
+ readOnlyHint: true,
48528
+ destructiveHint: false,
48529
+ idempotentHint: true,
48530
+ openWorldHint: true
48531
+ },
46656
48532
  description: "Find the best stations near a system to sell or buy one commodity, from Spansh market data, filtered and ranked with the commander's travel preferences. Stations show price, demand (sell) or supply (buy), market_age_hours and travel_min. Give amount (tonnes) to rank by value = price \xD7 amount \u2213 travel time \xD7 credits_per_hour (minus when selling, plus when buying); otherwise ranked by price. Searches within max_distance_ly of near (default the journal's current system) and only markets updated within max_price_age_days. Stations must have demand or supply of at least amount (else 1). Stations in permit-locked systems are left out unless permit_systems is allow. recommended names the best station and why. Cached 5 min.",
46657
48533
  inputSchema: {
46658
48534
  commodity: external_exports.string().describe("Exact English commodity name, e.g. Gold"),
@@ -46709,7 +48585,7 @@ function registerTradeTools(server, deps) {
46709
48585
  now
46710
48586
  });
46711
48587
  notes.push(...screen.notes);
46712
- const open3 = mode === "exclude" ? ranked.stations.filter((h) => !screen.locked(h.system)) : ranked.stations;
48588
+ const open4 = mode === "exclude" ? ranked.stations.filter((h) => !screen.locked(h.system)) : ranked.stations;
46713
48589
  const applied_preferences = describeRules(rules);
46714
48590
  if (!found.stations.length) {
46715
48591
  notes.push("No stations found; check the exact English commodity name");
@@ -46727,7 +48603,7 @@ function registerTradeTools(server, deps) {
46727
48603
  return {
46728
48604
  near: found.near,
46729
48605
  total: found.total,
46730
- matched: open3.length,
48606
+ matched: open4.length,
46731
48607
  stations: screen.kept.map((st) => ({
46732
48608
  ...st,
46733
48609
  ...screen.locked(st.system) ? { needs_permit: true } : {},
@@ -46757,14 +48633,150 @@ function registerTradeTools(server, deps) {
46757
48633
  );
46758
48634
  }
46759
48635
 
48636
+ // src/tools/settings.ts
48637
+ import { access as access2 } from "fs/promises";
48638
+ import path14 from "path";
48639
+ async function exists2(file2) {
48640
+ return access2(file2).then(
48641
+ () => true,
48642
+ () => false
48643
+ );
48644
+ }
48645
+ async function existing(dir, names) {
48646
+ const found = [];
48647
+ for (const n of names) if (await exists2(path14.join(dir, n))) found.push(n);
48648
+ return found;
48649
+ }
48650
+ function registerSettingsTools(server, deps) {
48651
+ server.registerTool(
48652
+ "settings_open",
48653
+ {
48654
+ annotations: {
48655
+ title: "Open Settings Page",
48656
+ // Not read-only: it starts a local page that saves keys.
48657
+ readOnlyHint: false,
48658
+ destructiveHint: false,
48659
+ idempotentHint: false,
48660
+ openWorldHint: false
48661
+ },
48662
+ description: "Open the ed-mcp settings page in the browser, where the user enters or clears Inara and EDSM API keys and commander names. Keys entered there apply at once, without a restart, and sync (encrypted) when a sync folder and password are set. Never ask the user to type a key into the chat; use this page."
48663
+ },
48664
+ wrap(async () => {
48665
+ const { url: url2, expires_at, opened } = await deps.page.open();
48666
+ return opened ? {
48667
+ opened: true,
48668
+ expires_at,
48669
+ next: "Ask the user to enter their keys in the browser tab that opened and press Save."
48670
+ } : {
48671
+ opened: false,
48672
+ url: url2,
48673
+ expires_at,
48674
+ next: "Ask the user to open this link in their browser, enter their keys and press Save."
48675
+ };
48676
+ })
48677
+ );
48678
+ server.registerTool(
48679
+ "settings_status",
48680
+ {
48681
+ annotations: {
48682
+ title: "Settings Status",
48683
+ // Not read-only: a status call can copy environment keys into the store once (seed-once).
48684
+ readOnlyHint: false,
48685
+ destructiveHint: false,
48686
+ idempotentHint: true,
48687
+ openWorldHint: false
48688
+ },
48689
+ description: "Where ed-mcp keeps its data: the sync folder and whether it is available, whether secrets are encrypted, where each API key comes from (environment or saved), whether a Frontier login is saved, any sync conflict copies, and notes on settings that look mismatched between Claude apps. An `update` line says when a newer ed-mcp release is available. Never returns key values."
48690
+ },
48691
+ wrap(async () => {
48692
+ let syncFolder = null;
48693
+ if (deps.sync) {
48694
+ try {
48695
+ await deps.sync.check();
48696
+ syncFolder = { path: deps.sync.dir, available: true };
48697
+ } catch (err) {
48698
+ if (!(err instanceof UserError)) throw err;
48699
+ syncFolder = { path: deps.sync.dir, available: false, error: err.message };
48700
+ }
48701
+ }
48702
+ let readable = true;
48703
+ let secretsError;
48704
+ let frontier = false;
48705
+ try {
48706
+ frontier = !!(await deps.secrets.get()).capi;
48707
+ } catch (err) {
48708
+ if (!(err instanceof UserError)) throw err;
48709
+ readable = false;
48710
+ secretsError = err.message;
48711
+ }
48712
+ const sources = await deps.settings.sources();
48713
+ const notes = [...deps.notes ?? []];
48714
+ if (deps.sync && deps.localDir && path14.resolve(deps.localDir) !== path14.resolve(deps.sync.dir)) {
48715
+ const own2 = path14.resolve(deps.secrets.file);
48716
+ const localDir = deps.localDir;
48717
+ const files = (await existing(localDir, [
48718
+ "plans.json",
48719
+ "preferences.json",
48720
+ "secrets.json",
48721
+ "secrets.enc"
48722
+ ])).filter((n) => path14.resolve(localDir, n) !== own2);
48723
+ if (files.length > 0) {
48724
+ notes.push(
48725
+ `The settings folder ${deps.localDir} also holds ${files.join(", ")}. Another Claude app on this computer may not have the sync folder set. Give every Claude app the same ED_MCP_SYNC_DIR and ED_MCP_SYNC_PASSWORD.`
48726
+ );
48727
+ }
48728
+ }
48729
+ if (deps.secrets.encrypted) {
48730
+ const dir = path14.dirname(deps.secrets.file);
48731
+ if (await exists2(path14.join(dir, "secrets.json"))) {
48732
+ notes.push(
48733
+ `${dir} holds both secrets.enc and secrets.json. A Claude app without ED_MCP_SYNC_PASSWORD uses secrets.json. Give every Claude app the same password, then delete secrets.json.`
48734
+ );
48735
+ }
48736
+ }
48737
+ return {
48738
+ sync_folder: syncFolder,
48739
+ secrets: {
48740
+ file: deps.secrets.file,
48741
+ encrypted: deps.secrets.encrypted,
48742
+ readable,
48743
+ ...secretsError ? { error: secretsError } : {}
48744
+ },
48745
+ inara: sources.inara,
48746
+ edsm: sources.edsm,
48747
+ frontier_login: frontier,
48748
+ conflicts: await deps.syncWarnings?.() ?? [],
48749
+ notes,
48750
+ update: await deps.update?.() ?? null
48751
+ };
48752
+ })
48753
+ );
48754
+ }
48755
+
46760
48756
  // src/server.ts
46761
- function createServer2(deps) {
48757
+ function createServer3(deps) {
46762
48758
  const server = new McpServer({ name: "ed-mcp", version: VERSION });
46763
48759
  if (deps.caches) {
46764
48760
  registerCacheTools(server, { caches: deps.caches, refreshables: deps.refreshables });
46765
48761
  }
46766
- if (deps.plans) registerPlanTools(server, { plans: deps.plans });
46767
- if (deps.prefs) registerPrefsTools(server, { prefs: deps.prefs });
48762
+ if (deps.plans)
48763
+ registerPlanTools(server, {
48764
+ plans: deps.plans,
48765
+ warnings: deps.planWarnings ?? deps.syncWarnings
48766
+ });
48767
+ if (deps.prefs) registerPrefsTools(server, { prefs: deps.prefs, warnings: deps.syncWarnings });
48768
+ if (deps.settingsPage && deps.settings && deps.secrets) {
48769
+ registerSettingsTools(server, {
48770
+ page: deps.settingsPage,
48771
+ settings: deps.settings,
48772
+ secrets: deps.secrets,
48773
+ sync: deps.sync,
48774
+ syncWarnings: deps.syncWarnings,
48775
+ localDir: deps.localDir,
48776
+ notes: deps.notes,
48777
+ update: deps.versionCheck ? () => deps.versionCheck.notice() : void 0
48778
+ });
48779
+ }
46768
48780
  if (deps.catalog) registerCatalogTools(server, { catalog: deps.catalog });
46769
48781
  if (deps.inara) registerInaraTools(server, { inara: deps.inara });
46770
48782
  if (deps.edsm) registerEdsmTools(server, { edsm: deps.edsm });
@@ -46789,6 +48801,13 @@ function createServer2(deps) {
46789
48801
  currentShip: deps.currentShip,
46790
48802
  jumpRange: deps.jumpRange
46791
48803
  });
48804
+ registerSystemTools(server, {
48805
+ spansh: deps.spansh,
48806
+ journal: deps.journal,
48807
+ prefs: deps.prefs,
48808
+ currentShip: deps.currentShip,
48809
+ jumpRange: deps.jumpRange
48810
+ });
46792
48811
  }
46793
48812
  if (deps.catalog && deps.spansh) {
46794
48813
  registerMaterialsTools(server, {
@@ -46825,7 +48844,12 @@ function createServer2(deps) {
46825
48844
  if (deps.capiAuth && deps.capi) {
46826
48845
  registerCapiTools(server, { capiAuth: deps.capiAuth, capi: deps.capi });
46827
48846
  }
46828
- if (deps.journal) registerJournalTools(server, { journal: deps.journal, catalog: deps.catalog });
48847
+ if (deps.journal)
48848
+ registerJournalTools(server, {
48849
+ journal: deps.journal,
48850
+ catalog: deps.catalog,
48851
+ update: deps.versionCheck ? () => deps.versionCheck.notice() : void 0
48852
+ });
46829
48853
  if (deps.journal) registerStationTools(server, { journal: deps.journal, catalog: deps.catalog });
46830
48854
  registerBuildTools(server, { catalog: deps.catalog });
46831
48855
  if (deps.ships) {
@@ -46843,7 +48867,9 @@ function createServer2(deps) {
46843
48867
  // src/index.ts
46844
48868
  async function main() {
46845
48869
  const config2 = loadConfig(process.env, defaultDirs());
46846
- const server = createServer2(buildDeps(config2));
48870
+ const deps = buildDeps(config2);
48871
+ deps.versionCheck?.start();
48872
+ const server = createServer3(deps);
46847
48873
  await server.connect(new StdioServerTransport());
46848
48874
  }
46849
48875
  main().catch((err) => {