ed-mcp 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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)
@@ -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,
@@ -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") {
@@ -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) {
@@ -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) {
@@ -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
@@ -28692,7 +28752,7 @@ async function canonical(p) {
28692
28752
  }
28693
28753
 
28694
28754
  // src/version.ts
28695
- var VERSION = "0.4.0";
28755
+ var VERSION = "0.5.1";
28696
28756
 
28697
28757
  // src/config.ts
28698
28758
  var ConfigError = class extends Error {
@@ -32080,25 +32140,159 @@ function parseSpanshJson(text) {
32080
32140
  return JSON.parse(text.replace(BIG_ID, '$1"$2"'));
32081
32141
  }
32082
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.coveredLy + 1e-9
32205
+ );
32206
+ return hit?.systems.filter((s) => distanceLy(s.coords, centre) <= radiusLy);
32207
+ }
32208
+ add(centre, radiusLy, systems, key) {
32209
+ const storedAt = key ? this.sweeps.find((s) => s.resume?.key === key)?.storedAt : void 0;
32210
+ if (key) this.sweeps = this.sweeps.filter((s) => s.resume?.key !== key);
32211
+ this.push({ centre, coveredLy: radiusLy, systems, storedAt: storedAt ?? this.now() });
32212
+ }
32213
+ savePartial(p) {
32214
+ const storedAt = this.sweeps.find((s) => s.resume?.key === p.key)?.storedAt ?? this.now();
32215
+ this.sweeps = this.sweeps.filter((s) => s.resume?.key !== p.key);
32216
+ this.push({
32217
+ centre: p.centre,
32218
+ coveredLy: p.coveredLy,
32219
+ systems: p.systems,
32220
+ storedAt,
32221
+ resume: p
32222
+ });
32223
+ }
32224
+ partial(key) {
32225
+ this.dropExpired();
32226
+ return this.sweeps.find((s) => s.resume?.key === key)?.resume;
32227
+ }
32228
+ push(s) {
32229
+ this.sweeps.push(s);
32230
+ while (this.sweeps.length > this.maxSweeps) this.sweeps.shift();
32231
+ }
32232
+ clear() {
32233
+ const n = this.sweeps.length;
32234
+ this.sweeps = [];
32235
+ return n;
32236
+ }
32237
+ stats() {
32238
+ this.dropExpired();
32239
+ const oldest = this.sweeps.length ? Math.min(...this.sweeps.map((s) => s.storedAt)) : void 0;
32240
+ return {
32241
+ name: this.name,
32242
+ entries: this.sweeps.length,
32243
+ oldestAgeSeconds: oldest === void 0 ? null : Math.round((this.now() - oldest) / 1e3)
32244
+ };
32245
+ }
32246
+ dropExpired() {
32247
+ const cutoff = this.now() - this.ttlMs;
32248
+ this.sweeps = this.sweeps.filter((s) => s.storedAt > cutoff);
32249
+ }
32250
+ };
32251
+
32083
32252
  // src/spansh/client.ts
32084
- var SERVICE_FILTERS = {
32253
+ var TRADER_FILTERS = {
32085
32254
  material_trader_raw: { material_trader: { value: ["Raw"] } },
32086
32255
  material_trader_manufactured: { material_trader: { value: ["Manufactured"] } },
32087
32256
  material_trader_encoded: { material_trader: { value: ["Encoded"] } },
32088
32257
  tech_broker_human: { technology_broker: { value: ["Human"] } },
32089
- tech_broker_guardian: { technology_broker: { value: ["Guardian"] } },
32090
- interstellar_factors: { services: [{ name: { value: ["Interstellar Factors Contact"] } }] }
32091
- };
32258
+ tech_broker_guardian: { technology_broker: { value: ["Guardian"] } }
32259
+ };
32260
+ var NAMED_SERVICES = {
32261
+ interstellar_factors: "Interstellar Factors Contact",
32262
+ black_market: "Black Market",
32263
+ universal_cartographics: "Universal Cartographics",
32264
+ vista_genomics: "Vista Genomics",
32265
+ search_and_rescue: "Search and Rescue",
32266
+ refuel: "Refuel",
32267
+ repair: "Repair",
32268
+ restock: "Restock",
32269
+ shipyard: "Shipyard",
32270
+ outfitting: "Outfitting",
32271
+ market: "Market",
32272
+ apex_interstellar: "Apex Interstellar",
32273
+ frontline_solutions: "Frontline Solutions",
32274
+ pioneer_supplies: "Pioneer Supplies",
32275
+ bartender: "Bartender",
32276
+ refinery_contact: "Refinery Contact",
32277
+ redemption_office: "Redemption Office",
32278
+ crew_lounge: "Crew Lounge",
32279
+ squadron_bank: "Squadron Bank",
32280
+ construction_services: "Construction Services",
32281
+ powerplay: "Powerplay"
32282
+ };
32283
+ var STATION_SERVICES = [
32284
+ ...Object.keys(TRADER_FILTERS),
32285
+ ...Object.keys(NAMED_SERVICES)
32286
+ ];
32287
+ function serviceFilter(s) {
32288
+ if (Object.hasOwn(TRADER_FILTERS, s)) return TRADER_FILTERS[s];
32289
+ return { services: [{ name: { value: [NAMED_SERVICES[s]] } }] };
32290
+ }
32092
32291
  var COUNT_CAP = 1e4;
