ed-mcp 0.4.0 → 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)
@@ -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.0";
28696
28756
 
28697
28757
  // src/config.ts
28698
28758
  var ConfigError = class extends Error {
@@ -32080,25 +32140,139 @@ 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.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
+
32083
32232
  // src/spansh/client.ts
32084
- var SERVICE_FILTERS = {
32233
+ var TRADER_FILTERS = {
32085
32234
  material_trader_raw: { material_trader: { value: ["Raw"] } },
32086
32235
  material_trader_manufactured: { material_trader: { value: ["Manufactured"] } },
32087
32236
  material_trader_encoded: { material_trader: { value: ["Encoded"] } },
32088
32237
  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
- };
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
+ }
32092
32271
  var COUNT_CAP = 1e4;
32093
32272
  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
32273
  var MAX_JOBS = 100;
32101
32274
  var SEARCH_PAGE_SIZE = 50;
32275
+ var SWEEP_PAGE_SIZE = 500;
32102
32276
  var CARRIER_TYPE = "Drake-Class Carrier";
32103
32277
  var NON_CARRIER_TYPES = [
32104
32278
  "Asteroid base",
@@ -32131,6 +32305,7 @@ var SpanshClient = class {
32131
32305
  this.now = opts.now ?? Date.now;
32132
32306
  this.systemCache = opts.systemCache ?? cache;
32133
32307
  this.marketCache = opts.marketCache ?? cache;
32308
+ this.sweepCache = opts.sweepCache ?? new SweepCache();
32134
32309
  }
32135
32310
  http;
32136
32311
  cache;
@@ -32141,10 +32316,12 @@ var SpanshClient = class {
32141
32316
  now;
32142
32317
  systemCache;
32143
32318
  marketCache;
32319
+ sweepCache;
32144
32320
  // Job ids by route form, so a call after a timeout collects the job instead of resubmitting.
32145
32321
  routeJobs = /* @__PURE__ */ new Map();
32146
32322
  tradeJobs = /* @__PURE__ */ new Map();
32147
32323
  tradeSubmits = /* @__PURE__ */ new Map();
32324
+ sweeps = /* @__PURE__ */ new Map();
32148
32325
  gate = Promise.resolve();
32149
32326
  lastStart = 0;
32150
32327
  async resolveShip(name) {
@@ -32169,7 +32346,7 @@ var SpanshClient = class {
32169
32346
  shipName = await this.resolveShip(q.ship);
32170
32347
  filters.ships = [{ name: { value: [shipName] } }];
32171
32348
  } else if (q.service) {
32172
- Object.assign(filters, SERVICE_FILTERS[q.service]);
32349
+ Object.assign(filters, serviceFilter(q.service));
32173
32350
  }
32174
32351
  if (q.largePad) filters.has_large_pad = { value: true };
32175
32352
  if (q.maxDistanceLy !== void 0) filters.distance = { min: 0, max: q.maxDistanceLy };
@@ -32236,7 +32413,7 @@ var SpanshClient = class {
32236
32413
  name: s.name,
32237
32414
  type: s.type ?? null,
32238
32415
  arrival_ls: s.distance_to_arrival ?? null,
32239
- pad: pad(s),
32416
+ pad: stationPad(s),
32240
32417
  market: s.has_market ?? false,
32241
32418
  market_id: s.market_id ?? null,
32242
32419
  outfitting: s.has_outfitting ?? false,
@@ -32253,6 +32430,66 @@ var SpanshClient = class {
32253
32430
  );
32254
32431
  return detail.record.needs_permit ?? null;
32255
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
+ }
32256
32493
  async route(q, opts = {}) {
32257
32494
  const form = new URLSearchParams({
32258
32495
  from: q.from,
@@ -32548,7 +32785,7 @@ var SpanshClient = class {
32548
32785
  distance_ly: s.distance ?? null,
32549
32786
  arrival_ls: s.distance_to_arrival ?? null,
32550
32787
  coords: { x, y, z: z2 },
32551
- pad: pad(s),
32788
+ pad: stationPad(s),
32552
32789
  type: s.type ?? null,
32553
32790
  planetary: s.is_planetary ?? false,
32554
32791
  market_age_hours: Number.isNaN(updated) ? null : Math.round((now - updated) / 36e5 * 10) / 10,
@@ -32579,7 +32816,7 @@ var SpanshClient = class {
32579
32816
  distance_ly: s.distance ?? 0,
32580
32817
  arrival_ls: s.distance_to_arrival ?? null,
32581
32818
  type: s.type ?? null,
32582
- pad: pad(s),
32819
+ pad: stationPad(s),
32583
32820
  planetary: s.is_planetary ?? null,
32584
32821
  price,
32585
32822
  market_updated_at: s.market_updated_at ?? null,
@@ -34610,9 +34847,12 @@ function buildDeps(config2, opts = {}) {
34610
34847
  caches.register(spanshSystemCache);
34611
34848
  const spanshMarketCache = new TtlCache("spansh-markets", 6e5);
34612
34849
  caches.register(spanshMarketCache);
34850
+ const spanshSweepCache = new SweepCache();
34851
+ caches.register(spanshSweepCache);
34613
34852
  const spansh = new SpanshClient(http, spanshCache, {
34614
34853
  systemCache: spanshSystemCache,
34615
- marketCache: spanshMarketCache
34854
+ marketCache: spanshMarketCache,
34855
+ sweepCache: spanshSweepCache
34616
34856
  });
34617
34857
  const capiHttp = new HttpClient({ userAgent: `EDCD-EdMcp-${VERSION}`, fetchFn: opts.fetchFn });
34618
34858
  const capiCache = new TtlCache("capi", 6e4);
@@ -41918,6 +42158,15 @@ var McpServer = class {
41918
42158
  this._promptHandlersInitialized = false;
41919
42159
  this.server = new Server(serverInfo, options);
41920
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
+ }
41921
42170
  }
41922
42171
  /**
41923
42172
  * Access experimental features.
@@ -43245,10 +43494,10 @@ function slotInfo(hull, slot, item) {
43245
43494
  return { slot, kind: kind2, size };
43246
43495
  }
43247
43496
  function bulkheadFromItem(hull, item) {
43248
- const lower = item.toLowerCase();
43497
+ const lower2 = item.toLowerCase();
43249
43498
  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}`));
43499
+ if (lower2.endsWith("_armour_grade1_default")) return hull.bulkheads[0] ?? null;
43500
+ const i = BULKHEAD_SUFFIXES.findIndex((s) => lower2.endsWith(`_armour_${s}`));
43252
43501
  return i < 0 ? null : hull.bulkheads[i + offset] ?? null;
43253
43502
  }
43254
43503
  function bulkheadItem(hull, module) {
@@ -44712,13 +44961,13 @@ function resolveRules(stored, override, usePreferences, auto, jumpAuto = { ly: n
44712
44961
  const prefs = applyPatch(usePreferences ? stored : OFF_PREFS, override ?? {});
44713
44962
  const p = prefs.stations.min_pad;
44714
44963
  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" };
44964
+ const pad = p === "auto" ? auto : p === "none" ? { size: null, source: fromRequest ? "request" : "none" } : { size: p, source: fromRequest ? "request" : "preferences" };
44716
44965
  const range = prefs.travel.jump_range_ly;
44717
44966
  const fromRequestRange = override?.travel?.jump_range_ly !== void 0;
44718
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" };
44719
44968
  return {
44720
44969
  prefs,
44721
- pad: pad2,
44970
+ pad,
44722
44971
  travel: { ...prefs.travel, jump_range_ly: ly },
44723
44972
  jumpRangeSource: source
44724
44973
  };
@@ -45092,11 +45341,11 @@ function union2(a, b) {
45092
45341
  async function resolveSearchRules(deps, preferences, usePreferences, largePad) {
45093
45342
  const stored = deps.prefs && usePreferences ? await deps.prefs.get() : DEFAULT_PREFS;
45094
45343
  const override = largePad ? { ...preferences, stations: { ...preferences?.stations, min_pad: "L" } } : preferences ?? {};
45095
- let pad2 = { size: null, source: "none" };
45344
+ let pad = { size: null, source: "none" };
45096
45345
  let jump2;
45097
- const first = resolveRules(stored, override, usePreferences, pad2);
45346
+ const first = resolveRules(stored, override, usePreferences, pad);
45098
45347
  if (first.prefs.stations.min_pad === "auto") {
45099
- pad2 = await autoPad(deps.currentShip ?? (async () => null));
45348
+ pad = await autoPad(deps.currentShip ?? (async () => null));
45100
45349
  }
45101
45350
  if (first.prefs.travel.jump_range_ly === "auto") {
45102
45351
  const max = await deps.jumpRange?.().catch(() => null);
@@ -45108,7 +45357,7 @@ async function resolveSearchRules(deps, preferences, usePreferences, largePad) {
45108
45357
  };
45109
45358
  }
45110
45359
  }
45111
- return resolveRules(stored, override, usePreferences, pad2, jump2);
45360
+ return resolveRules(stored, override, usePreferences, pad, jump2);
45112
45361
  }
45113
45362
 
45114
45363
  // src/tools/materials.ts
@@ -45299,7 +45548,7 @@ function legBetween(from, to, travel) {
45299
45548
  if (same(from.system, to.system) && same(from.station, to.station)) {
45300
45549
  return { jumps: 0, seconds: 0, distanceLy: 0 };
45301
45550
  }
45302
- const distanceLy = same(from.system, to.system) ? 0 : Math.hypot(
45551
+ const distanceLy2 = same(from.system, to.system) ? 0 : Math.hypot(
45303
45552
  from.coords.x - to.coords.x,
45304
45553
  from.coords.y - to.coords.y,
45305
45554
  from.coords.z - to.coords.z
@@ -45307,14 +45556,14 @@ function legBetween(from, to, travel) {
45307
45556
  return {
45308
45557
  ...legSeconds(
45309
45558
  {
45310
- distanceLy,
45559
+ distanceLy: distanceLy2,
45311
45560
  arrivalLs: to.arrivalLs,
45312
45561
  planetary: to.planetary,
45313
45562
  stationKnown: to.station !== null
45314
45563
  },
45315
45564
  travel
45316
45565
  ),
45317
- distanceLy
45566
+ distanceLy: distanceLy2
45318
45567
  };
45319
45568
  }
45320
45569
 
@@ -46369,6 +46618,335 @@ function registerLoopTools(server, deps) {
46369
46618
  }
46370
46619
  }
46371
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
+
46372
46950
  // src/tools/inara.ts
46373
46951
  function registerInaraTools(server, deps) {
46374
46952
  server.registerTool(
@@ -47541,15 +48119,8 @@ function registerStationTools(server, deps) {
47541
48119
  }
47542
48120
 
47543
48121
  // 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);
48122
+ var Service = external_exports.enum(STATION_SERVICES);
48123
+ var Limit2 = external_exports.number().int().min(1).max(50).default(10);
47553
48124
  function registerSpanshTools(server, deps) {
47554
48125
  const { spansh } = deps;
47555
48126
  const now = deps.now ?? Date.now;
@@ -47563,7 +48134,7 @@ function registerSpanshTools(server, deps) {
47563
48134
  idempotentHint: true,
47564
48135
  openWorldHint: true
47565
48136
  },
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.",
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.",
47567
48138
  inputSchema: {
47568
48139
  near: external_exports.string().describe("Exact reference system name"),
47569
48140
  module: external_exports.object({
@@ -47577,7 +48148,7 @@ function registerSpanshTools(server, deps) {
47577
48148
  max_distance_ly: external_exports.number().positive().optional(),
47578
48149
  preferences: PrefsPatchSchema.optional(),
47579
48150
  use_preferences: external_exports.boolean().default(true),
47580
- limit: Limit
48151
+ limit: Limit2
47581
48152
  }
47582
48153
  },
47583
48154
  wrap(
@@ -47611,7 +48182,7 @@ function registerSpanshTools(server, deps) {
47611
48182
  openWorldHint: true
47612
48183
  },
47613
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.",
47614
- inputSchema: { name: external_exports.string(), limit: Limit }
48185
+ inputSchema: { name: external_exports.string(), limit: Limit2 }
47615
48186
  },
47616
48187
  wrap(async (a) => spansh.system(a.name, a.limit))
47617
48188
  );
@@ -48230,6 +48801,13 @@ function createServer3(deps) {
48230
48801
  currentShip: deps.currentShip,
48231
48802
  jumpRange: deps.jumpRange
48232
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
+ });
48233
48811
  }
48234
48812
  if (deps.catalog && deps.spansh) {
48235
48813
  registerMaterialsTools(server, {