ed-mcp 0.5.0 → 0.6.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
@@ -103,7 +103,7 @@ var require_code = __commonJS({
103
103
  }
104
104
  exports._ = _;
105
105
  var plus = new _Code("+");
106
- function str5(strs, ...args) {
106
+ function str6(strs, ...args) {
107
107
  const expr = [safeStringify(strs[0])];
108
108
  let i = 0;
109
109
  while (i < args.length) {
@@ -114,7 +114,7 @@ var require_code = __commonJS({
114
114
  optimize(expr);
115
115
  return new _Code(expr);
116
116
  }
117
- exports.str = str5;
117
+ exports.str = str6;
118
118
  function addCodeArg(code, arg) {
119
119
  if (arg instanceof _Code)
120
120
  code.push(...arg._items);
@@ -157,7 +157,7 @@ var require_code = __commonJS({
157
157
  return;
158
158
  }
159
159
  function strConcat(c1, c2) {
160
- return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str5`${c1}${c2}`;
160
+ return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str6`${c1}${c2}`;
161
161
  }
162
162
  exports.strConcat = strConcat;
163
163
  function interpolate(x) {
@@ -1119,22 +1119,22 @@ var require_util = __commonJS({
1119
1119
  return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
1120
1120
  }
1121
1121
  exports.schemaRefOrVal = schemaRefOrVal;
1122
- function unescapeFragment(str5) {
1123
- return unescapeJsonPointer(decodeURIComponent(str5));
1122
+ function unescapeFragment(str6) {
1123
+ return unescapeJsonPointer(decodeURIComponent(str6));
1124
1124
  }
1125
1125
  exports.unescapeFragment = unescapeFragment;
1126
- function escapeFragment(str5) {
1127
- return encodeURIComponent(escapeJsonPointer(str5));
1126
+ function escapeFragment(str6) {
1127
+ return encodeURIComponent(escapeJsonPointer(str6));
1128
1128
  }
1129
1129
  exports.escapeFragment = escapeFragment;
1130
- function escapeJsonPointer(str5) {
1131
- if (typeof str5 == "number")
1132
- return `${str5}`;
1133
- return str5.replace(/~/g, "~0").replace(/\//g, "~1");
1130
+ function escapeJsonPointer(str6) {
1131
+ if (typeof str6 == "number")
1132
+ return `${str6}`;
1133
+ return str6.replace(/~/g, "~0").replace(/\//g, "~1");
1134
1134
  }
1135
1135
  exports.escapeJsonPointer = escapeJsonPointer;
1136
- function unescapeJsonPointer(str5) {
1137
- return str5.replace(/~1/g, "/").replace(/~0/g, "~");
1136
+ function unescapeJsonPointer(str6) {
1137
+ return str6.replace(/~1/g, "/").replace(/~0/g, "~");
1138
1138
  }
1139
1139
  exports.unescapeJsonPointer = unescapeJsonPointer;
1140
1140
  function eachItem(xs, f) {
@@ -2159,8 +2159,8 @@ var require_json_schema_traverse = __commonJS({
2159
2159
  post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
2160
2160
  }
2161
2161
  }
2162
- function escapeJsonPtr(str5) {
2163
- return str5.replace(/~/g, "~0").replace(/\//g, "~1");
2162
+ function escapeJsonPtr(str6) {
2163
+ return str6.replace(/~/g, "~0").replace(/\//g, "~1");
2164
2164
  }
2165
2165
  }
2166
2166
  });
@@ -3257,10 +3257,10 @@ var require_utils = __commonJS({
3257
3257
  isIPV6: true
3258
3258
  };
3259
3259
  }
3260
- function findToken(str5, token) {
3260
+ function findToken(str6, token) {
3261
3261
  let ind = 0;
3262
- for (let i = 0; i < str5.length; i++) {
3263
- if (str5[i] === token) ind++;
3262
+ for (let i = 0; i < str6.length; i++) {
3263
+ if (str6[i] === token) ind++;
3264
3264
  }
3265
3265
  return ind;
3266
3266
  }
@@ -4330,7 +4330,7 @@ var require_core = __commonJS({
4330
4330
  var util_1 = require_util();
4331
4331
  var $dataRefSchema = require_data();
4332
4332
  var uri_1 = require_uri();
4333
- var defaultRegExp = (str5, flags) => new RegExp(str5, flags);
4333
+ var defaultRegExp = (str6, flags) => new RegExp(str6, flags);
4334
4334
  defaultRegExp.code = "new RegExp";
4335
4335
  var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
4336
4336
  var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
@@ -5125,16 +5125,16 @@ var require_ucs2length = __commonJS({
5125
5125
  "node_modules/ajv/dist/runtime/ucs2length.js"(exports) {
5126
5126
  "use strict";
5127
5127
  Object.defineProperty(exports, "__esModule", { value: true });
5128
- function ucs2length(str5) {
5129
- const len = str5.length;
5128
+ function ucs2length(str6) {
5129
+ const len = str6.length;
5130
5130
  let length = 0;
5131
5131
  let pos = 0;
5132
5132
  let value;
5133
5133
  while (pos < len) {
5134
5134
  length++;
5135
- value = str5.charCodeAt(pos++);
5135
+ value = str6.charCodeAt(pos++);
5136
5136
  if (value >= 55296 && value <= 56319 && pos < len) {
5137
- value = str5.charCodeAt(pos);
5137
+ value = str6.charCodeAt(pos);
5138
5138
  if ((value & 64512) === 56320)
5139
5139
  pos++;
5140
5140
  }
@@ -7017,8 +7017,8 @@ var require_formats = __commonJS({
7017
7017
  }
7018
7018
  var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
7019
7019
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
7020
- function date5(str5) {
7021
- const matches2 = DATE.exec(str5);
7020
+ function date5(str6) {
7021
+ const matches2 = DATE.exec(str6);
7022
7022
  if (!matches2)
7023
7023
  return false;
7024
7024
  const year = +matches2[1];
@@ -7037,8 +7037,8 @@ var require_formats = __commonJS({
7037
7037
  }
7038
7038
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
7039
7039
  function getTime(strictTimeZone) {
7040
- return function time3(str5) {
7041
- const matches2 = TIME.exec(str5);
7040
+ return function time3(str6) {
7041
+ const matches2 = TIME.exec(str6);
7042
7042
  if (!matches2)
7043
7043
  return false;
7044
7044
  const hr = +matches2[1];
@@ -7084,8 +7084,8 @@ var require_formats = __commonJS({
7084
7084
  var DATE_TIME_SEPARATOR = /t|\s/i;
7085
7085
  function getDateTime(strictTimeZone) {
7086
7086
  const time3 = getTime(strictTimeZone);
7087
- return function date_time(str5) {
7088
- const dateTime = str5.split(DATE_TIME_SEPARATOR);
7087
+ return function date_time(str6) {
7088
+ const dateTime = str6.split(DATE_TIME_SEPARATOR);
7089
7089
  return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
7090
7090
  };
7091
7091
  }
@@ -7110,13 +7110,13 @@ var require_formats = __commonJS({
7110
7110
  }
7111
7111
  var NOT_URI_FRAGMENT = /\/|:/;
7112
7112
  var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
7113
- function uri(str5) {
7114
- return NOT_URI_FRAGMENT.test(str5) && URI.test(str5);
7113
+ function uri(str6) {
7114
+ return NOT_URI_FRAGMENT.test(str6) && URI.test(str6);
7115
7115
  }
7116
7116
  var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
7117
- function byte(str5) {
7117
+ function byte(str6) {
7118
7118
  BYTE.lastIndex = 0;
7119
- return BYTE.test(str5);
7119
+ return BYTE.test(str6);
7120
7120
  }
7121
7121
  var MIN_INT32 = -(2 ** 31);
7122
7122
  var MAX_INT32 = 2 ** 31 - 1;
@@ -7130,11 +7130,11 @@ var require_formats = __commonJS({
7130
7130
  return true;
7131
7131
  }
7132
7132
  var Z_ANCHOR = /[^\\]\\Z/;
7133
- function regex(str5) {
7134
- if (Z_ANCHOR.test(str5))
7133
+ function regex(str6) {
7134
+ if (Z_ANCHOR.test(str6))
7135
7135
  return false;
7136
7136
  try {
7137
- new RegExp(str5);
7137
+ new RegExp(str6);
7138
7138
  return true;
7139
7139
  } catch (e) {
7140
7140
  return false;
@@ -8095,14 +8095,14 @@ function promiseAllObject(promisesObj) {
8095
8095
  }
8096
8096
  function randomString(length = 10) {
8097
8097
  const chars = "abcdefghijklmnopqrstuvwxyz";
8098
- let str5 = "";
8098
+ let str6 = "";
8099
8099
  for (let i = 0; i < length; i++) {
8100
- str5 += chars[Math.floor(Math.random() * chars.length)];
8100
+ str6 += chars[Math.floor(Math.random() * chars.length)];
8101
8101
  }
8102
- return str5;
8102
+ return str6;
8103
8103
  }
8104
- function esc(str5) {
8105
- return JSON.stringify(str5);
8104
+ function esc(str6) {
8105
+ return JSON.stringify(str6);
8106
8106
  }
8107
8107
  function slugify(input2) {
8108
8108
  return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -8216,8 +8216,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
8216
8216
  "symbol",
8217
8217
  "undefined"
8218
8218
  ]);
8219
- function escapeRegex(str5) {
8220
- return str5.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8219
+ function escapeRegex(str6) {
8220
+ return str6.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8221
8221
  }
8222
8222
  function clone(inst, def, params) {
8223
8223
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -8483,13 +8483,13 @@ function getSizableOrigin(input2) {
8483
8483
  return "unknown";
8484
8484
  }
8485
8485
  var highSurrogate = /[\uD800-\uDBFF]/;
8486
- function codePointLength(str5) {
8487
- const units = str5.length;
8488
- if (!highSurrogate.test(str5))
8486
+ function codePointLength(str6) {
8487
+ const units = str6.length;
8488
+ if (!highSurrogate.test(str6))
8489
8489
  return units;
8490
8490
  let count = units;
8491
8491
  for (let i = 0; i < units - 1; i++) {
8492
- if ((str5.charCodeAt(i) & 64512) === 55296 && (str5.charCodeAt(i + 1) & 64512) === 56320) {
8492
+ if ((str6.charCodeAt(i) & 64512) === 55296 && (str6.charCodeAt(i + 1) & 64512) === 56320) {
8493
8493
  count--;
8494
8494
  i++;
8495
8495
  }
@@ -28752,7 +28752,7 @@ async function canonical(p) {
28752
28752
  }
28753
28753
 
28754
28754
  // src/version.ts
28755
- var VERSION = "0.5.0";
28755
+ var VERSION = "0.6.0";
28756
28756
 
28757
28757
  // src/config.ts
28758
28758
  var ConfigError = class extends Error {
@@ -28817,6 +28817,7 @@ function loadConfig(env2, dirs, platform = process.platform) {
28817
28817
  legacyPlansEnv: get(env2, "ED_MCP_PLANS"),
28818
28818
  plansMarkdownDir: path3.join(dataDir, "plans"),
28819
28819
  prefsFile: path3.join(dataDir, "preferences.json"),
28820
+ activityFile: path3.join(dataDir, "activity.json"),
28820
28821
  secretsFile: syncPassword ? path3.join(dataDir, "secrets.enc") : path3.join(dirs.config, "secrets.json"),
28821
28822
  locksDir: path3.join(dirs.config, "locks"),
28822
28823
  cacheDir: dirs.cache,
@@ -29754,8 +29755,8 @@ var Catalog = class {
29754
29755
  ).join(", ")}`
29755
29756
  );
29756
29757
  }
29757
- const same4 = m.trade_group ? this.d.materials.filter((x) => x.kind === m.kind && x.trade_group === m.trade_group).sort((a, b) => a.grade - b.grade) : [m];
29758
- return { ...m, same_group: same4, trade_rules: TRADE_RULES };
29758
+ const same6 = m.trade_group ? this.d.materials.filter((x) => x.kind === m.kind && x.trade_group === m.trade_group).sort((a, b) => a.grade - b.grade) : [m];
29759
+ return { ...m, same_group: same6, trade_rules: TRADE_RULES };
29759
29760
  }
29760
29761
  /** Coriolis feature ranges, [min, max], for a blueprint id at a grade on a module group, or null. */
29761
29762
  blueprintFeatures(blueprintId2, grade, grp) {
@@ -31701,6 +31702,333 @@ function buildState(events) {
31701
31702
  };
31702
31703
  }
31703
31704
 
31705
+ // src/journal/bodies.ts
31706
+ var BODY_EVENTS = [
31707
+ "Scan",
31708
+ "FSSDiscoveryScan",
31709
+ "FSSAllBodiesFound",
31710
+ "FSSBodySignals",
31711
+ "SAASignalsFound",
31712
+ "SAAScanComplete",
31713
+ "ScanOrganic",
31714
+ "SellOrganicData",
31715
+ "Died"
31716
+ ];
31717
+ var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
31718
+ var str3 = (v) => typeof v === "string" && v !== "" ? v : null;
31719
+ var objs2 = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "object" && x !== null) : [];
31720
+ var same = (a, b) => a !== null && a.toLowerCase() === b.toLowerCase();
31721
+ function systemAddress(events, system) {
31722
+ for (let i = events.length - 1; i >= 0; i--) {
31723
+ const e = events[i];
31724
+ if (same(str3(e.StarSystem) ?? str3(e.SystemName), system)) {
31725
+ const address = num3(e.SystemAddress);
31726
+ if (address !== null) return address;
31727
+ }
31728
+ }
31729
+ return null;
31730
+ }
31731
+ function signalsOf(e) {
31732
+ return objs2(e.Signals).map((s) => ({
31733
+ type: str3(s.Type_Localised) ?? str3(s.Type) ?? "unknown",
31734
+ count: num3(s.Count) ?? 0
31735
+ }));
31736
+ }
31737
+ function bioCount(e) {
31738
+ return objs2(e.Signals).filter((s) => typeof s.Type === "string" && s.Type.endsWith("SignalType_Biological;")).reduce((n, s) => n + (num3(s.Count) ?? 0), 0);
31739
+ }
31740
+ function bodyNames(events, address) {
31741
+ const names = /* @__PURE__ */ new Map();
31742
+ for (const e of events) {
31743
+ if (num3(e.SystemAddress) !== address) continue;
31744
+ const id = num3(e.BodyID);
31745
+ const name = str3(e.BodyName);
31746
+ if (id !== null && name) names.set(id, name);
31747
+ }
31748
+ return names;
31749
+ }
31750
+ function bodyMatches(name, system, wanted) {
31751
+ if (!name) return false;
31752
+ if (same(name, wanted)) return true;
31753
+ return system !== null && same(name.slice(system.length).trim(), wanted);
31754
+ }
31755
+ function exobiology(events, address, opts = {}) {
31756
+ const names = bodyNames(events, address);
31757
+ const bodies = /* @__PURE__ */ new Map();
31758
+ const body = (id) => {
31759
+ let b = bodies.get(id);
31760
+ if (!b) bodies.set(id, b = { bio: null, genera: /* @__PURE__ */ new Map(), species: /* @__PURE__ */ new Map() });
31761
+ return b;
31762
+ };
31763
+ let unsold = 0;
31764
+ let active2 = null;
31765
+ const drop = () => {
31766
+ if (active2?.species && !active2.species.analysed) {
31767
+ active2.species.samples = 0;
31768
+ active2.species.stage = "0/3";
31769
+ active2.species.abandoned = true;
31770
+ }
31771
+ active2 = null;
31772
+ };
31773
+ for (const e of events) {
31774
+ switch (e.event) {
31775
+ case "SellOrganicData":
31776
+ unsold = 0;
31777
+ break;
31778
+ case "Died":
31779
+ unsold = 0;
31780
+ drop();
31781
+ break;
31782
+ case "ScanOrganic": {
31783
+ if (e.ScanType === "Analyse") unsold++;
31784
+ const key = `${num3(e.SystemAddress)}|${num3(e.Body)}|${str3(e.Species)}`;
31785
+ if (e.ScanType === "Log" && active2 && active2.key !== key) drop();
31786
+ active2 = e.ScanType === "Analyse" ? null : { key, species: active2?.key === key ? active2.species : void 0 };
31787
+ break;
31788
+ }
31789
+ }
31790
+ if (num3(e.SystemAddress) !== address) continue;
31791
+ switch (e.event) {
31792
+ case "FSSBodySignals":
31793
+ case "SAASignalsFound": {
31794
+ const id = num3(e.BodyID);
31795
+ if (id === null) break;
31796
+ const count = bioCount(e);
31797
+ if (count > 0 || bodies.has(id)) body(id).bio = count;
31798
+ for (const g of objs2(e.Genuses)) {
31799
+ const key = str3(g.Genus);
31800
+ if (key) body(id).genera.set(key, str3(g.Genus_Localised) ?? key);
31801
+ }
31802
+ break;
31803
+ }
31804
+ case "ScanOrganic": {
31805
+ const id = num3(e.Body);
31806
+ const key = str3(e.Species);
31807
+ if (id === null || !key) break;
31808
+ const b = body(id);
31809
+ const genusKey = str3(e.Genus) ?? "";
31810
+ const s = b.species.get(key) ?? {
31811
+ genus: str3(e.Genus_Localised) ?? genusKey,
31812
+ genusKey,
31813
+ species: str3(e.Species_Localised) ?? key,
31814
+ variant: null,
31815
+ samples: 0,
31816
+ stage: "",
31817
+ analysed: false
31818
+ };
31819
+ s.variant = str3(e.Variant_Localised) ?? s.variant;
31820
+ if (e.ScanType === "Log") {
31821
+ if (!s.analysed) s.samples = 1;
31822
+ delete s.abandoned;
31823
+ } else if (e.ScanType === "Sample") s.samples = Math.min(3, Math.max(2, s.samples + 1));
31824
+ else if (e.ScanType === "Analyse") {
31825
+ s.samples = 3;
31826
+ s.analysed = true;
31827
+ }
31828
+ s.stage = `${s.samples}/3`;
31829
+ b.species.set(key, s);
31830
+ if (active2 && e.ScanType !== "Analyse") active2.species = s;
31831
+ if (genusKey && !b.genera.has(genusKey)) b.genera.set(genusKey, s.genus);
31832
+ break;
31833
+ }
31834
+ }
31835
+ }
31836
+ const out = [];
31837
+ for (const [id, b] of bodies) {
31838
+ const name = names.get(id) ?? `Body ${id}`;
31839
+ if (opts.body && !bodyMatches(name, opts.system ?? null, opts.body)) continue;
31840
+ const sampled = new Set(
31841
+ [...b.species.values()].filter((s) => !s.abandoned).map((s) => s.genusKey)
31842
+ );
31843
+ out.push({
31844
+ body: name,
31845
+ body_id: id,
31846
+ bio_signals: b.bio,
31847
+ genera: [...b.genera.values()],
31848
+ species: [...b.species.values()].map(({ genusKey: _, ...s }) => s),
31849
+ not_started: [...b.genera].filter(([k]) => !sampled.has(k)).map(([, v]) => v)
31850
+ });
31851
+ }
31852
+ out.sort((a, b) => a.body_id - b.body_id);
31853
+ return { bodies: out, unsold_analysed: unsold };
31854
+ }
31855
+ function exploration(events, address) {
31856
+ const discovery = {
31857
+ body_count: null,
31858
+ non_body_count: null,
31859
+ progress: null,
31860
+ all_bodies_found: false
31861
+ };
31862
+ const scans = /* @__PURE__ */ new Map();
31863
+ const mapped = /* @__PURE__ */ new Set();
31864
+ const signals = /* @__PURE__ */ new Map();
31865
+ for (const e of events) {
31866
+ if (num3(e.SystemAddress) !== address) continue;
31867
+ const id = num3(e.BodyID);
31868
+ switch (e.event) {
31869
+ case "FSSDiscoveryScan":
31870
+ discovery.body_count = num3(e.BodyCount) ?? discovery.body_count;
31871
+ discovery.non_body_count = num3(e.NonBodyCount) ?? discovery.non_body_count;
31872
+ discovery.progress = num3(e.Progress) ?? discovery.progress;
31873
+ break;
31874
+ case "FSSAllBodiesFound":
31875
+ discovery.all_bodies_found = true;
31876
+ discovery.body_count = num3(e.Count) ?? discovery.body_count;
31877
+ break;
31878
+ case "Scan":
31879
+ if (id !== null) scans.set(id, e);
31880
+ break;
31881
+ case "SAAScanComplete":
31882
+ if (id !== null) mapped.add(id);
31883
+ break;
31884
+ case "FSSBodySignals":
31885
+ case "SAASignalsFound":
31886
+ if (id !== null) signals.set(id, signalsOf(e));
31887
+ break;
31888
+ }
31889
+ }
31890
+ const bodies = [...scans].map(([id, s]) => {
31891
+ const distance2 = num3(s.DistanceFromArrivalLS);
31892
+ return {
31893
+ body: str3(s.BodyName) ?? `Body ${id}`,
31894
+ body_id: id,
31895
+ // Belt clusters have neither a planet class nor a star type.
31896
+ type: str3(s.PlanetClass) ?? (str3(s.StarType) ? `${s.StarType} star` : null),
31897
+ distance_ls: distance2 === null ? null : Math.round(distance2),
31898
+ landable: s.Landable === true,
31899
+ terraformable: s.TerraformState === "Terraformable",
31900
+ atmosphere: str3(s.Atmosphere),
31901
+ rings: objs2(s.Rings).map((r) => ({
31902
+ name: str3(r.Name) ?? "unknown",
31903
+ class: str3(r.RingClass)?.replace(/^eRingClass_/, "") ?? null
31904
+ })),
31905
+ was_discovered: typeof s.WasDiscovered === "boolean" ? s.WasDiscovered : null,
31906
+ was_mapped: typeof s.WasMapped === "boolean" ? s.WasMapped : null,
31907
+ mapped_by_you: mapped.has(id),
31908
+ signals: signals.get(id) ?? []
31909
+ };
31910
+ });
31911
+ bodies.sort((a, b) => (a.distance_ls ?? Infinity) - (b.distance_ls ?? Infinity));
31912
+ return { discovery, scanned: bodies.length, bodies };
31913
+ }
31914
+
31915
+ // src/journal/events.ts
31916
+ var MAX_ARRAY = 20;
31917
+ function truncate(value) {
31918
+ if (Array.isArray(value)) {
31919
+ const kept = value.slice(0, MAX_ARRAY).map(truncate);
31920
+ if (value.length > MAX_ARRAY) kept.push(`(${value.length - MAX_ARRAY} more not shown)`);
31921
+ return kept;
31922
+ }
31923
+ if (value && typeof value === "object") {
31924
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, truncate(v)]));
31925
+ }
31926
+ return value;
31927
+ }
31928
+ var LOCATION_EVENTS = /* @__PURE__ */ new Set([
31929
+ "Location",
31930
+ "FSDJump",
31931
+ "CarrierJump",
31932
+ "Docked"
31933
+ ]);
31934
+ function systemAt(locations, timestamp) {
31935
+ for (let i = locations.length - 1; i >= 0; i--) {
31936
+ const e = locations[i];
31937
+ if (e.timestamp <= timestamp && typeof e.StarSystem === "string") return e.StarSystem;
31938
+ }
31939
+ return null;
31940
+ }
31941
+ function matchFile(events, filter, start = null) {
31942
+ const names = filter.events?.length ? new Set(filter.events.map((n) => n.toLowerCase())) : void 0;
31943
+ const system = filter.system?.toLowerCase();
31944
+ let current = start?.toLowerCase() ?? null;
31945
+ const out = [];
31946
+ for (const e of events) {
31947
+ if (LOCATION_EVENTS.has(e.event) && typeof e.StarSystem === "string") {
31948
+ current = e.StarSystem.toLowerCase();
31949
+ }
31950
+ if (names && !names.has(e.event.toLowerCase())) continue;
31951
+ if (filter.since !== void 0 && !(Date.parse(e.timestamp) >= filter.since)) continue;
31952
+ if (system !== void 0) {
31953
+ const own2 = e.event === "StartJump" ? void 0 : e.StarSystem ?? e.SystemName;
31954
+ const at = typeof own2 === "string" ? own2.toLowerCase() : current;
31955
+ if (at !== system) continue;
31956
+ }
31957
+ out.push(e);
31958
+ }
31959
+ return out;
31960
+ }
31961
+
31962
+ // src/journal/massacre.ts
31963
+ var MASSACRE_EVENTS = [
31964
+ "Bounty",
31965
+ "FactionKillBond",
31966
+ "MissionRedirected"
31967
+ ];
31968
+ var same2 = (a, b) => a.toLowerCase() === b.toLowerCase();
31969
+ function massacre(missions, events) {
31970
+ const tracked = missions.filter((m) => (m.kill_count ?? 0) > 0 && m.target_faction && m.accepted).map((m) => ({
31971
+ mission: m,
31972
+ out: {
31973
+ id: m.id,
31974
+ name: m.name,
31975
+ accepted: m.accepted,
31976
+ expires: m.expires,
31977
+ reward: m.reward,
31978
+ kills_required: m.kill_count,
31979
+ kills_counted: 0,
31980
+ kills_left: m.kill_count,
31981
+ done: false
31982
+ }
31983
+ })).sort((a, b) => a.out.accepted.localeCompare(b.out.accepted) || a.out.id - b.out.id);
31984
+ const uncounted = missions.filter((m) => m.details_missing && /massacre/i.test(m.name));
31985
+ const byId = new Map(tracked.map((t) => [t.out.id, t]));
31986
+ for (const e of events) {
31987
+ if (e.event === "MissionRedirected") {
31988
+ const t = byId.get(typeof e.MissionID === "number" ? e.MissionID : -1);
31989
+ if (t?.mission.status === "complete") t.out.done = true;
31990
+ continue;
31991
+ }
31992
+ if (e.event !== "Bounty" && e.event !== "FactionKillBond") continue;
31993
+ const victim = typeof e.VictimFaction === "string" ? e.VictimFaction : null;
31994
+ if (!victim) continue;
31995
+ const credited = /* @__PURE__ */ new Set();
31996
+ for (const { mission, out } of tracked) {
31997
+ const giver = mission.faction ?? "";
31998
+ if (credited.has(giver) || out.done || out.accepted > e.timestamp) continue;
31999
+ if (!same2(mission.target_faction, victim)) continue;
32000
+ out.kills_counted++;
32001
+ credited.add(giver);
32002
+ if (out.kills_counted >= out.kills_required) out.done = true;
32003
+ }
32004
+ }
32005
+ const groups = /* @__PURE__ */ new Map();
32006
+ for (const { mission, out } of tracked) {
32007
+ if (mission.status === "complete") out.done = true;
32008
+ out.kills_left = out.done ? 0 : out.kills_required - out.kills_counted;
32009
+ const target = mission.target_faction;
32010
+ const giver = mission.faction ?? "unknown";
32011
+ const byGiver = groups.get(target) ?? /* @__PURE__ */ new Map();
32012
+ groups.set(target, byGiver);
32013
+ byGiver.set(giver, [...byGiver.get(giver) ?? [], out]);
32014
+ }
32015
+ return {
32016
+ groups: [...groups].map(([target, byGiver]) => {
32017
+ const factions = [...byGiver].map(([faction, list2]) => ({
32018
+ faction,
32019
+ kills_left: list2.reduce((n, m) => n + m.kills_left, 0),
32020
+ missions: list2
32021
+ }));
32022
+ return {
32023
+ target_faction: target,
32024
+ kills_left: Math.max(0, ...factions.map((f) => f.kills_left)),
32025
+ factions
32026
+ };
32027
+ }),
32028
+ uncounted
32029
+ };
32030
+ }
32031
+
31704
32032
  // src/journal/service.ts
31705
32033
  function errText(err) {
31706
32034
  return err.code ?? err.message ?? String(err);
@@ -31712,6 +32040,12 @@ function staleWarning(minutes2) {
31712
32040
  var STATE_WINDOW_MS = 14 * 864e5;
31713
32041
  var FRESH_MS = 864e5;
31714
32042
  var MEMO_MS = 1e3;
32043
+ var RAW_CACHE_FILES = 2;
32044
+ var KEPT_EVENTS = /* @__PURE__ */ new Set([
32045
+ ...STATE_EVENTS,
32046
+ ...BODY_EVENTS,
32047
+ ...MASSACRE_EVENTS
32048
+ ]);
31715
32049
  var STATION_FILES = {
31716
32050
  market: "Market.json",
31717
32051
  outfitting: "Outfitting.json",
@@ -31720,11 +32054,12 @@ var STATION_FILES = {
31720
32054
  var JournalService = class {
31721
32055
  constructor(dir, opts = {}) {
31722
32056
  this.dir = dir;
31723
- this.reader = opts.reader ?? new JournalReader({ keep: STATE_EVENTS });
32057
+ this.reader = opts.reader ?? new JournalReader({ keep: KEPT_EVENTS });
31724
32058
  this.now = opts.now ?? Date.now;
31725
32059
  }
31726
32060
  dir;
31727
32061
  reader;
32062
+ rawReader = new JournalReader();
31728
32063
  now;
31729
32064
  // Timed with performance.now so an injected test clock cannot freeze it.
31730
32065
  memo;
@@ -31850,7 +32185,37 @@ var JournalService = class {
31850
32185
  const ageMinutes = Number.isNaN(age) ? null : Math.max(0, Math.round(age / 6e4));
31851
32186
  if (ageMinutes !== null && age > STALE_MS) warnings.push(staleWarning(ageMinutes));
31852
32187
  const loadout = events.findLast((e) => e.event === "Loadout") ?? null;
31853
- return { ...state, age_minutes: ageMinutes, warnings, loadout };
32188
+ return { ...state, age_minutes: ageMinutes, warnings, loadout, events };
32189
+ }
32190
+ /**
32191
+ * Every recorded event that matches, newest first, from the state window. Files are read newest
32192
+ * first and reading stops once the limit is reached or the files are older than since.
32193
+ */
32194
+ async recentEvents(filter, limit) {
32195
+ const found = await this.files(STATE_WINDOW_MS);
32196
+ if (!("files" in found)) throw new UserError(`${found.missing}. Run journal_status for help.`);
32197
+ if (found.error) throw new UserError(`${found.error}. Run journal_status for help.`);
32198
+ const warnings = [];
32199
+ const out = [];
32200
+ const locations = filter.system ? (await this.state()).events.filter((e) => LOCATION_EVENTS.has(e.event)) : [];
32201
+ for (const file2 of [...found.files].reverse()) {
32202
+ if (out.length >= limit) break;
32203
+ let events;
32204
+ try {
32205
+ events = await this.rawReader.read(file2);
32206
+ } catch (err) {
32207
+ warnings.push(`Cannot read ${file2.name}: ${errText(err)}`);
32208
+ continue;
32209
+ }
32210
+ const first = events[0];
32211
+ const start = first && filter.system ? systemAt(locations, first.timestamp) : null;
32212
+ const matched = matchFile(events, filter, start);
32213
+ for (let i = matched.length - 1; i >= 0 && out.length < limit; i--) out.push(matched[i]);
32214
+ const last = events.at(-1);
32215
+ if (filter.since !== void 0 && last && Date.parse(last.timestamp) < filter.since) break;
32216
+ }
32217
+ this.rawReader.retain(found.files.slice(-RAW_CACHE_FILES).map((f) => f.path));
32218
+ return { events: out, warnings };
31854
32219
  }
31855
32220
  // Newer game builds keep the cargo list here instead of in the Cargo event. Syncthing can
31856
32221
  // deliver the logs before this file, so a file older than the newest Cargo event is ignored.
@@ -31894,16 +32259,16 @@ var JournalService = class {
31894
32259
  const f = await this.stationFile("market");
31895
32260
  if (!f) return null;
31896
32261
  const items = Array.isArray(f.data.Items) ? f.data.Items : [];
31897
- const num6 = (v) => typeof v === "number" ? v : 0;
32262
+ const num7 = (v) => typeof v === "number" ? v : 0;
31898
32263
  const rows = [];
31899
32264
  for (const i of items) {
31900
32265
  const name = i.Name_Localised ?? i.Name;
31901
32266
  if (typeof name !== "string") continue;
31902
32267
  rows.push({
31903
32268
  commodity: name,
31904
- buy_price: num6(i.BuyPrice),
31905
- sell_price: num6(i.SellPrice),
31906
- supply: num6(i.Stock),
32269
+ buy_price: num7(i.BuyPrice),
32270
+ sell_price: num7(i.SellPrice),
32271
+ supply: num7(i.Stock),
31907
32272
  demand: marketDemand(i)
31908
32273
  });
31909
32274
  }
@@ -32176,7 +32541,10 @@ function mapSystem(raw) {
32176
32541
  name: s.name,
32177
32542
  type: s.type ?? null,
32178
32543
  pad: stationPad(s),
32179
- arrival_ls: s.distance_to_arrival ?? null
32544
+ arrival_ls: s.distance_to_arrival ?? null,
32545
+ // System search gives service names; station search gives { name } objects.
32546
+ services: (s.services ?? []).map((x2) => typeof x2 === "string" ? x2 : x2.name),
32547
+ owner: s.controlling_minor_faction ?? null
32180
32548
  })),
32181
32549
  stars: (raw.bodies ?? []).filter((b) => b.type === "Star").map((b) => ({ name: b.name, arrival_ls: b.distance_to_arrival ?? 0 }))
32182
32550
  };
@@ -32201,12 +32569,32 @@ var SweepCache = class {
32201
32569
  find(centre, radiusLy) {
32202
32570
  this.dropExpired();
32203
32571
  const hit = this.sweeps.find(
32204
- (s) => distanceLy(s.centre, centre) + radiusLy <= s.radiusLy + 1e-9
32572
+ (s) => distanceLy(s.centre, centre) + radiusLy <= s.coveredLy + 1e-9
32205
32573
  );
32206
32574
  return hit?.systems.filter((s) => distanceLy(s.coords, centre) <= radiusLy);
32207
32575
  }
32208
- add(centre, radiusLy, systems) {
32209
- this.sweeps.push({ centre, radiusLy, systems, storedAt: this.now() });
32576
+ add(centre, radiusLy, systems, key) {
32577
+ const storedAt = key ? this.sweeps.find((s) => s.resume?.key === key)?.storedAt : void 0;
32578
+ if (key) this.sweeps = this.sweeps.filter((s) => s.resume?.key !== key);
32579
+ this.push({ centre, coveredLy: radiusLy, systems, storedAt: storedAt ?? this.now() });
32580
+ }
32581
+ savePartial(p) {
32582
+ const storedAt = this.sweeps.find((s) => s.resume?.key === p.key)?.storedAt ?? this.now();
32583
+ this.sweeps = this.sweeps.filter((s) => s.resume?.key !== p.key);
32584
+ this.push({
32585
+ centre: p.centre,
32586
+ coveredLy: p.coveredLy,
32587
+ systems: p.systems,
32588
+ storedAt,
32589
+ resume: p
32590
+ });
32591
+ }
32592
+ partial(key) {
32593
+ this.dropExpired();
32594
+ return this.sweeps.find((s) => s.resume?.key === key)?.resume;
32595
+ }
32596
+ push(s) {
32597
+ this.sweeps.push(s);
32210
32598
  while (this.sweeps.length > this.maxSweeps) this.sweeps.shift();
32211
32599
  }
32212
32600
  clear() {
@@ -32216,7 +32604,7 @@ var SweepCache = class {
32216
32604
  }
32217
32605
  stats() {
32218
32606
  this.dropExpired();
32219
- const oldest = this.sweeps[0]?.storedAt;
32607
+ const oldest = this.sweeps.length ? Math.min(...this.sweeps.map((s) => s.storedAt)) : void 0;
32220
32608
  return {
32221
32609
  name: this.name,
32222
32610
  entries: this.sweeps.length,
@@ -32306,6 +32694,7 @@ var SpanshClient = class {
32306
32694
  this.systemCache = opts.systemCache ?? cache;
32307
32695
  this.marketCache = opts.marketCache ?? cache;
32308
32696
  this.sweepCache = opts.sweepCache ?? new SweepCache();
32697
+ this.warCache = opts.warCache ?? cache;
32309
32698
  }
32310
32699
  http;
32311
32700
  cache;
@@ -32317,11 +32706,13 @@ var SpanshClient = class {
32317
32706
  systemCache;
32318
32707
  marketCache;
32319
32708
  sweepCache;
32709
+ warCache;
32320
32710
  // Job ids by route form, so a call after a timeout collects the job instead of resubmitting.
32321
32711
  routeJobs = /* @__PURE__ */ new Map();
32322
32712
  tradeJobs = /* @__PURE__ */ new Map();
32323
32713
  tradeSubmits = /* @__PURE__ */ new Map();
32324
32714
  sweeps = /* @__PURE__ */ new Map();
32715
+ warSearches = /* @__PURE__ */ new Map();
32325
32716
  gate = Promise.resolve();
32326
32717
  lastStart = 0;
32327
32718
  async resolveShip(name) {
@@ -32430,25 +32821,96 @@ var SpanshClient = class {
32430
32821
  );
32431
32822
  return detail.record.needs_permit ?? null;
32432
32823
  }
32433
- /** Populated systems within radiusLy of near. Stops paging at opts.deadline; a partial result is not cached. */
32824
+ /** Populated systems within radiusLy of near. Stops paging at opts.deadline; a partial result is cached and the next call resumes it. */
32434
32825
  async populatedSystems(near, radiusLy, opts = {}) {
32435
32826
  const centre = (await this.system(near, 0)).coords;
32436
32827
  const cached2 = this.sweepCache.find(centre, radiusLy);
32437
- if (cached2) return { centre, systems: cached2, total: cached2.length, partial: false };
32828
+ if (cached2)
32829
+ return { centre, systems: cached2, total: cached2.length, partial: false, coveredLy: radiusLy };
32438
32830
  const key = `${near.toLowerCase()}:${radiusLy}`;
32439
32831
  let running = this.sweeps.get(key);
32440
32832
  if (!running) {
32441
- running = this.sweepPages(near, radiusLy, centre, opts).finally(
32833
+ running = this.sweepPages(near, radiusLy, centre, key, opts).finally(
32442
32834
  () => this.sweeps.delete(key)
32443
32835
  );
32444
32836
  this.sweeps.set(key, running);
32445
32837
  }
32446
32838
  return running;
32447
32839
  }
32448
- async sweepPages(near, radiusLy, centre, opts) {
32840
+ async sweepPages(near, radiusLy, centre, key, opts) {
32841
+ const resume = this.sweepCache.partial(key);
32842
+ const systems = resume ? [...resume.systems] : [];
32843
+ const seen = new Set(systems.map((s) => s.id64));
32844
+ let total = resume?.total ?? 0;
32845
+ let partial2 = false;
32846
+ for (let page2 = 0; ; page2++) {
32847
+ if (page2 > 0 && opts.deadline !== void 0 && this.now() >= opts.deadline) {
32848
+ partial2 = true;
32849
+ break;
32850
+ }
32851
+ const body = {
32852
+ filters: {
32853
+ // Resuming by distance, not page: Spansh's list can shift between calls an hour apart.
32854
+ distance: { min: resume?.coveredLy ?? 0, max: radiusLy },
32855
+ population: { comparison: "<=>", value: [1, 1e11] }
32856
+ },
32857
+ sort: [{ distance: { direction: "asc" } }],
32858
+ size: SWEEP_PAGE_SIZE,
32859
+ page: page2,
32860
+ reference_system: near
32861
+ };
32862
+ let res;
32863
+ try {
32864
+ res = await this.post("/systems/search", JSON.stringify(body), "application/json");
32865
+ } catch (err) {
32866
+ if (err instanceof SpanshError && err.status === 400) {
32867
+ throw new UserError(
32868
+ `Spansh rejected the search (${err.detail}). Check that "${near}" is an exact system name (spansh_system).`
32869
+ );
32870
+ }
32871
+ throw err;
32872
+ }
32873
+ if (!resume) total = res.count;
32874
+ const results = res.results ?? [];
32875
+ for (const raw of results) {
32876
+ const s = mapSystem(raw);
32877
+ if (s && !seen.has(s.id64)) {
32878
+ seen.add(s.id64);
32879
+ systems.push(s);
32880
+ }
32881
+ }
32882
+ if (results.length < SWEEP_PAGE_SIZE) break;
32883
+ }
32884
+ if (!partial2) {
32885
+ this.sweepCache.add(centre, radiusLy, systems, key);
32886
+ return { centre, systems, total, partial: partial2, coveredLy: radiusLy };
32887
+ }
32888
+ const last = systems[systems.length - 1];
32889
+ const coveredLy = Math.max(
32890
+ resume?.coveredLy ?? 0,
32891
+ last ? distanceLy(last.coords, centre) - 0.01 : 0
32892
+ );
32893
+ this.sweepCache.savePartial({ key, centre, coveredLy, systems, total });
32894
+ return { centre, systems, total, partial: partial2, coveredLy };
32895
+ }
32896
+ /** Systems within radiusLy of near with a faction at war and a Frontline Solutions station. */
32897
+ async frontlineWarSystems(near, radiusLy, opts = {}) {
32898
+ const centre = (await this.system(near, 0)).coords;
32899
+ const key = `wars:${near.toLowerCase()}:${radiusLy}`;
32900
+ const hit = this.warCache.get(key);
32901
+ if (hit) return { centre, systems: hit, partial: false };
32902
+ let running = this.warSearches.get(key);
32903
+ if (!running) {
32904
+ running = this.warPages(near, radiusLy, centre, key, opts).finally(
32905
+ () => this.warSearches.delete(key)
32906
+ );
32907
+ this.warSearches.set(key, running);
32908
+ }
32909
+ return running;
32910
+ }
32911
+ async warPages(near, radiusLy, centre, key, opts) {
32449
32912
  const systems = [];
32450
32913
  const seen = /* @__PURE__ */ new Set();
32451
- let total = 0;
32452
32914
  let partial2 = false;
32453
32915
  for (let page2 = 0; ; page2++) {
32454
32916
  if (page2 > 0 && opts.deadline !== void 0 && this.now() >= opts.deadline) {
@@ -32458,7 +32920,8 @@ var SpanshClient = class {
32458
32920
  const body = {
32459
32921
  filters: {
32460
32922
  distance: { min: 0, max: radiusLy },
32461
- population: { comparison: "<=>", value: [1, 1e11] }
32923
+ minor_faction_presences: [{ state: { value: ["War", "Civil War"] } }],
32924
+ stations: [{ services: { value: ["Frontline Solutions"] } }]
32462
32925
  },
32463
32926
  sort: [{ distance: { direction: "asc" } }],
32464
32927
  size: SWEEP_PAGE_SIZE,
@@ -32476,7 +32939,6 @@ var SpanshClient = class {
32476
32939
  }
32477
32940
  throw err;
32478
32941
  }
32479
- total = res.count;
32480
32942
  const results = res.results ?? [];
32481
32943
  for (const raw of results) {
32482
32944
  const s = mapSystem(raw);
@@ -32487,8 +32949,28 @@ var SpanshClient = class {
32487
32949
  }
32488
32950
  if (results.length < SWEEP_PAGE_SIZE) break;
32489
32951
  }
32490
- if (!partial2) this.sweepCache.add(centre, radiusLy, systems);
32491
- return { centre, systems, total, partial: partial2 };
32952
+ if (!partial2) this.warCache.set(key, systems);
32953
+ return { centre, systems, partial: partial2 };
32954
+ }
32955
+ /** The bodies of a system, with the stations and settlements on each. Bodies rarely change. */
32956
+ async systemBodies(id64) {
32957
+ return await this.systemCache.getOrLoad(
32958
+ `bodies:${id64}`,
32959
+ async () => {
32960
+ const raw = await this.get(`/dump/${id64}`);
32961
+ const bodies = raw.system?.bodies ?? raw.bodies ?? [];
32962
+ return bodies.map((b) => ({
32963
+ name: b.name,
32964
+ bodyId: b.bodyId,
32965
+ subType: b.subType ?? null,
32966
+ arrival_ls: b.distanceToArrival ?? 0,
32967
+ parentPlanetId: (b.parents ?? []).map((p) => p.Planet).find((x) => x !== void 0) ?? null,
32968
+ stations: (b.stations ?? []).map((s) => s.name)
32969
+ }));
32970
+ },
32971
+ // An empty list is probably a dump that was not ready yet; fetch it again next time.
32972
+ { keep: (b) => b.length > 0 }
32973
+ );
32492
32974
  }
32493
32975
  async route(q, opts = {}) {
32494
32976
  const form = new URLSearchParams({
@@ -32891,10 +33373,8 @@ function mapRoute(r) {
32891
33373
  };
32892
33374
  }
32893
33375
 
32894
- // src/plans/store.ts
32895
- import { randomBytes as randomBytes2 } from "crypto";
32896
- import { lstat as lstat2, mkdir as mkdir3, rename as rename3, rm, writeFile as writeFile3 } from "fs/promises";
32897
- import path8 from "path";
33376
+ // src/activity/store.ts
33377
+ import { lstat as lstat2, mkdir as mkdir3, unlink as unlink2 } from "fs/promises";
32898
33378
 
32899
33379
  // src/fs/lock.ts
32900
33380
  import { createHash as createHash2, randomUUID } from "crypto";
@@ -33058,6 +33538,276 @@ async function failure2(err, file2) {
33058
33538
  return { kind: "unsafe", reason: `not readable (${code ?? "unknown error"})` };
33059
33539
  }
33060
33540
 
33541
+ // src/plans/schema.ts
33542
+ var ItemKind = external_exports.enum([
33543
+ "buy",
33544
+ "engineer",
33545
+ "experimental",
33546
+ "unlock_engineer",
33547
+ "gather",
33548
+ "trade",
33549
+ "refit"
33550
+ ]);
33551
+ var ItemStatus = external_exports.enum(["todo", "in_progress", "done", "skipped"]);
33552
+ var PlanItemSchema = external_exports.object({
33553
+ id: external_exports.string().min(1),
33554
+ kind: ItemKind,
33555
+ description: external_exports.string().min(1),
33556
+ status: ItemStatus.default("todo"),
33557
+ station: external_exports.string().optional(),
33558
+ engineer: external_exports.string().optional(),
33559
+ materials: external_exports.array(external_exports.object({ name: external_exports.string().min(1), count: external_exports.number().int().positive() })).optional(),
33560
+ notes: external_exports.string().optional()
33561
+ });
33562
+ var StopKind = external_exports.enum([
33563
+ "shop",
33564
+ "trader",
33565
+ "engineer",
33566
+ "exobiology",
33567
+ "exploration",
33568
+ "combat",
33569
+ "mission",
33570
+ "hand_in",
33571
+ "trade",
33572
+ "mining",
33573
+ "fuel",
33574
+ "other"
33575
+ ]);
33576
+ var StopStatus = external_exports.enum(["todo", "done", "skipped"]);
33577
+ var StopSchema = external_exports.object({
33578
+ system: external_exports.string().min(1),
33579
+ // Older files can hold station "", which named nothing; it reads as no station.
33580
+ station: external_exports.string().transform((v) => v === "" ? void 0 : v).optional(),
33581
+ body: external_exports.string().min(1).optional(),
33582
+ purpose: external_exports.string().min(1),
33583
+ label: external_exports.string().min(1).optional(),
33584
+ kind: StopKind.optional(),
33585
+ after: external_exports.array(external_exports.string().min(1)).optional(),
33586
+ status: StopStatus.default("todo"),
33587
+ notes: external_exports.string().optional()
33588
+ });
33589
+ var PlanRouteSchema = external_exports.object({
33590
+ computed_at: external_exports.string(),
33591
+ from: external_exports.string(),
33592
+ stops: external_exports.array(external_exports.string()),
33593
+ total_min: external_exports.number(),
33594
+ plotted: external_exports.boolean(),
33595
+ // For each entry of stops, the indexes in the plan's stops that it visits. Names alone do not
33596
+ // map back: an engineer stop takes the engineer's name, and repeated names get -2, -3.
33597
+ // Optional, so routes saved before it load.
33598
+ stop_indexes: external_exports.array(external_exports.array(external_exports.number().int().min(0))).optional()
33599
+ });
33600
+ var PlanId = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be lowercase letters, digits and dashes");
33601
+ var PlanInputSchema = external_exports.object({
33602
+ id: PlanId,
33603
+ name: external_exports.string().min(1),
33604
+ target_build: external_exports.unknown().optional(),
33605
+ items: external_exports.array(PlanItemSchema).default([]),
33606
+ stops: external_exports.array(StopSchema).default([]),
33607
+ route: PlanRouteSchema.optional(),
33608
+ notes: external_exports.string().optional()
33609
+ });
33610
+ var PlanSchema = PlanInputSchema.extend({
33611
+ created_at: external_exports.string(),
33612
+ updated_at: external_exports.string()
33613
+ });
33614
+ var PlanFileSchema = external_exports.object({
33615
+ version: external_exports.literal(1),
33616
+ plans: external_exports.array(PlanSchema)
33617
+ });
33618
+
33619
+ // src/activity/schema.ts
33620
+ var ActivityKind = external_exports.enum([
33621
+ "exobiology",
33622
+ "exploration",
33623
+ "massacre",
33624
+ "missions",
33625
+ "hauling",
33626
+ "trading",
33627
+ "mining",
33628
+ "engineering",
33629
+ "combat",
33630
+ "other"
33631
+ ]);
33632
+ var MAX_SUMMARY = 500;
33633
+ var ActivityInputSchema = external_exports.object({
33634
+ kind: ActivityKind,
33635
+ summary: external_exports.string().trim().min(1).max(MAX_SUMMARY),
33636
+ // Bounded like the other fields, so the note cannot outgrow the size limit for data files.
33637
+ plan_id: PlanId.optional()
33638
+ });
33639
+ var ActivitySchema = ActivityInputSchema.extend({
33640
+ updated_at: external_exports.string()
33641
+ });
33642
+ var ActivityFileSchema = ActivitySchema.extend({ version: external_exports.literal(1) });
33643
+
33644
+ // src/activity/store.ts
33645
+ var LOCK_TIMEOUT_MS = 5e3;
33646
+ var LOCKED = "Activity file is locked by another process; try again";
33647
+ var ActivityStore = class {
33648
+ constructor(file2, now = () => /* @__PURE__ */ new Date(), opts = {}) {
33649
+ this.file = file2;
33650
+ this.now = now;
33651
+ this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
33652
+ this.lockDir = opts.lockDir;
33653
+ this.guard = opts.guard;
33654
+ }
33655
+ file;
33656
+ now;
33657
+ queue = Promise.resolve();
33658
+ lockTimeoutMs;
33659
+ lockDir;
33660
+ guard;
33661
+ /** The saved note, or null when there is none. */
33662
+ async get() {
33663
+ await this.guard?.();
33664
+ const read2 = await readRegularText(this.file, MAX_DATA_FILE);
33665
+ if (read2.kind === "missing") return null;
33666
+ if (read2.kind === "unsafe") {
33667
+ throw new UserError(`Activity file ${this.file} is invalid: ${read2.reason}`);
33668
+ }
33669
+ let json3;
33670
+ try {
33671
+ json3 = JSON.parse(read2.text);
33672
+ } catch {
33673
+ throw new UserError(`Activity file ${this.file} is invalid: not JSON`);
33674
+ }
33675
+ const r = ActivityFileSchema.safeParse(json3);
33676
+ if (!r.success) {
33677
+ throw new UserError(`Activity file ${this.file} is invalid: ${external_exports.prettifyError(r.error)}`);
33678
+ }
33679
+ const { kind: kind2, summary, plan_id, updated_at } = r.data;
33680
+ return { kind: kind2, summary, ...plan_id === void 0 ? {} : { plan_id }, updated_at };
33681
+ }
33682
+ /** Replaces the note. */
33683
+ set(input2) {
33684
+ return this.serial(async () => {
33685
+ const parsed = ActivityInputSchema.safeParse(input2);
33686
+ if (!parsed.success) {
33687
+ throw new UserError(`Invalid activity: ${external_exports.prettifyError(parsed.error)}`);
33688
+ }
33689
+ const activity = { ...parsed.data, updated_at: this.now().toISOString() };
33690
+ await this.guard?.();
33691
+ return lockedWrite(
33692
+ this.file,
33693
+ { timeoutMs: this.lockTimeoutMs, lockDir: this.lockDir, lockedMessage: LOCKED },
33694
+ async () => ({
33695
+ content: JSON.stringify({ version: 1, ...activity }, null, 2),
33696
+ result: activity
33697
+ })
33698
+ );
33699
+ });
33700
+ }
33701
+ /** Deletes the note. Returns false when there was none. */
33702
+ clear() {
33703
+ return this.serial(async () => {
33704
+ await this.guard?.();
33705
+ const missing = await lstat2(this.file).then(
33706
+ () => false,
33707
+ (err) => {
33708
+ if (err.code === "ENOENT") return true;
33709
+ throw err;
33710
+ }
33711
+ );
33712
+ if (missing) return false;
33713
+ if (this.lockDir) await mkdir3(this.lockDir, { recursive: true });
33714
+ const release = await acquireLock(this.file, this.lockTimeoutMs, LOCKED, this.lockDir);
33715
+ try {
33716
+ await unlink2(this.file);
33717
+ return true;
33718
+ } catch (err) {
33719
+ const code = err.code;
33720
+ if (code === "ENOENT") return false;
33721
+ if (code === "EPERM" || code === "EACCES" || code === "EBUSY") {
33722
+ throw new UserError("Activity file is in use by another program; try again");
33723
+ }
33724
+ throw err;
33725
+ } finally {
33726
+ await release();
33727
+ }
33728
+ });
33729
+ }
33730
+ // One change at a time in this process; the lock file covers other processes.
33731
+ serial(fn) {
33732
+ const run = this.queue.then(fn);
33733
+ this.queue = run.catch(() => void 0);
33734
+ return run;
33735
+ }
33736
+ };
33737
+
33738
+ // src/plans/store.ts
33739
+ import { randomBytes as randomBytes2 } from "crypto";
33740
+ import { lstat as lstat3, mkdir as mkdir4, rename as rename3, rm, writeFile as writeFile3 } from "fs/promises";
33741
+ import path8 from "path";
33742
+
33743
+ // src/plans/stops.ts
33744
+ var lc = (s) => s.toLowerCase();
33745
+ function stopName(s) {
33746
+ return s.label ?? s.station ?? s.body ?? s.system;
33747
+ }
33748
+ function namesOf(s) {
33749
+ return [s.label, s.station, s.body, s.system].flatMap((n) => n ? [lc(n)] : []);
33750
+ }
33751
+ function visitOrder(plan) {
33752
+ const all = plan.stops.map((_, i) => i);
33753
+ const route = plan.route;
33754
+ if (!route) return all;
33755
+ const used = /* @__PURE__ */ new Set();
33756
+ const order = [];
33757
+ const take = (i) => {
33758
+ if (i === void 0 || used.has(i) || i >= plan.stops.length) return false;
33759
+ used.add(i);
33760
+ order.push(i);
33761
+ return true;
33762
+ };
33763
+ if (route.stop_indexes && route.stop_indexes.length === route.stops.length) {
33764
+ for (const entry2 of route.stop_indexes) for (const i of entry2) take(i);
33765
+ } else {
33766
+ const primary = plan.stops.map((s) => lc(stopName(s)));
33767
+ for (const raw of route.stops) {
33768
+ const name = lc(raw);
33769
+ if (take(all.find((j) => !used.has(j) && primary[j] === name))) continue;
33770
+ const m = /^(.*)-(\d+)$/.exec(name);
33771
+ if (m && take(all.filter((j) => primary[j] === m[1])[Number(m[2]) - 1])) continue;
33772
+ take(all.find((j) => !used.has(j) && namesOf(plan.stops[j]).includes(name)));
33773
+ }
33774
+ }
33775
+ return [...order, ...all.filter((i) => !used.has(i))];
33776
+ }
33777
+ function nextStop(plan) {
33778
+ const i = visitOrder(plan).find((j) => plan.stops[j].status === "todo");
33779
+ return i === void 0 ? null : { index: i, ...plan.stops[i] };
33780
+ }
33781
+ function findStop(plan, ref) {
33782
+ if (ref.label === void 0 && ref.index === void 0) {
33783
+ throw new UserError("Give the stop's label or its index");
33784
+ }
33785
+ if (ref.label !== void 0 && ref.index !== void 0) {
33786
+ throw new UserError("Give the stop's label or its index, not both");
33787
+ }
33788
+ if (plan.stops.length === 0) throw new UserError(`Plan "${plan.id}" has no stops.`);
33789
+ if (ref.index !== void 0) {
33790
+ if (!Number.isInteger(ref.index) || ref.index < 0 || ref.index >= plan.stops.length) {
33791
+ throw new UserError(
33792
+ `Plan "${plan.id}" has no stop ${ref.index}; indexes run from 0 to ${plan.stops.length - 1}`
33793
+ );
33794
+ }
33795
+ return ref.index;
33796
+ }
33797
+ const want = lc(ref.label);
33798
+ const byLabel = plan.stops.flatMap((s, i) => s.label && lc(s.label) === want ? [i] : []);
33799
+ const byName = plan.stops.flatMap((s, i) => lc(stopName(s)) === want ? [i] : []);
33800
+ const hits = byLabel.length ? byLabel : byName.length ? byName : plan.stops.flatMap((s, i) => namesOf(s).includes(want) ? [i] : []);
33801
+ if (hits.length === 1) return hits[0];
33802
+ if (hits.length > 1) {
33803
+ throw new UserError(
33804
+ `${hits.length} stops in plan "${plan.id}" match "${ref.label}"; give the index instead (${hits.join(", ")})`
33805
+ );
33806
+ }
33807
+ const known = plan.stops.map((s, i) => `${i}: ${stopName(s)}`).join(", ");
33808
+ throw new UserError(`No stop "${ref.label}" in plan "${plan.id}". Stops: ${known}`);
33809
+ }
33810
+
33061
33811
  // src/plans/markdown.ts
33062
33812
  var box = {
33063
33813
  todo: "[ ]",
@@ -33065,6 +33815,7 @@ var box = {
33065
33815
  done: "[x]",
33066
33816
  skipped: "[-]"
33067
33817
  };
33818
+ var stopBox = { todo: "[ ]", done: "[x]", skipped: "[-]" };
33068
33819
  function planToMarkdown(plan) {
33069
33820
  const done = plan.items.filter((i) => i.status === "done" || i.status === "skipped").length;
33070
33821
  const out = [
@@ -33093,8 +33844,13 @@ function planToMarkdown(plan) {
33093
33844
  }
33094
33845
  if (plan.stops.length) {
33095
33846
  out.push("## Stops", "");
33847
+ const next = nextStop(plan)?.index;
33096
33848
  plan.stops.forEach((s, n) => {
33097
- out.push(`${n + 1}. ${s.system}${s.station ? ` \u2014 ${s.station}` : ""}: ${s.purpose}`);
33849
+ const place2 = [s.system, s.body, s.station].filter(Boolean).join(" \u2014 ");
33850
+ const kind2 = s.kind ? `**${s.kind}** ` : "";
33851
+ const mark = n === next ? " \u2190 next" : "";
33852
+ out.push(`${n + 1}. ${stopBox[s.status]} ${kind2}${place2}: ${s.purpose}${mark}`);
33853
+ if (s.notes) out.push(` - Notes: ${s.notes}`);
33098
33854
  });
33099
33855
  out.push("");
33100
33856
  }
@@ -33112,63 +33868,8 @@ function planToMarkdown(plan) {
33112
33868
  return out.join("\n");
33113
33869
  }
33114
33870
 
33115
- // src/plans/schema.ts
33116
- var ItemKind = external_exports.enum([
33117
- "buy",
33118
- "engineer",
33119
- "experimental",
33120
- "unlock_engineer",
33121
- "gather",
33122
- "trade",
33123
- "refit"
33124
- ]);
33125
- var ItemStatus = external_exports.enum(["todo", "in_progress", "done", "skipped"]);
33126
- var PlanItemSchema = external_exports.object({
33127
- id: external_exports.string().min(1),
33128
- kind: ItemKind,
33129
- description: external_exports.string().min(1),
33130
- status: ItemStatus.default("todo"),
33131
- station: external_exports.string().optional(),
33132
- engineer: external_exports.string().optional(),
33133
- materials: external_exports.array(external_exports.object({ name: external_exports.string().min(1), count: external_exports.number().int().positive() })).optional(),
33134
- notes: external_exports.string().optional()
33135
- });
33136
- var StopKind = external_exports.enum(["shop", "trader", "engineer", "other"]);
33137
- var StopSchema = external_exports.object({
33138
- system: external_exports.string().min(1),
33139
- station: external_exports.string().optional(),
33140
- purpose: external_exports.string().min(1),
33141
- label: external_exports.string().min(1).optional(),
33142
- kind: StopKind.optional(),
33143
- after: external_exports.array(external_exports.string().min(1)).optional()
33144
- });
33145
- var PlanRouteSchema = external_exports.object({
33146
- computed_at: external_exports.string(),
33147
- from: external_exports.string(),
33148
- stops: external_exports.array(external_exports.string()),
33149
- total_min: external_exports.number(),
33150
- plotted: external_exports.boolean()
33151
- });
33152
- var PlanInputSchema = external_exports.object({
33153
- id: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be lowercase letters, digits and dashes"),
33154
- name: external_exports.string().min(1),
33155
- target_build: external_exports.unknown().optional(),
33156
- items: external_exports.array(PlanItemSchema).default([]),
33157
- stops: external_exports.array(StopSchema).default([]),
33158
- route: PlanRouteSchema.optional(),
33159
- notes: external_exports.string().optional()
33160
- });
33161
- var PlanSchema = PlanInputSchema.extend({
33162
- created_at: external_exports.string(),
33163
- updated_at: external_exports.string()
33164
- });
33165
- var PlanFileSchema = external_exports.object({
33166
- version: external_exports.literal(1),
33167
- plans: external_exports.array(PlanSchema)
33168
- });
33169
-
33170
33871
  // src/plans/store.ts
33171
- var LOCK_TIMEOUT_MS = 5e3;
33872
+ var LOCK_TIMEOUT_MS2 = 5e3;
33172
33873
  var PlanExistsError = class extends UserError {
33173
33874
  constructor(id) {
33174
33875
  super(`Plan ${id} exists; pass overwrite: true to replace it`);
@@ -33178,7 +33879,7 @@ var PlanStore = class {
33178
33879
  constructor(file2, now = () => /* @__PURE__ */ new Date(), opts = {}) {
33179
33880
  this.file = file2;
33180
33881
  this.now = now;
33181
- this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
33882
+ this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS2;
33182
33883
  this.lockDir = opts.lockDir;
33183
33884
  this.guard = opts.guard;
33184
33885
  this.markdownDir = opts.markdownDir;
@@ -33198,6 +33899,7 @@ var PlanStore = class {
33198
33899
  name: p.name,
33199
33900
  items: p.items.length,
33200
33901
  done: p.items.filter((i) => i.status === "done" || i.status === "skipped").length,
33902
+ next_stop: nextStop(p),
33201
33903
  updated_at: p.updated_at
33202
33904
  }));
33203
33905
  }
@@ -33215,11 +33917,16 @@ var PlanStore = class {
33215
33917
  if (idx >= 0 && opts.overwrite === false) {
33216
33918
  throw new PlanExistsError(parsed.data.id);
33217
33919
  }
33218
- const plan = {
33920
+ const old = idx >= 0 ? data.plans[idx] : void 0;
33921
+ const plan = cleanRoute({
33219
33922
  ...parsed.data,
33220
- created_at: idx >= 0 ? data.plans[idx].created_at : stamp,
33923
+ created_at: old ? old.created_at : stamp,
33221
33924
  updated_at: stamp
33222
- };
33925
+ });
33926
+ const route = plan.route;
33927
+ if (route?.stop_indexes && old?.route?.computed_at === route.computed_at && !sameStops(old.stops, plan.stops)) {
33928
+ delete route.stop_indexes;
33929
+ }
33223
33930
  if (idx >= 0) data.plans[idx] = plan;
33224
33931
  else data.plans.push(plan);
33225
33932
  return plan;
@@ -33237,10 +33944,30 @@ var PlanStore = class {
33237
33944
  return plan;
33238
33945
  });
33239
33946
  }
33240
- setRoute(planId, route) {
33947
+ updateStop(planId, ref, patch) {
33948
+ return this.mutate((data) => {
33949
+ const plan = data.plans.find((p) => p.id === planId);
33950
+ if (!plan) throw new UserError(`Plan "${planId}" not found`);
33951
+ const stop = plan.stops[findStop(plan, ref)];
33952
+ if (patch.status !== void 0) stop.status = patch.status;
33953
+ if (patch.notes !== void 0) stop.notes = patch.notes;
33954
+ plan.updated_at = this.now().toISOString();
33955
+ return plan;
33956
+ });
33957
+ }
33958
+ /**
33959
+ * Saves a computed route. With `stops`, the stops the route was computed from, it refuses when
33960
+ * the plan's stops have changed since, because the route's stop_indexes would point at others.
33961
+ */
33962
+ setRoute(planId, route, opts = {}) {
33241
33963
  return this.mutate((data) => {
33242
33964
  const plan = data.plans.find((p) => p.id === planId);
33243
33965
  if (!plan) throw new UserError(`Plan "${planId}" not found`);
33966
+ if (opts.stops && !sameStops(opts.stops, plan.stops)) {
33967
+ throw new UserError(
33968
+ `The stops of plan "${planId}" changed while the route was computed; run route_stops again to save it`
33969
+ );
33970
+ }
33244
33971
  plan.route = route;
33245
33972
  plan.updated_at = this.now().toISOString();
33246
33973
  return plan;
@@ -33271,6 +33998,7 @@ var PlanStore = class {
33271
33998
  if (!parsed.success) {
33272
33999
  throw new UserError(`Plan file ${this.file} is invalid: ${external_exports.prettifyError(parsed.error)}`);
33273
34000
  }
34001
+ parsed.data.plans.forEach(cleanRoute);
33274
34002
  return parsed.data;
33275
34003
  }
33276
34004
  // Read-modify-write runs one at a time so concurrent tool calls cannot lose updates.
@@ -33322,7 +34050,7 @@ var PlanStore = class {
33322
34050
  if (!dir) return null;
33323
34051
  let st;
33324
34052
  try {
33325
- st = await lstat2(dir);
34053
+ st = await lstat3(dir);
33326
34054
  } catch {
33327
34055
  return null;
33328
34056
  }
@@ -33335,7 +34063,7 @@ var PlanStore = class {
33335
34063
  async writeMarkdown(data, before) {
33336
34064
  const dir = this.markdownDir;
33337
34065
  try {
33338
- await mkdir3(dir, { recursive: true });
34066
+ await mkdir4(dir, { recursive: true });
33339
34067
  const problem = await this.directoryProblem();
33340
34068
  if (problem) {
33341
34069
  console.error(problem);
@@ -33358,7 +34086,7 @@ var PlanStore = class {
33358
34086
  const n = `${id}.md`;
33359
34087
  if (keep.has(n) || !PRUNABLE.test(n)) continue;
33360
34088
  const file2 = path8.join(dir, n);
33361
- const st = await lstat2(file2).catch(() => null);
34089
+ const st = await lstat3(file2).catch(() => null);
33362
34090
  if (st?.isFile()) await rm(file2, { force: true });
33363
34091
  }
33364
34092
  this.lastCopyError = null;
@@ -33368,6 +34096,20 @@ var PlanStore = class {
33368
34096
  }
33369
34097
  }
33370
34098
  };
34099
+ function cleanRoute(plan) {
34100
+ const route = plan.route;
34101
+ if (!route?.stop_indexes) return plan;
34102
+ if (route.stop_indexes.length !== route.stops.length) {
34103
+ delete route.stop_indexes;
34104
+ } else {
34105
+ route.stop_indexes = route.stop_indexes.map((e) => e.filter((i) => i < plan.stops.length));
34106
+ }
34107
+ return plan;
34108
+ }
34109
+ var stopKey = (s) => JSON.stringify([s.system, s.station, s.body, s.label, s.purpose]);
34110
+ function sameStops(a, b) {
34111
+ return a.length === b.length && a.every((s, i) => stopKey(s) === stopKey(b[i]));
34112
+ }
33371
34113
  var PRUNABLE = /^[a-z0-9][a-z0-9-]{0,63}\.md$/;
33372
34114
 
33373
34115
  // src/prefs/schema.ts
@@ -33490,11 +34232,11 @@ function applyPatch(base2, patch) {
33490
34232
  }
33491
34233
 
33492
34234
  // src/prefs/store.ts
33493
- var LOCK_TIMEOUT_MS2 = 5e3;
34235
+ var LOCK_TIMEOUT_MS3 = 5e3;
33494
34236
  var PreferencesStore = class {
33495
34237
  constructor(file2, opts = {}) {
33496
34238
  this.file = file2;
33497
- this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS2;
34239
+ this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3;
33498
34240
  this.lockDir = opts.lockDir;
33499
34241
  this.guard = opts.guard;
33500
34242
  }
@@ -33555,7 +34297,7 @@ var PreferencesStore = class {
33555
34297
  };
33556
34298
 
33557
34299
  // src/ships/data.ts
33558
- import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
34300
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
33559
34301
  import path9 from "path";
33560
34302
  import { gunzipSync } from "zlib";
33561
34303
 
@@ -33757,7 +34499,7 @@ var ships_default = {
33757
34499
 
33758
34500
  // src/ships/tar.ts
33759
34501
  var BLOCK = 512;
33760
- function str3(buf, start, len) {
34502
+ function str4(buf, start, len) {
33761
34503
  const field = buf.subarray(start, start + len);
33762
34504
  const end = field.indexOf(0);
33763
34505
  return field.subarray(0, end < 0 ? len : end).toString("utf8");
@@ -33785,7 +34527,7 @@ function readTar(buf) {
33785
34527
  while (pos + BLOCK <= buf.length) {
33786
34528
  const h = buf.subarray(pos, pos + BLOCK);
33787
34529
  if (h.every((b) => b === 0)) break;
33788
- const size = Number.parseInt(str3(h, 124, 12).trim() || "0", 8);
34530
+ const size = Number.parseInt(str4(h, 124, 12).trim() || "0", 8);
33789
34531
  const type = String.fromCharCode(h[156] ?? 0);
33790
34532
  const start = pos + BLOCK;
33791
34533
  if (start + size > buf.length) throw new Error("Truncated tar archive");
@@ -33799,9 +34541,9 @@ function readTar(buf) {
33799
34541
  comment = paxFields(data).comment ?? comment;
33800
34542
  continue;
33801
34543
  }
33802
- let path15 = str3(h, 0, 100);
33803
- if (str3(h, 257, 6) === "ustar") {
33804
- const prefix = str3(h, 345, 155);
34544
+ let path15 = str4(h, 0, 100);
34545
+ if (str4(h, 257, 6) === "ustar") {
34546
+ const prefix = str4(h, 345, 155);
33805
34547
  if (prefix) path15 = `${prefix}/${path15}`;
33806
34548
  }
33807
34549
  if (nextPath !== null) path15 = nextPath;
@@ -34083,7 +34825,7 @@ var ShipDataService = class {
34083
34825
  }
34084
34826
  }
34085
34827
  async save(snapshot) {
34086
- await mkdir4(path9.dirname(this.file), { recursive: true });
34828
+ await mkdir5(path9.dirname(this.file), { recursive: true });
34087
34829
  const tmp = `${this.file}.${process.pid}.tmp`;
34088
34830
  await writeFile4(tmp, JSON.stringify(snapshot));
34089
34831
  await rename4(tmp, this.file);
@@ -34092,7 +34834,7 @@ var ShipDataService = class {
34092
34834
  var message = (err) => err instanceof Error ? err.message : String(err);
34093
34835
 
34094
34836
  // src/settings/secrets.ts
34095
- import { lstat as lstat3, mkdir as mkdir5, rm as rm2 } from "fs/promises";
34837
+ import { lstat as lstat4, mkdir as mkdir6, rm as rm2 } from "fs/promises";
34096
34838
  import path10 from "path";
34097
34839
 
34098
34840
  // src/settings/crypto.ts
@@ -34195,7 +34937,7 @@ var SecretsSchema = external_exports.strictObject({
34195
34937
  });
34196
34938
  var WrongPasswordError = class extends SecretsError {
34197
34939
  };
34198
- var LOCK_TIMEOUT_MS3 = 5e3;
34940
+ var LOCK_TIMEOUT_MS4 = 5e3;
34199
34941
  var SecretStore = class _SecretStore {
34200
34942
  constructor(file2, opts = {}) {
34201
34943
  this.file = file2;
@@ -34229,7 +34971,7 @@ var SecretStore = class _SecretStore {
34229
34971
  await lockedWrite(
34230
34972
  this.file,
34231
34973
  {
34232
- timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
34974
+ timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS4,
34233
34975
  lockedMessage: "Secrets file is locked by another process; try again",
34234
34976
  lockDir: this.opts.lockDir,
34235
34977
  mode: 384
@@ -34258,11 +35000,11 @@ var SecretStore = class _SecretStore {
34258
35000
  reset() {
34259
35001
  const run = this.queue.then(async () => {
34260
35002
  await this.opts.guard?.();
34261
- await mkdir5(path10.dirname(this.file), { recursive: true });
34262
- if (this.opts.lockDir) await mkdir5(this.opts.lockDir, { recursive: true });
35003
+ await mkdir6(path10.dirname(this.file), { recursive: true });
35004
+ if (this.opts.lockDir) await mkdir6(this.opts.lockDir, { recursive: true });
34263
35005
  const release = await acquireLock(
34264
35006
  this.file,
34265
- this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
35007
+ this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS4,
34266
35008
  "Secrets file is locked by another process; try again",
34267
35009
  this.opts.lockDir
34268
35010
  );
@@ -34298,7 +35040,7 @@ var SecretStore = class _SecretStore {
34298
35040
  async checkFile() {
34299
35041
  let st;
34300
35042
  try {
34301
- st = await lstat3(this.file);
35043
+ st = await lstat4(this.file);
34302
35044
  } catch (err) {
34303
35045
  const code = err.code;
34304
35046
  if (code === "ENOENT") return null;
@@ -34356,7 +35098,7 @@ var SecretStore = class _SecretStore {
34356
35098
  return lockedWrite(
34357
35099
  this.file,
34358
35100
  {
34359
- timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
35101
+ timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS4,
34360
35102
  lockedMessage: "Secrets file is locked by another process; try again",
34361
35103
  lockDir: this.opts.lockDir,
34362
35104
  mode: 384
@@ -34834,7 +35576,7 @@ function buildDeps(config2, opts = {}) {
34834
35576
  markdownDir: config2.plansMarkdownDir
34835
35577
  });
34836
35578
  const legacyPlans = config2.legacyPlansEnv ? `ED_MCP_PLANS is no longer used. Plans are now in ${config2.plansFile}. Move your old plans file there, or use plan_import, then remove ED_MCP_PLANS.` : void 0;
34837
- const syncWarnings = sync ? () => sync.warnings([config2.plansFile, config2.prefsFile, config2.secretsFile]) : void 0;
35579
+ const syncWarnings = sync ? () => sync.warnings([config2.plansFile, config2.prefsFile, config2.activityFile, config2.secretsFile]) : void 0;
34838
35580
  const inaraCache = new TtlCache("inara", 6e4);
34839
35581
  caches.register(inaraCache);
34840
35582
  const inara = new InaraClient(http, () => settings.inara(), inaraCache);
@@ -34849,10 +35591,13 @@ function buildDeps(config2, opts = {}) {
34849
35591
  caches.register(spanshMarketCache);
34850
35592
  const spanshSweepCache = new SweepCache();
34851
35593
  caches.register(spanshSweepCache);
35594
+ const spanshWarCache = new TtlCache("spansh-wars", 6e5);
35595
+ caches.register(spanshWarCache);
34852
35596
  const spansh = new SpanshClient(http, spanshCache, {
34853
35597
  systemCache: spanshSystemCache,
34854
35598
  marketCache: spanshMarketCache,
34855
- sweepCache: spanshSweepCache
35599
+ sweepCache: spanshSweepCache,
35600
+ warCache: spanshWarCache
34856
35601
  });
34857
35602
  const capiHttp = new HttpClient({ userAgent: `EDCD-EdMcp-${VERSION}`, fetchFn: opts.fetchFn });
34858
35603
  const capiCache = new TtlCache("capi", 6e4);
@@ -34898,6 +35643,10 @@ function buildDeps(config2, opts = {}) {
34898
35643
  lockDir: lockFor(config2.prefsFile),
34899
35644
  guard: guardFor(config2.prefsFile)
34900
35645
  }),
35646
+ activity: new ActivityStore(config2.activityFile, void 0, {
35647
+ lockDir: lockFor(config2.activityFile),
35648
+ guard: guardFor(config2.activityFile)
35649
+ }),
34901
35650
  sync,
34902
35651
  syncWarnings,
34903
35652
  versionCheck: new VersionCheck({ http, cacheDir: config2.cacheDir, current: VERSION }),
@@ -42950,6 +43699,124 @@ var EMPTY_COMPLETION_RESULT = {
42950
43699
  }
42951
43700
  };
42952
43701
 
43702
+ // src/tools/result.ts
43703
+ function ok(value) {
43704
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
43705
+ }
43706
+ function fail(message2) {
43707
+ return { isError: true, content: [{ type: "text", text: message2 }] };
43708
+ }
43709
+ function wrap(fn) {
43710
+ return async (args) => {
43711
+ try {
43712
+ return ok(await fn(args));
43713
+ } catch (err) {
43714
+ if (err instanceof UserError) return fail(err.message);
43715
+ console.error(err);
43716
+ return fail(`Internal error: ${err instanceof Error ? err.message : String(err)}`);
43717
+ }
43718
+ };
43719
+ }
43720
+ function wrapWarn(fn, warnings) {
43721
+ const inner = wrap(fn);
43722
+ return async (args) => {
43723
+ const result = await inner(args);
43724
+ const list2 = warnings ? await warnings().catch(() => []) : [];
43725
+ if (!list2.length) return result;
43726
+ return {
43727
+ ...result,
43728
+ content: [
43729
+ ...result.content,
43730
+ { type: "text", text: `Warnings:
43731
+ ${list2.map((w) => `- ${w}`).join("\n")}` }
43732
+ ]
43733
+ };
43734
+ };
43735
+ }
43736
+ function reason(err) {
43737
+ if (err instanceof UserError) return err.message;
43738
+ console.error(err);
43739
+ return err instanceof Error ? err.message : String(err);
43740
+ }
43741
+
43742
+ // src/tools/activity.ts
43743
+ var NO_NOTE = "No activity is saved. Ask the player what they are doing, then save it.";
43744
+ function registerActivityTools(server, deps) {
43745
+ const now = deps.now ?? Date.now;
43746
+ const location = async () => {
43747
+ if (!deps.journal) return { location: null, location_note: "The journal is not available." };
43748
+ try {
43749
+ const s = await deps.journal.state();
43750
+ return {
43751
+ location: {
43752
+ system: s.location.system,
43753
+ station: s.location.station,
43754
+ docked: s.location.docked,
43755
+ as_of: s.as_of,
43756
+ age_minutes: s.age_minutes,
43757
+ warnings: s.warnings
43758
+ }
43759
+ };
43760
+ } catch (err) {
43761
+ return { location: null, location_note: `The journal could not be read: ${reason(err)}` };
43762
+ }
43763
+ };
43764
+ server.registerTool(
43765
+ "activity_get",
43766
+ {
43767
+ annotations: {
43768
+ title: "Get Activity",
43769
+ readOnlyHint: true,
43770
+ destructiveHint: false,
43771
+ idempotentHint: true,
43772
+ openWorldHint: false
43773
+ },
43774
+ description: "What the commander was last doing: a short saved note (kind, summary, optional plan_id), its age, and the commander's current location from the journal. Call it at the start of a conversation. When the location or age says the player has moved on, ask before relying on the note."
43775
+ },
43776
+ wrapWarn(async () => {
43777
+ const [activity, here] = await Promise.all([deps.activity.get(), location()]);
43778
+ if (!activity) return { activity: null, note: NO_NOTE, ...here };
43779
+ const saved = Date.parse(activity.updated_at);
43780
+ const age = Number.isNaN(saved) ? null : Math.max(0, Math.round((now() - saved) / 6e4));
43781
+ return { activity, age_minutes: age, ...here };
43782
+ }, deps.warnings)
43783
+ );
43784
+ server.registerTool(
43785
+ "activity_set",
43786
+ {
43787
+ annotations: {
43788
+ title: "Set Activity",
43789
+ readOnlyHint: false,
43790
+ destructiveHint: true,
43791
+ idempotentHint: true,
43792
+ openWorldHint: false
43793
+ },
43794
+ description: `Replace the saved note of what the commander is doing, so the next conversation can pick it up. kind and summary (at most ${MAX_SUMMARY} characters: where, the goal, and progress) are required; plan_id names the active route or build plan. Update it when the player starts something new or the goal changes. clear: true deletes the note.`,
43795
+ inputSchema: {
43796
+ kind: ActivityKind.optional(),
43797
+ summary: external_exports.string().max(MAX_SUMMARY).optional(),
43798
+ plan_id: PlanId.optional(),
43799
+ clear: external_exports.boolean().optional()
43800
+ }
43801
+ },
43802
+ wrapWarn(
43803
+ async (a) => {
43804
+ if (a.clear) {
43805
+ if (a.kind !== void 0 || a.summary !== void 0 || a.plan_id !== void 0) {
43806
+ throw new UserError("Give clear: true alone, or kind and summary without clear");
43807
+ }
43808
+ return { cleared: await deps.activity.clear() };
43809
+ }
43810
+ if (a.kind === void 0 || a.summary === void 0) {
43811
+ throw new UserError("Give kind and summary, or clear: true");
43812
+ }
43813
+ return deps.activity.set({ kind: a.kind, summary: a.summary, plan_id: a.plan_id });
43814
+ },
43815
+ deps.warnings
43816
+ )
43817
+ );
43818
+ }
43819
+
42953
43820
  // src/ships/constants.ts
42954
43821
  var MODULE_GROUP_NAMES = {
42955
43822
  pp: "Power Plant",
@@ -43686,8 +44553,8 @@ var family = (symbol2) => (symbol2 ?? "").toLowerCase().replace(/size\d+|class\d
43686
44553
  function pick2(candidates, current) {
43687
44554
  let list2 = candidates;
43688
44555
  if (current?.module.symbol) {
43689
- const same4 = list2.filter((m) => family(m.symbol) === family(current.module.symbol));
43690
- if (same4.length) list2 = same4;
44556
+ const same6 = list2.filter((m) => family(m.symbol) === family(current.module.symbol));
44557
+ if (same6.length) list2 = same6;
43691
44558
  }
43692
44559
  const plain = list2.filter((m) => m.name === void 0 && m.preEngineered === void 0);
43693
44560
  return (plain.length ? plain : list2)[0] ?? null;
@@ -43912,8 +44779,8 @@ function toSlef(build) {
43912
44779
 
43913
44780
  // src/ships/calc/core.ts
43914
44781
  var CARGO_HATCH_MW = 0.6;
43915
- var num3 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
43916
- var prop = (b, name) => num3(b.ship.properties[name]);
44782
+ var num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
44783
+ var prop = (b, name) => num4(b.ship.properties[name]);
43917
44784
  var find = (b, grp) => b.modules.find((m) => m.module.grp === grp) ?? null;
43918
44785
  function scale(m, feature) {
43919
44786
  const v = m.engineering?.modifiers[feature];
@@ -43922,17 +44789,17 @@ function scale(m, feature) {
43922
44789
  function moduleStat(m, feature) {
43923
44790
  const mods = m.engineering?.modifiers ?? {};
43924
44791
  if ((feature === "minmass" || feature === "maxmass") && mods[feature] === void 0) {
43925
- const base2 = num3(m.module[feature]);
44792
+ const base2 = num4(m.module[feature]);
43926
44793
  const optmass = scale(m, "optmass");
43927
44794
  if (SHIELD_GENERATORS.has(m.module.grp) && feature === "maxmass" && optmass < 1) return base2;
43928
44795
  return base2 * optmass;
43929
44796
  }
43930
44797
  const mul = /^(min|opt|max)mul(.*)$/.exec(feature);
43931
44798
  if (mul) {
43932
- const base2 = num3(m.module[feature]) || num3(m.module[`${mul[1]}mul`]);
44799
+ const base2 = num4(m.module[feature]) || num4(m.module[`${mul[1]}mul`]);
43933
44800
  return base2 * scale(m, "optmul");
43934
44801
  }
43935
- return num3(modifiedValue(m.module, mods, feature));
44802
+ return num4(modifiedValue(m.module, mods, feature));
43936
44803
  }
43937
44804
  var sum = (b, feature, round2 = (v) => v) => b.modules.reduce((s, m) => s + round2(moduleStat(m, feature)), 0);
43938
44805
  function massFuel(b) {
@@ -44069,8 +44936,8 @@ var BROKEN_DELAY_S = 16;
44069
44936
  var HRPS = /* @__PURE__ */ new Set(["hr", "ghrp", "mahr"]);
44070
44937
  var MRPS = /* @__PURE__ */ new Set(["mrp", "gmrp"]);
44071
44938
  var RESISTANCES = { explosive: "explres", kinetic: "kinres", thermal: "thermres" };
44072
- var num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
44073
- var prop2 = (b, name) => num4(b.ship.properties[name]);
44939
+ var num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
44940
+ var prop2 = (b, name) => num5(b.ship.properties[name]);
44074
44941
  var active = (b, grps) => b.modules.filter((m) => grps.has(m.module.grp) && online(b, m));
44075
44942
  function pipRecharge(rate, pips) {
44076
44943
  return rate * Math.pow(pips / 4, 1.1);
@@ -44244,46 +45111,6 @@ function weapons(b) {
44244
45111
  };
44245
45112
  }
44246
45113
 
44247
- // src/tools/result.ts
44248
- function ok(value) {
44249
- return { content: [{ type: "text", text: JSON.stringify(value) }] };
44250
- }
44251
- function fail(message2) {
44252
- return { isError: true, content: [{ type: "text", text: message2 }] };
44253
- }
44254
- function wrap(fn) {
44255
- return async (args) => {
44256
- try {
44257
- return ok(await fn(args));
44258
- } catch (err) {
44259
- if (err instanceof UserError) return fail(err.message);
44260
- console.error(err);
44261
- return fail(`Internal error: ${err instanceof Error ? err.message : String(err)}`);
44262
- }
44263
- };
44264
- }
44265
- function wrapWarn(fn, warnings) {
44266
- const inner = wrap(fn);
44267
- return async (args) => {
44268
- const result = await inner(args);
44269
- const list2 = warnings ? await warnings().catch(() => []) : [];
44270
- if (!list2.length) return result;
44271
- return {
44272
- ...result,
44273
- content: [
44274
- ...result.content,
44275
- { type: "text", text: `Warnings:
44276
- ${list2.map((w) => `- ${w}`).join("\n")}` }
44277
- ]
44278
- };
44279
- };
44280
- }
44281
- function reason(err) {
44282
- if (err instanceof UserError) return err.message;
44283
- console.error(err);
44284
- return err instanceof Error ? err.message : String(err);
44285
- }
44286
-
44287
45114
  // src/tools/build.ts
44288
45115
  var SCO_NOTE = "Jump range uses the drive's normal values, not supercruise overcharge.";
44289
45116
  var SlefInput = external_exports.union([external_exports.string(), external_exports.array(external_exports.unknown()), external_exports.record(external_exports.string(), external_exports.unknown())]);
@@ -45502,7 +46329,7 @@ async function locate(deps) {
45502
46329
  // src/routes/legs.ts
45503
46330
  var STATION_LIMIT = 500;
45504
46331
  var PLANETARY = /planetary|surface|settlement/i;
45505
- var same = (a, b) => a !== null && b !== null && a.toLowerCase() === b.toLowerCase();
46332
+ var same3 = (a, b) => a !== null && b !== null && a.toLowerCase() === b.toLowerCase();
45506
46333
  function createPlacer(spansh, notes) {
45507
46334
  const systems = /* @__PURE__ */ new Map();
45508
46335
  const lookup2 = (name) => {
@@ -45514,7 +46341,7 @@ function createPlacer(spansh, notes) {
45514
46341
  return {
45515
46342
  async place(system, station) {
45516
46343
  const info = await lookup2(system);
45517
- const hit = station ? info.stations.find((s) => same(s.name, station)) : void 0;
46344
+ const hit = station ? info.stations.find((s) => same3(s.name, station)) : void 0;
45518
46345
  if (station && !hit) {
45519
46346
  notes.add(`Station ${station} not found in ${info.name}; assumed 1000 ls from arrival.`);
45520
46347
  }
@@ -45528,7 +46355,7 @@ function createPlacer(spansh, notes) {
45528
46355
  },
45529
46356
  async placeEngineer(e) {
45530
46357
  const info = await lookup2(e.system);
45531
- const hit = info.stations.find((s) => same(s.name, e.base));
46358
+ const hit = info.stations.find((s) => same3(s.name, e.base));
45532
46359
  if (!hit) {
45533
46360
  notes.add(
45534
46361
  `${e.name}'s base ${e.base} is not in Spansh; assumed a planetary base 1000 ls from arrival.`
@@ -45545,10 +46372,10 @@ function createPlacer(spansh, notes) {
45545
46372
  };
45546
46373
  }
45547
46374
  function legBetween(from, to, travel) {
45548
- if (same(from.system, to.system) && same(from.station, to.station)) {
46375
+ if (same3(from.system, to.system) && same3(from.station, to.station)) {
45549
46376
  return { jumps: 0, seconds: 0, distanceLy: 0 };
45550
46377
  }
45551
- const distanceLy2 = same(from.system, to.system) ? 0 : Math.hypot(
46378
+ const distanceLy2 = same3(from.system, to.system) ? 0 : Math.hypot(
45552
46379
  from.coords.x - to.coords.x,
45553
46380
  from.coords.y - to.coords.y,
45554
46381
  from.coords.z - to.coords.z
@@ -45935,6 +46762,13 @@ function registerMissionTools(server, deps) {
45935
46762
  // src/tools/journal.ts
45936
46763
  var FRESHNESS_TEXT = "as_of is the time of the newest journal event and age_minutes its age; warnings flag stale data (over 30 minutes old) and unreadable journal files.";
45937
46764
  var NAMES_MISSING = 'Material names are not available until the game data catalog is downloaded. Call any catalog tool (for example catalog_material) or cache_refresh with source "catalog".';
46765
+ var MASSACRE_NOTE = "Kill counts are an estimate from the journal. Kills by wing mates, kills before a mission was accepted elsewhere and the game's own rules can differ; the mission panel in the game is authoritative.";
46766
+ var READ_ONLY = {
46767
+ readOnlyHint: true,
46768
+ destructiveHint: false,
46769
+ idempotentHint: true,
46770
+ openWorldHint: false
46771
+ };
45938
46772
  var freshness = (s) => ({
45939
46773
  as_of: s.as_of,
45940
46774
  age_minutes: s.age_minutes,
@@ -46054,7 +46888,104 @@ function registerJournalTools(server, deps) {
46054
46888
  return { engineers: s.engineers, ...freshness(s) };
46055
46889
  })
46056
46890
  );
46891
+ server.registerTool(
46892
+ "journal_exobiology",
46893
+ {
46894
+ annotations: { title: "Journal Exobiology", ...READ_ONLY },
46895
+ description: 'Exobiology progress in a system from the local game journal (last 14 days). For each body with biological signals: bio_signals (the count from the FSS or surface scan), genera found by the surface scan, each species sampled with samples taken (stage 0/3 to 3/3, analysed once complete; abandoned when another species was started first, which drops an unfinished sample, or after a death), and not_started genera with no sample yet. unsold_analysed counts species analysed since the last sale of organic data (or death), in any system. The journal has no sample positions. system defaults to the current system; body is a full body name or the part after the system name, such as "A 4". ' + FRESHNESS_TEXT,
46896
+ inputSchema: {
46897
+ system: external_exports.string().optional().describe("System name; default the current system"),
46898
+ body: external_exports.string().optional().describe('Body name, or the part after the system ("A 4")')
46899
+ }
46900
+ },
46901
+ wrap(async (a) => {
46902
+ const s = await deps.journal.state();
46903
+ const system = systemOf(a.system, s.location.system);
46904
+ const address = systemAddress(s.events, system);
46905
+ const r = exobiology(s.events, address ?? -1, { system, body: a.body });
46906
+ return {
46907
+ system,
46908
+ ...r,
46909
+ ...address === null ? { note: notVisited(system) } : {},
46910
+ ...freshness(s)
46911
+ };
46912
+ })
46913
+ );
46914
+ server.registerTool(
46915
+ "journal_exploration",
46916
+ {
46917
+ annotations: { title: "Journal Exploration", ...READ_ONLY },
46918
+ description: "Exploration scans of a system from the local game journal (last 14 days). discovery has the FSS discovery scan body count and progress, and all_bodies_found once the FSS is complete. bodies lists each scanned body, nearest first: type, distance_ls from the arrival star, landable, terraformable, atmosphere, rings, was_discovered and was_mapped (by someone before you), mapped_by_you (surface scan complete) and signals. Values are not estimated. system defaults to the current system. " + FRESHNESS_TEXT,
46919
+ inputSchema: {
46920
+ system: external_exports.string().optional().describe("System name; default the current system")
46921
+ }
46922
+ },
46923
+ wrap(async (a) => {
46924
+ const s = await deps.journal.state();
46925
+ const system = systemOf(a.system, s.location.system);
46926
+ const address = systemAddress(s.events, system);
46927
+ return {
46928
+ system,
46929
+ ...exploration(s.events, address ?? -1),
46930
+ ...address === null ? { note: notVisited(system) } : {},
46931
+ ...freshness(s)
46932
+ };
46933
+ })
46934
+ );
46935
+ server.registerTool(
46936
+ "journal_massacre",
46937
+ {
46938
+ annotations: { title: "Journal Massacre Progress", ...READ_ONLY },
46939
+ description: "Progress of active massacre missions from the local game journal, grouped by target faction and then by giving faction. Each mission has kills_required, kills_counted (from bounties and combat bonds against the target since it was accepted) and kills_left. A kill counts toward the oldest open mission of each giving faction, as the game stacks them; a mission redirected back to its giver is done. A target's kills_left is the most any one giver still needs. The counts are an estimate. uncounted lists massacre missions accepted before the 14-day journal window. " + FRESHNESS_TEXT
46940
+ },
46941
+ wrap(async () => {
46942
+ const s = await deps.journal.state();
46943
+ const r = massacre(s.missions, s.events);
46944
+ return {
46945
+ targets: r.groups,
46946
+ uncounted: r.uncounted.map((m) => ({ id: m.id, name: m.name, expires: m.expires })),
46947
+ note: MASSACRE_NOTE,
46948
+ ...freshness(s)
46949
+ };
46950
+ })
46951
+ );
46952
+ server.registerTool(
46953
+ "journal_events",
46954
+ {
46955
+ annotations: { title: "Journal Events", ...READ_ONLY },
46956
+ description: "Raw events from the local game journal (last 14 days), newest first, as the game wrote them. Use it for anything the other journal tools do not cover, instead of reading the journal files. events limits the event names (for example Bounty or Docked); since is an ISO time; system keeps events in that system. limit defaults to 50 (at most 200). Arrays longer than 20 entries are cut, with a last entry that says how many were left out. " + FRESHNESS_TEXT,
46957
+ inputSchema: {
46958
+ events: external_exports.array(external_exports.string()).optional().describe("Event names, such as Bounty or Docked"),
46959
+ since: external_exports.string().optional().describe("Only events at or after this ISO time"),
46960
+ system: external_exports.string().optional().describe("Only events in this system"),
46961
+ limit: external_exports.number().int().min(1).max(200).default(50)
46962
+ }
46963
+ },
46964
+ wrap(async (a) => {
46965
+ const since = a.since === void 0 ? void 0 : Date.parse(a.since);
46966
+ if (since !== void 0 && Number.isNaN(since)) {
46967
+ throw new UserError(`since is not a time: ${a.since}. Use an ISO time.`);
46968
+ }
46969
+ const s = await deps.journal.state();
46970
+ const r = await deps.journal.recentEvents(
46971
+ { events: a.events, since, system: a.system },
46972
+ a.limit
46973
+ );
46974
+ return {
46975
+ events: r.events.map(truncate),
46976
+ ...freshness({ ...s, warnings: [...s.warnings, ...r.warnings] })
46977
+ };
46978
+ })
46979
+ );
46057
46980
  }
46981
+ function systemOf(given, current) {
46982
+ const system = given?.trim() || current;
46983
+ if (!system) {
46984
+ throw new UserError("The journal has no current system. Give a system name.");
46985
+ }
46986
+ return system;
46987
+ }
46988
+ var notVisited = (system) => `The journal has no events in ${system} in the last 14 days. Check the system name.`;
46058
46989
 
46059
46990
  // src/trade/loops.ts
46060
46991
  function bestCargo(from, to, cargo, capital) {
@@ -46211,7 +47142,7 @@ var NO_LOOPS = "No profitable loop found. Try a larger radius, another centre, o
46211
47142
  var PERMIT_SPARES = 30;
46212
47143
  var PERMIT_EXTRA_MS = 8e3;
46213
47144
  var Station = external_exports.object({ system: external_exports.string(), station: external_exports.string() });
46214
- var same2 = (a, b) => a.toLowerCase() === b.toLowerCase();
47145
+ var same4 = (a, b) => a.toLowerCase() === b.toLowerCase();
46215
47146
  var tenth = (n) => Math.round(n * 10) / 10;
46216
47147
  var minutes = (seconds) => tenth(seconds / 60);
46217
47148
  var placeOf = (s) => ({
@@ -46416,11 +47347,11 @@ function registerLoopTools(server, deps) {
46416
47347
  if (here) {
46417
47348
  const name = centre.station;
46418
47349
  let startMarket = found.stations.find(
46419
- (st) => same2(st.name, name) && same2(st.system, centre.system)
47350
+ (st) => same4(st.name, name) && same4(st.system, centre.system)
46420
47351
  );
46421
47352
  if (!startMarket) {
46422
47353
  const info = await spansh.system(centre.system, STATION_LIMIT);
46423
- const entry2 = info.stations.find((st) => same2(st.name, name));
47354
+ const entry2 = info.stations.find((st) => same4(st.name, name));
46424
47355
  if (entry2?.market_id == null) {
46425
47356
  throw new UserError(`Spansh has no market for ${name} in ${info.name}.`);
46426
47357
  }
@@ -46609,7 +47540,7 @@ function registerLoopTools(server, deps) {
46609
47540
  );
46610
47541
  async function includeMarket(inc) {
46611
47542
  const info = await spansh.system(inc.system, STATION_LIMIT);
46612
- const id = info.stations.find((st) => same2(st.name, inc.station))?.market_id;
47543
+ const id = info.stations.find((st) => same4(st.name, inc.station))?.market_id;
46613
47544
  if (id == null) return null;
46614
47545
  return spansh.stationMarket(id).catch((err) => {
46615
47546
  if (err instanceof UserError) return null;
@@ -46618,6 +47549,116 @@ function registerLoopTools(server, deps) {
46618
47549
  }
46619
47550
  }
46620
47551
 
47552
+ // src/systems/groundcz.ts
47553
+ var AT_WAR = /* @__PURE__ */ new Set(["war", "civil war"]);
47554
+ var FRONTLINE = "Frontline Solutions";
47555
+ var PAD_RANK2 = { S: 1, M: 2, L: 3 };
47556
+ var padOk2 = (pad, min3) => !min3 || (PAD_RANK2[pad ?? ""] ?? 0) >= PAD_RANK2[min3];
47557
+ var smallestGap = (s) => s.settlements[0]?.gap_ls ?? Infinity;
47558
+ function groundCzTargets(systems, centre, opts) {
47559
+ const leftOut = { stale: 0, no_warring_settlements: 0, no_station_near: 0, pad_too_small: 0 };
47560
+ const targets = [];
47561
+ for (const system of systems) {
47562
+ const updated = system.updated_at ? Date.parse(system.updated_at) : NaN;
47563
+ const ageHours2 = Number.isNaN(updated) ? Infinity : (opts.now - updated) / 36e5;
47564
+ if (ageHours2 > opts.maxAgeHours) {
47565
+ leftOut.stale++;
47566
+ continue;
47567
+ }
47568
+ const warring = system.factions.flatMap((f) => {
47569
+ const state = f.active_states.find((s) => AT_WAR.has(s.toLowerCase()));
47570
+ return state ? [{ name: f.name, state }] : [];
47571
+ });
47572
+ const sides = new Set(warring.map((w) => w.name));
47573
+ const settlements = system.stations.filter(
47574
+ (s) => s.type === "Settlement" && s.owner !== null && sides.has(s.owner) && s.arrival_ls !== null
47575
+ );
47576
+ if (!settlements.length) {
47577
+ leftOut.no_warring_settlements++;
47578
+ continue;
47579
+ }
47580
+ const warringSettlements = {};
47581
+ for (const s of settlements)
47582
+ warringSettlements[s.owner] = (warringSettlements[s.owner] ?? 0) + 1;
47583
+ const stations = system.stations.filter((s) => s.services.includes(FRONTLINE) && s.arrival_ls !== null).map((s) => {
47584
+ const planetary = PLANETARY.test(s.type ?? "");
47585
+ const near = settlements.map((x) => ({
47586
+ name: x.name,
47587
+ owner: x.owner,
47588
+ arrival_ls: x.arrival_ls,
47589
+ gap_ls: Math.abs(x.arrival_ls - s.arrival_ls)
47590
+ })).filter((x) => x.gap_ls <= opts.maxGapLs).sort((a, b) => a.gap_ls - b.gap_ls);
47591
+ return {
47592
+ name: s.name,
47593
+ type: s.type,
47594
+ pad: s.pad,
47595
+ arrival_ls: s.arrival_ls,
47596
+ planetary,
47597
+ dock_s: opts.dockSeconds(s.arrival_ls, planetary),
47598
+ settlements: near
47599
+ };
47600
+ }).filter((s) => s.settlements.length > 0);
47601
+ const fit = stations.filter((s) => padOk2(s.pad, opts.minPad)).sort(
47602
+ (a, b) => a.dock_s - b.dock_s || b.settlements.length - a.settlements.length || smallestGap(a) - smallestGap(b)
47603
+ );
47604
+ if (!stations.length) {
47605
+ leftOut.no_station_near++;
47606
+ continue;
47607
+ }
47608
+ if (!fit.length) {
47609
+ leftOut.pad_too_small++;
47610
+ continue;
47611
+ }
47612
+ targets.push({
47613
+ system,
47614
+ distanceLy: distanceLy(system.coords, centre),
47615
+ ageHours: ageHours2,
47616
+ warring,
47617
+ best: fit[0],
47618
+ others: fit.slice(1),
47619
+ warringSettlements
47620
+ });
47621
+ }
47622
+ targets.sort(
47623
+ (a, b) => Math.floor(a.ageHours) - Math.floor(b.ageHours) || b.best.settlements.length - a.best.settlements.length || smallestGap(a.best) - smallestGap(b.best) || a.distanceLy - b.distanceLy
47624
+ );
47625
+ return { targets, leftOut };
47626
+ }
47627
+ var isStar = (b) => /Star$/.test(b.subType ?? "");
47628
+ function czDetail(best, bodies, maxGapLs) {
47629
+ const bodyOf = (station) => bodies.find((b) => b.stations.includes(station)) ?? null;
47630
+ const settlementBodies = {};
47631
+ for (const s of best.settlements) {
47632
+ const b = bodyOf(s.name);
47633
+ if (b) settlementBodies[s.name] = b.name;
47634
+ }
47635
+ const counts2 = /* @__PURE__ */ new Map();
47636
+ for (const name of Object.values(settlementBodies)) counts2.set(name, (counts2.get(name) ?? 0) + 1);
47637
+ const body_family = bodies.filter((b) => counts2.has(b.name)).map((b) => ({ name: b.name, subType: b.subType, settlements: counts2.get(b.name) }));
47638
+ const onBody = (b) => !!b && counts2.has(b.name);
47639
+ if (best.planetary) {
47640
+ const own2 = bodyOf(best.name);
47641
+ return {
47642
+ drop: onBody(own2) ? "same_body" : "gap_only",
47643
+ near_body: own2?.name ?? null,
47644
+ settlementBodies,
47645
+ body_family
47646
+ };
47647
+ }
47648
+ const near = bodies.filter((b) => !isStar(b) && Math.abs(b.arrival_ls - best.arrival_ls) <= maxGapLs).sort(
47649
+ (a, b) => Math.abs(a.arrival_ls - best.arrival_ls) - Math.abs(b.arrival_ls - best.arrival_ls)
47650
+ )[0];
47651
+ if (!near) return { drop: "gap_only", near_body: null, settlementBodies, body_family };
47652
+ const root = near.parentPlanetId ?? near.bodyId;
47653
+ const family2 = bodies.filter((b) => b.bodyId === root || b.parentPlanetId === root);
47654
+ return {
47655
+ drop: family2.some(onBody) ? "orbital" : "gap_only",
47656
+ near_body: near.name,
47657
+ settlementBodies,
47658
+ body_family
47659
+ };
47660
+ }
47661
+
46621
47662
  // src/systems/massacre.ts
46622
47663
  var SOURCE_RANGE_LY = 10;
46623
47664
  var NO_MASSACRES = /* @__PURE__ */ new Set(["war", "civil war", "election"]);
@@ -46726,6 +47767,7 @@ function systemsByState(systems, centre, radiusLy, q) {
46726
47767
  // src/tools/systems.ts
46727
47768
  var TIME_LIMIT_MS2 = 2e4;
46728
47769
  var MASSACRE_MARGIN_LY = 2 * SOURCE_RANGE_LY;
47770
+ var partialNote = (coveredLy) => `Spansh returned systems out to ${Math.floor(coveredLy * 10) / 10} ly within the time limit. Call again to continue the search.`;
46729
47771
  var STALE_HOURS = 72;
46730
47772
  var FAR_STAR_LS = 1e3;
46731
47773
  var Radius = external_exports.number().min(1).max(50).default(50);
@@ -46809,12 +47851,17 @@ function registerSystemTools(server, deps) {
46809
47851
  const sweep = await deps.spansh.populatedSystems(near, a.radius_ly, {
46810
47852
  deadline: now() + TIME_LIMIT_MS2
46811
47853
  });
46812
- const matches2 = systemsByState(sweep.systems, sweep.centre, a.radius_ly, q);
47854
+ const matches2 = systemsByState(
47855
+ sweep.systems,
47856
+ sweep.centre,
47857
+ Math.min(a.radius_ly, sweep.coveredLy),
47858
+ q
47859
+ );
46813
47860
  const shown = matches2.slice(0, a.limit);
46814
47861
  const stale = shown.filter((m) => (ageHours2(m.system) ?? 0) > STALE_HOURS).length;
46815
47862
  if (stale) notes.push(`${stale} results have data more than ${STALE_HOURS / 24} days old.`);
46816
47863
  if (sweep.partial) {
46817
- notes.push("Spansh did not return every system in time; results may be missing.");
47864
+ notes.push(partialNote(sweep.coveredLy));
46818
47865
  }
46819
47866
  return {
46820
47867
  near,
@@ -46822,6 +47869,7 @@ function registerSystemTools(server, deps) {
46822
47869
  travel: describeRules(rules).travel,
46823
47870
  total_matches: matches2.length,
46824
47871
  partial: sweep.partial,
47872
+ ...sweep.partial ? { covered_ly: Math.floor(sweep.coveredLy * 10) / 10 } : {},
46825
47873
  systems: shown.map((m) => {
46826
47874
  const s = m.system;
46827
47875
  const war = m.factions.some(
@@ -46886,7 +47934,9 @@ function registerSystemTools(server, deps) {
46886
47934
  deadline: now() + TIME_LIMIT_MS2
46887
47935
  });
46888
47936
  const { targets, leftOutSpread } = massacreTargets(sweep.systems, sweep.centre, {
46889
- radiusLy: a.radius_ly,
47937
+ // Past covered_ly - margin a source could have a pirate neighbour that was not fetched.
47938
+ // A negative radius lists nothing, not even a target at distance 0.
47939
+ radiusLy: sweep.partial ? Math.min(a.radius_ly, sweep.coveredLy - MASSACRE_MARGIN_LY) : a.radius_ly,
46890
47940
  maxStarLs: a.max_star_ls
46891
47941
  });
46892
47942
  notes.push(
@@ -46898,15 +47948,14 @@ function registerSystemTools(server, deps) {
46898
47948
  );
46899
47949
  }
46900
47950
  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
- );
47951
+ notes.push(partialNote(sweep.coveredLy));
46904
47952
  }
46905
47953
  return {
46906
47954
  near,
46907
47955
  searched: sweep.systems.length,
46908
47956
  left_out_spread: leftOutSpread,
46909
47957
  partial: sweep.partial,
47958
+ ...sweep.partial ? { covered_ly: Math.floor(sweep.coveredLy * 10) / 10 } : {},
46910
47959
  travel: describeRules(rules).travel,
46911
47960
  targets: targets.slice(0, a.limit).map((t) => ({
46912
47961
  target_faction: t.faction,
@@ -46945,6 +47994,121 @@ function registerSystemTools(server, deps) {
46945
47994
  }
46946
47995
  )
46947
47996
  );
47997
+ server.registerTool(
47998
+ "ground_cz_targets",
47999
+ {
48000
+ annotations: {
48001
+ title: "Find Ground Conflict Zones",
48002
+ readOnlyHint: true,
48003
+ destructiveHint: false,
48004
+ idempotentHint: true,
48005
+ openWorldHint: true
48006
+ },
48007
+ description: "Find likely ground (on-foot) conflict zones near you: systems with a faction at War or Civil War, settlements owned by the warring factions, and a Frontline Solutions station close to those settlements (within max_gap_ls of distance from the arrival star, so on the same planet or its moons) that meets the minimum pad; left_out.pad_too_small counts systems where only smaller-pad stations are close. Data older than max_age_hours (default 6) is left out: wars often end at the daily tick. The best station is the quickest to dock at (a planetary landing counts against it), then the one with most warring settlements nearby. For the top detail_count results, body detail says whether the station is a planetary port on the settlements' planet (drop: same_body) or an orbital station next to that planet and its moons (drop: orbital). drop: gap_only means the station and settlements are at about the same distance from the arrival star, but the body data could not confirm the same planet or its moons: check each settlement's body. drop: null (with near_body and body_family null, and no body on settlements) means no body detail was fetched: the result is beyond detail_count, the time limit was reached, or the fetch failed. Zones are likely, not confirmed. near defaults to the journal location.",
48008
+ inputSchema: {
48009
+ near: external_exports.string().optional().describe("Exact reference system name"),
48010
+ radius_ly: Radius,
48011
+ max_age_hours: external_exports.number().min(1).max(168).default(6),
48012
+ max_gap_ls: external_exports.number().min(0.1).max(20).default(1),
48013
+ detail_count: external_exports.number().int().min(0).max(15).default(8),
48014
+ limit: external_exports.number().int().min(1).max(100).default(20),
48015
+ preferences: PrefsPatchSchema.optional(),
48016
+ use_preferences: external_exports.boolean().default(true)
48017
+ }
48018
+ },
48019
+ wrap(
48020
+ async (a) => {
48021
+ const notes = [];
48022
+ const near = await resolveNear(a.near, notes);
48023
+ const rules = await resolveSearchRules(deps, a.preferences, a.use_preferences);
48024
+ const deadline = now() + TIME_LIMIT_MS2;
48025
+ const found = await deps.spansh.frontlineWarSystems(near, a.radius_ly, { deadline });
48026
+ const { targets, leftOut } = groundCzTargets(found.systems, found.centre, {
48027
+ now: now(),
48028
+ maxAgeHours: a.max_age_hours,
48029
+ maxGapLs: a.max_gap_ls,
48030
+ minPad: rules.pad.size,
48031
+ dockSeconds: (arrivalLs, planetary) => legSeconds({ distanceLy: 0, arrivalLs, planetary, stationKnown: true }, rules.travel).seconds
48032
+ });
48033
+ const shown = targets.slice(0, a.limit);
48034
+ const details = /* @__PURE__ */ new Map();
48035
+ let timedOut = 0;
48036
+ for (const t of shown.slice(0, a.detail_count)) {
48037
+ if (now() >= deadline) {
48038
+ timedOut++;
48039
+ continue;
48040
+ }
48041
+ try {
48042
+ const bodies = await deps.spansh.systemBodies(t.system.id64);
48043
+ if (!bodies.length)
48044
+ notes.push(`No body detail for ${t.system.name}: Spansh returned no bodies.`);
48045
+ else details.set(t.system.id64, czDetail(t.best, bodies, a.max_gap_ls));
48046
+ } catch (err) {
48047
+ notes.push(
48048
+ `No body detail for ${t.system.name}: ${err instanceof Error ? err.message : String(err)}`
48049
+ );
48050
+ }
48051
+ }
48052
+ notes.push(
48053
+ "These are likely ground conflict zones, not confirmed: Spansh does not see live conflict zone signals, and a war can end at the daily tick."
48054
+ );
48055
+ if (timedOut) {
48056
+ notes.push(
48057
+ `${timedOut} ${timedOut === 1 ? "result is" : "results are"} without body detail: the time limit was reached. Call again to fill them in.`
48058
+ );
48059
+ }
48060
+ if (found.partial) {
48061
+ notes.push("Spansh did not return every system in time; results may be missing.");
48062
+ }
48063
+ const minutes2 = (s) => Math.round(s / 6) / 10;
48064
+ return {
48065
+ near,
48066
+ searched: found.systems.length,
48067
+ left_out: leftOut,
48068
+ partial: found.partial,
48069
+ travel: describeRules(rules).travel,
48070
+ targets: shown.map((t) => {
48071
+ const d = details.get(t.system.id64) ?? null;
48072
+ const l = leg(t.distanceLy, rules.travel);
48073
+ return {
48074
+ system: t.system.name,
48075
+ distance_ly: Math.round(t.distanceLy * 100) / 100,
48076
+ jumps: l.jumps,
48077
+ travel_min: minutes2(l.seconds),
48078
+ age_hours: Math.round(t.ageHours * 10) / 10,
48079
+ updated_at: t.system.updated_at,
48080
+ warring_factions: t.warring,
48081
+ best_station: {
48082
+ name: t.best.name,
48083
+ type: t.best.type,
48084
+ pad: t.best.pad,
48085
+ arrival_ls: Math.round(t.best.arrival_ls * 100) / 100,
48086
+ dock_min: minutes2(t.best.dock_s),
48087
+ drop: d?.drop ?? null,
48088
+ near_body: d?.near_body ?? null,
48089
+ settlements: t.best.settlements.map((s) => ({
48090
+ name: s.name,
48091
+ owner: s.owner,
48092
+ gap_ls: Math.round(s.gap_ls * 1e3) / 1e3,
48093
+ ...d ? { body: d.settlementBodies[s.name] ?? null } : {}
48094
+ }))
48095
+ },
48096
+ other_frontline_stations: t.others.map((s) => ({
48097
+ name: s.name,
48098
+ type: s.type,
48099
+ pad: s.pad,
48100
+ arrival_ls: Math.round(s.arrival_ls * 100) / 100,
48101
+ settlements: s.settlements.length
48102
+ })),
48103
+ warring_settlements: t.warringSettlements,
48104
+ body_family: d?.body_family ?? null
48105
+ };
48106
+ }),
48107
+ notes
48108
+ };
48109
+ }
48110
+ )
48111
+ );
46948
48112
  }
46949
48113
 
46950
48114
  // src/tools/inara.ts
@@ -47017,8 +48181,8 @@ function registerEdsmTools(server, deps) {
47017
48181
  }
47018
48182
 
47019
48183
  // src/routes/pool.ts
47020
- var lc = (s) => s.toLowerCase();
47021
- var namesOf = (s) => [s.label, ...s.aliases];
48184
+ var lc2 = (s) => s.toLowerCase();
48185
+ var namesOf2 = (s) => [s.label, ...s.aliases];
47022
48186
  function joinPurposes(a, b) {
47023
48187
  const parts = [a, b].flatMap((p) => p ? p.split("; ") : []);
47024
48188
  return parts.length ? [...new Set(parts)].join("; ") : null;
@@ -47027,8 +48191,8 @@ function buildPool(a, catalog) {
47027
48191
  const used = /* @__PURE__ */ new Set();
47028
48192
  const unique = (label) => {
47029
48193
  let candidate = label;
47030
- for (let n = 2; used.has(lc(candidate)); n++) candidate = `${label}-${n}`;
47031
- used.add(lc(candidate));
48194
+ for (let n = 2; used.has(lc2(candidate)); n++) candidate = `${label}-${n}`;
48195
+ used.add(lc2(candidate));
47032
48196
  return candidate;
47033
48197
  };
47034
48198
  const resolveEngineer = (name) => {
@@ -47047,13 +48211,16 @@ function buildPool(a, catalog) {
47047
48211
  );
47048
48212
  }
47049
48213
  const station = input2.station ?? (input2.system ? void 0 : engineer2?.base);
47050
- const key = engineer2 ? `${lc(engineer2.system)}|${lc(engineer2.base)}` : `${lc(system)}|${lc(station ?? "")}`;
47051
- const name = input2.label ?? engineer2?.name ?? input2.station ?? system;
48214
+ const key = engineer2 ? `${lc2(engineer2.system)}|${lc2(engineer2.base)}` : `${lc2(system)}|${lc2(station ?? "")}${!station && input2.body ? `|${lc2(input2.body)}` : ""}`;
48215
+ const name = input2.label ?? engineer2?.name ?? input2.station ?? input2.body ?? system;
47052
48216
  const hit = byKey.get(key);
47053
48217
  if (hit) {
47054
- if (!namesOf(hit).some((n) => lc(n) === lc(name))) hit.aliases.push(unique(name));
48218
+ if (!namesOf2(hit).some((n) => lc2(n) === lc2(name))) hit.aliases.push(unique(name));
47055
48219
  if (input2.after) hit.after = [.../* @__PURE__ */ new Set([...hit.after ?? [], ...input2.after])];
47056
48220
  hit.purpose = joinPurposes(hit.purpose, over.purpose ?? null);
48221
+ hit.notes = joinPurposes(hit.notes, over.notes ?? null);
48222
+ hit.body ??= input2.body ?? null;
48223
+ if (over.planStop !== void 0) hit.planStops.push(over.planStop);
47057
48224
  hit.engineer ??= engineer2;
47058
48225
  if (engineer2 || (over.kind ?? input2.kind) === "engineer") hit.kind = "engineer";
47059
48226
  return hit;
@@ -47064,34 +48231,43 @@ function buildPool(a, catalog) {
47064
48231
  aliases: [],
47065
48232
  system,
47066
48233
  station: station ?? null,
48234
+ body: input2.body ?? null,
47067
48235
  engineer: engineer2,
47068
48236
  kind: over.kind ?? input2.kind ?? (engineer2 ? "engineer" : "other"),
47069
48237
  purpose: over.purpose ?? null,
47070
- after: input2.after
48238
+ notes: over.notes ?? null,
48239
+ after: input2.after,
48240
+ planStops: over.planStop === void 0 ? [] : [over.planStop]
47071
48241
  };
47072
48242
  byKey.set(key, stop);
47073
48243
  return stop;
47074
48244
  };
47075
48245
  const entries = a.stops ?? [];
47076
48246
  const candidates = [];
48247
+ const finished = /* @__PURE__ */ new Map();
47077
48248
  if (a.plan) {
48249
+ const planStops = a.plan.stops.filter((s) => s.status !== "done" && s.status !== "skipped");
48250
+ for (const s of a.plan.stops) {
48251
+ if (s.status !== "done" && s.status !== "skipped") continue;
48252
+ for (const n of [s.label, s.station, s.body, s.system]) if (n) finished.set(lc2(n), n);
48253
+ }
47078
48254
  const named = /* @__PURE__ */ new Set();
47079
- for (const s of a.plan.stops) if (s.kind === "engineer" && s.label) named.add(lc(s.label));
48255
+ for (const s of planStops) if (s.kind === "engineer" && s.label) named.add(lc2(s.label));
47080
48256
  for (const e of entries) {
47081
48257
  if ("one_of" in e) continue;
47082
- if (e.engineer) named.add(lc(e.engineer));
47083
- if (e.kind === "engineer" && e.label) named.add(lc(e.label));
48258
+ if (e.engineer) named.add(lc2(e.engineer));
48259
+ if (e.kind === "engineer" && e.label) named.add(lc2(e.label));
47084
48260
  }
47085
48261
  const byEngineer = /* @__PURE__ */ new Map();
47086
48262
  for (const item of a.plan.items) {
47087
48263
  if (!item.engineer || item.status === "done" || item.status === "skipped") continue;
47088
- const entry2 = byEngineer.get(lc(item.engineer)) ?? {
48264
+ const entry2 = byEngineer.get(lc2(item.engineer)) ?? {
47089
48265
  id: item.id,
47090
48266
  name: item.engineer,
47091
48267
  descriptions: []
47092
48268
  };
47093
48269
  entry2.descriptions.push(item.description);
47094
- byEngineer.set(lc(item.engineer), entry2);
48270
+ byEngineer.set(lc2(item.engineer), entry2);
47095
48271
  }
47096
48272
  const baseFor = /* @__PURE__ */ new Map();
47097
48273
  if (catalog) {
@@ -47102,26 +48278,27 @@ function buildPool(a, catalog) {
47102
48278
  } catch {
47103
48279
  continue;
47104
48280
  }
47105
- const here = a.plan.stops.filter(
47106
- (s) => s.kind === "engineer" && !s.station && !baseFor.has(s) && lc(s.system ?? "") === lc(e.system)
48281
+ const here = planStops.filter(
48282
+ (s) => s.kind === "engineer" && !s.station && !baseFor.has(s) && lc2(s.system ?? "") === lc2(e.system)
47107
48283
  );
47108
- const mentions = (s) => lc(`${s.label ?? ""} ${s.purpose ?? ""}`).includes(lc(e.name));
48284
+ const mentions = (s) => lc2(`${s.label ?? ""} ${s.purpose ?? ""}`).includes(lc2(e.name));
47109
48285
  const match = here.find(mentions) ?? (named.has(key) ? void 0 : here[0]);
47110
48286
  if (match) baseFor.set(match, { base: e.base, engineer: e.name });
47111
48287
  }
47112
48288
  }
47113
- for (const s of a.plan.stops) {
48289
+ for (const s of planStops) {
47114
48290
  candidates.push({
47115
48291
  fixed: make(
47116
48292
  {
47117
48293
  label: s.label,
47118
48294
  system: s.system,
47119
48295
  station: s.station ?? baseFor.get(s)?.base,
48296
+ body: s.body,
47120
48297
  engineer: baseFor.get(s)?.engineer,
47121
48298
  kind: s.kind,
47122
48299
  after: s.after
47123
48300
  },
47124
- { purpose: s.purpose }
48301
+ { purpose: s.purpose, notes: s.notes, planStop: a.plan.stops.indexOf(s) }
47125
48302
  )
47126
48303
  });
47127
48304
  }
@@ -47153,26 +48330,30 @@ function buildPool(a, catalog) {
47153
48330
  const notes = [];
47154
48331
  const known = /* @__PURE__ */ new Map();
47155
48332
  for (const c of candidates) {
47156
- if ("group" in c) known.set(lc(c.group), c.group);
48333
+ if ("group" in c) known.set(lc2(c.group), c.group);
47157
48334
  for (const s of "fixed" in c ? [c.fixed] : c.options) {
47158
- for (const n of namesOf(s)) known.set(lc(n), n);
48335
+ for (const n of namesOf2(s)) known.set(lc2(n), n);
47159
48336
  }
47160
48337
  }
47161
48338
  for (const l of [...a.only ?? [], ...a.skip ?? []]) {
47162
- if (!known.has(lc(l))) {
48339
+ if (!known.has(lc2(l)) && finished.has(lc2(l))) {
48340
+ notes.push(`"${l}" is done or skipped in the plan, so the route leaves it out.`);
48341
+ continue;
48342
+ }
48343
+ if (!known.has(lc2(l))) {
47163
48344
  throw new UserError(`Unknown stop label "${l}". Known: ${[...known.values()].join(", ")}`);
47164
48345
  }
47165
48346
  }
47166
- const only = a.only ? new Set(a.only.map(lc)) : null;
47167
- const skip = new Set((a.skip ?? []).map(lc));
48347
+ const only = a.only ? new Set(a.only.map(lc2)) : null;
48348
+ const skip = new Set((a.skip ?? []).map(lc2));
47168
48349
  const keep = (s, viaGroup) => {
47169
- const names = namesOf(s).map(lc);
48350
+ const names = namesOf2(s).map(lc2);
47170
48351
  return (!only || viaGroup || names.some((n) => only.has(n))) && !names.some((n) => skip.has(n));
47171
48352
  };
47172
48353
  const kept = candidates.map((c) => {
47173
48354
  if ("fixed" in c) return keep(c.fixed, false) ? [c.fixed] : [];
47174
- if (skip.has(lc(c.group))) return [];
47175
- const viaGroup = !!only?.has(lc(c.group));
48355
+ if (skip.has(lc2(c.group))) return [];
48356
+ const viaGroup = !!only?.has(lc2(c.group));
47176
48357
  return [...new Set(c.options.filter((o) => keep(o, viaGroup)))];
47177
48358
  });
47178
48359
  const fixedStops = new Set(
@@ -47190,8 +48371,8 @@ function buildPool(a, catalog) {
47190
48371
  }
47191
48372
  const met = options.length > 1 ? options.find((o) => fixedStops.has(o)) : void 0;
47192
48373
  groupLabels.set(
47193
- lc(c.group),
47194
- (met ? [met] : options).map((o) => lc(o.label))
48374
+ lc2(c.group),
48375
+ (met ? [met] : options).map((o) => lc2(o.label))
47195
48376
  );
47196
48377
  if (options.length === 0) {
47197
48378
  notes.push(`Group "${c.group}" has no stops left after only/skip; dropped.`);
@@ -47208,17 +48389,17 @@ function buildPool(a, catalog) {
47208
48389
  });
47209
48390
  if (stops.length === 0 && groups.length === 0) throw new UserError("No stops to route.");
47210
48391
  const all = [.../* @__PURE__ */ new Set([...stops, ...groups.flatMap((g) => g.options)])];
47211
- const present = new Map(all.flatMap((s) => namesOf(s).map((n) => [lc(n), lc(s.label)])));
48392
+ const present = new Map(all.flatMap((s) => namesOf2(s).map((n) => [lc2(n), lc2(s.label)])));
47212
48393
  const rulesApplied = [];
47213
48394
  const after = /* @__PURE__ */ new Map();
47214
48395
  const planFallback = !!a.plan && a.planRules;
47215
48396
  const fallbackTargets = all.filter((s) => s.kind === "shop" || s.kind === "trader");
47216
48397
  let fallbackUsed = false;
47217
48398
  for (const s of all) {
47218
- const key = lc(s.label);
48399
+ const key = lc2(s.label);
47219
48400
  if (s.after === void 0) {
47220
48401
  if (!planFallback || s.kind !== "engineer") continue;
47221
- const targets = fallbackTargets.filter((t) => t !== s).map((t) => lc(t.label));
48402
+ const targets = fallbackTargets.filter((t) => t !== s).map((t) => lc2(t.label));
47222
48403
  if (targets.length === 0) continue;
47223
48404
  after.set(key, targets);
47224
48405
  fallbackUsed = true;
@@ -47227,8 +48408,8 @@ function buildPool(a, catalog) {
47227
48408
  const resolved = [];
47228
48409
  const named = [];
47229
48410
  for (const l of s.after) {
47230
- const main2 = present.get(lc(l));
47231
- const expanded = groupLabels.get(lc(l)) ?? (main2 ? [main2] : []);
48411
+ const main2 = present.get(lc2(l));
48412
+ const expanded = groupLabels.get(lc2(l)) ?? (main2 ? [main2] : []);
47232
48413
  const targets = expanded.filter((t) => t !== key);
47233
48414
  if (targets.length === 0) {
47234
48415
  notes.push(`Rule "${s.label} after ${l}" ignored: ${l} is not in this route.`);
@@ -47301,16 +48482,17 @@ function solveRoute(input2) {
47301
48482
 
47302
48483
  // src/tools/routes.ts
47303
48484
  var min2 = (s) => Math.round(s / 6) / 10;
47304
- var lc2 = (s) => s.toLowerCase();
48485
+ var lc3 = (s) => s.toLowerCase();
47305
48486
  var LONG_LEG_LY = 200;
47306
48487
  var PLOT_ROUNDS = 2;
47307
48488
  var PLOT_DIFF = 0.2;
47308
48489
  var PLOT_PENDING_NOTE = "Spansh is still plotting; call again to finish.";
47309
48490
  var PLOT_BUDGET_MS = 4e4;
47310
48491
  var StopInputSchema = external_exports.object({
47311
- label: external_exports.string().min(1).optional().describe("Name for rules and output; default the engineer, station or system"),
48492
+ label: external_exports.string().min(1).optional().describe("Name for rules and output; default the engineer, station, body or system"),
47312
48493
  system: external_exports.string().optional(),
47313
48494
  station: external_exports.string().optional(),
48495
+ body: external_exports.string().min(1).optional().describe("Planet or moon, for a stop with no station"),
47314
48496
  engineer: external_exports.string().optional().describe("Places the stop from the engineer catalog"),
47315
48497
  kind: StopKind.optional(),
47316
48498
  after: external_exports.array(external_exports.string()).optional().describe("Labels of stops that must come before this one")
@@ -47334,7 +48516,7 @@ function registerRouteTools(server, deps) {
47334
48516
  idempotentHint: true,
47335
48517
  openWorldHint: true
47336
48518
  },
47337
- description: "Order the stops for a build: shops, material traders and engineers, from a plan (plan_id) or a list (stops). Uses Spansh coordinates and the travel-time model. For each engineer stop, set `after` to the shops and traders that supply what that engineer needs (the module to buy, the materials to trade). Give `after: []` to an engineer who needs nothing from the other stops, so they can come first. Without your rules, a plan puts every engineer after every shop and trader. Use `one_of` for alternatives, for example two engineers who both do the work; the tool tries every combination (at most 16) and keeps the fastest. `only` and `skip` choose stops from the plan, so one plan can serve several trips. One place (the same engineer, or the same system and station) can be an option in several groups; it is visited once, and each stop's `meets` lists the groups it covers. Leg times are estimates from straight-line jumps; pass plot: true to plot the chosen route with Spansh.",
48519
+ description: "Order the stops of a plan (plan_id) or a list (stops): shops, material traders and engineers for a build, or the systems, bodies and stations of a route. Plan stops marked done or skipped are left out. Uses Spansh coordinates and the travel-time model. For each engineer stop, set `after` to the shops and traders that supply what that engineer needs (the module to buy, the materials to trade). Give `after: []` to an engineer who needs nothing from the other stops, so they can come first. Without your rules, a plan puts every engineer after every shop and trader. Use `one_of` for alternatives, for example two engineers who both do the work; the tool tries every combination (at most 16) and keeps the fastest. `only` and `skip` choose stops from the plan, so one plan can serve several trips. One place (the same engineer, or the same system and station) can be an option in several groups; it is visited once, and each stop's `meets` lists the groups it covers. Leg times are estimates from straight-line jumps; pass plot: true to plot the chosen route with Spansh.",
47338
48520
  inputSchema: {
47339
48521
  plan_id: external_exports.string().optional(),
47340
48522
  stops: external_exports.array(StopEntrySchema).optional(),
@@ -47369,9 +48551,9 @@ function registerRouteTools(server, deps) {
47369
48551
  countCombinations(pool.groups.map((g) => g.options.length));
47370
48552
  const all = [.../* @__PURE__ */ new Set([...pool.stops, ...pool.groups.flatMap((g) => g.options)])];
47371
48553
  const names = (s) => [s.label, ...s.aliases];
47372
- const labelIndex = new Map(all.map((s, i) => [lc2(s.label), i]));
48554
+ const labelIndex = new Map(all.map((s, i) => [lc3(s.label), i]));
47373
48555
  const graph = all.map(
47374
- (s) => (pool.after.get(lc2(s.label)) ?? []).flatMap((l) => labelIndex.get(l) ?? [])
48556
+ (s) => (pool.after.get(lc3(s.label)) ?? []).flatMap((l) => labelIndex.get(l) ?? [])
47375
48557
  );
47376
48558
  const cycle = findCycle(graph);
47377
48559
  if (cycle) {
@@ -47409,7 +48591,7 @@ function registerRouteTools(server, deps) {
47409
48591
  const seen = nodeOfStop.get(s);
47410
48592
  if (seen !== void 0) return seen;
47411
48593
  const p = await placeStop(s);
47412
- const key2 = p.station ? `${lc2(p.system)}|${lc2(p.station)}` : null;
48594
+ const key2 = p.station ? `${lc3(p.system)}|${lc3(p.station)}` : null;
47413
48595
  const hit = merge2 && key2 ? byStation.get(key2) : void 0;
47414
48596
  let idx;
47415
48597
  if (hit !== void 0) {
@@ -47418,6 +48600,9 @@ function registerRouteTools(server, deps) {
47418
48600
  node2.engineer ??= s.engineer?.name ?? null;
47419
48601
  if (s.kind === "engineer" || s.engineer) node2.kind = "engineer";
47420
48602
  node2.purpose = joinPurposes(node2.purpose, s.purpose);
48603
+ node2.notes = joinPurposes(node2.notes, s.notes);
48604
+ node2.body ??= s.body;
48605
+ node2.planStops.push(...s.planStops);
47421
48606
  idx = hit;
47422
48607
  } else {
47423
48608
  nodes.push({
@@ -47425,12 +48610,15 @@ function registerRouteTools(server, deps) {
47425
48610
  place: p,
47426
48611
  engineer: s.engineer?.name ?? null,
47427
48612
  kind: s.kind,
47428
- purpose: s.purpose
48613
+ purpose: s.purpose,
48614
+ body: s.body,
48615
+ notes: s.notes,
48616
+ planStops: [...s.planStops]
47429
48617
  });
47430
48618
  idx = nodes.length - 1;
47431
48619
  if (merge2 && key2) byStation.set(key2, idx);
47432
48620
  }
47433
- nodeOf.set(lc2(s.label), idx);
48621
+ nodeOf.set(lc3(s.label), idx);
47434
48622
  nodeOfStop.set(s, idx);
47435
48623
  return idx;
47436
48624
  };
@@ -47493,7 +48681,7 @@ function registerRouteTools(server, deps) {
47493
48681
  ...returnPlace ? [returnPlace] : []
47494
48682
  ];
47495
48683
  const leg = (from, to) => legBetween(places[from], places[to], travel);
47496
- const sameSystem = (f, t) => lc2(places[f].system) === lc2(places[t].system);
48684
+ const sameSystem = (f, t) => lc3(places[f].system) === lc3(places[t].system);
47497
48685
  const plottedJumps = /* @__PURE__ */ new Map();
47498
48686
  const unfinished = /* @__PURE__ */ new Set();
47499
48687
  const failed = /* @__PURE__ */ new Map();
@@ -47587,6 +48775,8 @@ function registerRouteTools(server, deps) {
47587
48775
  engineer: node2?.engineer ?? null,
47588
48776
  kind: node2 ? node2.kind : "other",
47589
48777
  purpose: node2 ? node2.purpose : null,
48778
+ ...node2?.body ? { body: node2.body } : {},
48779
+ ...node2?.notes ? { notes: node2.notes } : {},
47590
48780
  meets: allChosen.filter((c) => c.option === t).map((c) => c.group),
47591
48781
  jumps: plottedJumps.get(key(f, t)) ?? l.jumps,
47592
48782
  leg_min: min2(seconds),
@@ -47622,18 +48812,26 @@ function registerRouteTools(server, deps) {
47622
48812
  let saved = false;
47623
48813
  if (a.save_to_plan && a.plan_id) {
47624
48814
  try {
47625
- await deps.plans.setRoute(a.plan_id, {
47626
- computed_at: new Date(now()).toISOString(),
47627
- from: startPlace.system,
47628
- stops: [
47629
- ...solved.order.map(optionLabel),
47630
- ...returnPlace ? [
47631
- `return to ${returnPlace.system}${returnPlace.station ? ` \u2014 ${returnPlace.station}` : ""}`
47632
- ] : []
47633
- ],
47634
- total_min: total,
47635
- plotted: a.plot && allPlotted
47636
- });
48815
+ await deps.plans.setRoute(
48816
+ a.plan_id,
48817
+ {
48818
+ computed_at: new Date(now()).toISOString(),
48819
+ from: startPlace.system,
48820
+ stops: [
48821
+ ...solved.order.map(optionLabel),
48822
+ ...returnPlace ? [
48823
+ `return to ${returnPlace.system}${returnPlace.station ? ` \u2014 ${returnPlace.station}` : ""}`
48824
+ ] : []
48825
+ ],
48826
+ total_min: total,
48827
+ plotted: a.plot && allPlotted,
48828
+ stop_indexes: [
48829
+ ...solved.order.map((i) => [...new Set(nodes[i].planStops)]),
48830
+ ...returnPlace ? [[]] : []
48831
+ ]
48832
+ },
48833
+ { stops: plan?.stops }
48834
+ );
47637
48835
  saved = true;
47638
48836
  } catch (err) {
47639
48837
  notes.add(`The route was not saved to plan "${a.plan_id}": ${reason(err)}`);
@@ -47663,7 +48861,7 @@ function registerRouteTools(server, deps) {
47663
48861
 
47664
48862
  // src/tools/plans.ts
47665
48863
  import { randomUUID as randomUUID2 } from "crypto";
47666
- import { mkdir as mkdir6, readFile as readFile7, rename as rename5, stat as stat7, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
48864
+ import { mkdir as mkdir7, readFile as readFile7, rename as rename5, stat as stat7, unlink as unlink3, writeFile as writeFile5 } from "fs/promises";
47667
48865
  import path13 from "path";
47668
48866
  var MAX_ECHOED_PLAN = 100 * 1024;
47669
48867
  function registerPlanTools(server, deps) {
@@ -47703,7 +48901,7 @@ ${tail}`);
47703
48901
  idempotentHint: true,
47704
48902
  openWorldHint: false
47705
48903
  },
47706
- description: "List saved build plans with item counts and progress."
48904
+ description: "List saved plans with item counts, progress and next_stop (the first stop still to do). A plan is a ship build or a route, or both."
47707
48905
  },
47708
48906
  wrapWarn(async () => plans.list(), warnings)
47709
48907
  );
@@ -47717,10 +48915,13 @@ ${tail}`);
47717
48915
  idempotentHint: true,
47718
48916
  openWorldHint: false
47719
48917
  },
47720
- description: "Get a saved plan by id.",
48918
+ description: "Get a saved plan (a build or a route) by id. next_stop is the first stop still to do, in the saved route's order when there is one, else in the order of stops.",
47721
48919
  inputSchema: { id: external_exports.string() }
47722
48920
  },
47723
- wrapWarn(async ({ id }) => plans.get(id), warnings)
48921
+ wrapWarn(async ({ id }) => {
48922
+ const plan = await plans.get(id);
48923
+ return { ...plan, next_stop: nextStop(plan) };
48924
+ }, warnings)
47724
48925
  );
47725
48926
  server.registerTool(
47726
48927
  "plan_upsert",
@@ -47732,7 +48933,7 @@ ${tail}`);
47732
48933
  idempotentHint: true,
47733
48934
  openWorldHint: false
47734
48935
  },
47735
- description: "Create or replace a build plan. Items are things to buy, engineer, unlock, gather or trade, plus refits (moving modules between slots, storing modules, or a fitting decision); stops are the places to visit (optionally with label, kind and after rules); route is the saved visiting order. Replacing keeps created_at.",
48936
+ description: "Create or replace a plan. A plan is a ship build, a route, or both: for example system X, planet Y for bios, then planet Z, then station A to hand in. Items are things to buy, engineer, unlock, gather or trade, plus refits (moving modules between slots, storing modules, or a fitting decision). Stops are the places to visit, each with a system and optionally a station or body (planet or moon), a label, a kind, a status (todo, done, skipped), notes and after rules. Without a saved route, the order of stops is the visiting order; route is the order route_stops saved. Replacing keeps created_at.",
47736
48937
  inputSchema: { plan: PlanInputSchema }
47737
48938
  },
47738
48939
  wrapWarn(
@@ -47781,6 +48982,54 @@ ${tail}`);
47781
48982
  warnings
47782
48983
  )
47783
48984
  );
48985
+ server.registerTool(
48986
+ "plan_update_stop",
48987
+ {
48988
+ annotations: {
48989
+ title: "Update Plan Stop",
48990
+ readOnlyHint: false,
48991
+ destructiveHint: false,
48992
+ idempotentHint: true,
48993
+ openWorldHint: false
48994
+ },
48995
+ description: "Set the status (todo, done, skipped) and/or notes of one stop in a plan. Name the stop by label (its label, else its station, body or system) or by index in the plan's stops, from 0; give exactly one. Mark a stop done when the player has finished there; returns the plan with the new next_stop.",
48996
+ inputSchema: {
48997
+ plan_id: external_exports.string(),
48998
+ label: external_exports.string().min(1).optional(),
48999
+ index: external_exports.number().int().min(0).optional(),
49000
+ status: StopStatus.optional(),
49001
+ notes: external_exports.string().optional()
49002
+ }
49003
+ },
49004
+ wrapWarn(
49005
+ async (a) => {
49006
+ if (a.label === void 0 === (a.index === void 0)) {
49007
+ throw new UserError("Give exactly one of label or index to name the stop");
49008
+ }
49009
+ if (a.status === void 0 && a.notes === void 0) {
49010
+ throw new UserError("Give status or notes to update");
49011
+ }
49012
+ const which = a.label !== void 0 ? `"${a.label}"` : String(a.index);
49013
+ try {
49014
+ const plan = await plans.updateStop(
49015
+ a.plan_id,
49016
+ { label: a.label, index: a.index },
49017
+ { status: a.status, notes: a.notes }
49018
+ );
49019
+ return { ...plan, next_stop: nextStop(plan) };
49020
+ } catch (err) {
49021
+ const changes = [
49022
+ ...a.status !== void 0 ? [`status: ${a.status}`] : [],
49023
+ ...a.notes !== void 0 ? ["notes changed"] : []
49024
+ ].join(", ");
49025
+ throw new UserError(
49026
+ `Stop ${which} in plan "${a.plan_id}" was not updated (${changes}). ${reason(err)}`
49027
+ );
49028
+ }
49029
+ },
49030
+ warnings
49031
+ )
49032
+ );
47784
49033
  server.registerTool(
47785
49034
  "plan_delete",
47786
49035
  {
@@ -47831,13 +49080,13 @@ ${tail}`);
47831
49080
  if (!a.overwrite && await exists(target)) {
47832
49081
  throw new UserError(`${target} exists; pass overwrite: true to replace it`);
47833
49082
  }
47834
- await mkdir6(path13.dirname(target), { recursive: true });
49083
+ await mkdir7(path13.dirname(target), { recursive: true });
47835
49084
  const tmp = `${target}.${process.pid}.${randomUUID2()}.tmp`;
47836
49085
  try {
47837
49086
  await writeFile5(tmp, text);
47838
49087
  await rename5(tmp, target);
47839
49088
  } catch (err) {
47840
- await unlink2(tmp).catch(() => void 0);
49089
+ await unlink3(tmp).catch(() => void 0);
47841
49090
  throw err;
47842
49091
  }
47843
49092
  });
@@ -47945,35 +49194,35 @@ var TOP = 5;
47945
49194
  var NO_CATALOG = 'Module names need the game data catalog. Call any catalog tool (for example catalog_module) or cache_refresh with source "catalog".';
47946
49195
  var cap = (s) => s[0].toUpperCase() + s.slice(1);
47947
49196
  var titleCase = (symbol2) => symbol2.split("_").filter(Boolean).map(cap).join(" ");
47948
- var str4 = (v) => typeof v === "string" ? v : void 0;
47949
- var num5 = (v) => typeof v === "number" ? v : 0;
49197
+ var str5 = (v) => typeof v === "string" ? v : void 0;
49198
+ var num6 = (v) => typeof v === "number" ? v : 0;
47950
49199
  var list = (v) => Array.isArray(v) ? v : [];
47951
- var same3 = (a, b) => a.toLowerCase() === b.toLowerCase();
49200
+ var same5 = (a, b) => a.toLowerCase() === b.toLowerCase();
47952
49201
  var byText = (a, b) => a.localeCompare(b);
47953
49202
  function vsMean(price, mean) {
47954
49203
  return mean > 0 ? Math.round((price - mean) / mean * 100) : null;
47955
49204
  }
47956
49205
  function marketRows(file2, commodity) {
47957
49206
  const rows = list(file2.data.Items).map((i) => ({
47958
- name: str4(i.Name_Localised) ?? str4(i.Name) ?? "?",
47959
- category: str4(i.Category_Localised) ?? str4(i.Category) ?? "",
49207
+ name: str5(i.Name_Localised) ?? str5(i.Name) ?? "?",
49208
+ category: str5(i.Category_Localised) ?? str5(i.Category) ?? "",
47960
49209
  i
47961
49210
  })).filter((r) => !commodity || r.name.toLowerCase().includes(commodity.toLowerCase())).sort((a, b) => byText(a.category, b.category) || byText(a.name, b.name));
47962
- const sells = rows.filter((r) => num5(r.i.Stock) > 0 && num5(r.i.BuyPrice) > 0).map((r) => ({
49211
+ const sells = rows.filter((r) => num6(r.i.Stock) > 0 && num6(r.i.BuyPrice) > 0).map((r) => ({
47963
49212
  name: r.name,
47964
49213
  category: r.category,
47965
- buy_price: num5(r.i.BuyPrice),
47966
- stock: num5(r.i.Stock),
47967
- mean_price: num5(r.i.MeanPrice),
47968
- vs_mean_pct: vsMean(num5(r.i.BuyPrice), num5(r.i.MeanPrice))
49214
+ buy_price: num6(r.i.BuyPrice),
49215
+ stock: num6(r.i.Stock),
49216
+ mean_price: num6(r.i.MeanPrice),
49217
+ vs_mean_pct: vsMean(num6(r.i.BuyPrice), num6(r.i.MeanPrice))
47969
49218
  }));
47970
49219
  const buys = rows.filter((r) => marketDemand(r.i) > 0).map((r) => ({
47971
49220
  name: r.name,
47972
49221
  category: r.category,
47973
- sell_price: num5(r.i.SellPrice),
49222
+ sell_price: num6(r.i.SellPrice),
47974
49223
  demand: marketDemand(r.i),
47975
- mean_price: num5(r.i.MeanPrice),
47976
- vs_mean_pct: vsMean(num5(r.i.SellPrice), num5(r.i.MeanPrice))
49224
+ mean_price: num6(r.i.MeanPrice),
49225
+ vs_mean_pct: vsMean(num6(r.i.SellPrice), num6(r.i.MeanPrice))
47977
49226
  }));
47978
49227
  return { sells, buys };
47979
49228
  }
@@ -47985,7 +49234,7 @@ async function modules(file2, catalog, notes) {
47985
49234
  const cat = await catalog?.peek().catch(() => null);
47986
49235
  if (!cat) notes.push(NO_CATALOG);
47987
49236
  return list(file2.data.Items).flatMap((i) => {
47988
- const symbol2 = str4(i.Name);
49237
+ const symbol2 = str5(i.Name);
47989
49238
  if (!symbol2) return [];
47990
49239
  const found = cat?.moduleBySymbol(symbol2);
47991
49240
  return [
@@ -47995,7 +49244,7 @@ async function modules(file2, catalog, notes) {
47995
49244
  class: found?.class ?? null,
47996
49245
  rating: found?.rating ?? null,
47997
49246
  group: found?.group ?? null,
47998
- price: num5(i.BuyPrice)
49247
+ price: num6(i.BuyPrice)
47999
49248
  }
48000
49249
  ];
48001
49250
  });
@@ -48024,8 +49273,8 @@ function groupModules(mods) {
48024
49273
  }
48025
49274
  function ships(file2) {
48026
49275
  return list(file2.data.PriceList).map((s) => ({
48027
- name: str4(s.ShipType_Localised) ?? titleCase(str4(s.ShipType) ?? "?"),
48028
- price: num5(s.ShipPrice)
49276
+ name: str5(s.ShipType_Localised) ?? titleCase(str5(s.ShipType) ?? "?"),
49277
+ price: num6(s.ShipPrice)
48029
49278
  })).sort((a, b) => a.price - b.price);
48030
49279
  }
48031
49280
  function registerStationTools(server, deps) {
@@ -48069,7 +49318,7 @@ function registerStationTools(server, deps) {
48069
49318
  notes.push(`No ${kind2} data. Open the ${kind2} screen at a station first.`);
48070
49319
  continue;
48071
49320
  }
48072
- if (docked && !same3(file2.station, docked)) {
49321
+ if (docked && !same5(file2.station, docked)) {
48073
49322
  notes.push(
48074
49323
  `${label} last opened at ${file2.station} (${file2.system}), not the station where you are docked.`
48075
49324
  );
@@ -48717,6 +49966,7 @@ function registerSettingsTools(server, deps) {
48717
49966
  const files = (await existing(localDir, [
48718
49967
  "plans.json",
48719
49968
  "preferences.json",
49969
+ "activity.json",
48720
49970
  "secrets.json",
48721
49971
  "secrets.enc"
48722
49972
  ])).filter((n) => path14.resolve(localDir, n) !== own2);
@@ -48765,6 +50015,13 @@ function createServer3(deps) {
48765
50015
  warnings: deps.planWarnings ?? deps.syncWarnings
48766
50016
  });
48767
50017
  if (deps.prefs) registerPrefsTools(server, { prefs: deps.prefs, warnings: deps.syncWarnings });
50018
+ if (deps.activity) {
50019
+ registerActivityTools(server, {
50020
+ activity: deps.activity,
50021
+ journal: deps.journal,
50022
+ warnings: deps.syncWarnings
50023
+ });
50024
+ }
48768
50025
  if (deps.settingsPage && deps.settings && deps.secrets) {
48769
50026
  registerSettingsTools(server, {
48770
50027
  page: deps.settingsPage,