32093
32292
  var shipKey = (s) => s.toLowerCase().replace(/[\s-]/g, "");
32094
- function pad(s) {
32095
- if (s.has_large_pad || (s.large_pads ?? 0) > 0) return "L";
32096
- if ((s.medium_pads ?? 0) > 0) return "M";
32097
- if ((s.small_pads ?? 0) > 0) return "S";
32098
- return null;
32099
- }
32100
32293
  var MAX_JOBS = 100;
32101
32294
  var SEARCH_PAGE_SIZE = 50;
32295
+ var SWEEP_PAGE_SIZE = 500;
32102
32296
  var CARRIER_TYPE = "Drake-Class Carrier";
32103
32297
  var NON_CARRIER_TYPES = [
32104
32298
  "Asteroid base",
@@ -32131,6 +32325,7 @@ var SpanshClient = class {
32131
32325
  this.now = opts.now ?? Date.now;
32132
32326
  this.systemCache = opts.systemCache ?? cache;
32133
32327
  this.marketCache = opts.marketCache ?? cache;
32328
+ this.sweepCache = opts.sweepCache ?? new SweepCache();
32134
32329
  }
32135
32330
  http;
32136
32331
  cache;
@@ -32141,10 +32336,12 @@ var SpanshClient = class {
32141
32336
  now;
32142
32337
  systemCache;
32143
32338
  marketCache;
32339
+ sweepCache;
32144
32340
  // Job ids by route form, so a call after a timeout collects the job instead of resubmitting.
32145
32341
  routeJobs = /* @__PURE__ */ new Map();
32146
32342
  tradeJobs = /* @__PURE__ */ new Map();
32147
32343
  tradeSubmits = /* @__PURE__ */ new Map();
32344
+ sweeps = /* @__PURE__ */ new Map();
32148
32345
  gate = Promise.resolve();
32149
32346
  lastStart = 0;
32150
32347
  async resolveShip(name) {
@@ -32169,7 +32366,7 @@ var SpanshClient = class {
32169
32366
  shipName = await this.resolveShip(q.ship);
32170
32367
  filters.ships = [{ name: { value: [shipName] } }];
32171
32368
  } else if (q.service) {
32172
- Object.assign(filters, SERVICE_FILTERS[q.service]);
32369
+ Object.assign(filters, serviceFilter(q.service));
32173
32370
  }
32174
32371
  if (q.largePad) filters.has_large_pad = { value: true };
32175
32372
  if (q.maxDistanceLy !== void 0) filters.distance = { min: 0, max: q.maxDistanceLy };
@@ -32236,7 +32433,7 @@ var SpanshClient = class {
32236
32433
  name: s.name,
32237
32434
  type: s.type ?? null,
32238
32435
  arrival_ls: s.distance_to_arrival ?? null,
32239
- pad: pad(s),
32436
+ pad: stationPad(s),
32240
32437
  market: s.has_market ?? false,
32241
32438
  market_id: s.market_id ?? null,
32242
32439
  outfitting: s.has_outfitting ?? false,
@@ -32253,6 +32450,78 @@ var SpanshClient = class {
32253
32450
  );
32254
32451
  return detail.record.needs_permit ?? null;
32255
32452
  }
32453
+ /** Populated systems within radiusLy of near. Stops paging at opts.deadline; a partial result is cached and the next call resumes it. */
32454
+ async populatedSystems(near, radiusLy, opts = {}) {
32455
+ const centre = (await this.system(near, 0)).coords;
32456
+ const cached2 = this.sweepCache.find(centre, radiusLy);
32457
+ if (cached2)
32458
+ return { centre, systems: cached2, total: cached2.length, partial: false, coveredLy: radiusLy };
32459
+ const key = `${near.toLowerCase()}:${radiusLy}`;
32460
+ let running = this.sweeps.get(key);
32461
+ if (!running) {
32462
+ running = this.sweepPages(near, radiusLy, centre, key, opts).finally(
32463
+ () => this.sweeps.delete(key)
32464
+ );
32465
+ this.sweeps.set(key, running);
32466
+ }
32467
+ return running;
32468
+ }
32469
+ async sweepPages(near, radiusLy, centre, key, opts) {
32470
+ const resume = this.sweepCache.partial(key);
32471
+ const systems = resume ? [...resume.systems] : [];
32472
+ const seen = new Set(systems.map((s) => s.id64));
32473
+ let total = resume?.total ?? 0;
32474
+ let partial2 = false;
32475
+ for (let page2 = 0; ; page2++) {
32476
+ if (page2 > 0 && opts.deadline !== void 0 && this.now() >= opts.deadline) {
32477
+ partial2 = true;
32478
+ break;
32479
+ }
32480
+ const body = {
32481
+ filters: {
32482
+ // Resuming by distance, not page: Spansh's list can shift between calls an hour apart.
32483
+ distance: { min: resume?.coveredLy ?? 0, max: radiusLy },
32484
+ population: { comparison: "<=>", value: [1, 1e11] }
32485
+ },
32486
+ sort: [{ distance: { direction: "asc" } }],
32487
+ size: SWEEP_PAGE_SIZE,
32488
+ page: page2,
32489
+ reference_system: near
32490
+ };
32491
+ let res;
32492
+ try {
32493
+ res = await this.post("/systems/search", JSON.stringify(body), "application/json");
32494
+ } catch (err) {
32495
+ if (err instanceof SpanshError && err.status === 400) {
32496
+ throw new UserError(
32497
+ `Spansh rejected the search (${err.detail}). Check that "${near}" is an exact system name (spansh_system).`
32498
+ );
32499
+ }
32500
+ throw err;
32501
+ }
32502
+ if (!resume) total = res.count;
32503
+ const results = res.results ?? [];
32504
+ for (const raw of results) {
32505
+ const s = mapSystem(raw);
32506
+ if (s && !seen.has(s.id64)) {
32507
+ seen.add(s.id64);
32508
+ systems.push(s);
32509
+ }
32510
+ }
32511
+ if (results.length < SWEEP_PAGE_SIZE) break;
32512
+ }
32513
+ if (!partial2) {
32514
+ this.sweepCache.add(centre, radiusLy, systems, key);
32515
+ return { centre, systems, total, partial: partial2, coveredLy: radiusLy };
32516
+ }
32517
+ const last = systems[systems.length - 1];
32518
+ const coveredLy = Math.max(
32519
+ resume?.coveredLy ?? 0,
32520
+ last ? distanceLy(last.coords, centre) - 0.01 : 0
32521
+ );
32522
+ this.sweepCache.savePartial({ key, centre, coveredLy, systems, total });
32523
+ return { centre, systems, total, partial: partial2, coveredLy };
32524
+ }
32256
32525
  async route(q, opts = {}) {
32257
32526
  const form = new URLSearchParams({
32258
32527
  from: q.from,
@@ -32548,7 +32817,7 @@ var SpanshClient = class {
32548
32817
  distance_ly: s.distance ?? null,
32549
32818
  arrival_ls: s.distance_to_arrival ?? null,
32550
32819
  coords: { x, y, z: z2 },
32551
- pad: pad(s),
32820
+ pad: stationPad(s),
32552
32821
  type: s.type ?? null,
32553
32822
  planetary: s.is_planetary ?? false,
32554
32823
  market_age_hours: Number.isNaN(updated) ? null : Math.round((now - updated) / 36e5 * 10) / 10,
@@ -32579,7 +32848,7 @@ var SpanshClient = class {
32579
32848
  distance_ly: s.distance ?? 0,
32580
32849
  arrival_ls: s.distance_to_arrival ?? null,
32581
32850
  type: s.type ?? null,
32582
- pad: pad(s),
32851
+ pad: stationPad(s),
32583
32852
  planetary: s.is_planetary ?? null,
32584
32853
  price,
32585
32854
  market_updated_at: s.market_updated_at ?? null,
@@ -34610,9 +34879,12 @@ function buildDeps(config2, opts = {}) {
34610
34879
  caches.register(spanshSystemCache);
34611
34880
  const spanshMarketCache = new TtlCache("spansh-markets", 6e5);
34612
34881
  caches.register(spanshMarketCache);
34882
+ const spanshSweepCache = new SweepCache();
34883
+ caches.register(spanshSweepCache);
34613
34884
  const spansh = new SpanshClient(http, spanshCache, {
34614
34885
  systemCache: spanshSystemCache,
34615
- marketCache: spanshMarketCache
34886
+ marketCache: spanshMarketCache,
34887
+ sweepCache: spanshSweepCache
34616
34888
  });
34617
34889
  const capiHttp = new HttpClient({ userAgent: `EDCD-EdMcp-${VERSION}`, fetchFn: opts.fetchFn });
34618
34890
  const capiCache = new TtlCache("capi", 6e4);
@@ -41918,6 +42190,15 @@ var McpServer = class {
41918
42190
  this._promptHandlersInitialized = false;
41919
42191
  this.server = new Server(serverInfo, options);
41920
42192
  this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
42193
+ if (options?.capabilities?.tools) {
42194
+ this.setToolRequestHandlers();
42195
+ }
42196
+ if (options?.capabilities?.resources) {
42197
+ this.setResourceRequestHandlers();
42198
+ }
42199
+ if (options?.capabilities?.prompts) {
42200
+ this.setPromptRequestHandlers();
42201
+ }
41921
42202
  }
41922
42203
  /**
41923
42204
  * Access experimental features.
@@ -43245,10 +43526,10 @@ function slotInfo(hull, slot, item) {
43245
43526
  return { slot, kind: kind2, size };
43246
43527
  }
43247
43528
  function bulkheadFromItem(hull, item) {
43248
- const lower = item.toLowerCase();
43529
+ const lower2 = item.toLowerCase();
43249
43530
  const offset = hull.bulkheads.length > 5 ? 1 : 0;
43250
- if (lower.endsWith("_armour_grade1_default")) return hull.bulkheads[0] ?? null;
43251
- const i = BULKHEAD_SUFFIXES.findIndex((s) => lower.endsWith(`_armour_${s}`));
43531
+ if (lower2.endsWith("_armour_grade1_default")) return hull.bulkheads[0] ?? null;
43532
+ const i = BULKHEAD_SUFFIXES.findIndex((s) => lower2.endsWith(`_armour_${s}`));
43252
43533
  return i < 0 ? null : hull.bulkheads[i + offset] ?? null;
43253
43534
  }
43254
43535
  function bulkheadItem(hull, module) {
@@ -44712,13 +44993,13 @@ function resolveRules(stored, override, usePreferences, auto, jumpAuto = { ly: n
44712
44993
  const prefs = applyPatch(usePreferences ? stored : OFF_PREFS, override ?? {});
44713
44994
  const p = prefs.stations.min_pad;
44714
44995
  const fromRequest = override?.stations?.min_pad !== void 0;
44715
- const pad2 = p === "auto" ? auto : p === "none" ? { size: null, source: fromRequest ? "request" : "none" } : { size: p, source: fromRequest ? "request" : "preferences" };
44996
+ const pad = p === "auto" ? auto : p === "none" ? { size: null, source: fromRequest ? "request" : "none" } : { size: p, source: fromRequest ? "request" : "preferences" };
44716
44997
  const range = prefs.travel.jump_range_ly;
44717
44998
  const fromRequestRange = override?.travel?.jump_range_ly !== void 0;
44718
44999
  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" };
44719
45000
  return {
44720
45001
  prefs,
44721
- pad: pad2,
45002
+ pad,
44722
45003
  travel: { ...prefs.travel, jump_range_ly: ly },
44723
45004
  jumpRangeSource: source
44724
45005
  };
@@ -45092,11 +45373,11 @@ function union2(a, b) {
45092
45373
  async function resolveSearchRules(deps, preferences, usePreferences, largePad) {
45093
45374
  const stored = deps.prefs && usePreferences ? await deps.prefs.get() : DEFAULT_PREFS;
45094
45375
  const override = largePad ? { ...preferences, stations: { ...preferences?.stations, min_pad: "L" } } : preferences ?? {};
45095
- let pad2 = { size: null, source: "none" };
45376
+ let pad = { size: null, source: "none" };
45096
45377
  let jump2;
45097
- const first = resolveRules(stored, override, usePreferences, pad2);
45378
+ const first = resolveRules(stored, override, usePreferences, pad);
45098
45379
  if (first.prefs.stations.min_pad === "auto") {
45099
- pad2 = await autoPad(deps.currentShip ?? (async () => null));
45380
+ pad = await autoPad(deps.currentShip ?? (async () => null));
45100
45381
  }
45101
45382
  if (first.prefs.travel.jump_range_ly === "auto") {
45102
45383
  const max = await deps.jumpRange?.().catch(() => null);
@@ -45108,7 +45389,7 @@ async function resolveSearchRules(deps, preferences, usePreferences, largePad) {
45108
45389
  };
45109
45390
  }
45110
45391
  }
45111
- return resolveRules(stored, override, usePreferences, pad2, jump2);
45392
+ return resolveRules(stored, override, usePreferences, pad, jump2);
45112
45393
  }
45113
45394
 
45114
45395
  // src/tools/materials.ts
@@ -45299,7 +45580,7 @@ function legBetween(from, to, travel) {
45299
45580
  if (same(from.system, to.system) && same(from.station, to.station)) {
45300
45581
  return { jumps: 0, seconds: 0, distanceLy: 0 };
45301
45582
  }
45302
- const distanceLy = same(from.system, to.system) ? 0 : Math.hypot(
45583
+ const distanceLy2 = same(from.system, to.system) ? 0 : Math.hypot(
45303
45584
  from.coords.x - to.coords.x,
45304
45585
  from.coords.y - to.coords.y,
45305
45586
  from.coords.z - to.coords.z
@@ -45307,14 +45588,14 @@ function legBetween(from, to, travel) {
45307
45588
  return {
45308
45589
  ...legSeconds(
45309
45590
  {
45310
- distanceLy,
45591
+ distanceLy: distanceLy2,
45311
45592
  arrivalLs: to.arrivalLs,
45312
45593
  planetary: to.planetary,
45313
45594
  stationKnown: to.station !== null
45314
45595
  },
45315
45596
  travel
45316
45597
  ),
45317
- distanceLy
45598
+ distanceLy: distanceLy2
45318
45599
  };
45319
45600
  }
45320
45601
 
@@ -46369,6 +46650,343 @@ function registerLoopTools(server, deps) {
46369
46650
  }
46370
46651
  }
46371
46652
 
46653
+ // src/systems/massacre.ts
46654
+ var SOURCE_RANGE_LY = 10;
46655
+ var NO_MASSACRES = /* @__PURE__ */ new Set(["war", "civil war", "election"]);
46656
+ function massacreTargets(systems, centre, opts) {
46657
+ const pirates = systems.flatMap(
46658
+ (s) => s.factions.filter(isAnarchy).map((f) => ({ faction: f.name, system: s }))
46659
+ );
46660
+ const piratesNear = /* @__PURE__ */ new Map();
46661
+ const piratesInRange = (s) => {
46662
+ let list2 = piratesNear.get(s.id64);
46663
+ if (!list2) {
46664
+ 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 }));
46665
+ piratesNear.set(s.id64, list2);
46666
+ }
46667
+ return list2;
46668
+ };
46669
+ const candidates = systems.filter((s) => s.stations.length > 0);
46670
+ const targets = [];
46671
+ let leftOutSpread = 0;
46672
+ for (const p of pirates) {
46673
+ const distance2 = distanceLy(p.system.coords, centre);
46674
+ if (distance2 > opts.radiusLy) continue;
46675
+ const spreadLs = Math.max(0, ...p.system.stars.map((s) => s.arrival_ls));
46676
+ if (spreadLs > opts.maxStarLs) {
46677
+ leftOutSpread++;
46678
+ continue;
46679
+ }
46680
+ const sources = [];
46681
+ const excluded = [];
46682
+ for (const s of candidates) {
46683
+ if (s.id64 === p.system.id64) continue;
46684
+ const toTarget = distanceLy(s.coords, p.system.coords);
46685
+ if (toTarget > SOURCE_RANGE_LY) continue;
46686
+ const blocked = s.factions.flatMap(
46687
+ (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 }))
46688
+ );
46689
+ if (blocked.length) {
46690
+ excluded.push(...blocked);
46691
+ continue;
46692
+ }
46693
+ const others = piratesInRange(s).filter((x) => x.faction !== p.faction);
46694
+ sources.push({
46695
+ system: s,
46696
+ distanceToTargetLy: toTarget,
46697
+ primary: others.length === 0,
46698
+ otherPirates: others
46699
+ });
46700
+ }
46701
+ const primary = sources.filter((s) => s.primary);
46702
+ if (!primary.length) continue;
46703
+ const givers = new Set(
46704
+ primary.flatMap((s) => s.system.factions.filter((f) => !isAnarchy(f)).map((f) => f.name))
46705
+ );
46706
+ if (givers.size === 0) continue;
46707
+ sources.sort(
46708
+ (a, b) => Number(b.primary) - Number(a.primary) || a.distanceToTargetLy - b.distanceToTargetLy
46709
+ );
46710
+ targets.push({
46711
+ faction: p.faction,
46712
+ system: p.system,
46713
+ distanceLy: distance2,
46714
+ spreadLs,
46715
+ stackSize: givers.size,
46716
+ sources,
46717
+ excluded
46718
+ });
46719
+ }
46720
+ targets.sort(
46721
+ (a, b) => b.stackSize - a.stackSize || a.spreadLs - b.spreadLs || a.distanceLy - b.distanceLy
46722
+ );
46723
+ return { targets, leftOutSpread };
46724
+ }
46725
+
46726
+ // src/systems/state.ts
46727
+ var lower = (xs) => xs?.length ? new Set(xs.map((x) => x.toLowerCase())) : null;
46728
+ var has = (set2, v) => !set2 || v !== null && set2.has(v.toLowerCase());
46729
+ function hasFilter(q) {
46730
+ return [q.states, q.government, q.allegiance, q.power, q.security].some((x) => x?.length);
46731
+ }
46732
+ function systemsByState(systems, centre, radiusLy, q) {
46733
+ const states = lower(q.states);
46734
+ const government = lower(q.government);
46735
+ const allegiance = lower(q.allegiance);
46736
+ const power2 = lower(q.power);
46737
+ const security = lower(q.security);
46738
+ const out = [];
46739
+ for (const s of systems) {
46740
+ const d = distanceLy(s.coords, centre);
46741
+ if (d > radiusLy) continue;
46742
+ if (!has(government, s.government) || !has(allegiance, s.allegiance)) continue;
46743
+ if (!has(power2, s.controlling_power) || !has(security, s.security)) continue;
46744
+ let factions = [];
46745
+ if (states) {
46746
+ factions = s.factions.filter(
46747
+ (f) => (!q.controllingOnly || f.controlling) && [...f.active_states, ...q.includePending ? f.pending_states : []].some(
46748
+ (x) => states.has(x.toLowerCase())
46749
+ )
46750
+ );
46751
+ if (!factions.length) continue;
46752
+ }
46753
+ out.push({ system: s, distanceLy: d, factions });
46754
+ }
46755
+ return out.sort((a, b) => a.distanceLy - b.distanceLy);
46756
+ }
46757
+
46758
+ // src/tools/systems.ts
46759
+ var TIME_LIMIT_MS2 = 2e4;
46760
+ var MASSACRE_MARGIN_LY = 2 * SOURCE_RANGE_LY;
46761
+ var partialNote = (coveredLy) => `Spansh returned systems out to ${Math.floor(coveredLy * 10) / 10} ly within the time limit. Call again to continue the search.`;
46762
+ var STALE_HOURS = 72;
46763
+ var FAR_STAR_LS = 1e3;
46764
+ var Radius = external_exports.number().min(1).max(50).default(50);
46765
+ var Limit = external_exports.number().int().min(1).max(100).default(50);
46766
+ var Values = external_exports.array(external_exports.string()).optional();
46767
+ function registerSystemTools(server, deps) {
46768
+ const now = deps.now ?? Date.now;
46769
+ const resolveNear = async (near, notes) => {
46770
+ if (near) return near;
46771
+ if (deps.journal) {
46772
+ try {
46773
+ const system = (await deps.journal.state())?.location.system;
46774
+ if (system) return system;
46775
+ } catch (err) {
46776
+ notes.push(`Journal unavailable: ${err instanceof Error ? err.message : String(err)}`);
46777
+ }
46778
+ }
46779
+ const why = notes.length ? ` (${notes.join(" ")})` : "";
46780
+ throw new UserError(`Give near, or set up the journal to use the current system${why}.`);
46781
+ };
46782
+ const ageHours2 = (s) => s.updated_at ? Math.round((now() - Date.parse(s.updated_at)) / 36e5 * 10) / 10 : null;
46783
+ const leg = (ly, t) => legSeconds({ distanceLy: ly, arrivalLs: null, planetary: false, stationKnown: false }, t);
46784
+ const nearestWith = (s, ok2) => {
46785
+ const ls = s.stations.filter((x) => ok2(x.pad)).map((x) => x.arrival_ls ?? Infinity);
46786
+ const min3 = Math.min(...ls);
46787
+ return Number.isFinite(min3) ? Math.round(min3) : null;
46788
+ };
46789
+ const pads = (s) => ({
46790
+ L: nearestWith(s, (p) => p === "L"),
46791
+ M: nearestWith(s, (p) => p === "L" || p === "M"),
46792
+ S: nearestWith(s, (p) => p !== null)
46793
+ });
46794
+ server.registerTool(
46795
+ "systems_by_state",
46796
+ {
46797
+ annotations: {
46798
+ title: "Systems by Faction State",
46799
+ readOnlyHint: true,
46800
+ destructiveHint: false,
46801
+ idempotentHint: true,
46802
+ openWorldHint: true
46803
+ },
46804
+ 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.",
46805
+ inputSchema: {
46806
+ near: external_exports.string().optional().describe("Exact reference system name"),
46807
+ radius_ly: Radius,
46808
+ state: Values.describe("Faction states to match, e.g. ['War', 'Civil War']"),
46809
+ include_pending: external_exports.boolean().default(false),
46810
+ controlling_only: external_exports.boolean().default(false),
46811
+ government: Values,
46812
+ allegiance: Values,
46813
+ power: Values.describe("Controlling power"),
46814
+ security: Values,
46815
+ limit: Limit
46816
+ }
46817
+ },
46818
+ wrap(
46819
+ async (a) => {
46820
+ const q = {
46821
+ states: a.state,
46822
+ includePending: a.include_pending,
46823
+ controllingOnly: a.controlling_only,
46824
+ government: a.government,
46825
+ allegiance: a.allegiance,
46826
+ power: a.power,
46827
+ security: a.security
46828
+ };
46829
+ if (!hasFilter(q)) {
46830
+ throw new UserError(
46831
+ "Give at least one of state, government, allegiance, power or security."
46832
+ );
46833
+ }
46834
+ if (a.controlling_only && !a.state?.length) {
46835
+ throw new UserError(
46836
+ "controlling_only needs state: it limits the state match to the controlling faction."
46837
+ );
46838
+ }
46839
+ const notes = [];
46840
+ const near = await resolveNear(a.near, notes);
46841
+ const rules = await resolveSearchRules(deps, void 0, true);
46842
+ const sweep = await deps.spansh.populatedSystems(near, a.radius_ly, {
46843
+ deadline: now() + TIME_LIMIT_MS2
46844
+ });
46845
+ const matches2 = systemsByState(
46846
+ sweep.systems,
46847
+ sweep.centre,
46848
+ Math.min(a.radius_ly, sweep.coveredLy),
46849
+ q
46850
+ );
46851
+ const shown = matches2.slice(0, a.limit);
46852
+ const stale = shown.filter((m) => (ageHours2(m.system) ?? 0) > STALE_HOURS).length;
46853
+ if (stale) notes.push(`${stale} results have data more than ${STALE_HOURS / 24} days old.`);
46854
+ if (sweep.partial) {
46855
+ notes.push(partialNote(sweep.coveredLy));
46856
+ }
46857
+ return {
46858
+ near,
46859
+ searched: sweep.systems.length,
46860
+ travel: describeRules(rules).travel,
46861
+ total_matches: matches2.length,
46862
+ partial: sweep.partial,
46863
+ ...sweep.partial ? { covered_ly: Math.floor(sweep.coveredLy * 10) / 10 } : {},
46864
+ systems: shown.map((m) => {
46865
+ const s = m.system;
46866
+ const war = m.factions.some(
46867
+ (f) => f.active_states.some((x) => /^(war|civil war)$/i.test(x))
46868
+ );
46869
+ const nearest3 = pads(s);
46870
+ const bestPad = nearest3.L !== null ? "L" : nearest3.M !== null ? "M" : nearest3.S !== null ? "S" : null;
46871
+ return {
46872
+ system: s.name,
46873
+ distance_ly: Math.round(m.distanceLy * 100) / 100,
46874
+ jumps: leg(m.distanceLy, rules.travel).jumps,
46875
+ matching_factions: m.factions.map((f) => ({
46876
+ name: f.name,
46877
+ controlling: f.controlling,
46878
+ influence: f.influence,
46879
+ active_states: f.active_states,
46880
+ pending_states: f.pending_states
46881
+ })),
46882
+ government: s.government,
46883
+ allegiance: s.allegiance,
46884
+ security: s.security,
46885
+ controlling_power: s.controlling_power,
46886
+ power_state: s.power_state,
46887
+ best_pad: bestPad,
46888
+ nearest_station_ls: bestPad ? nearest3[bestPad] : null,
46889
+ updated_at: s.updated_at,
46890
+ age_hours: ageHours2(s),
46891
+ ...war ? { conflict_zones: "likely" } : {}
46892
+ };
46893
+ }),
46894
+ notes
46895
+ };
46896
+ }
46897
+ )
46898
+ );
46899
+ server.registerTool(
46900
+ "massacre_targets",
46901
+ {
46902
+ annotations: {
46903
+ title: "Find Massacre Targets",
46904
+ readOnlyHint: true,
46905
+ destructiveHint: false,
46906
+ idempotentHint: true,
46907
+ openWorldHint: true
46908
+ },
46909
+ 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.",
46910
+ inputSchema: {
46911
+ near: external_exports.string().optional().describe("Exact reference system name"),
46912
+ radius_ly: Radius.describe("Targets within this distance"),
46913
+ max_star_ls: external_exports.number().min(0).default(1e4),
46914
+ limit: Limit,
46915
+ preferences: PrefsPatchSchema.optional(),
46916
+ use_preferences: external_exports.boolean().default(true)
46917
+ }
46918
+ },
46919
+ wrap(
46920
+ async (a) => {
46921
+ const notes = [];
46922
+ const near = await resolveNear(a.near, notes);
46923
+ const rules = await resolveSearchRules(deps, a.preferences, a.use_preferences);
46924
+ const sweep = await deps.spansh.populatedSystems(near, a.radius_ly + MASSACRE_MARGIN_LY, {
46925
+ deadline: now() + TIME_LIMIT_MS2
46926
+ });
46927
+ const { targets, leftOutSpread } = massacreTargets(sweep.systems, sweep.centre, {
46928
+ // Past covered_ly - margin a source could have a pirate neighbour that was not fetched.
46929
+ // A negative radius lists nothing, not even a target at distance 0.
46930
+ radiusLy: sweep.partial ? Math.min(a.radius_ly, sweep.coveredLy - MASSACRE_MARGIN_LY) : a.radius_ly,
46931
+ maxStarLs: a.max_star_ls
46932
+ });
46933
+ notes.push(
46934
+ "These are candidates: whether the mission boards have massacre missions now depends on recent player activity."
46935
+ );
46936
+ if (leftOutSpread) {
46937
+ notes.push(
46938
+ `${leftOutSpread} targets left out: a star beyond ${a.max_star_ls} ls. Raise max_star_ls to see them.`
46939
+ );
46940
+ }
46941
+ if (sweep.partial) {
46942
+ notes.push(partialNote(sweep.coveredLy));
46943
+ }
46944
+ return {
46945
+ near,
46946
+ searched: sweep.systems.length,
46947
+ left_out_spread: leftOutSpread,
46948
+ partial: sweep.partial,
46949
+ ...sweep.partial ? { covered_ly: Math.floor(sweep.coveredLy * 10) / 10 } : {},
46950
+ travel: describeRules(rules).travel,
46951
+ targets: targets.slice(0, a.limit).map((t) => ({
46952
+ target_faction: t.faction,
46953
+ target_system: t.system.name,
46954
+ distance_ly: Math.round(t.distanceLy * 100) / 100,
46955
+ travel_min: Math.round(leg(t.distanceLy, rules.travel).seconds / 6) / 10,
46956
+ stack_size: t.stackSize,
46957
+ spread_ls: Math.round(t.spreadLs),
46958
+ 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) })),
46959
+ sources: t.sources.map((s) => {
46960
+ const counts2 = { federation: 0, empire: 0, alliance: 0, independent: 0 };
46961
+ for (const f of s.system.factions) {
46962
+ const k = f.allegiance?.toLowerCase();
46963
+ if (!isAnarchy(f) && k && k in counts2) counts2[k]++;
46964
+ }
46965
+ return {
46966
+ system: s.system.name,
46967
+ primary: s.primary,
46968
+ other_pirates: s.otherPirates,
46969
+ distance_to_target_ly: Math.round(s.distanceToTargetLy * 100) / 100,
46970
+ allegiance_counts: counts2,
46971
+ pads: pads(s.system),
46972
+ factions: s.system.factions.map((f) => ({
46973
+ name: f.name,
46974
+ allegiance: f.allegiance,
46975
+ active_states: f.active_states
46976
+ })),
46977
+ updated_at: s.system.updated_at,
46978
+ age_hours: ageHours2(s.system)
46979
+ };
46980
+ }),
46981
+ excluded: t.excluded
46982
+ })),
46983
+ notes
46984
+ };
46985
+ }
46986
+ )
46987
+ );
46988
+ }
46989
+
46372
46990
  // src/tools/inara.ts
