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/README.md +67 -10
- package/dist/index.js +1558 -301
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 =
|
|
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 :
|
|
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(
|
|
1123
|
-
return unescapeJsonPointer(decodeURIComponent(
|
|
1122
|
+
function unescapeFragment(str6) {
|
|
1123
|
+
return unescapeJsonPointer(decodeURIComponent(str6));
|
|
1124
1124
|
}
|
|
1125
1125
|
exports.unescapeFragment = unescapeFragment;
|
|
1126
|
-
function escapeFragment(
|
|
1127
|
-
return encodeURIComponent(escapeJsonPointer(
|
|
1126
|
+
function escapeFragment(str6) {
|
|
1127
|
+
return encodeURIComponent(escapeJsonPointer(str6));
|
|
1128
1128
|
}
|
|
1129
1129
|
exports.escapeFragment = escapeFragment;
|
|
1130
|
-
function escapeJsonPointer(
|
|
1131
|
-
if (typeof
|
|
1132
|
-
return `${
|
|
1133
|
-
return
|
|
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(
|
|
1137
|
-
return
|
|
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(
|
|
2163
|
-
return
|
|
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(
|
|
3260
|
+
function findToken(str6, token) {
|
|
3261
3261
|
let ind = 0;
|
|
3262
|
-
for (let i = 0; i <
|
|
3263
|
-
if (
|
|
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 = (
|
|
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(
|
|
5129
|
-
const len =
|
|
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 =
|
|
5135
|
+
value = str6.charCodeAt(pos++);
|
|
5136
5136
|
if (value >= 55296 && value <= 56319 && pos < len) {
|
|
5137
|
-
value =
|
|
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(
|
|
7021
|
-
const matches2 = DATE.exec(
|
|
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(
|
|
7041
|
-
const matches2 = TIME.exec(
|
|
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(
|
|
7088
|
-
const dateTime =
|
|
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(
|
|
7114
|
-
return NOT_URI_FRAGMENT.test(
|
|
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(
|
|
7117
|
+
function byte(str6) {
|
|
7118
7118
|
BYTE.lastIndex = 0;
|
|
7119
|
-
return BYTE.test(
|
|
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(
|
|
7134
|
-
if (Z_ANCHOR.test(
|
|
7133
|
+
function regex(str6) {
|
|
7134
|
+
if (Z_ANCHOR.test(str6))
|
|
7135
7135
|
return false;
|
|
7136
7136
|
try {
|
|
7137
|
-
new RegExp(
|
|
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
|
|
8098
|
+
let str6 = "";
|
|
8099
8099
|
for (let i = 0; i < length; i++) {
|
|
8100
|
-
|
|
8100
|
+
str6 += chars[Math.floor(Math.random() * chars.length)];
|
|
8101
8101
|
}
|
|
8102
|
-
return
|
|
8102
|
+
return str6;
|
|
8103
8103
|
}
|
|
8104
|
-
function esc(
|
|
8105
|
-
return JSON.stringify(
|
|
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(
|
|
8220
|
-
return
|
|
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(
|
|
8487
|
-
const units =
|
|
8488
|
-
if (!highSurrogate.test(
|
|
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 ((
|
|
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.
|
|
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
|
|
29758
|
-
return { ...m, same_group:
|
|
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:
|
|
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
|
|
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:
|
|
31905
|
-
sell_price:
|
|
31906
|
-
supply:
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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.
|
|
32491
|
-
return { centre, systems,
|
|
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/
|
|
32895
|
-
import {
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
33920
|
+
const old = idx >= 0 ? data.plans[idx] : void 0;
|
|
33921
|
+
const plan = cleanRoute({
|
|
33219
33922
|
...parsed.data,
|
|
33220
|
-
created_at:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
33803
|
-
if (
|
|
33804
|
-
const prefix =
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
34262
|
-
if (this.opts.lockDir) await
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
|
43690
|
-
if (
|
|
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
|
|
43916
|
-
var prop = (b, 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 =
|
|
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 =
|
|
44799
|
+
const base2 = num4(m.module[feature]) || num4(m.module[`${mul[1]}mul`]);
|
|
43933
44800
|
return base2 * scale(m, "optmul");
|
|
43934
44801
|
}
|
|
43935
|
-
return
|
|
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
|
|
44073
|
-
var prop2 = (b, 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
|
|
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) =>
|
|
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) =>
|
|
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 (
|
|
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 =
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
47021
|
-
var
|
|
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(
|
|
47031
|
-
used.add(
|
|
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 ? `${
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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(
|
|
47083
|
-
if (e.kind === "engineer" && e.label) named.add(
|
|
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(
|
|
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(
|
|
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 =
|
|
47106
|
-
(s) => s.kind === "engineer" && !s.station && !baseFor.has(s) &&
|
|
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) =>
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
47167
|
-
const skip = new Set((a.skip ?? []).map(
|
|
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 =
|
|
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(
|
|
47175
|
-
const viaGroup = !!only?.has(
|
|
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
|
-
|
|
47194
|
-
(met ? [met] : options).map((o) =>
|
|
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) =>
|
|
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 =
|
|
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) =>
|
|
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(
|
|
47231
|
-
const expanded = groupLabels.get(
|
|
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
|
|
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
|
|
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) => [
|
|
48554
|
+
const labelIndex = new Map(all.map((s, i) => [lc3(s.label), i]));
|
|
47373
48555
|
const graph = all.map(
|
|
47374
|
-
(s) => (pool.after.get(
|
|
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 ? `${
|
|
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(
|
|
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) =>
|
|
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(
|
|
47626
|
-
|
|
47627
|
-
|
|
47628
|
-
|
|
47629
|
-
|
|
47630
|
-
|
|
47631
|
-
|
|
47632
|
-
|
|
47633
|
-
|
|
47634
|
-
|
|
47635
|
-
|
|
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
|
|
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
|
|
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 }) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
47949
|
-
var
|
|
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
|
|
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:
|
|
47959
|
-
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) =>
|
|
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:
|
|
47966
|
-
stock:
|
|
47967
|
-
mean_price:
|
|
47968
|
-
vs_mean_pct: vsMean(
|
|
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:
|
|
49222
|
+
sell_price: num6(r.i.SellPrice),
|
|
47974
49223
|
demand: marketDemand(r.i),
|
|
47975
|
-
mean_price:
|
|
47976
|
-
vs_mean_pct: vsMean(
|
|
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 =
|
|
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:
|
|
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:
|
|
48028
|
-
price:
|
|
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 && !
|
|
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,
|