46373
46991
  function registerInaraTools(server, deps) {
46374
46992
  server.registerTool(
@@ -47541,15 +48159,8 @@ function registerStationTools(server, deps) {
47541
48159
  }
47542
48160
 
47543
48161
  // src/tools/spansh.ts
47544
- var Service = external_exports.enum([
47545
- "material_trader_raw",
47546
- "material_trader_manufactured",
47547
- "material_trader_encoded",
47548
- "tech_broker_human",
47549
- "tech_broker_guardian",
47550
- "interstellar_factors"
47551
- ]);
47552
- var Limit = external_exports.number().int().min(1).max(50).default(10);
48162
+ var Service = external_exports.enum(STATION_SERVICES);
48163
+ var Limit2 = external_exports.number().int().min(1).max(50).default(10);
47553
48164
  function registerSpanshTools(server, deps) {
47554
48165
  const { spansh } = deps;
47555
48166
  const now = deps.now ?? Date.now;
@@ -47563,7 +48174,7 @@ function registerSpanshTools(server, deps) {
47563
48174
  idempotentHint: true,
47564
48175
  openWorldHint: true
47565
48176
  },
47566
- 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.",
48177
+ 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.",
47567
48178
  inputSchema: {
47568
48179
  near: external_exports.string().describe("Exact reference system name"),
47569
48180
  module: external_exports.object({
@@ -47577,7 +48188,7 @@ function registerSpanshTools(server, deps) {
47577
48188
  max_distance_ly: external_exports.number().positive().optional(),
47578
48189
  preferences: PrefsPatchSchema.optional(),
47579
48190
  use_preferences: external_exports.boolean().default(true),
47580
- limit: Limit
48191
+ limit: Limit2
47581
48192
  }
47582
48193
  },
47583
48194
  wrap(
@@ -47611,7 +48222,7 @@ function registerSpanshTools(server, deps) {
47611
48222
  openWorldHint: true
47612
48223
  },
47613
48224
  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.",
47614
- inputSchema: { name: external_exports.string(), limit: Limit }
48225
+ inputSchema: { name: external_exports.string(), limit: Limit2 }
47615
48226
  },
47616
48227
  wrap(async (a) => spansh.system(a.name, a.limit))
47617
48228
  );
@@ -48230,6 +48841,13 @@ function createServer3(deps) {
48230
48841
  currentShip: deps.currentShip,
48231
48842
  jumpRange: deps.jumpRange
48232
48843
  });
48844
+ registerSystemTools(server, {
48845
+ spansh: deps.spansh,
48846
+ journal: deps.journal,
48847
+ prefs: deps.prefs,
48848
+ currentShip: deps.currentShip,
48849
+ jumpRange: deps.jumpRange
48850
+ });
48233
48851
  }
48234
48852
  if (deps.catalog && deps.spansh) {
48235
48853
  registerMaterialsTools(server, {