ed-mcp 0.3.0 → 0.4.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 -17
- package/dist/index.js +1933 -463
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -408,11 +408,11 @@ var require_codegen = __commonJS({
|
|
|
408
408
|
const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
|
|
409
409
|
return `${varKind} ${this.name}${rhs};` + _n;
|
|
410
410
|
}
|
|
411
|
-
optimizeNames(names,
|
|
411
|
+
optimizeNames(names, constants3) {
|
|
412
412
|
if (!names[this.name.str])
|
|
413
413
|
return;
|
|
414
414
|
if (this.rhs)
|
|
415
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
415
|
+
this.rhs = optimizeExpr(this.rhs, names, constants3);
|
|
416
416
|
return this;
|
|
417
417
|
}
|
|
418
418
|
get names() {
|
|
@@ -429,10 +429,10 @@ var require_codegen = __commonJS({
|
|
|
429
429
|
render({ _n }) {
|
|
430
430
|
return `${this.lhs} = ${this.rhs};` + _n;
|
|
431
431
|
}
|
|
432
|
-
optimizeNames(names,
|
|
432
|
+
optimizeNames(names, constants3) {
|
|
433
433
|
if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
|
|
434
434
|
return;
|
|
435
|
-
this.rhs = optimizeExpr(this.rhs, names,
|
|
435
|
+
this.rhs = optimizeExpr(this.rhs, names, constants3);
|
|
436
436
|
return this;
|
|
437
437
|
}
|
|
438
438
|
get names() {
|
|
@@ -493,8 +493,8 @@ var require_codegen = __commonJS({
|
|
|
493
493
|
optimizeNodes() {
|
|
494
494
|
return `${this.code}` ? this : void 0;
|
|
495
495
|
}
|
|
496
|
-
optimizeNames(names,
|
|
497
|
-
this.code = optimizeExpr(this.code, names,
|
|
496
|
+
optimizeNames(names, constants3) {
|
|
497
|
+
this.code = optimizeExpr(this.code, names, constants3);
|
|
498
498
|
return this;
|
|
499
499
|
}
|
|
500
500
|
get names() {
|
|
@@ -523,12 +523,12 @@ var require_codegen = __commonJS({
|
|
|
523
523
|
}
|
|
524
524
|
return nodes.length > 0 ? this : void 0;
|
|
525
525
|
}
|
|
526
|
-
optimizeNames(names,
|
|
526
|
+
optimizeNames(names, constants3) {
|
|
527
527
|
const { nodes } = this;
|
|
528
528
|
let i = nodes.length;
|
|
529
529
|
while (i--) {
|
|
530
530
|
const n = nodes[i];
|
|
531
|
-
if (n.optimizeNames(names,
|
|
531
|
+
if (n.optimizeNames(names, constants3))
|
|
532
532
|
continue;
|
|
533
533
|
subtractNames(names, n.names);
|
|
534
534
|
nodes.splice(i, 1);
|
|
@@ -581,12 +581,12 @@ var require_codegen = __commonJS({
|
|
|
581
581
|
return void 0;
|
|
582
582
|
return this;
|
|
583
583
|
}
|
|
584
|
-
optimizeNames(names,
|
|
584
|
+
optimizeNames(names, constants3) {
|
|
585
585
|
var _a3;
|
|
586
|
-
this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names,
|
|
587
|
-
if (!(super.optimizeNames(names,
|
|
586
|
+
this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants3);
|
|
587
|
+
if (!(super.optimizeNames(names, constants3) || this.else))
|
|
588
588
|
return;
|
|
589
|
-
this.condition = optimizeExpr(this.condition, names,
|
|
589
|
+
this.condition = optimizeExpr(this.condition, names, constants3);
|
|
590
590
|
return this;
|
|
591
591
|
}
|
|
592
592
|
get names() {
|
|
@@ -609,10 +609,10 @@ var require_codegen = __commonJS({
|
|
|
609
609
|
render(opts) {
|
|
610
610
|
return `for(${this.iteration})` + super.render(opts);
|
|
611
611
|
}
|
|
612
|
-
optimizeNames(names,
|
|
613
|
-
if (!super.optimizeNames(names,
|
|
612
|
+
optimizeNames(names, constants3) {
|
|
613
|
+
if (!super.optimizeNames(names, constants3))
|
|
614
614
|
return;
|
|
615
|
-
this.iteration = optimizeExpr(this.iteration, names,
|
|
615
|
+
this.iteration = optimizeExpr(this.iteration, names, constants3);
|
|
616
616
|
return this;
|
|
617
617
|
}
|
|
618
618
|
get names() {
|
|
@@ -648,10 +648,10 @@ var require_codegen = __commonJS({
|
|
|
648
648
|
render(opts) {
|
|
649
649
|
return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
|
|
650
650
|
}
|
|
651
|
-
optimizeNames(names,
|
|
652
|
-
if (!super.optimizeNames(names,
|
|
651
|
+
optimizeNames(names, constants3) {
|
|
652
|
+
if (!super.optimizeNames(names, constants3))
|
|
653
653
|
return;
|
|
654
|
-
this.iterable = optimizeExpr(this.iterable, names,
|
|
654
|
+
this.iterable = optimizeExpr(this.iterable, names, constants3);
|
|
655
655
|
return this;
|
|
656
656
|
}
|
|
657
657
|
get names() {
|
|
@@ -693,11 +693,11 @@ var require_codegen = __commonJS({
|
|
|
693
693
|
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
|
|
694
694
|
return this;
|
|
695
695
|
}
|
|
696
|
-
optimizeNames(names,
|
|
696
|
+
optimizeNames(names, constants3) {
|
|
697
697
|
var _a3, _b;
|
|
698
|
-
super.optimizeNames(names,
|
|
699
|
-
(_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names,
|
|
700
|
-
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names,
|
|
698
|
+
super.optimizeNames(names, constants3);
|
|
699
|
+
(_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants3);
|
|
700
|
+
(_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants3);
|
|
701
701
|
return this;
|
|
702
702
|
}
|
|
703
703
|
get names() {
|
|
@@ -998,7 +998,7 @@ var require_codegen = __commonJS({
|
|
|
998
998
|
function addExprNames(names, from) {
|
|
999
999
|
return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
|
|
1000
1000
|
}
|
|
1001
|
-
function optimizeExpr(expr, names,
|
|
1001
|
+
function optimizeExpr(expr, names, constants3) {
|
|
1002
1002
|
if (expr instanceof code_1.Name)
|
|
1003
1003
|
return replaceName(expr);
|
|
1004
1004
|
if (!canOptimize(expr))
|
|
@@ -1013,14 +1013,14 @@ var require_codegen = __commonJS({
|
|
|
1013
1013
|
return items;
|
|
1014
1014
|
}, []));
|
|
1015
1015
|
function replaceName(n) {
|
|
1016
|
-
const c =
|
|
1016
|
+
const c = constants3[n.str];
|
|
1017
1017
|
if (c === void 0 || names[n.str] !== 1)
|
|
1018
1018
|
return n;
|
|
1019
1019
|
delete names[n.str];
|
|
1020
1020
|
return c;
|
|
1021
1021
|
}
|
|
1022
1022
|
function canOptimize(e) {
|
|
1023
|
-
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 &&
|
|
1023
|
+
return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants3[c.str] !== void 0);
|
|
1024
1024
|
}
|
|
1025
1025
|
}
|
|
1026
1026
|
function subtractNames(names, from) {
|
|
@@ -3260,8 +3260,8 @@ var require_utils = __commonJS({
|
|
|
3260
3260
|
}
|
|
3261
3261
|
return ind;
|
|
3262
3262
|
}
|
|
3263
|
-
function removeDotSegments(
|
|
3264
|
-
let input2 =
|
|
3263
|
+
function removeDotSegments(path15) {
|
|
3264
|
+
let input2 = path15;
|
|
3265
3265
|
const output3 = [];
|
|
3266
3266
|
let nextSlash = -1;
|
|
3267
3267
|
let len = 0;
|
|
@@ -3670,8 +3670,8 @@ var require_schemes = __commonJS({
|
|
|
3670
3670
|
}
|
|
3671
3671
|
if (wsComponent.resourceName) {
|
|
3672
3672
|
const queryIndex = wsComponent.resourceName.indexOf("?");
|
|
3673
|
-
const
|
|
3674
|
-
wsComponent.path =
|
|
3673
|
+
const path15 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
|
|
3674
|
+
wsComponent.path = path15 && path15 !== "/" ? path15 : void 0;
|
|
3675
3675
|
wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
|
|
3676
3676
|
wsComponent.resourceName = void 0;
|
|
3677
3677
|
}
|
|
@@ -3835,7 +3835,7 @@ var require_fast_uri = __commonJS({
|
|
|
3835
3835
|
normalizeString(uri, options);
|
|
3836
3836
|
} else if (typeof uri === "object") {
|
|
3837
3837
|
uri = /** @type {T} */
|
|
3838
|
-
|
|
3838
|
+
parse4(serialize(uri, options), options);
|
|
3839
3839
|
}
|
|
3840
3840
|
return uri;
|
|
3841
3841
|
}
|
|
@@ -3875,8 +3875,8 @@ var require_fast_uri = __commonJS({
|
|
|
3875
3875
|
function resolveComponent(base2, relative, options, skipNormalization) {
|
|
3876
3876
|
const target = {};
|
|
3877
3877
|
if (!skipNormalization) {
|
|
3878
|
-
base2 =
|
|
3879
|
-
relative =
|
|
3878
|
+
base2 = parse4(serialize(base2, options), options);
|
|
3879
|
+
relative = parse4(serialize(relative, options), options);
|
|
3880
3880
|
}
|
|
3881
3881
|
options = options || {};
|
|
3882
3882
|
if (!options.tolerant && relative.scheme) {
|
|
@@ -4175,7 +4175,7 @@ var require_fast_uri = __commonJS({
|
|
|
4175
4175
|
}
|
|
4176
4176
|
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
|
|
4177
4177
|
}
|
|
4178
|
-
function
|
|
4178
|
+
function parse4(uri, opts) {
|
|
4179
4179
|
return parseWithStatus(uri, opts).parsed;
|
|
4180
4180
|
}
|
|
4181
4181
|
function normalizeString(uri, opts) {
|
|
@@ -4212,7 +4212,7 @@ var require_fast_uri = __commonJS({
|
|
|
4212
4212
|
resolveComponent,
|
|
4213
4213
|
equal,
|
|
4214
4214
|
serialize,
|
|
4215
|
-
parse:
|
|
4215
|
+
parse: parse4
|
|
4216
4216
|
};
|
|
4217
4217
|
module.exports = fastUri;
|
|
4218
4218
|
module.exports.default = fastUri;
|
|
@@ -8017,10 +8017,10 @@ function mergeDefs(...defs) {
|
|
|
8017
8017
|
function cloneDef(schema) {
|
|
8018
8018
|
return mergeDefs(schema._zod.def);
|
|
8019
8019
|
}
|
|
8020
|
-
function getElementAtPath(obj,
|
|
8021
|
-
if (!
|
|
8020
|
+
function getElementAtPath(obj, path15) {
|
|
8021
|
+
if (!path15)
|
|
8022
8022
|
return obj;
|
|
8023
|
-
return
|
|
8023
|
+
return path15.reduce((acc, key) => acc?.[key], obj);
|
|
8024
8024
|
}
|
|
8025
8025
|
function promiseAllObject(promisesObj) {
|
|
8026
8026
|
const keys = Object.keys(promisesObj);
|
|
@@ -8237,7 +8237,7 @@ var BIGINT_FORMAT_RANGES = {
|
|
|
8237
8237
|
int64: [/* @__PURE__ */ BigInt("-9223372036854775808"), /* @__PURE__ */ BigInt("9223372036854775807")],
|
|
8238
8238
|
uint64: [/* @__PURE__ */ BigInt(0), /* @__PURE__ */ BigInt("18446744073709551615")]
|
|
8239
8239
|
};
|
|
8240
|
-
function pick(schema,
|
|
8240
|
+
function pick(schema, mask2) {
|
|
8241
8241
|
const currDef = schema._zod.def;
|
|
8242
8242
|
const checks = currDef.checks;
|
|
8243
8243
|
const hasChecks = checks && checks.length > 0;
|
|
@@ -8245,29 +8245,29 @@ function pick(schema, mask) {
|
|
|
8245
8245
|
throw new Error(".pick() cannot be used on object schemas containing refinements");
|
|
8246
8246
|
}
|
|
8247
8247
|
const newShape = {};
|
|
8248
|
-
mirrorShape(newShape, schema, maskedKeys(schema,
|
|
8248
|
+
mirrorShape(newShape, schema, maskedKeys(schema, mask2));
|
|
8249
8249
|
return clone(schema, mergeDefs(currDef, { shape: newShape, checks: [] }));
|
|
8250
8250
|
}
|
|
8251
|
-
function maskedKeys(schema,
|
|
8251
|
+
function maskedKeys(schema, mask2) {
|
|
8252
8252
|
const raw = sourceShape(schema);
|
|
8253
8253
|
const keys = [];
|
|
8254
|
-
for (const key of Reflect.ownKeys(
|
|
8254
|
+
for (const key of Reflect.ownKeys(mask2)) {
|
|
8255
8255
|
if (!Object.getOwnPropertyDescriptor(raw, key)?.enumerable) {
|
|
8256
8256
|
throw new Error(`Unrecognized key: "${String(key)}"`);
|
|
8257
8257
|
}
|
|
8258
|
-
if (
|
|
8258
|
+
if (mask2[key])
|
|
8259
8259
|
keys.push(key);
|
|
8260
8260
|
}
|
|
8261
8261
|
return keys;
|
|
8262
8262
|
}
|
|
8263
|
-
function omit(schema,
|
|
8263
|
+
function omit(schema, mask2) {
|
|
8264
8264
|
const currDef = schema._zod.def;
|
|
8265
8265
|
const checks = currDef.checks;
|
|
8266
8266
|
const hasChecks = checks && checks.length > 0;
|
|
8267
8267
|
if (hasChecks) {
|
|
8268
8268
|
throw new Error(".omit() cannot be used on object schemas containing refinements");
|
|
8269
8269
|
}
|
|
8270
|
-
const omitted = new Set(maskedKeys(schema,
|
|
8270
|
+
const omitted = new Set(maskedKeys(schema, mask2));
|
|
8271
8271
|
const newShape = {};
|
|
8272
8272
|
mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)).filter((key) => !omitted.has(key)));
|
|
8273
8273
|
return clone(schema, mergeDefs(currDef, { shape: newShape, checks: [] }));
|
|
@@ -8319,20 +8319,20 @@ function merge(a, b) {
|
|
|
8319
8319
|
});
|
|
8320
8320
|
return clone(a, def);
|
|
8321
8321
|
}
|
|
8322
|
-
function partial(Class2, schema,
|
|
8322
|
+
function partial(Class2, schema, mask2, name = "partial") {
|
|
8323
8323
|
const currDef = schema._zod.def;
|
|
8324
8324
|
const checks = currDef.checks;
|
|
8325
8325
|
const hasChecks = checks && checks.length > 0;
|
|
8326
8326
|
if (hasChecks) {
|
|
8327
8327
|
throw new Error(`.${name}() cannot be used on object schemas containing refinements`);
|
|
8328
8328
|
}
|
|
8329
|
-
const selected =
|
|
8329
|
+
const selected = mask2 ? new Set(maskedKeys(schema, mask2)) : void 0;
|
|
8330
8330
|
const newShape = {};
|
|
8331
8331
|
mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)), Class2 && ((value, key) => selected && !selected.has(key) ? value : new Class2({ type: "optional", innerType: value })));
|
|
8332
8332
|
return clone(schema, mergeDefs(schema._zod.def, { shape: newShape, checks: [] }));
|
|
8333
8333
|
}
|
|
8334
|
-
function required(Class2, schema,
|
|
8335
|
-
const selected =
|
|
8334
|
+
function required(Class2, schema, mask2) {
|
|
8335
|
+
const selected = mask2 ? new Set(maskedKeys(schema, mask2)) : void 0;
|
|
8336
8336
|
const newShape = {};
|
|
8337
8337
|
mirrorShape(newShape, schema, Reflect.ownKeys(sourceShape(schema)), (value, key) => (
|
|
8338
8338
|
// overwrite with non-optional
|
|
@@ -8360,11 +8360,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
8360
8360
|
}
|
|
8361
8361
|
return false;
|
|
8362
8362
|
}
|
|
8363
|
-
function prefixIssues(
|
|
8363
|
+
function prefixIssues(path15, issues) {
|
|
8364
8364
|
return issues.map((iss) => {
|
|
8365
8365
|
var _a3;
|
|
8366
8366
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
8367
|
-
iss.path.unshift(
|
|
8367
|
+
iss.path.unshift(path15);
|
|
8368
8368
|
return iss;
|
|
8369
8369
|
});
|
|
8370
8370
|
}
|
|
@@ -8826,16 +8826,16 @@ function flattenError(error62, mapper = (issue2) => issue2.message) {
|
|
|
8826
8826
|
}
|
|
8827
8827
|
function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
8828
8828
|
const fieldErrors = { _errors: [] };
|
|
8829
|
-
const processError = (error63,
|
|
8829
|
+
const processError = (error63, path15 = []) => {
|
|
8830
8830
|
for (const issue2 of error63.issues) {
|
|
8831
8831
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
8832
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
8832
|
+
issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
|
|
8833
8833
|
} else if (issue2.code === "invalid_key") {
|
|
8834
|
-
processError({ issues: issue2.issues }, [...
|
|
8834
|
+
processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
|
|
8835
8835
|
} else if (issue2.code === "invalid_element") {
|
|
8836
|
-
processError({ issues: issue2.issues }, [...
|
|
8836
|
+
processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
|
|
8837
8837
|
} else {
|
|
8838
|
-
const fullpath = [...
|
|
8838
|
+
const fullpath = [...path15, ...issue2.path];
|
|
8839
8839
|
if (fullpath.length === 0) {
|
|
8840
8840
|
fieldErrors._errors.push(mapper(issue2));
|
|
8841
8841
|
} else {
|
|
@@ -8874,17 +8874,17 @@ function formatError(error62, mapper = (issue2) => issue2.message) {
|
|
|
8874
8874
|
}
|
|
8875
8875
|
function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
8876
8876
|
const result = { errors: [] };
|
|
8877
|
-
const processError = (error63,
|
|
8877
|
+
const processError = (error63, path15 = []) => {
|
|
8878
8878
|
var _a3;
|
|
8879
8879
|
for (const issue2 of error63.issues) {
|
|
8880
8880
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
8881
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
8881
|
+
issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
|
|
8882
8882
|
} else if (issue2.code === "invalid_key") {
|
|
8883
|
-
processError({ issues: issue2.issues }, [...
|
|
8883
|
+
processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
|
|
8884
8884
|
} else if (issue2.code === "invalid_element") {
|
|
8885
|
-
processError({ issues: issue2.issues }, [...
|
|
8885
|
+
processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
|
|
8886
8886
|
} else {
|
|
8887
|
-
const fullpath = [...
|
|
8887
|
+
const fullpath = [...path15, ...issue2.path];
|
|
8888
8888
|
if (fullpath.length === 0) {
|
|
8889
8889
|
result.errors.push(mapper(issue2));
|
|
8890
8890
|
continue;
|
|
@@ -8923,8 +8923,8 @@ function treeifyError(error62, mapper = (issue2) => issue2.message) {
|
|
|
8923
8923
|
}
|
|
8924
8924
|
function toDotPath(_path) {
|
|
8925
8925
|
const segs = [];
|
|
8926
|
-
const
|
|
8927
|
-
for (const seg of
|
|
8926
|
+
const path15 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
8927
|
+
for (const seg of path15) {
|
|
8928
8928
|
if (typeof seg === "number")
|
|
8929
8929
|
segs.push(`[${seg}]`);
|
|
8930
8930
|
else if (typeof seg === "symbol")
|
|
@@ -9058,18 +9058,18 @@ var validateAsync = async (schema, value, _ctx) => {
|
|
|
9058
9058
|
return result.issues.length === 0;
|
|
9059
9059
|
};
|
|
9060
9060
|
var _encode = (_Err) => {
|
|
9061
|
-
const
|
|
9061
|
+
const parse4 = _parse(_Err);
|
|
9062
9062
|
const fn = (schema, value, _ctx, _params) => {
|
|
9063
9063
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
9064
|
-
return
|
|
9064
|
+
return parse4(schema, value, ctx, finalizeParams(fn, _params));
|
|
9065
9065
|
};
|
|
9066
9066
|
return fn;
|
|
9067
9067
|
};
|
|
9068
9068
|
var encode = /* @__PURE__ */ _encode($ZodRealError);
|
|
9069
9069
|
var _decode = (_Err) => {
|
|
9070
|
-
const
|
|
9070
|
+
const parse4 = _parse(_Err);
|
|
9071
9071
|
const fn = (schema, value, _ctx, _params) => {
|
|
9072
|
-
return
|
|
9072
|
+
return parse4(schema, value, _ctx, finalizeParams(fn, _params));
|
|
9073
9073
|
};
|
|
9074
9074
|
return fn;
|
|
9075
9075
|
};
|
|
@@ -10564,7 +10564,7 @@ function capSize(inst, size) {
|
|
|
10564
10564
|
if (!caps.length)
|
|
10565
10565
|
return;
|
|
10566
10566
|
const cap2 = Math.min(...caps.map((c) => c[0]));
|
|
10567
|
-
const
|
|
10567
|
+
const parse4 = inst._zod.parse;
|
|
10568
10568
|
inst._zod.parse = (payload, ctx) => {
|
|
10569
10569
|
const n = size(payload.value);
|
|
10570
10570
|
if (n > cap2 && !ctx?.skipChecks && ctx?.direction !== "backward") {
|
|
@@ -10578,7 +10578,7 @@ function capSize(inst, size) {
|
|
|
10578
10578
|
}
|
|
10579
10579
|
}
|
|
10580
10580
|
}
|
|
10581
|
-
return
|
|
10581
|
+
return parse4(payload, ctx);
|
|
10582
10582
|
};
|
|
10583
10583
|
}
|
|
10584
10584
|
var arraySize = (input2) => Array.isArray(input2) ? input2.length : -1;
|
|
@@ -12423,9 +12423,9 @@ var memo = {
|
|
|
12423
12423
|
if (!bucket)
|
|
12424
12424
|
return empty;
|
|
12425
12425
|
handoff = void 0;
|
|
12426
|
-
const
|
|
12427
|
-
bucket.set(payload.value,
|
|
12428
|
-
open.push(
|
|
12426
|
+
const entry2 = { value: empty, issues: null };
|
|
12427
|
+
bucket.set(payload.value, entry2);
|
|
12428
|
+
open.push(entry2);
|
|
12429
12429
|
return empty;
|
|
12430
12430
|
},
|
|
12431
12431
|
guard(inst) {
|
|
@@ -12499,16 +12499,16 @@ var memo = {
|
|
|
12499
12499
|
const depth = open.length;
|
|
12500
12500
|
const result = base2(payload, ctx);
|
|
12501
12501
|
handoff = void 0;
|
|
12502
|
-
const
|
|
12502
|
+
const entry2 = open.length > depth ? open.pop() : void 0;
|
|
12503
12503
|
if (result instanceof Promise) {
|
|
12504
12504
|
return result.then((r) => {
|
|
12505
|
-
if (
|
|
12506
|
-
|
|
12505
|
+
if (entry2)
|
|
12506
|
+
entry2.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
|
|
12507
12507
|
return r;
|
|
12508
12508
|
});
|
|
12509
12509
|
}
|
|
12510
|
-
if (
|
|
12511
|
-
|
|
12510
|
+
if (entry2)
|
|
12511
|
+
entry2.issues = result.issues.length ? cloneIssues(result.issues) : NO_ISSUES;
|
|
12512
12512
|
return result;
|
|
12513
12513
|
};
|
|
12514
12514
|
inst._zod.parse = wrapped;
|
|
@@ -22719,8 +22719,8 @@ function describe(description) {
|
|
|
22719
22719
|
const ch = new $ZodCheck({ check: "describe" });
|
|
22720
22720
|
ch._zod.onattach = [
|
|
22721
22721
|
(inst) => {
|
|
22722
|
-
const
|
|
22723
|
-
globalRegistry.add(inst, { ...
|
|
22722
|
+
const existing2 = globalRegistry.get(inst) ?? {};
|
|
22723
|
+
globalRegistry.add(inst, { ...existing2, description });
|
|
22724
22724
|
}
|
|
22725
22725
|
];
|
|
22726
22726
|
ch._zod.check = () => {
|
|
@@ -22732,8 +22732,8 @@ function meta(metadata) {
|
|
|
22732
22732
|
const ch = new $ZodCheck({ check: "meta" });
|
|
22733
22733
|
ch._zod.onattach = [
|
|
22734
22734
|
(inst) => {
|
|
22735
|
-
const
|
|
22736
|
-
globalRegistry.add(inst, { ...
|
|
22735
|
+
const existing2 = globalRegistry.get(inst) ?? {};
|
|
22736
|
+
globalRegistry.add(inst, { ...existing2, ...metadata });
|
|
22737
22737
|
}
|
|
22738
22738
|
];
|
|
22739
22739
|
ch._zod.check = () => {
|
|
@@ -22922,42 +22922,42 @@ function extractDefs(ctx, schema) {
|
|
|
22922
22922
|
if (ctx.external && ctx.sharedDefsExtractedFor === ctx.external)
|
|
22923
22923
|
return;
|
|
22924
22924
|
const idToSchema = /* @__PURE__ */ new Map();
|
|
22925
|
-
for (const
|
|
22926
|
-
const id = ctx.metadataRegistry.get(
|
|
22925
|
+
for (const entry2 of ctx.seen.entries()) {
|
|
22926
|
+
const id = ctx.metadataRegistry.get(entry2[0])?.id;
|
|
22927
22927
|
if (id) {
|
|
22928
|
-
const
|
|
22929
|
-
if (
|
|
22928
|
+
const existing2 = idToSchema.get(id);
|
|
22929
|
+
if (existing2 && existing2 !== entry2[0]) {
|
|
22930
22930
|
throw new Error(`Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);
|
|
22931
22931
|
}
|
|
22932
|
-
idToSchema.set(id,
|
|
22932
|
+
idToSchema.set(id, entry2[0]);
|
|
22933
22933
|
}
|
|
22934
22934
|
}
|
|
22935
|
-
const makeURI = (
|
|
22935
|
+
const makeURI = (entry2) => {
|
|
22936
22936
|
const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
|
|
22937
22937
|
if (ctx.external) {
|
|
22938
|
-
const externalId = ctx.external.registry.get(
|
|
22938
|
+
const externalId = ctx.external.registry.get(entry2[0])?.id;
|
|
22939
22939
|
const uriGenerator = ctx.external.uri ?? ((id2) => id2);
|
|
22940
22940
|
if (externalId) {
|
|
22941
22941
|
return { ref: uriGenerator(externalId) };
|
|
22942
22942
|
}
|
|
22943
|
-
const id =
|
|
22944
|
-
|
|
22943
|
+
const id = entry2[1].defId ?? entry2[1].schema.id ?? `schema${ctx.counter++}`;
|
|
22944
|
+
entry2[1].defId = id;
|
|
22945
22945
|
return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${encodeJSONPointerSegment(id)}` };
|
|
22946
22946
|
}
|
|
22947
22947
|
const uriPrefix = `#`;
|
|
22948
22948
|
const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
|
|
22949
|
-
if (
|
|
22949
|
+
if (entry2[1] === root && !entry2[1].schema.id) {
|
|
22950
22950
|
return { ref: uriPrefix };
|
|
22951
22951
|
}
|
|
22952
|
-
const defId =
|
|
22952
|
+
const defId = entry2[1].schema.id ?? `__schema${ctx.counter++}`;
|
|
22953
22953
|
return { defId, ref: defUriPrefix + encodeJSONPointerSegment(defId) };
|
|
22954
22954
|
};
|
|
22955
|
-
const extractToDef = (
|
|
22956
|
-
if (
|
|
22955
|
+
const extractToDef = (entry2) => {
|
|
22956
|
+
if (entry2[1].schema.$ref) {
|
|
22957
22957
|
return;
|
|
22958
22958
|
}
|
|
22959
|
-
const seen =
|
|
22960
|
-
const { ref, defId } = makeURI(
|
|
22959
|
+
const seen = entry2[1];
|
|
22960
|
+
const { ref, defId } = makeURI(entry2);
|
|
22961
22961
|
seen.def = { ...seen.schema };
|
|
22962
22962
|
if (defId)
|
|
22963
22963
|
seen.defId = defId;
|
|
@@ -22968,8 +22968,8 @@ function extractDefs(ctx, schema) {
|
|
|
22968
22968
|
schema2.$ref = ref;
|
|
22969
22969
|
};
|
|
22970
22970
|
if (ctx.cycles === "throw") {
|
|
22971
|
-
for (const
|
|
22972
|
-
const seen =
|
|
22971
|
+
for (const entry2 of ctx.seen.entries()) {
|
|
22972
|
+
const seen = entry2[1];
|
|
22973
22973
|
if (seen.cycle) {
|
|
22974
22974
|
throw new Error(`Cycle detected: #/${seen.cycle?.join("/")}/<root>
|
|
22975
22975
|
|
|
@@ -22977,31 +22977,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
22977
22977
|
}
|
|
22978
22978
|
}
|
|
22979
22979
|
}
|
|
22980
|
-
for (const
|
|
22981
|
-
const seen =
|
|
22982
|
-
if (schema ===
|
|
22983
|
-
extractToDef(
|
|
22980
|
+
for (const entry2 of ctx.seen.entries()) {
|
|
22981
|
+
const seen = entry2[1];
|
|
22982
|
+
if (schema === entry2[0]) {
|
|
22983
|
+
extractToDef(entry2);
|
|
22984
22984
|
continue;
|
|
22985
22985
|
}
|
|
22986
22986
|
if (ctx.external) {
|
|
22987
|
-
const ext = ctx.external.registry.get(
|
|
22988
|
-
if (schema !==
|
|
22989
|
-
extractToDef(
|
|
22987
|
+
const ext = ctx.external.registry.get(entry2[0])?.id;
|
|
22988
|
+
if (schema !== entry2[0] && ext) {
|
|
22989
|
+
extractToDef(entry2);
|
|
22990
22990
|
continue;
|
|
22991
22991
|
}
|
|
22992
22992
|
}
|
|
22993
|
-
const id = ctx.metadataRegistry.get(
|
|
22993
|
+
const id = ctx.metadataRegistry.get(entry2[0])?.id;
|
|
22994
22994
|
if (id) {
|
|
22995
|
-
extractToDef(
|
|
22995
|
+
extractToDef(entry2);
|
|
22996
22996
|
continue;
|
|
22997
22997
|
}
|
|
22998
22998
|
if (seen.cycle) {
|
|
22999
|
-
extractToDef(
|
|
22999
|
+
extractToDef(entry2);
|
|
23000
23000
|
continue;
|
|
23001
23001
|
}
|
|
23002
23002
|
if (seen.count > 1) {
|
|
23003
23003
|
if (ctx.reused === "ref") {
|
|
23004
|
-
extractToDef(
|
|
23004
|
+
extractToDef(entry2);
|
|
23005
23005
|
}
|
|
23006
23006
|
}
|
|
23007
23007
|
}
|
|
@@ -23183,12 +23183,12 @@ function finalize(ctx, schema) {
|
|
|
23183
23183
|
});
|
|
23184
23184
|
};
|
|
23185
23185
|
if (!ctx.external || ctx.sharedEmitDoneFor !== ctx.external) {
|
|
23186
|
-
for (const
|
|
23187
|
-
flattenRef(
|
|
23186
|
+
for (const entry2 of [...ctx.seen.entries()].reverse()) {
|
|
23187
|
+
flattenRef(entry2[0]);
|
|
23188
23188
|
}
|
|
23189
23189
|
if (ctx.target !== "openapi-3.0") {
|
|
23190
|
-
for (const
|
|
23191
|
-
compactTypeUnion(
|
|
23190
|
+
for (const entry2 of ctx.seen.entries()) {
|
|
23191
|
+
compactTypeUnion(entry2[1].def ?? entry2[1].schema);
|
|
23192
23192
|
}
|
|
23193
23193
|
}
|
|
23194
23194
|
for (const rewrite of ctx.deferred)
|
|
@@ -23200,9 +23200,9 @@ function finalize(ctx, schema) {
|
|
|
23200
23200
|
const allOf = json3?.allOf;
|
|
23201
23201
|
if (!Array.isArray(allOf))
|
|
23202
23202
|
continue;
|
|
23203
|
-
const
|
|
23204
|
-
if (
|
|
23205
|
-
|
|
23203
|
+
const existing2 = carriers.get(allOf);
|
|
23204
|
+
if (existing2)
|
|
23205
|
+
existing2.push(json3);
|
|
23206
23206
|
else
|
|
23207
23207
|
carriers.set(allOf, [json3]);
|
|
23208
23208
|
}
|
|
@@ -23235,8 +23235,8 @@ function finalize(ctx, schema) {
|
|
|
23235
23235
|
delete result.id;
|
|
23236
23236
|
const defs = ctx.external?.defs ?? {};
|
|
23237
23237
|
if (!ctx.external || ctx.sharedEmitDoneFor !== ctx.external) {
|
|
23238
|
-
for (const
|
|
23239
|
-
const seen =
|
|
23238
|
+
for (const entry2 of ctx.seen.entries()) {
|
|
23239
|
+
const seen = entry2[1];
|
|
23240
23240
|
if (seen.def && seen.defId) {
|
|
23241
23241
|
if (seen.def.id === seen.defId)
|
|
23242
23242
|
delete seen.def.id;
|
|
@@ -23848,9 +23848,9 @@ function stringifyKeyNames(bySchema, json3, visited) {
|
|
|
23848
23848
|
var pendingRecords = /* @__PURE__ */ new WeakMap();
|
|
23849
23849
|
function rewriteKeyNames(ctx) {
|
|
23850
23850
|
const bySchema = /* @__PURE__ */ new Map();
|
|
23851
|
-
for (const
|
|
23852
|
-
if (
|
|
23853
|
-
bySchema.set(
|
|
23851
|
+
for (const entry2 of ctx.seen.values()) {
|
|
23852
|
+
if (entry2.def && !bySchema.has(entry2.schema))
|
|
23853
|
+
bySchema.set(entry2.schema, entry2);
|
|
23854
23854
|
}
|
|
23855
23855
|
const rewrites = /* @__PURE__ */ new Map();
|
|
23856
23856
|
for (const record2 of pendingRecords.get(ctx) ?? []) {
|
|
@@ -23864,8 +23864,8 @@ function rewriteKeyNames(ctx) {
|
|
|
23864
23864
|
}
|
|
23865
23865
|
if (!rewrites.size)
|
|
23866
23866
|
return;
|
|
23867
|
-
for (const
|
|
23868
|
-
for (const carrier of [
|
|
23867
|
+
for (const entry2 of ctx.seen.values()) {
|
|
23868
|
+
for (const carrier of [entry2.schema, entry2.def]) {
|
|
23869
23869
|
const rewritten = carrier && rewrites.get(carrier.propertyNames);
|
|
23870
23870
|
if (rewritten)
|
|
23871
23871
|
carrier.propertyNames = rewritten;
|
|
@@ -24059,8 +24059,8 @@ function toJSONSchema(input2, params) {
|
|
|
24059
24059
|
const registry2 = input2;
|
|
24060
24060
|
const ctx2 = initializeContext({ ...params, processors: allProcessors });
|
|
24061
24061
|
const defs = {};
|
|
24062
|
-
for (const
|
|
24063
|
-
const [_, schema] =
|
|
24062
|
+
for (const entry2 of registry2._idmap.entries()) {
|
|
24063
|
+
const [_, schema] = entry2;
|
|
24064
24064
|
processSchema(schema, ctx2);
|
|
24065
24065
|
}
|
|
24066
24066
|
const schemas = {};
|
|
@@ -24070,8 +24070,8 @@ function toJSONSchema(input2, params) {
|
|
|
24070
24070
|
defs
|
|
24071
24071
|
};
|
|
24072
24072
|
ctx2.external = external;
|
|
24073
|
-
for (const
|
|
24074
|
-
const [key, schema] =
|
|
24073
|
+
for (const entry2 of registry2._idmap.entries()) {
|
|
24074
|
+
const [key, schema] = entry2;
|
|
24075
24075
|
extractDefs(ctx2, schema);
|
|
24076
24076
|
assignProp(schemas, key, finalize(ctx2, schema));
|
|
24077
24077
|
}
|
|
@@ -25292,11 +25292,11 @@ var ZodObject = /* @__PURE__ */ $constructor("ZodObject", (inst, def) => {
|
|
|
25292
25292
|
merge(other) {
|
|
25293
25293
|
return util_exports.merge(this, other);
|
|
25294
25294
|
},
|
|
25295
|
-
pick(
|
|
25296
|
-
return util_exports.pick(this,
|
|
25295
|
+
pick(mask2) {
|
|
25296
|
+
return util_exports.pick(this, mask2);
|
|
25297
25297
|
},
|
|
25298
|
-
omit(
|
|
25299
|
-
return util_exports.omit(this,
|
|
25298
|
+
omit(mask2) {
|
|
25299
|
+
return util_exports.omit(this, mask2);
|
|
25300
25300
|
},
|
|
25301
25301
|
partial(...args) {
|
|
25302
25302
|
return util_exports.partial(ZodOptional, this, args[0]);
|
|
@@ -26062,13 +26062,13 @@ function resolveRef(ref, ctx) {
|
|
|
26062
26062
|
if (!ref.startsWith("#")) {
|
|
26063
26063
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
26064
26064
|
}
|
|
26065
|
-
const
|
|
26066
|
-
if (
|
|
26065
|
+
const path15 = ref.slice(1).split("/").filter(Boolean);
|
|
26066
|
+
if (path15.length === 0) {
|
|
26067
26067
|
return ctx.rootSchema;
|
|
26068
26068
|
}
|
|
26069
26069
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
26070
|
-
if (
|
|
26071
|
-
const key =
|
|
26070
|
+
if (path15[0] === defsKey) {
|
|
26071
|
+
const key = path15[1] === void 0 ? void 0 : decodeJSONPointerSegment(path15[1]);
|
|
26072
26072
|
if (!key || !ctx.defs[key]) {
|
|
26073
26073
|
throw new Error(`Reference not found: ${ref}`);
|
|
26074
26074
|
}
|
|
@@ -28692,7 +28692,7 @@ async function canonical(p) {
|
|
|
28692
28692
|
}
|
|
28693
28693
|
|
|
28694
28694
|
// src/version.ts
|
|
28695
|
-
var VERSION = "0.
|
|
28695
|
+
var VERSION = "0.4.0";
|
|
28696
28696
|
|
|
28697
28697
|
// src/config.ts
|
|
28698
28698
|
var ConfigError = class extends Error {
|
|
@@ -28724,6 +28724,7 @@ function userPath(env2, key) {
|
|
|
28724
28724
|
}
|
|
28725
28725
|
}
|
|
28726
28726
|
var ED_MCP_CAPI_CLIENT_ID = "320100b9-b71e-4b8a-ab80-210f7192d330";
|
|
28727
|
+
var MIN_PASSWORD = 12;
|
|
28727
28728
|
function loadConfig(env2, dirs, platform = process.platform) {
|
|
28728
28729
|
const devMode = bool(env2, "INARA_DEV_MODE", false);
|
|
28729
28730
|
const inaraKey = get(env2, "INARA_API_KEY");
|
|
@@ -28731,26 +28732,39 @@ function loadConfig(env2, dirs, platform = process.platform) {
|
|
|
28731
28732
|
const edsmKey = get(env2, "EDSM_API_KEY");
|
|
28732
28733
|
const edsmCmdr = get(env2, "EDSM_CMDR");
|
|
28733
28734
|
const capiClientId = get(env2, "CAPI_CLIENT_ID") ?? ED_MCP_CAPI_CLIENT_ID;
|
|
28735
|
+
const syncDir = userPath(env2, "ED_MCP_SYNC_DIR");
|
|
28736
|
+
const rawPassword = env2.ED_MCP_SYNC_PASSWORD;
|
|
28737
|
+
const syncPassword = rawPassword && !UNSET_PLACEHOLDER.test(rawPassword.trim()) ? rawPassword : void 0;
|
|
28738
|
+
if (syncPassword !== void 0 && syncPassword.length < MIN_PASSWORD) {
|
|
28739
|
+
throw new ConfigError(`ED_MCP_SYNC_PASSWORD must be at least ${MIN_PASSWORD} characters.`);
|
|
28740
|
+
}
|
|
28741
|
+
const dataDir = syncDir ?? dirs.config;
|
|
28742
|
+
const appName = get(env2, "INARA_APP_NAME") ?? "ed-mcp";
|
|
28734
28743
|
return {
|
|
28735
28744
|
inara: inaraKey && inaraCmdr ? {
|
|
28736
28745
|
apiKey: inaraKey,
|
|
28737
28746
|
cmdr: inaraCmdr,
|
|
28738
|
-
appName
|
|
28747
|
+
appName,
|
|
28739
28748
|
devMode
|
|
28740
28749
|
} : void 0,
|
|
28741
28750
|
edsm: edsmKey && edsmCmdr ? { apiKey: edsmKey, cmdr: edsmCmdr } : void 0,
|
|
28742
28751
|
capi: capiClientId ? { clientId: capiClientId } : void 0,
|
|
28743
28752
|
journalDir: userPath(env2, "ED_JOURNAL_DIR") ?? (platform === "win32" ? path3.join(os3.homedir(), "Saved Games", "Frontier Developments", "Elite Dangerous") : void 0),
|
|
28744
|
-
|
|
28753
|
+
inaraApp: { appName, devMode },
|
|
28754
|
+
syncDir,
|
|
28755
|
+
syncPassword,
|
|
28756
|
+
plansFile: path3.join(dataDir, "plans.json"),
|
|
28757
|
+
legacyPlansEnv: get(env2, "ED_MCP_PLANS"),
|
|
28758
|
+
plansMarkdownDir: path3.join(dataDir, "plans"),
|
|
28759
|
+
prefsFile: path3.join(dataDir, "preferences.json"),
|
|
28760
|
+
secretsFile: syncPassword ? path3.join(dataDir, "secrets.enc") : path3.join(dirs.config, "secrets.json"),
|
|
28761
|
+
locksDir: path3.join(dirs.config, "locks"),
|
|
28745
28762
|
cacheDir: dirs.cache,
|
|
28746
28763
|
configDir: dirs.config,
|
|
28747
28764
|
userAgent: `ed-mcp/${VERSION} (+https://github.com/pilotso11/ed-mcp)`
|
|
28748
28765
|
};
|
|
28749
28766
|
}
|
|
28750
28767
|
|
|
28751
|
-
// src/deps.ts
|
|
28752
|
-
import path10 from "path";
|
|
28753
|
-
|
|
28754
28768
|
// src/capi/auth.ts
|
|
28755
28769
|
import { createServer } from "http";
|
|
28756
28770
|
|
|
@@ -28844,10 +28858,41 @@ var HttpClient = class {
|
|
|
28844
28858
|
|
|
28845
28859
|
// src/capi/browser.ts
|
|
28846
28860
|
import { spawn } from "child_process";
|
|
28861
|
+
var KEEP = {
|
|
28862
|
+
all: ["PATH", "HOME", "USER", "LOGNAME"],
|
|
28863
|
+
win32: [
|
|
28864
|
+
"SYSTEMROOT",
|
|
28865
|
+
"COMSPEC",
|
|
28866
|
+
"USERPROFILE",
|
|
28867
|
+
"APPDATA",
|
|
28868
|
+
"LOCALAPPDATA",
|
|
28869
|
+
"TEMP",
|
|
28870
|
+
"TMP",
|
|
28871
|
+
"PATHEXT"
|
|
28872
|
+
],
|
|
28873
|
+
linux: ["DISPLAY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS"]
|
|
28874
|
+
};
|
|
28875
|
+
function browserEnv(env2 = process.env, platform = process.platform) {
|
|
28876
|
+
const names = [
|
|
28877
|
+
...KEEP.all,
|
|
28878
|
+
...platform === "win32" ? KEEP.win32 : [],
|
|
28879
|
+
...platform === "linux" ? KEEP.linux : []
|
|
28880
|
+
];
|
|
28881
|
+
const out = {};
|
|
28882
|
+
for (const n of names) {
|
|
28883
|
+
const key = Object.keys(env2).find((k) => k.toUpperCase() === n);
|
|
28884
|
+
if (key && env2[key] !== void 0) out[key] = env2[key];
|
|
28885
|
+
}
|
|
28886
|
+
return out;
|
|
28887
|
+
}
|
|
28847
28888
|
async function openInBrowser(url2) {
|
|
28848
28889
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url2]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url2]] : ["xdg-open", [url2]];
|
|
28849
28890
|
await new Promise((resolve, reject) => {
|
|
28850
|
-
const child = spawn(cmd, args, {
|
|
28891
|
+
const child = spawn(cmd, args, {
|
|
28892
|
+
detached: true,
|
|
28893
|
+
stdio: "ignore",
|
|
28894
|
+
env: browserEnv()
|
|
28895
|
+
});
|
|
28851
28896
|
child.once("error", reject);
|
|
28852
28897
|
child.once("spawn", () => {
|
|
28853
28898
|
child.unref();
|
|
@@ -28887,6 +28932,8 @@ var CapiAuth = class {
|
|
|
28887
28932
|
pending = null;
|
|
28888
28933
|
failure = null;
|
|
28889
28934
|
refreshing = null;
|
|
28935
|
+
// Logout bumps this, so a refresh that began before it cannot write tokens back.
|
|
28936
|
+
session = 0;
|
|
28890
28937
|
authBase;
|
|
28891
28938
|
openUrl;
|
|
28892
28939
|
now;
|
|
@@ -29025,6 +29072,7 @@ var CapiAuth = class {
|
|
|
29025
29072
|
}
|
|
29026
29073
|
async accessToken() {
|
|
29027
29074
|
const cfg = this.requireConfig();
|
|
29075
|
+
const session = this.session;
|
|
29028
29076
|
const t = await this.tokens.load();
|
|
29029
29077
|
if (!t) throw new UserError("Not logged in to Frontier. Call capi_login first.");
|
|
29030
29078
|
if (this.now() - t.authorized_at > REFRESH_WINDOW) {
|
|
@@ -29034,7 +29082,7 @@ var CapiAuth = class {
|
|
|
29034
29082
|
);
|
|
29035
29083
|
}
|
|
29036
29084
|
if (t.access_token && t.expires_at - 6e4 > this.now()) return t.access_token;
|
|
29037
|
-
this.refreshing ??= this.refresh(cfg, t).finally(() => this.refreshing = null);
|
|
29085
|
+
this.refreshing ??= this.refresh(cfg, t, session).finally(() => this.refreshing = null);
|
|
29038
29086
|
return this.refreshing;
|
|
29039
29087
|
}
|
|
29040
29088
|
/** Forces the next accessToken() call to refresh (after a 401 from CAPI). */
|
|
@@ -29047,6 +29095,8 @@ var CapiAuth = class {
|
|
|
29047
29095
|
}
|
|
29048
29096
|
async logout() {
|
|
29049
29097
|
this.pending?.cancel();
|
|
29098
|
+
this.session++;
|
|
29099
|
+
await this.refreshing?.catch(() => void 0);
|
|
29050
29100
|
await this.tokens.clear();
|
|
29051
29101
|
this.failure = null;
|
|
29052
29102
|
this.onSessionChange();
|
|
@@ -29073,7 +29123,7 @@ var CapiAuth = class {
|
|
|
29073
29123
|
this.onSessionChange();
|
|
29074
29124
|
return true;
|
|
29075
29125
|
}
|
|
29076
|
-
async refresh(cfg, t, retried = false) {
|
|
29126
|
+
async refresh(cfg, t, session, retried = false) {
|
|
29077
29127
|
let res;
|
|
29078
29128
|
try {
|
|
29079
29129
|
res = await this.token({
|
|
@@ -29089,15 +29139,18 @@ var CapiAuth = class {
|
|
|
29089
29139
|
}
|
|
29090
29140
|
const latest = await this.tokens.load();
|
|
29091
29141
|
if (!retried && latest && latest.refresh_token !== t.refresh_token) {
|
|
29092
|
-
if (latest.access_token && latest.expires_at - 6e4 > this.now())
|
|
29142
|
+
if (latest.access_token && latest.expires_at - 6e4 > this.now()) {
|
|
29143
|
+
if (session !== this.session)
|
|
29144
|
+
throw new UserError("Logged out while the login was refreshing.");
|
|
29093
29145
|
return latest.access_token;
|
|
29094
|
-
|
|
29146
|
+
}
|
|
29147
|
+
return this.refresh(cfg, latest, session, true);
|
|
29095
29148
|
}
|
|
29096
|
-
await this.tokens.clear();
|
|
29097
29149
|
throw new UserError(
|
|
29098
|
-
`Frontier refused to refresh the login (${err.message}).
|
|
29150
|
+
`Frontier refused to refresh the login (${err.message}). If you use ed-mcp on another computer, wait for your sync app to catch up and try again; otherwise call capi_login.`
|
|
29099
29151
|
);
|
|
29100
29152
|
}
|
|
29153
|
+
if (session !== this.session) throw new UserError("Logged out while the login was refreshing.");
|
|
29101
29154
|
await this.tokens.save({
|
|
29102
29155
|
refresh_token: res.refresh_token,
|
|
29103
29156
|
access_token: res.access_token,
|
|
@@ -29201,8 +29254,8 @@ function parseModifiers(list2) {
|
|
|
29201
29254
|
return label && value !== null ? [{ label, value, original_value: num(o.OriginalValue) }] : [];
|
|
29202
29255
|
});
|
|
29203
29256
|
}
|
|
29204
|
-
function parseEntry(
|
|
29205
|
-
const o =
|
|
29257
|
+
function parseEntry(entry2) {
|
|
29258
|
+
const o = entry2;
|
|
29206
29259
|
const data = o?.data ?? o;
|
|
29207
29260
|
const header = o?.data ? o.header ?? {} : {};
|
|
29208
29261
|
const ship = str(data?.Ship);
|
|
@@ -29289,10 +29342,10 @@ var CapiClient = class {
|
|
|
29289
29342
|
}))
|
|
29290
29343
|
};
|
|
29291
29344
|
}
|
|
29292
|
-
async get(
|
|
29345
|
+
async get(path15) {
|
|
29293
29346
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
29294
29347
|
const token = await this.auth.accessToken();
|
|
29295
|
-
const url2 = `${this.base}${
|
|
29348
|
+
const url2 = `${this.base}${path15}`;
|
|
29296
29349
|
const res = await this.http.request(url2, {
|
|
29297
29350
|
headers: { authorization: `Bearer ${token}` }
|
|
29298
29351
|
});
|
|
@@ -29347,52 +29400,30 @@ function mapShip(s) {
|
|
|
29347
29400
|
}
|
|
29348
29401
|
|
|
29349
29402
|
// src/capi/tokens.ts
|
|
29350
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
29351
|
-
import path4 from "path";
|
|
29352
|
-
var StoredTokensSchema = external_exports.object({
|
|
29353
|
-
refresh_token: external_exports.string(),
|
|
29354
|
-
access_token: external_exports.string().nullable(),
|
|
29355
|
-
expires_at: external_exports.number(),
|
|
29356
|
-
authorized_at: external_exports.number()
|
|
29357
|
-
});
|
|
29358
29403
|
var TokenStore = class {
|
|
29359
|
-
constructor(
|
|
29360
|
-
this.
|
|
29404
|
+
constructor(secrets) {
|
|
29405
|
+
this.secrets = secrets;
|
|
29361
29406
|
}
|
|
29362
|
-
|
|
29407
|
+
secrets;
|
|
29363
29408
|
async load() {
|
|
29364
|
-
|
|
29365
|
-
try {
|
|
29366
|
-
json3 = JSON.parse(await readFile(this.file, "utf8"));
|
|
29367
|
-
} catch (err) {
|
|
29368
|
-
if (err.code === "ENOENT") return null;
|
|
29369
|
-
if (err instanceof SyntaxError) return null;
|
|
29370
|
-
throw err;
|
|
29371
|
-
}
|
|
29372
|
-
const parsed = StoredTokensSchema.safeParse(json3);
|
|
29373
|
-
return parsed.success ? parsed.data : null;
|
|
29409
|
+
return (await this.secrets.get()).capi ?? null;
|
|
29374
29410
|
}
|
|
29375
|
-
// The refresh token grants account access, so only the user may read the file.
|
|
29376
29411
|
async save(t) {
|
|
29377
|
-
await
|
|
29378
|
-
|
|
29379
|
-
|
|
29380
|
-
await chmod(tmp, 384);
|
|
29381
|
-
try {
|
|
29382
|
-
await rename(tmp, this.file);
|
|
29383
|
-
} catch (err) {
|
|
29384
|
-
await rm(tmp, { force: true });
|
|
29385
|
-
throw err;
|
|
29386
|
-
}
|
|
29412
|
+
await this.secrets.update((s) => {
|
|
29413
|
+
s.capi = t;
|
|
29414
|
+
});
|
|
29387
29415
|
}
|
|
29388
29416
|
async clear() {
|
|
29389
|
-
await
|
|
29417
|
+
if (!(await this.secrets.get()).capi) return;
|
|
29418
|
+
await this.secrets.update((s) => {
|
|
29419
|
+
delete s.capi;
|
|
29420
|
+
});
|
|
29390
29421
|
}
|
|
29391
29422
|
};
|
|
29392
29423
|
|
|
29393
29424
|
// src/catalog/service.ts
|
|
29394
|
-
import { mkdir
|
|
29395
|
-
import
|
|
29425
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
29426
|
+
import path4 from "path";
|
|
29396
29427
|
|
|
29397
29428
|
// node_modules/fastest-levenshtein/esm/mod.js
|
|
29398
29429
|
var peq = new Uint32Array(65536);
|
|
@@ -29666,8 +29697,10 @@ var Catalog = class {
|
|
|
29666
29697
|
const same4 = m.trade_group ? this.d.materials.filter((x) => x.kind === m.kind && x.trade_group === m.trade_group).sort((a, b) => a.grade - b.grade) : [m];
|
|
29667
29698
|
return { ...m, same_group: same4, trade_rules: TRADE_RULES };
|
|
29668
29699
|
}
|
|
29669
|
-
/** Coriolis feature ranges, [min, max], for a blueprint id at a grade, or null. */
|
|
29670
|
-
blueprintFeatures(blueprintId2, grade) {
|
|
29700
|
+
/** Coriolis feature ranges, [min, max], for a blueprint id at a grade on a module group, or null. */
|
|
29701
|
+
blueprintFeatures(blueprintId2, grade, grp) {
|
|
29702
|
+
const own2 = this.d.groups.find((g) => g.id === grp)?.features?.[blueprintId2];
|
|
29703
|
+
if (own2) return own2[String(grade)] ?? null;
|
|
29671
29704
|
const b = this.d.blueprints.find((x) => x.id === blueprintId2);
|
|
29672
29705
|
return b?.features?.[String(grade)] ?? null;
|
|
29673
29706
|
}
|
|
@@ -29806,11 +29839,15 @@ function buildCatalog(raw, statics, opts = {}) {
|
|
|
29806
29839
|
const spJson = json2(raw.specials, "specials");
|
|
29807
29840
|
const modJson = json2(raw.modifications, "modifications");
|
|
29808
29841
|
const actJson = raw.actions === void 0 ? null : json2(raw.actions, "actions");
|
|
29809
|
-
const
|
|
29842
|
+
const warnings = [];
|
|
29843
|
+
const idOf = /* @__PURE__ */ new Map();
|
|
29844
|
+
const featuresOf = /* @__PURE__ */ new Map();
|
|
29845
|
+
const byId = /* @__PURE__ */ new Map();
|
|
29846
|
+
for (const [key, v] of Object.entries(bpJson)) {
|
|
29810
29847
|
const b = v;
|
|
29811
|
-
|
|
29812
|
-
id: b.fdname ??
|
|
29813
|
-
name: b.name ??
|
|
29848
|
+
const bp = {
|
|
29849
|
+
id: b.fdname ?? key,
|
|
29850
|
+
name: b.name ?? key,
|
|
29814
29851
|
modules: b.modulename ?? [],
|
|
29815
29852
|
grades: Object.fromEntries(
|
|
29816
29853
|
Object.entries(b.grades ?? {}).map(([g, x]) => [g, counts(x.components)])
|
|
@@ -29819,7 +29856,22 @@ function buildCatalog(raw, statics, opts = {}) {
|
|
|
29819
29856
|
Object.entries(b.grades ?? {}).map(([g, x]) => [g, ranges(x.features)])
|
|
29820
29857
|
)
|
|
29821
29858
|
};
|
|
29822
|
-
|
|
29859
|
+
idOf.set(key, bp.id);
|
|
29860
|
+
featuresOf.set(key, bp.features);
|
|
29861
|
+
const prev = byId.get(bp.id);
|
|
29862
|
+
if (!prev) {
|
|
29863
|
+
byId.set(bp.id, bp);
|
|
29864
|
+
continue;
|
|
29865
|
+
}
|
|
29866
|
+
if (JSON.stringify(prev.grades) !== JSON.stringify(bp.grades)) {
|
|
29867
|
+
warnings.push(
|
|
29868
|
+
`Blueprints with the name ${bp.id} cost different materials; using one of them.`
|
|
29869
|
+
);
|
|
29870
|
+
}
|
|
29871
|
+
const main2 = key === bp.id ? bp : prev;
|
|
29872
|
+
byId.set(bp.id, { ...main2, modules: [.../* @__PURE__ */ new Set([...prev.modules, ...bp.modules])] });
|
|
29873
|
+
}
|
|
29874
|
+
const blueprints = [...byId.values()];
|
|
29823
29875
|
const experimentals = Object.entries(spJson).map(([id, v]) => {
|
|
29824
29876
|
const s = v;
|
|
29825
29877
|
const edname = s.edname ?? id;
|
|
@@ -29833,12 +29885,12 @@ function buildCatalog(raw, statics, opts = {}) {
|
|
|
29833
29885
|
});
|
|
29834
29886
|
const groups = Object.entries(modJson).map(([id, v]) => {
|
|
29835
29887
|
const g = v;
|
|
29836
|
-
|
|
29888
|
+
const group = {
|
|
29837
29889
|
id,
|
|
29838
29890
|
names: statics.groups[id] ?? [],
|
|
29839
29891
|
blueprints: Object.fromEntries(
|
|
29840
|
-
Object.entries(g.blueprints ?? {}).map(([
|
|
29841
|
-
|
|
29892
|
+
Object.entries(g.blueprints ?? {}).map(([key, x]) => [
|
|
29893
|
+
idOf.get(key) ?? key,
|
|
29842
29894
|
Object.fromEntries(
|
|
29843
29895
|
Object.entries(x.grades ?? {}).map(([grade, y]) => [grade, y.engineers ?? []])
|
|
29844
29896
|
)
|
|
@@ -29846,6 +29898,8 @@ function buildCatalog(raw, statics, opts = {}) {
|
|
|
29846
29898
|
),
|
|
29847
29899
|
experimentals: g.specials ?? []
|
|
29848
29900
|
};
|
|
29901
|
+
const features = Object.keys(g.blueprints ?? {}).filter((key) => idOf.has(key) && idOf.get(key) !== key).map((key) => [idOf.get(key), featuresOf.get(key)]);
|
|
29902
|
+
return features.length ? { ...group, features: Object.fromEntries(features) } : group;
|
|
29849
29903
|
});
|
|
29850
29904
|
const materials = parseCsv(raw.materials).filter((r) => r.symbol && r.name && ["Raw", "Manufactured", "Encoded"].includes(r.type ?? "")).map((r) => ({
|
|
29851
29905
|
symbol: r.symbol,
|
|
@@ -29881,7 +29935,6 @@ function buildCatalog(raw, statics, opts = {}) {
|
|
|
29881
29935
|
outfitting,
|
|
29882
29936
|
engineers: statics.engineers
|
|
29883
29937
|
});
|
|
29884
|
-
const warnings = [];
|
|
29885
29938
|
if (!actJson)
|
|
29886
29939
|
warnings.push("No modifierActions.json: experimental effects have no fixed values.");
|
|
29887
29940
|
const known = new Set(materials.map((m) => normKey(m.name)));
|
|
@@ -30920,7 +30973,7 @@ var DEFAULT_STATICS = {
|
|
|
30920
30973
|
var CatalogService = class {
|
|
30921
30974
|
constructor(http, cacheDir, opts = {}) {
|
|
30922
30975
|
this.http = http;
|
|
30923
|
-
this.dir =
|
|
30976
|
+
this.dir = path4.join(cacheDir, "catalog");
|
|
30924
30977
|
this.statics = opts.statics ?? DEFAULT_STATICS;
|
|
30925
30978
|
this.now = opts.now ?? Date.now;
|
|
30926
30979
|
this.maxAgeMs = opts.maxAgeMs ?? 7 * 864e5;
|
|
@@ -30982,11 +31035,11 @@ var CatalogService = class {
|
|
|
30982
31035
|
}
|
|
30983
31036
|
async loadDisk() {
|
|
30984
31037
|
try {
|
|
30985
|
-
const meta3 = JSON.parse(await
|
|
31038
|
+
const meta3 = JSON.parse(await readFile(path4.join(this.dir, "meta.json"), "utf8"));
|
|
30986
31039
|
const texts = {};
|
|
30987
31040
|
for (const k of KEYS) {
|
|
30988
|
-
const file2 =
|
|
30989
|
-
const text = await
|
|
31041
|
+
const file2 = path4.join(this.dir, `${k}.raw`);
|
|
31042
|
+
const text = await readFile(file2, "utf8").catch((err) => {
|
|
30990
31043
|
if (OPTIONAL.has(k) && err.code === "ENOENT") return void 0;
|
|
30991
31044
|
throw err;
|
|
30992
31045
|
});
|
|
@@ -31051,11 +31104,11 @@ var CatalogService = class {
|
|
|
31051
31104
|
changed.push(k);
|
|
31052
31105
|
}
|
|
31053
31106
|
const built = buildCatalog(next, this.statics, this.minimums);
|
|
31054
|
-
await
|
|
31107
|
+
await mkdir(this.dir, { recursive: true });
|
|
31055
31108
|
for (const k of changed)
|
|
31056
|
-
await atomicWrite(
|
|
31109
|
+
await atomicWrite(path4.join(this.dir, `${k}.raw`), next[k]);
|
|
31057
31110
|
const meta3 = { version: 1, checked_at: stamp, files };
|
|
31058
|
-
await atomicWrite(
|
|
31111
|
+
await atomicWrite(path4.join(this.dir, "meta.json"), JSON.stringify(meta3, null, 2));
|
|
31059
31112
|
this.catalog = built.catalog;
|
|
31060
31113
|
this.warnings = [...missing, ...built.warnings];
|
|
31061
31114
|
this.texts = next;
|
|
@@ -31070,8 +31123,8 @@ var CatalogService = class {
|
|
|
31070
31123
|
};
|
|
31071
31124
|
async function atomicWrite(file2, text) {
|
|
31072
31125
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
31073
|
-
await
|
|
31074
|
-
await
|
|
31126
|
+
await writeFile(tmp, text);
|
|
31127
|
+
await rename(tmp, file2);
|
|
31075
31128
|
}
|
|
31076
31129
|
|
|
31077
31130
|
// src/http/cache.ts
|
|
@@ -31174,12 +31227,12 @@ var CacheRegistry = class {
|
|
|
31174
31227
|
};
|
|
31175
31228
|
|
|
31176
31229
|
// src/journal/service.ts
|
|
31177
|
-
import { readFile as
|
|
31178
|
-
import
|
|
31230
|
+
import { readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
31231
|
+
import path6 from "path";
|
|
31179
31232
|
|
|
31180
31233
|
// src/journal/files.ts
|
|
31181
31234
|
import { readdir, stat as stat2 } from "fs/promises";
|
|
31182
|
-
import
|
|
31235
|
+
import path5 from "path";
|
|
31183
31236
|
var NEW = /^Journal\.(\d{4})-(\d{2})-(\d{2})T(\d{2})(\d{2})(\d{2})\.(\d{2})\.log$/;
|
|
31184
31237
|
var OLD = /^Journal\.(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{2})\.log$/;
|
|
31185
31238
|
function parseJournalName(name) {
|
|
@@ -31203,7 +31256,7 @@ async function listJournals(dir, opts = {}) {
|
|
|
31203
31256
|
}
|
|
31204
31257
|
const files = await Promise.all(
|
|
31205
31258
|
candidates.map(async (n) => {
|
|
31206
|
-
const file2 =
|
|
31259
|
+
const file2 = path5.join(dir, n.name);
|
|
31207
31260
|
const mtimeMs = await stat2(file2).then(
|
|
31208
31261
|
(st) => st.mtimeMs,
|
|
31209
31262
|
() => null
|
|
@@ -31220,7 +31273,7 @@ async function listJournals(dir, opts = {}) {
|
|
|
31220
31273
|
}
|
|
31221
31274
|
|
|
31222
31275
|
// src/journal/reader.ts
|
|
31223
|
-
import { readFile as
|
|
31276
|
+
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
31224
31277
|
var JournalReader = class {
|
|
31225
31278
|
keep;
|
|
31226
31279
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -31234,7 +31287,7 @@ var JournalReader = class {
|
|
|
31234
31287
|
if (hit && hit.size === st.size && hit.mtimeMs === st.mtimeMs) return hit.events;
|
|
31235
31288
|
const events = [];
|
|
31236
31289
|
let last;
|
|
31237
|
-
for (const line of (await
|
|
31290
|
+
for (const line of (await readFile2(file2.path, "utf8")).split(/\r?\n/)) {
|
|
31238
31291
|
if (!line.trim()) continue;
|
|
31239
31292
|
try {
|
|
31240
31293
|
const e = JSON.parse(line);
|
|
@@ -31333,12 +31386,12 @@ function buildState(events) {
|
|
|
31333
31386
|
const symbol2 = name.toLowerCase();
|
|
31334
31387
|
const target = kind2 ? materials.get(kind2) : KINDS.map((k) => materials.get(k)).find((m) => m.has(symbol2));
|
|
31335
31388
|
if (!target) return;
|
|
31336
|
-
const
|
|
31389
|
+
const entry2 = target.get(symbol2) ?? { symbol: symbol2, name: symbol2, count: 0 };
|
|
31337
31390
|
const label = str2(localised);
|
|
31338
|
-
if (label)
|
|
31339
|
-
|
|
31340
|
-
if (
|
|
31341
|
-
else target.set(symbol2,
|
|
31391
|
+
if (label) entry2.name = label;
|
|
31392
|
+
entry2.count = Math.max(0, entry2.count + delta);
|
|
31393
|
+
if (entry2.count === 0) target.delete(symbol2);
|
|
31394
|
+
else target.set(symbol2, entry2);
|
|
31342
31395
|
};
|
|
31343
31396
|
const setLocation = (e, docked) => {
|
|
31344
31397
|
location.system = str2(e.StarSystem) ?? location.system;
|
|
@@ -31743,7 +31796,7 @@ var JournalService = class {
|
|
|
31743
31796
|
// deliver the logs before this file, so a file older than the newest Cargo event is ignored.
|
|
31744
31797
|
async readCargoFile(dir, newestCargoEvent) {
|
|
31745
31798
|
try {
|
|
31746
|
-
const j = JSON.parse(await
|
|
31799
|
+
const j = JSON.parse(await readFile3(path6.join(dir, "Cargo.json"), "utf8"));
|
|
31747
31800
|
if (!Array.isArray(j.Inventory) || j.Vessel !== void 0 && j.Vessel !== "Ship") {
|
|
31748
31801
|
return null;
|
|
31749
31802
|
}
|
|
@@ -31759,7 +31812,7 @@ var JournalService = class {
|
|
|
31759
31812
|
async stationFile(kind2) {
|
|
31760
31813
|
let j;
|
|
31761
31814
|
try {
|
|
31762
|
-
j = JSON.parse(await
|
|
31815
|
+
j = JSON.parse(await readFile3(path6.join(this.requireDir(), STATION_FILES[kind2]), "utf8"));
|
|
31763
31816
|
} catch (err) {
|
|
31764
31817
|
if (err instanceof NotConfiguredError) throw err;
|
|
31765
31818
|
return null;
|
|
@@ -31823,7 +31876,7 @@ function marketDemand(item) {
|
|
|
31823
31876
|
return demand > 1 ? demand : 0;
|
|
31824
31877
|
}
|
|
31825
31878
|
async function isSettingsFolder(dir) {
|
|
31826
|
-
return stat4(
|
|
31879
|
+
return stat4(path6.join(dir, "Options")).then(
|
|
31827
31880
|
(s) => s.isDirectory(),
|
|
31828
31881
|
() => false
|
|
31829
31882
|
);
|
|
@@ -31845,12 +31898,14 @@ var InaraClient = class {
|
|
|
31845
31898
|
endpoint;
|
|
31846
31899
|
now;
|
|
31847
31900
|
async profile(name) {
|
|
31901
|
+
const cfg = await this.requireConfig();
|
|
31902
|
+
const who = name ?? cfg.cmdr;
|
|
31848
31903
|
const ev = await this.cache.getOrLoad(
|
|
31849
|
-
`profile:${name ?? ""}`,
|
|
31850
|
-
() => this.send("getCommanderProfile", name ? { searchName: name } : {})
|
|
31904
|
+
`profile:${cfg.cmdr}:${name ?? ""}`,
|
|
31905
|
+
() => this.send(cfg, "getCommanderProfile", name ? { searchName: name } : {})
|
|
31851
31906
|
);
|
|
31852
31907
|
if (ev.eventStatus === 204) {
|
|
31853
|
-
throw new UserError(`Inara: no commander found for "${
|
|
31908
|
+
throw new UserError(`Inara: no commander found for "${who}"`);
|
|
31854
31909
|
}
|
|
31855
31910
|
const d = ev.eventData ?? {};
|
|
31856
31911
|
const ship = d.commanderMainShip;
|
|
@@ -31884,7 +31939,7 @@ var InaraClient = class {
|
|
|
31884
31939
|
async communityGoals() {
|
|
31885
31940
|
const ev = await this.cache.getOrLoad(
|
|
31886
31941
|
"cgs",
|
|
31887
|
-
() => this.send("getCommunityGoalsRecent", [])
|
|
31942
|
+
async () => this.send(await this.requireConfig(), "getCommunityGoalsRecent", [])
|
|
31888
31943
|
);
|
|
31889
31944
|
if (ev.eventStatus === 204) return [];
|
|
31890
31945
|
const goals = ev.eventData ?? [];
|
|
@@ -31903,12 +31958,15 @@ var InaraClient = class {
|
|
|
31903
31958
|
description: String(g.goalDescriptionText ?? "").slice(0, 300)
|
|
31904
31959
|
}));
|
|
31905
31960
|
}
|
|
31961
|
+
// One call resolves the config once and passes it down, so a key change mid-call cannot
|
|
31962
|
+
// mix the old and new values.
|
|
31963
|
+
async requireConfig() {
|
|
31964
|
+
const cfg = typeof this.cfg === "function" ? await this.cfg() : this.cfg;
|
|
31965
|
+
if (!cfg) throw new NotConfiguredError("Inara", ["INARA_API_KEY", "INARA_CMDR"]);
|
|
31966
|
+
return cfg;
|
|
31967
|
+
}
|
|
31906
31968
|
// Inara asks clients not to hammer the API, so requests run one at a time.
|
|
31907
|
-
send(eventName, eventData) {
|
|
31908
|
-
const cfg = this.cfg;
|
|
31909
|
-
if (!cfg) {
|
|
31910
|
-
return Promise.reject(new NotConfiguredError("Inara", ["INARA_API_KEY", "INARA_CMDR"]));
|
|
31911
|
-
}
|
|
31969
|
+
send(cfg, eventName, eventData) {
|
|
31912
31970
|
const run = this.queue.then(async () => {
|
|
31913
31971
|
const body = {
|
|
31914
31972
|
header: {
|
|
@@ -31934,7 +31992,7 @@ var InaraClient = class {
|
|
|
31934
31992
|
});
|
|
31935
31993
|
if (res.header?.eventStatus === 400) {
|
|
31936
31994
|
throw new UserError(
|
|
31937
|
-
`Inara rejected the request: ${String(res.header.eventStatusText ?? "error").replace(/\.+$/, "")}. Check INARA_API_KEY, and that the app name "${cfg.appName}" has been whitelisted by Inara.`
|
|
31995
|
+
`Inara rejected the request: ${String(res.header.eventStatusText ?? "error").replace(/\.+$/, "")}. Check INARA_API_KEY or the key saved on the settings page (settings_open), and that the app name "${cfg.appName}" has been whitelisted by Inara.`
|
|
31938
31996
|
);
|
|
31939
31997
|
}
|
|
31940
31998
|
const ev = res.events?.[0];
|
|
@@ -31984,8 +32042,11 @@ var EdsmClient = class {
|
|
|
31984
32042
|
}
|
|
31985
32043
|
return out;
|
|
31986
32044
|
}
|
|
32045
|
+
config() {
|
|
32046
|
+
return typeof this.cfg === "function" ? this.cfg() : Promise.resolve(this.cfg);
|
|
32047
|
+
}
|
|
31987
32048
|
async call(endpoint, params) {
|
|
31988
|
-
const cfg = this.
|
|
32049
|
+
const cfg = await this.config();
|
|
31989
32050
|
if (!cfg) throw new NotConfiguredError("EDSM", ["EDSM_API_KEY", "EDSM_CMDR"]);
|
|
31990
32051
|
const url2 = new URL(`${this.baseUrl}/${endpoint}`);
|
|
31991
32052
|
url2.search = new URLSearchParams({
|
|
@@ -32007,7 +32068,7 @@ function check2(b) {
|
|
|
32007
32068
|
if (b.msgnum === 100) return;
|
|
32008
32069
|
if (b.msgnum === 203) {
|
|
32009
32070
|
throw new UserError(
|
|
32010
|
-
"EDSM: commander name or API key not found. Check EDSM_CMDR and EDSM_API_KEY, and that EDSM sync is on in EDMC."
|
|
32071
|
+
"EDSM: commander name or API key not found. Check EDSM_CMDR and EDSM_API_KEY, or the values saved on the settings page (settings_open), and that EDSM sync is on in EDMC."
|
|
32011
32072
|
);
|
|
32012
32073
|
}
|
|
32013
32074
|
throw new UserError(`EDSM error ${b.msgnum}: ${b.msg}`);
|
|
@@ -32417,8 +32478,8 @@ var SpanshClient = class {
|
|
|
32417
32478
|
let droppedOld = 0;
|
|
32418
32479
|
let loaded = 0;
|
|
32419
32480
|
let partial2 = false;
|
|
32420
|
-
for (let
|
|
32421
|
-
if (
|
|
32481
|
+
for (let page2 = 0; loaded < q.maxStations; page2++) {
|
|
32482
|
+
if (page2 > 0 && deadline !== void 0 && this.now() >= deadline) {
|
|
32422
32483
|
partial2 = true;
|
|
32423
32484
|
break;
|
|
32424
32485
|
}
|
|
@@ -32426,7 +32487,7 @@ var SpanshClient = class {
|
|
|
32426
32487
|
filters,
|
|
32427
32488
|
sort: [{ distance: { direction: "asc" } }],
|
|
32428
32489
|
size: SEARCH_PAGE_SIZE,
|
|
32429
|
-
page,
|
|
32490
|
+
page: page2,
|
|
32430
32491
|
reference_system: q.near
|
|
32431
32492
|
};
|
|
32432
32493
|
let res;
|
|
@@ -32529,14 +32590,14 @@ var SpanshClient = class {
|
|
|
32529
32590
|
cached(key, load) {
|
|
32530
32591
|
return this.cache.getOrLoad(key, load);
|
|
32531
32592
|
}
|
|
32532
|
-
async get(
|
|
32533
|
-
const { status, body } = await this.raw(
|
|
32593
|
+
async get(path15) {
|
|
32594
|
+
const { status, body } = await this.raw(path15);
|
|
32534
32595
|
if (status >= 400) throw new SpanshError(status, body);
|
|
32535
32596
|
return body;
|
|
32536
32597
|
}
|
|
32537
|
-
async post(
|
|
32598
|
+
async post(path15, body, contentType, opts) {
|
|
32538
32599
|
const res = await this.raw(
|
|
32539
|
-
|
|
32600
|
+
path15,
|
|
32540
32601
|
{ method: "POST", body, headers: { "content-type": contentType } },
|
|
32541
32602
|
opts
|
|
32542
32603
|
);
|
|
@@ -32544,7 +32605,7 @@ var SpanshClient = class {
|
|
|
32544
32605
|
return res.body;
|
|
32545
32606
|
}
|
|
32546
32607
|
// Spansh publishes no quota; spacing requests keeps us a polite client.
|
|
32547
|
-
async raw(
|
|
32608
|
+
async raw(path15, init = {}, opts) {
|
|
32548
32609
|
const turn = this.gate.then(async () => {
|
|
32549
32610
|
const wait = this.lastStart + this.minIntervalMs - Date.now();
|
|
32550
32611
|
if (wait > 0) await this.sleep(wait);
|
|
@@ -32552,7 +32613,7 @@ var SpanshClient = class {
|
|
|
32552
32613
|
});
|
|
32553
32614
|
this.gate = turn.catch(() => void 0);
|
|
32554
32615
|
await turn;
|
|
32555
|
-
const { status, text } = await this.http.text(`${this.baseUrl}${
|
|
32616
|
+
const { status, text } = await this.http.text(`${this.baseUrl}${path15}`, init, opts);
|
|
32556
32617
|
let body;
|
|
32557
32618
|
try {
|
|
32558
32619
|
body = text ? parseSpanshJson(text) : void 0;
|
|
@@ -32594,17 +32655,34 @@ function mapRoute(r) {
|
|
|
32594
32655
|
}
|
|
32595
32656
|
|
|
32596
32657
|
// src/plans/store.ts
|
|
32597
|
-
import {
|
|
32658
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
32659
|
+
import { lstat as lstat2, mkdir as mkdir3, rename as rename3, rm, writeFile as writeFile3 } from "fs/promises";
|
|
32660
|
+
import path8 from "path";
|
|
32598
32661
|
|
|
32599
32662
|
// src/fs/lock.ts
|
|
32600
|
-
import { randomUUID } from "crypto";
|
|
32601
|
-
import {
|
|
32602
|
-
|
|
32663
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
32664
|
+
import {
|
|
32665
|
+
chmod,
|
|
32666
|
+
link,
|
|
32667
|
+
mkdir as mkdir2,
|
|
32668
|
+
open as open2,
|
|
32669
|
+
readFile as readFile4,
|
|
32670
|
+
rename as rename2,
|
|
32671
|
+
stat as stat5,
|
|
32672
|
+
unlink,
|
|
32673
|
+
writeFile as writeFile2
|
|
32674
|
+
} from "fs/promises";
|
|
32675
|
+
import path7 from "path";
|
|
32603
32676
|
var LOCK_RETRY_MS = 50;
|
|
32604
32677
|
var LOCK_STALE_MS = 3e4;
|
|
32605
32678
|
var PENDING_DELETE_MS = 1e3;
|
|
32606
|
-
|
|
32607
|
-
|
|
32679
|
+
function lockPath(file2, lockDir) {
|
|
32680
|
+
if (!lockDir) return `${file2}.lock`;
|
|
32681
|
+
const hash2 = createHash2("sha256").update(path7.resolve(file2)).digest("hex").slice(0, 16);
|
|
32682
|
+
return path7.join(lockDir, `${hash2}.lock`);
|
|
32683
|
+
}
|
|
32684
|
+
async function acquireLock(file2, timeoutMs, lockedMessage, lockDir) {
|
|
32685
|
+
const lock = lockPath(file2, lockDir);
|
|
32608
32686
|
const deadline = Date.now() + timeoutMs;
|
|
32609
32687
|
let pendingSince;
|
|
32610
32688
|
const waitForDelete = async (err) => {
|
|
@@ -32641,7 +32719,7 @@ async function acquireLock(file2, timeoutMs, lockedMessage) {
|
|
|
32641
32719
|
}
|
|
32642
32720
|
const token = randomUUID();
|
|
32643
32721
|
const release = async () => {
|
|
32644
|
-
const held = await
|
|
32722
|
+
const held = await readFile4(lock, "utf8").catch(() => void 0);
|
|
32645
32723
|
if (held === token) await unlink(lock).catch(() => void 0);
|
|
32646
32724
|
};
|
|
32647
32725
|
try {
|
|
@@ -32660,18 +32738,21 @@ function pendingDelete(err) {
|
|
|
32660
32738
|
return process.platform === "win32" && (code === "EPERM" || code === "EACCES");
|
|
32661
32739
|
}
|
|
32662
32740
|
async function lockedWrite(file2, opts, update) {
|
|
32663
|
-
await
|
|
32664
|
-
|
|
32741
|
+
await mkdir2(path7.dirname(file2), { recursive: true });
|
|
32742
|
+
if (opts.lockDir) await mkdir2(opts.lockDir, { recursive: true });
|
|
32743
|
+
const release = await acquireLock(file2, opts.timeoutMs, opts.lockedMessage, opts.lockDir);
|
|
32665
32744
|
try {
|
|
32666
32745
|
const { content, result } = await update();
|
|
32667
32746
|
const tmp = `${file2}.${process.pid}.${randomUUID()}.tmp`;
|
|
32668
32747
|
try {
|
|
32669
|
-
await
|
|
32670
|
-
await
|
|
32748
|
+
await writeFile2(tmp, content, opts.mode === void 0 ? void 0 : { mode: opts.mode });
|
|
32749
|
+
if (opts.mode !== void 0) await chmod(tmp, opts.mode);
|
|
32750
|
+
await rename2(tmp, file2);
|
|
32671
32751
|
} catch (err) {
|
|
32672
32752
|
await unlink(tmp).catch(() => void 0);
|
|
32673
32753
|
throw err;
|
|
32674
32754
|
}
|
|
32755
|
+
await opts.afterWrite?.();
|
|
32675
32756
|
return result;
|
|
32676
32757
|
} finally {
|
|
32677
32758
|
await release();
|
|
@@ -32680,7 +32761,7 @@ async function lockedWrite(file2, opts, update) {
|
|
|
32680
32761
|
async function breakStaleLock(lock) {
|
|
32681
32762
|
const aside = `${lock}.${process.pid}.${Date.now()}.stale`;
|
|
32682
32763
|
try {
|
|
32683
|
-
await
|
|
32764
|
+
await rename2(lock, aside);
|
|
32684
32765
|
} catch (err) {
|
|
32685
32766
|
if (err.code === "ENOENT") return true;
|
|
32686
32767
|
throw err;
|
|
@@ -32695,6 +32776,105 @@ async function breakStaleLock(lock) {
|
|
|
32695
32776
|
return true;
|
|
32696
32777
|
}
|
|
32697
32778
|
|
|
32779
|
+
// src/fs/regular.ts
|
|
32780
|
+
import { constants } from "fs";
|
|
32781
|
+
import { lstat, open as open3 } from "fs/promises";
|
|
32782
|
+
var MAX_SECRETS_FILE = 1024 * 1024;
|
|
32783
|
+
var MAX_DATA_FILE = 8 * 1024 * 1024;
|
|
32784
|
+
var NOFOLLOW = constants.O_NOFOLLOW;
|
|
32785
|
+
async function readRegularText(file2, max) {
|
|
32786
|
+
if (NOFOLLOW === void 0) {
|
|
32787
|
+
try {
|
|
32788
|
+
if (!(await lstat(file2)).isFile()) return { kind: "unsafe", reason: "not a regular file" };
|
|
32789
|
+
} catch (err) {
|
|
32790
|
+
return failure2(err, file2);
|
|
32791
|
+
}
|
|
32792
|
+
}
|
|
32793
|
+
let handle;
|
|
32794
|
+
try {
|
|
32795
|
+
handle = await open3(
|
|
32796
|
+
file2,
|
|
32797
|
+
constants.O_RDONLY | (NOFOLLOW ?? 0) | (NOFOLLOW ? constants.O_NONBLOCK : 0)
|
|
32798
|
+
);
|
|
32799
|
+
} catch (err) {
|
|
32800
|
+
if (err.code === "ELOOP") {
|
|
32801
|
+
return { kind: "unsafe", reason: "not a regular file" };
|
|
32802
|
+
}
|
|
32803
|
+
return failure2(err, file2);
|
|
32804
|
+
}
|
|
32805
|
+
try {
|
|
32806
|
+
const st = await handle.stat();
|
|
32807
|
+
if (!st.isFile()) return { kind: "unsafe", reason: "not a regular file" };
|
|
32808
|
+
if (st.size > max) return { kind: "unsafe", reason: "too large" };
|
|
32809
|
+
return { kind: "ok", text: await handle.readFile("utf8") };
|
|
32810
|
+
} catch (err) {
|
|
32811
|
+
return failure2(err, file2);
|
|
32812
|
+
} finally {
|
|
32813
|
+
await handle.close().catch(() => void 0);
|
|
32814
|
+
}
|
|
32815
|
+
}
|
|
32816
|
+
async function failure2(err, file2) {
|
|
32817
|
+
const code = err.code;
|
|
32818
|
+
if (code === "ENOENT") return { kind: "missing" };
|
|
32819
|
+
const st = await lstat(file2).catch(() => null);
|
|
32820
|
+
if (st && !st.isFile()) return { kind: "unsafe", reason: "not a regular file" };
|
|
32821
|
+
return { kind: "unsafe", reason: `not readable (${code ?? "unknown error"})` };
|
|
32822
|
+
}
|
|
32823
|
+
|
|
32824
|
+
// src/plans/markdown.ts
|
|
32825
|
+
var box = {
|
|
32826
|
+
todo: "[ ]",
|
|
32827
|
+
in_progress: "[~]",
|
|
32828
|
+
done: "[x]",
|
|
32829
|
+
skipped: "[-]"
|
|
32830
|
+
};
|
|
32831
|
+
function planToMarkdown(plan) {
|
|
32832
|
+
const done = plan.items.filter((i) => i.status === "done" || i.status === "skipped").length;
|
|
32833
|
+
const out = [
|
|
32834
|
+
`# ${plan.name}`,
|
|
32835
|
+
"",
|
|
32836
|
+
`Plan \`${plan.id}\` \xB7 updated ${plan.updated_at} \xB7 ${done}/${plan.items.length} done`,
|
|
32837
|
+
""
|
|
32838
|
+
];
|
|
32839
|
+
if (plan.notes) out.push(plan.notes, "");
|
|
32840
|
+
if (plan.items.length) {
|
|
32841
|
+
out.push("## Items", "");
|
|
32842
|
+
for (const i of plan.items) {
|
|
32843
|
+
const where = [
|
|
32844
|
+
i.station ? `at ${i.station}` : void 0,
|
|
32845
|
+
i.engineer ? `engineer ${i.engineer}` : void 0
|
|
32846
|
+
].filter(Boolean);
|
|
32847
|
+
out.push(
|
|
32848
|
+
`- ${box[i.status]} **${i.kind}** ${i.description}${where.length ? ` \u2014 ${where.join(", ")}` : ""}`
|
|
32849
|
+
);
|
|
32850
|
+
if (i.materials?.length) {
|
|
32851
|
+
out.push(` - Materials: ${i.materials.map((m) => `${m.count}\xD7 ${m.name}`).join(", ")}`);
|
|
32852
|
+
}
|
|
32853
|
+
if (i.notes) out.push(` - Notes: ${i.notes}`);
|
|
32854
|
+
}
|
|
32855
|
+
out.push("");
|
|
32856
|
+
}
|
|
32857
|
+
if (plan.stops.length) {
|
|
32858
|
+
out.push("## Stops", "");
|
|
32859
|
+
plan.stops.forEach((s, n) => {
|
|
32860
|
+
out.push(`${n + 1}. ${s.system}${s.station ? ` \u2014 ${s.station}` : ""}: ${s.purpose}`);
|
|
32861
|
+
});
|
|
32862
|
+
out.push("");
|
|
32863
|
+
}
|
|
32864
|
+
if (plan.route) {
|
|
32865
|
+
out.push("## Route", "");
|
|
32866
|
+
plan.route.stops.forEach((s, n) => out.push(`${n + 1}. ${s}`));
|
|
32867
|
+
const r = plan.route;
|
|
32868
|
+
const kind2 = r.plotted ? "plotted" : "estimate";
|
|
32869
|
+
out.push(
|
|
32870
|
+
"",
|
|
32871
|
+
`From ${r.from}; ${kind2}. Total ${r.total_min} min, computed ${r.computed_at}`,
|
|
32872
|
+
""
|
|
32873
|
+
);
|
|
32874
|
+
}
|
|
32875
|
+
return out.join("\n");
|
|
32876
|
+
}
|
|
32877
|
+
|
|
32698
32878
|
// src/plans/schema.ts
|
|
32699
32879
|
var ItemKind = external_exports.enum([
|
|
32700
32880
|
"buy",
|
|
@@ -32752,16 +32932,28 @@ var PlanFileSchema = external_exports.object({
|
|
|
32752
32932
|
|
|
32753
32933
|
// src/plans/store.ts
|
|
32754
32934
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
32935
|
+
var PlanExistsError = class extends UserError {
|
|
32936
|
+
constructor(id) {
|
|
32937
|
+
super(`Plan ${id} exists; pass overwrite: true to replace it`);
|
|
32938
|
+
}
|
|
32939
|
+
};
|
|
32755
32940
|
var PlanStore = class {
|
|
32756
32941
|
constructor(file2, now = () => /* @__PURE__ */ new Date(), opts = {}) {
|
|
32757
32942
|
this.file = file2;
|
|
32758
32943
|
this.now = now;
|
|
32759
32944
|
this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
|
|
32945
|
+
this.lockDir = opts.lockDir;
|
|
32946
|
+
this.guard = opts.guard;
|
|
32947
|
+
this.markdownDir = opts.markdownDir;
|
|
32760
32948
|
}
|
|
32761
32949
|
file;
|
|
32762
32950
|
now;
|
|
32951
|
+
lastCopyError = null;
|
|
32763
32952
|
queue = Promise.resolve();
|
|
32764
32953
|
lockTimeoutMs;
|
|
32954
|
+
lockDir;
|
|
32955
|
+
guard;
|
|
32956
|
+
markdownDir;
|
|
32765
32957
|
async list() {
|
|
32766
32958
|
const { plans } = await this.read();
|
|
32767
32959
|
return plans.map((p) => ({
|
|
@@ -32784,7 +32976,7 @@ var PlanStore = class {
|
|
|
32784
32976
|
const stamp = this.now().toISOString();
|
|
32785
32977
|
const idx = data.plans.findIndex((p) => p.id === parsed.data.id);
|
|
32786
32978
|
if (idx >= 0 && opts.overwrite === false) {
|
|
32787
|
-
throw new
|
|
32979
|
+
throw new PlanExistsError(parsed.data.id);
|
|
32788
32980
|
}
|
|
32789
32981
|
const plan = {
|
|
32790
32982
|
...parsed.data,
|
|
@@ -32825,13 +33017,13 @@ var PlanStore = class {
|
|
|
32825
33017
|
});
|
|
32826
33018
|
}
|
|
32827
33019
|
async read() {
|
|
32828
|
-
|
|
32829
|
-
|
|
32830
|
-
|
|
32831
|
-
|
|
32832
|
-
|
|
32833
|
-
throw err;
|
|
33020
|
+
await this.guard?.();
|
|
33021
|
+
const read2 = await readRegularText(this.file, MAX_DATA_FILE);
|
|
33022
|
+
if (read2.kind === "missing") return { version: 1, plans: [] };
|
|
33023
|
+
if (read2.kind === "unsafe") {
|
|
33024
|
+
throw new UserError(`Plan file ${this.file} is invalid: ${read2.reason}`);
|
|
32834
33025
|
}
|
|
33026
|
+
const raw = read2.text;
|
|
32835
33027
|
let json3;
|
|
32836
33028
|
try {
|
|
32837
33029
|
json3 = JSON.parse(raw);
|
|
@@ -32847,27 +33039,99 @@ var PlanStore = class {
|
|
|
32847
33039
|
// Read-modify-write runs one at a time so concurrent tool calls cannot lose updates.
|
|
32848
33040
|
// The lock file extends that across processes (e.g. Claude Desktop and Claude Code).
|
|
32849
33041
|
mutate(fn) {
|
|
32850
|
-
const run = this.queue.then(
|
|
32851
|
-
()
|
|
33042
|
+
const run = this.queue.then(async () => {
|
|
33043
|
+
await this.guard?.();
|
|
33044
|
+
let latest = null;
|
|
33045
|
+
let before = [];
|
|
33046
|
+
return lockedWrite(
|
|
32852
33047
|
this.file,
|
|
32853
33048
|
{
|
|
32854
33049
|
timeoutMs: this.lockTimeoutMs,
|
|
32855
|
-
|
|
33050
|
+
lockDir: this.lockDir,
|
|
33051
|
+
lockedMessage: "Plan file is locked by another process; try again",
|
|
33052
|
+
// Under the lock, so the copies always match the last saved file.
|
|
33053
|
+
afterWrite: async () => {
|
|
33054
|
+
if (latest && this.markdownDir) await this.writeMarkdown(latest, before);
|
|
33055
|
+
}
|
|
32856
33056
|
},
|
|
32857
33057
|
async () => {
|
|
32858
33058
|
const data = await this.read();
|
|
33059
|
+
before = data.plans.map((p) => p.id);
|
|
32859
33060
|
const result = fn(data);
|
|
32860
|
-
|
|
33061
|
+
latest = data;
|
|
33062
|
+
const content = JSON.stringify(data, null, 2);
|
|
33063
|
+
if (Buffer.byteLength(content) > MAX_DATA_FILE) {
|
|
33064
|
+
throw new UserError(
|
|
33065
|
+
"Plans file would be larger than 8 MB; delete plans or shorten notes first."
|
|
33066
|
+
);
|
|
33067
|
+
}
|
|
33068
|
+
return { content, result };
|
|
32861
33069
|
}
|
|
32862
|
-
)
|
|
32863
|
-
);
|
|
33070
|
+
);
|
|
33071
|
+
});
|
|
32864
33072
|
this.queue = run.catch(() => void 0);
|
|
32865
33073
|
return run;
|
|
32866
33074
|
}
|
|
33075
|
+
/** Why the Markdown copies are not being written, or null when they are fine. */
|
|
33076
|
+
async markdownProblem() {
|
|
33077
|
+
const dir = this.markdownDir;
|
|
33078
|
+
if (!dir) return null;
|
|
33079
|
+
const fixed = await this.directoryProblem();
|
|
33080
|
+
if (fixed) return fixed;
|
|
33081
|
+
return this.lastCopyError === null ? null : `Plan copies could not be written to ${dir}: ${this.lastCopyError}.`;
|
|
33082
|
+
}
|
|
33083
|
+
async directoryProblem() {
|
|
33084
|
+
const dir = this.markdownDir;
|
|
33085
|
+
if (!dir) return null;
|
|
33086
|
+
let st;
|
|
33087
|
+
try {
|
|
33088
|
+
st = await lstat2(dir);
|
|
33089
|
+
} catch {
|
|
33090
|
+
return null;
|
|
33091
|
+
}
|
|
33092
|
+
if (st.isSymbolicLink()) return `Plan copies were not written: ${dir} is a symbolic link.`;
|
|
33093
|
+
if (!st.isDirectory()) return `Plan copies were not written: ${dir} is not a folder.`;
|
|
33094
|
+
return null;
|
|
33095
|
+
}
|
|
33096
|
+
// Claude on mobile can read these through the Google Drive connector; they are copies only.
|
|
33097
|
+
// The sync folder may be shared, so nothing here follows a link or deletes a file we did not write.
|
|
33098
|
+
async writeMarkdown(data, before) {
|
|
33099
|
+
const dir = this.markdownDir;
|
|
33100
|
+
try {
|
|
33101
|
+
await mkdir3(dir, { recursive: true });
|
|
33102
|
+
const problem = await this.directoryProblem();
|
|
33103
|
+
if (problem) {
|
|
33104
|
+
console.error(problem);
|
|
33105
|
+
return;
|
|
33106
|
+
}
|
|
33107
|
+
const keep = /* @__PURE__ */ new Set();
|
|
33108
|
+
for (const p of data.plans) {
|
|
33109
|
+
const name = `${p.id}.md`;
|
|
33110
|
+
keep.add(name);
|
|
33111
|
+
const tmp = path8.join(dir, `${name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
33112
|
+
try {
|
|
33113
|
+
await writeFile3(tmp, planToMarkdown(p), { flag: "wx" });
|
|
33114
|
+
await rename3(tmp, path8.join(dir, name));
|
|
33115
|
+
} catch (err) {
|
|
33116
|
+
await rm(tmp, { force: true });
|
|
33117
|
+
throw err;
|
|
33118
|
+
}
|
|
33119
|
+
}
|
|
33120
|
+
for (const id of before) {
|
|
33121
|
+
const n = `${id}.md`;
|
|
33122
|
+
if (keep.has(n) || !PRUNABLE.test(n)) continue;
|
|
33123
|
+
const file2 = path8.join(dir, n);
|
|
33124
|
+
const st = await lstat2(file2).catch(() => null);
|
|
33125
|
+
if (st?.isFile()) await rm(file2, { force: true });
|
|
33126
|
+
}
|
|
33127
|
+
this.lastCopyError = null;
|
|
33128
|
+
} catch (err) {
|
|
33129
|
+
this.lastCopyError = err.message;
|
|
33130
|
+
console.error(`Could not write Markdown plan copies to ${dir}: ${err.message}`);
|
|
33131
|
+
}
|
|
33132
|
+
}
|
|
32867
33133
|
};
|
|
32868
|
-
|
|
32869
|
-
// src/prefs/store.ts
|
|
32870
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
33134
|
+
var PRUNABLE = /^[a-z0-9][a-z0-9-]{0,63}\.md$/;
|
|
32871
33135
|
|
|
32872
33136
|
// src/prefs/schema.ts
|
|
32873
33137
|
var Mode = external_exports.enum(["soft", "hard", "off"]);
|
|
@@ -32994,18 +33258,22 @@ var PreferencesStore = class {
|
|
|
32994
33258
|
constructor(file2, opts = {}) {
|
|
32995
33259
|
this.file = file2;
|
|
32996
33260
|
this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS2;
|
|
33261
|
+
this.lockDir = opts.lockDir;
|
|
33262
|
+
this.guard = opts.guard;
|
|
32997
33263
|
}
|
|
32998
33264
|
file;
|
|
32999
33265
|
queue = Promise.resolve();
|
|
33000
33266
|
lockTimeoutMs;
|
|
33267
|
+
lockDir;
|
|
33268
|
+
guard;
|
|
33001
33269
|
async get() {
|
|
33002
|
-
|
|
33003
|
-
|
|
33004
|
-
|
|
33005
|
-
|
|
33006
|
-
|
|
33007
|
-
throw err;
|
|
33270
|
+
await this.guard?.();
|
|
33271
|
+
const read2 = await readRegularText(this.file, MAX_DATA_FILE);
|
|
33272
|
+
if (read2.kind === "missing") return DEFAULT_PREFS;
|
|
33273
|
+
if (read2.kind === "unsafe") {
|
|
33274
|
+
throw new UserError(`Preferences file ${this.file} is invalid: ${read2.reason}`);
|
|
33008
33275
|
}
|
|
33276
|
+
const raw = read2.text;
|
|
33009
33277
|
let json3;
|
|
33010
33278
|
try {
|
|
33011
33279
|
json3 = JSON.parse(raw);
|
|
@@ -33027,22 +33295,30 @@ var PreferencesStore = class {
|
|
|
33027
33295
|
}
|
|
33028
33296
|
async write(patch) {
|
|
33029
33297
|
const parsed = parsePatch(patch);
|
|
33298
|
+
await this.guard?.();
|
|
33030
33299
|
return lockedWrite(
|
|
33031
33300
|
this.file,
|
|
33032
33301
|
{
|
|
33033
33302
|
timeoutMs: this.lockTimeoutMs,
|
|
33303
|
+
lockDir: this.lockDir,
|
|
33034
33304
|
lockedMessage: "Preferences file is locked by another process; try again"
|
|
33035
33305
|
},
|
|
33036
33306
|
async () => {
|
|
33037
33307
|
const next = applyPatch(await this.get(), parsed);
|
|
33038
|
-
|
|
33308
|
+
const content = JSON.stringify(next, null, 2);
|
|
33309
|
+
if (Buffer.byteLength(content) > MAX_DATA_FILE) {
|
|
33310
|
+
throw new UserError(
|
|
33311
|
+
"Preferences file would be larger than 8 MB; shorten the preferences first."
|
|
33312
|
+
);
|
|
33313
|
+
}
|
|
33314
|
+
return { content, result: next };
|
|
33039
33315
|
}
|
|
33040
33316
|
);
|
|
33041
33317
|
}
|
|
33042
33318
|
};
|
|
33043
33319
|
|
|
33044
33320
|
// src/ships/data.ts
|
|
33045
|
-
import { mkdir as mkdir4, readFile as
|
|
33321
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
33046
33322
|
import path9 from "path";
|
|
33047
33323
|
import { gunzipSync } from "zlib";
|
|
33048
33324
|
|
|
@@ -33286,14 +33562,14 @@ function readTar(buf) {
|
|
|
33286
33562
|
comment = paxFields(data).comment ?? comment;
|
|
33287
33563
|
continue;
|
|
33288
33564
|
}
|
|
33289
|
-
let
|
|
33565
|
+
let path15 = str3(h, 0, 100);
|
|
33290
33566
|
if (str3(h, 257, 6) === "ustar") {
|
|
33291
33567
|
const prefix = str3(h, 345, 155);
|
|
33292
|
-
if (prefix)
|
|
33568
|
+
if (prefix) path15 = `${prefix}/${path15}`;
|
|
33293
33569
|
}
|
|
33294
|
-
if (nextPath !== null)
|
|
33570
|
+
if (nextPath !== null) path15 = nextPath;
|
|
33295
33571
|
nextPath = null;
|
|
33296
|
-
if (type === "0" || type === "\0") files.push({ path:
|
|
33572
|
+
if (type === "0" || type === "\0") files.push({ path: path15, data });
|
|
33297
33573
|
}
|
|
33298
33574
|
return { files, comment };
|
|
33299
33575
|
}
|
|
@@ -33559,7 +33835,7 @@ var ShipDataService = class {
|
|
|
33559
33835
|
}
|
|
33560
33836
|
async loadDisk() {
|
|
33561
33837
|
try {
|
|
33562
|
-
const snap = JSON.parse(await
|
|
33838
|
+
const snap = JSON.parse(await readFile5(this.file, "utf8"));
|
|
33563
33839
|
if (snap.version !== 1 || typeof snap.commit !== "string") return;
|
|
33564
33840
|
const data = buildShipData(snap.commit, snap.files);
|
|
33565
33841
|
if (this.data) return;
|
|
@@ -33578,18 +33854,756 @@ var ShipDataService = class {
|
|
|
33578
33854
|
};
|
|
33579
33855
|
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
33580
33856
|
|
|
33857
|
+
// src/settings/secrets.ts
|
|
33858
|
+
import { lstat as lstat3, mkdir as mkdir5, rm as rm2 } from "fs/promises";
|
|
33859
|
+
import path10 from "path";
|
|
33860
|
+
|
|
33861
|
+
// src/settings/crypto.ts
|
|
33862
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, scryptSync } from "crypto";
|
|
33863
|
+
var SCRYPT = { N: 2 ** 17, r: 8, p: 1 };
|
|
33864
|
+
var SCRYPT_MAXMEM = 256 * 1024 * 1024;
|
|
33865
|
+
var SecretsError = class extends UserError {
|
|
33866
|
+
name = "SecretsError";
|
|
33867
|
+
};
|
|
33868
|
+
var EnvelopeSchema = external_exports.object({
|
|
33869
|
+
format: external_exports.literal("ed-mcp-secrets"),
|
|
33870
|
+
version: external_exports.number().int(),
|
|
33871
|
+
kdf: external_exports.literal("scrypt"),
|
|
33872
|
+
// With the memory check in open(), these bounds stop a crafted file from making scrypt use gigabytes.
|
|
33873
|
+
N: external_exports.number().int().min(2).max(2 ** 17).refine((n) => (n & n - 1) === 0),
|
|
33874
|
+
r: external_exports.number().int().min(1).max(8),
|
|
33875
|
+
p: external_exports.number().int().min(1).max(2),
|
|
33876
|
+
salt: external_exports.string(),
|
|
33877
|
+
iv: external_exports.string(),
|
|
33878
|
+
tag: external_exports.string(),
|
|
33879
|
+
data: external_exports.string()
|
|
33880
|
+
});
|
|
33881
|
+
var SecretCipher = class {
|
|
33882
|
+
constructor(password, params = SCRYPT) {
|
|
33883
|
+
this.password = password;
|
|
33884
|
+
this.params = params;
|
|
33885
|
+
}
|
|
33886
|
+
password;
|
|
33887
|
+
params;
|
|
33888
|
+
// Deriving the key is slow on purpose, so each salt's key is derived once per process.
|
|
33889
|
+
keys = /* @__PURE__ */ new Map();
|
|
33890
|
+
seal(plaintext, salt = randomBytes3(16).toString("base64")) {
|
|
33891
|
+
const key = this.key(salt, this.params);
|
|
33892
|
+
const iv = randomBytes3(12);
|
|
33893
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
33894
|
+
const data = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
33895
|
+
return {
|
|
33896
|
+
format: "ed-mcp-secrets",
|
|
33897
|
+
version: 1,
|
|
33898
|
+
kdf: "scrypt",
|
|
33899
|
+
...this.params,
|
|
33900
|
+
salt,
|
|
33901
|
+
iv: iv.toString("base64"),
|
|
33902
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
33903
|
+
data: data.toString("base64")
|
|
33904
|
+
};
|
|
33905
|
+
}
|
|
33906
|
+
open(raw) {
|
|
33907
|
+
const env2 = EnvelopeSchema.safeParse(raw);
|
|
33908
|
+
if (!env2.success) throw new SecretsError("not an ed-mcp secrets file");
|
|
33909
|
+
const e = env2.data;
|
|
33910
|
+
if (e.version !== 1) {
|
|
33911
|
+
throw new SecretsError(
|
|
33912
|
+
e.version > 1 ? `made by a newer ed-mcp (version ${e.version})` : `unknown version ${e.version}`
|
|
33913
|
+
);
|
|
33914
|
+
}
|
|
33915
|
+
if (128 * e.N * e.r >= SCRYPT_MAXMEM) throw new SecretsError("not an ed-mcp secrets file");
|
|
33916
|
+
const key = this.key(e.salt, { N: e.N, r: e.r, p: e.p });
|
|
33917
|
+
try {
|
|
33918
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(e.iv, "base64"), {
|
|
33919
|
+
authTagLength: 16
|
|
33920
|
+
});
|
|
33921
|
+
decipher.setAuthTag(Buffer.from(e.tag, "base64"));
|
|
33922
|
+
const plaintext = Buffer.concat([
|
|
33923
|
+
decipher.update(Buffer.from(e.data, "base64")),
|
|
33924
|
+
decipher.final()
|
|
33925
|
+
]).toString("utf8");
|
|
33926
|
+
return { plaintext, salt: e.salt };
|
|
33927
|
+
} catch {
|
|
33928
|
+
throw new SecretsError("wrong password");
|
|
33929
|
+
}
|
|
33930
|
+
}
|
|
33931
|
+
key(salt, params) {
|
|
33932
|
+
const id = `${salt}:${params.N}:${params.r}:${params.p}`;
|
|
33933
|
+
let key = this.keys.get(id);
|
|
33934
|
+
if (!key) {
|
|
33935
|
+
key = scryptSync(this.password, Buffer.from(salt, "base64"), 32, {
|
|
33936
|
+
...params,
|
|
33937
|
+
maxmem: SCRYPT_MAXMEM
|
|
33938
|
+
});
|
|
33939
|
+
this.keys.set(id, key);
|
|
33940
|
+
}
|
|
33941
|
+
return key;
|
|
33942
|
+
}
|
|
33943
|
+
};
|
|
33944
|
+
|
|
33945
|
+
// src/settings/secrets.ts
|
|
33946
|
+
var StoredTokensSchema = external_exports.object({
|
|
33947
|
+
refresh_token: external_exports.string(),
|
|
33948
|
+
access_token: external_exports.string().nullable(),
|
|
33949
|
+
expires_at: external_exports.number(),
|
|
33950
|
+
authorized_at: external_exports.number()
|
|
33951
|
+
});
|
|
33952
|
+
var ServiceKeySchema = external_exports.strictObject({ apiKey: external_exports.string().min(1), cmdr: external_exports.string().min(1) });
|
|
33953
|
+
var SecretsSchema = external_exports.strictObject({
|
|
33954
|
+
version: external_exports.literal(1),
|
|
33955
|
+
inara: ServiceKeySchema.optional(),
|
|
33956
|
+
edsm: ServiceKeySchema.optional(),
|
|
33957
|
+
capi: StoredTokensSchema.optional()
|
|
33958
|
+
});
|
|
33959
|
+
var WrongPasswordError = class extends SecretsError {
|
|
33960
|
+
};
|
|
33961
|
+
var LOCK_TIMEOUT_MS3 = 5e3;
|
|
33962
|
+
var SecretStore = class _SecretStore {
|
|
33963
|
+
constructor(file2, opts = {}) {
|
|
33964
|
+
this.file = file2;
|
|
33965
|
+
this.opts = opts;
|
|
33966
|
+
this.encrypted = opts.password !== void 0;
|
|
33967
|
+
if (opts.password !== void 0) this.cipher = new SecretCipher(opts.password, opts.scrypt);
|
|
33968
|
+
}
|
|
33969
|
+
file;
|
|
33970
|
+
opts;
|
|
33971
|
+
encrypted;
|
|
33972
|
+
cipher;
|
|
33973
|
+
cached = null;
|
|
33974
|
+
salt;
|
|
33975
|
+
queue = Promise.resolve();
|
|
33976
|
+
async get() {
|
|
33977
|
+
return this.read();
|
|
33978
|
+
}
|
|
33979
|
+
update(fn) {
|
|
33980
|
+
const run = this.queue.then(() => this.write(fn));
|
|
33981
|
+
this.queue = run.catch(() => void 0);
|
|
33982
|
+
return run;
|
|
33983
|
+
}
|
|
33984
|
+
/** Decrypts with the old password and writes the same secrets with the current one. */
|
|
33985
|
+
reencrypt(oldPassword) {
|
|
33986
|
+
const cipher = this.cipher;
|
|
33987
|
+
if (!cipher) {
|
|
33988
|
+
return Promise.reject(new SecretsError("Secrets are not encrypted; nothing to re-encrypt."));
|
|
33989
|
+
}
|
|
33990
|
+
const run = this.queue.then(async () => {
|
|
33991
|
+
await this.opts.guard?.();
|
|
33992
|
+
await lockedWrite(
|
|
33993
|
+
this.file,
|
|
33994
|
+
{
|
|
33995
|
+
timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
|
|
33996
|
+
lockedMessage: "Secrets file is locked by another process; try again",
|
|
33997
|
+
lockDir: this.opts.lockDir,
|
|
33998
|
+
mode: 384
|
|
33999
|
+
},
|
|
34000
|
+
async () => {
|
|
34001
|
+
const st = await this.checkFile();
|
|
34002
|
+
if (!st) throw new SecretsError(`${this.file} does not exist; nothing to re-encrypt.`);
|
|
34003
|
+
const raw = await this.readText();
|
|
34004
|
+
const old = new _SecretStore(this.file, {
|
|
34005
|
+
password: oldPassword,
|
|
34006
|
+
scrypt: this.opts.scrypt
|
|
34007
|
+
});
|
|
34008
|
+
const { value } = old.parse(raw);
|
|
34009
|
+
const content = JSON.stringify(cipher.seal(JSON.stringify(value, null, 2)), null, 2);
|
|
34010
|
+
return { content, result: void 0 };
|
|
34011
|
+
}
|
|
34012
|
+
);
|
|
34013
|
+
});
|
|
34014
|
+
this.queue = run.catch(() => void 0);
|
|
34015
|
+
return run.finally(() => {
|
|
34016
|
+
this.cached = null;
|
|
34017
|
+
this.salt = void 0;
|
|
34018
|
+
});
|
|
34019
|
+
}
|
|
34020
|
+
/** Removes the secrets file: keys and the Frontier login are forgotten. */
|
|
34021
|
+
reset() {
|
|
34022
|
+
const run = this.queue.then(async () => {
|
|
34023
|
+
await this.opts.guard?.();
|
|
34024
|
+
await mkdir5(path10.dirname(this.file), { recursive: true });
|
|
34025
|
+
if (this.opts.lockDir) await mkdir5(this.opts.lockDir, { recursive: true });
|
|
34026
|
+
const release = await acquireLock(
|
|
34027
|
+
this.file,
|
|
34028
|
+
this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
|
|
34029
|
+
"Secrets file is locked by another process; try again",
|
|
34030
|
+
this.opts.lockDir
|
|
34031
|
+
);
|
|
34032
|
+
try {
|
|
34033
|
+
await rm2(this.file, { force: true });
|
|
34034
|
+
} finally {
|
|
34035
|
+
await release();
|
|
34036
|
+
}
|
|
34037
|
+
});
|
|
34038
|
+
this.queue = run.catch(() => void 0);
|
|
34039
|
+
return run.finally(() => {
|
|
34040
|
+
this.cached = null;
|
|
34041
|
+
this.salt = void 0;
|
|
34042
|
+
});
|
|
34043
|
+
}
|
|
34044
|
+
async read() {
|
|
34045
|
+
await this.opts.guard?.();
|
|
34046
|
+
const st = await this.checkFile();
|
|
34047
|
+
if (!st) {
|
|
34048
|
+
this.cached = null;
|
|
34049
|
+
return { version: 1 };
|
|
34050
|
+
}
|
|
34051
|
+
if (this.cached && this.cached.mtimeMs === st.mtimeMs && this.cached.size === st.size) {
|
|
34052
|
+
return structuredClone(this.cached.value);
|
|
34053
|
+
}
|
|
34054
|
+
const raw = await this.readText();
|
|
34055
|
+
const { value, salt } = this.parse(raw);
|
|
34056
|
+
this.cached = { mtimeMs: st.mtimeMs, size: st.size, value };
|
|
34057
|
+
this.salt = salt;
|
|
34058
|
+
return structuredClone(value);
|
|
34059
|
+
}
|
|
34060
|
+
// Returns null when the file is missing. Refuses anything that is not a small regular file.
|
|
34061
|
+
async checkFile() {
|
|
34062
|
+
let st;
|
|
34063
|
+
try {
|
|
34064
|
+
st = await lstat3(this.file);
|
|
34065
|
+
} catch (err) {
|
|
34066
|
+
const code = err.code;
|
|
34067
|
+
if (code === "ENOENT") return null;
|
|
34068
|
+
throw new SecretsError(`${this.file} is not readable (${code ?? "unknown error"}).`);
|
|
34069
|
+
}
|
|
34070
|
+
if (!st.isFile()) throw new SecretsError(`${this.file} is not readable (not a regular file).`);
|
|
34071
|
+
if (st.size > MAX_SECRETS_FILE)
|
|
34072
|
+
throw new SecretsError(`${this.file} is not readable (too large).`);
|
|
34073
|
+
return st;
|
|
34074
|
+
}
|
|
34075
|
+
async readText() {
|
|
34076
|
+
const read2 = await readRegularText(this.file, MAX_SECRETS_FILE);
|
|
34077
|
+
if (read2.kind === "ok") return read2.text;
|
|
34078
|
+
const reason2 = read2.kind === "missing" ? "not readable (ENOENT)" : read2.reason;
|
|
34079
|
+
throw new SecretsError(
|
|
34080
|
+
reason2.startsWith("not readable") ? `${this.file} is ${reason2}.` : `${this.file} is not readable (${reason2}).`
|
|
34081
|
+
);
|
|
34082
|
+
}
|
|
34083
|
+
parse(raw) {
|
|
34084
|
+
let json3;
|
|
34085
|
+
try {
|
|
34086
|
+
json3 = JSON.parse(raw);
|
|
34087
|
+
} catch {
|
|
34088
|
+
throw new SecretsError(`${this.file} is not readable (not JSON).`);
|
|
34089
|
+
}
|
|
34090
|
+
let salt;
|
|
34091
|
+
if (!this.cipher && json3?.format === "ed-mcp-secrets") {
|
|
34092
|
+
throw new SecretsError(`${this.file} is not readable (encrypted; set ED_MCP_SYNC_PASSWORD).`);
|
|
34093
|
+
}
|
|
34094
|
+
if (this.cipher) {
|
|
34095
|
+
try {
|
|
34096
|
+
const opened = this.cipher.open(json3);
|
|
34097
|
+
salt = opened.salt;
|
|
34098
|
+
json3 = JSON.parse(opened.plaintext);
|
|
34099
|
+
} catch (err) {
|
|
34100
|
+
if (err instanceof SecretsError && err.message === "wrong password") {
|
|
34101
|
+
throw new WrongPasswordError(`Can't decrypt ${this.file}: wrong ED_MCP_SYNC_PASSWORD.`);
|
|
34102
|
+
}
|
|
34103
|
+
const reason2 = err instanceof SecretsError ? err.message : "not JSON";
|
|
34104
|
+
throw new SecretsError(`${this.file} is not readable (${reason2}).`);
|
|
34105
|
+
}
|
|
34106
|
+
}
|
|
34107
|
+
const parsed = SecretsSchema.safeParse(json3);
|
|
34108
|
+
if (!parsed.success) {
|
|
34109
|
+
const issue2 = parsed.error.issues[0];
|
|
34110
|
+
const where = issue2?.path.length ? `${issue2.path.join(".")}: ` : "";
|
|
34111
|
+
throw new SecretsError(
|
|
34112
|
+
`${this.file} is not readable (${where}${issue2?.message ?? "invalid"}).`
|
|
34113
|
+
);
|
|
34114
|
+
}
|
|
34115
|
+
return { value: parsed.data, salt };
|
|
34116
|
+
}
|
|
34117
|
+
async write(fn) {
|
|
34118
|
+
await this.opts.guard?.();
|
|
34119
|
+
return lockedWrite(
|
|
34120
|
+
this.file,
|
|
34121
|
+
{
|
|
34122
|
+
timeoutMs: this.opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS3,
|
|
34123
|
+
lockedMessage: "Secrets file is locked by another process; try again",
|
|
34124
|
+
lockDir: this.opts.lockDir,
|
|
34125
|
+
mode: 384
|
|
34126
|
+
},
|
|
34127
|
+
async () => {
|
|
34128
|
+
this.cached = null;
|
|
34129
|
+
const next = await this.read();
|
|
34130
|
+
fn(next);
|
|
34131
|
+
const result = SecretsSchema.safeParse(next);
|
|
34132
|
+
if (!result.success) {
|
|
34133
|
+
const issue2 = result.error.issues[0]?.message ?? "invalid";
|
|
34134
|
+
throw new SecretsError(`${this.file} would be invalid (${issue2}).`);
|
|
34135
|
+
}
|
|
34136
|
+
const checked = result.data;
|
|
34137
|
+
const body = JSON.stringify(checked, null, 2);
|
|
34138
|
+
const content = this.cipher ? JSON.stringify(this.cipher.seal(body, this.salt), null, 2) : body;
|
|
34139
|
+
return { content, result: checked };
|
|
34140
|
+
}
|
|
34141
|
+
).finally(() => {
|
|
34142
|
+
this.cached = null;
|
|
34143
|
+
});
|
|
34144
|
+
}
|
|
34145
|
+
};
|
|
34146
|
+
|
|
34147
|
+
// src/settings/sync.ts
|
|
34148
|
+
import { constants as constants2 } from "fs";
|
|
34149
|
+
import { access, readdir as readdir2, stat as stat6 } from "fs/promises";
|
|
34150
|
+
import path11 from "path";
|
|
34151
|
+
var escape2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34152
|
+
function conflictPattern(file2) {
|
|
34153
|
+
const ext = path11.extname(file2);
|
|
34154
|
+
const base2 = escape2(path11.basename(file2, ext));
|
|
34155
|
+
const e = escape2(ext);
|
|
34156
|
+
return new RegExp(
|
|
34157
|
+
[
|
|
34158
|
+
`^${base2} \\(\\d+\\)${e}$`,
|
|
34159
|
+
// Google Drive
|
|
34160
|
+
`^${base2} \\d+${e}$`,
|
|
34161
|
+
// iCloud
|
|
34162
|
+
`^${base2}-[^.\\s]+${e}$`,
|
|
34163
|
+
// OneDrive: name-COMPUTERNAME
|
|
34164
|
+
`^${base2}\\.sync-conflict-\\d{8}-\\d{6}-[A-Z0-9]{7}${e}$`
|
|
34165
|
+
// Syncthing
|
|
34166
|
+
].join("|")
|
|
34167
|
+
);
|
|
34168
|
+
}
|
|
34169
|
+
var SyncFolder = class {
|
|
34170
|
+
dir;
|
|
34171
|
+
constructor(dir) {
|
|
34172
|
+
this.dir = path11.resolve(dir);
|
|
34173
|
+
}
|
|
34174
|
+
contains(file2) {
|
|
34175
|
+
const rel = path11.relative(this.dir, path11.resolve(file2));
|
|
34176
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${path11.sep}`) && !path11.isAbsolute(rel);
|
|
34177
|
+
}
|
|
34178
|
+
// No fallback to the local folder: writing there while the sync app is down would split
|
|
34179
|
+
// the plans into two copies that never merge.
|
|
34180
|
+
async check() {
|
|
34181
|
+
const fail2 = (reason2) => new UserError(
|
|
34182
|
+
`Sync folder ${this.dir} is not available: ${reason2}. Start your sync app, or fix ED_MCP_SYNC_DIR.`
|
|
34183
|
+
);
|
|
34184
|
+
let st;
|
|
34185
|
+
try {
|
|
34186
|
+
st = await stat6(this.dir);
|
|
34187
|
+
} catch (err) {
|
|
34188
|
+
const code = err.code;
|
|
34189
|
+
throw fail2(code === "ENOENT" ? "it does not exist" : String(code ?? err));
|
|
34190
|
+
}
|
|
34191
|
+
if (!st.isDirectory()) throw fail2("it is not a folder");
|
|
34192
|
+
await access(this.dir, constants2.W_OK).catch(() => {
|
|
34193
|
+
throw fail2("it is not writable");
|
|
34194
|
+
});
|
|
34195
|
+
}
|
|
34196
|
+
async conflicts(file2) {
|
|
34197
|
+
const names = await readdir2(path11.dirname(file2)).catch(() => []);
|
|
34198
|
+
const re = conflictPattern(file2);
|
|
34199
|
+
const own2 = path11.basename(file2);
|
|
34200
|
+
return names.filter((n) => n !== own2 && re.test(n));
|
|
34201
|
+
}
|
|
34202
|
+
async warnings(files) {
|
|
34203
|
+
const out = [];
|
|
34204
|
+
for (const f of files.filter((f2) => this.contains(f2))) {
|
|
34205
|
+
for (const copy of await this.conflicts(f)) {
|
|
34206
|
+
out.push(
|
|
34207
|
+
`Sync conflict: ${copy} exists next to ${path11.basename(f)}. Compare them and delete the copy you don't want.`
|
|
34208
|
+
);
|
|
34209
|
+
}
|
|
34210
|
+
}
|
|
34211
|
+
return out;
|
|
34212
|
+
}
|
|
34213
|
+
};
|
|
34214
|
+
|
|
34215
|
+
// src/settings/settings.ts
|
|
34216
|
+
var Settings = class {
|
|
34217
|
+
constructor(env2, inaraApp, secrets) {
|
|
34218
|
+
this.env = env2;
|
|
34219
|
+
this.inaraApp = inaraApp;
|
|
34220
|
+
this.secrets = secrets;
|
|
34221
|
+
}
|
|
34222
|
+
env;
|
|
34223
|
+
inaraApp;
|
|
34224
|
+
secrets;
|
|
34225
|
+
seeded = null;
|
|
34226
|
+
async inara() {
|
|
34227
|
+
const k = await this.key("inara");
|
|
34228
|
+
return k ? { ...k, ...this.inaraApp } : void 0;
|
|
34229
|
+
}
|
|
34230
|
+
async edsm() {
|
|
34231
|
+
return this.key("edsm");
|
|
34232
|
+
}
|
|
34233
|
+
async sources() {
|
|
34234
|
+
let stored = {};
|
|
34235
|
+
try {
|
|
34236
|
+
await this.seed();
|
|
34237
|
+
stored = await this.secrets.get();
|
|
34238
|
+
} catch (err) {
|
|
34239
|
+
if (!(err instanceof UserError)) throw err;
|
|
34240
|
+
}
|
|
34241
|
+
const info = (s) => {
|
|
34242
|
+
const env2 = this.env[s];
|
|
34243
|
+
const saved = stored[s];
|
|
34244
|
+
if (env2) {
|
|
34245
|
+
const differs = !!saved && (saved.apiKey !== env2.apiKey || saved.cmdr !== env2.cmdr);
|
|
34246
|
+
return { source: "environment", cmdr: env2.cmdr, differs_from_stored: differs };
|
|
34247
|
+
}
|
|
34248
|
+
if (saved) return { source: "stored", cmdr: saved.cmdr, differs_from_stored: false };
|
|
34249
|
+
return { source: "not set", cmdr: null, differs_from_stored: false };
|
|
34250
|
+
};
|
|
34251
|
+
return { inara: info("inara"), edsm: info("edsm") };
|
|
34252
|
+
}
|
|
34253
|
+
// The environment wins on this computer, so the store is read only when it has no value.
|
|
34254
|
+
async key(s) {
|
|
34255
|
+
await this.seed().catch(() => void 0);
|
|
34256
|
+
if (this.env[s]) return this.env[s];
|
|
34257
|
+
return (await this.secrets.get())[s];
|
|
34258
|
+
}
|
|
34259
|
+
// Copies a key set in the environment into an empty slot once, so other computers that
|
|
34260
|
+
// share the sync folder get it. A stored value is never overwritten from the environment.
|
|
34261
|
+
seed() {
|
|
34262
|
+
this.seeded ??= (async () => {
|
|
34263
|
+
const current = await this.secrets.get();
|
|
34264
|
+
const missing = ["inara", "edsm"].filter((s) => this.env[s] && !current[s]);
|
|
34265
|
+
if (!missing.length) return;
|
|
34266
|
+
await this.secrets.update((d) => {
|
|
34267
|
+
for (const s of missing) d[s] ??= this.env[s];
|
|
34268
|
+
});
|
|
34269
|
+
})().catch((err) => {
|
|
34270
|
+
this.seeded = null;
|
|
34271
|
+
throw err;
|
|
34272
|
+
});
|
|
34273
|
+
return this.seeded;
|
|
34274
|
+
}
|
|
34275
|
+
};
|
|
34276
|
+
|
|
34277
|
+
// src/settings/page.ts
|
|
34278
|
+
import { randomBytes as randomBytes4, timingSafeEqual } from "crypto";
|
|
34279
|
+
import { createServer as createServer2 } from "http";
|
|
34280
|
+
var TIMEOUT_MS = 10 * 6e4;
|
|
34281
|
+
var MAX_BODY = 16 * 1024;
|
|
34282
|
+
var BodyTooLarge = class extends Error {
|
|
34283
|
+
};
|
|
34284
|
+
var esc2 = (v) => v.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
34285
|
+
var mask = (k) => !k ? "not set" : k.length <= 8 ? "set" : `\u2026${esc2(k.slice(-4))}`;
|
|
34286
|
+
var page = (body) => `<!doctype html><meta charset="utf-8"><title>ed-mcp settings</title><style>body{font:15px system-ui;max-width:640px;margin:2em auto}label{display:block;margin:.6em 0}input[type=text],input[type=password]{width:100%}fieldset{margin:1em 0}</style>${body}`;
|
|
34287
|
+
var SettingsPage = class {
|
|
34288
|
+
constructor(deps, opts = {}) {
|
|
34289
|
+
this.deps = deps;
|
|
34290
|
+
this.inSyncFolder = opts.inSyncFolder ?? false;
|
|
34291
|
+
this.openUrl = opts.openUrl ?? openInBrowser;
|
|
34292
|
+
this.timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
34293
|
+
}
|
|
34294
|
+
deps;
|
|
34295
|
+
server = null;
|
|
34296
|
+
closed = Promise.resolve();
|
|
34297
|
+
opening = Promise.resolve();
|
|
34298
|
+
openUrl;
|
|
34299
|
+
timeoutMs;
|
|
34300
|
+
inSyncFolder;
|
|
34301
|
+
isOpen() {
|
|
34302
|
+
return this.server !== null;
|
|
34303
|
+
}
|
|
34304
|
+
waitForClose() {
|
|
34305
|
+
return this.closed;
|
|
34306
|
+
}
|
|
34307
|
+
// Calls run one after another, so two overlapping calls cannot leave a server nobody tracks.
|
|
34308
|
+
open() {
|
|
34309
|
+
const run = this.opening.then(() => this.start());
|
|
34310
|
+
this.opening = run.catch(() => void 0);
|
|
34311
|
+
return run;
|
|
34312
|
+
}
|
|
34313
|
+
async start() {
|
|
34314
|
+
this.close();
|
|
34315
|
+
const key = randomBytes4(24).toString("hex");
|
|
34316
|
+
let done;
|
|
34317
|
+
this.closed = new Promise((r) => done = r);
|
|
34318
|
+
const server = createServer2((req, res) => {
|
|
34319
|
+
this.handle(req, res, key, server).catch((err) => {
|
|
34320
|
+
if (!(err instanceof UserError)) console.error(err instanceof Error ? err.message : err);
|
|
34321
|
+
const text = err instanceof UserError ? esc2(err.message) : "Something went wrong.";
|
|
34322
|
+
if (!res.headersSent) res.writeHead(500).end(page(`<p>${text}</p>`));
|
|
34323
|
+
});
|
|
34324
|
+
});
|
|
34325
|
+
server.on("close", done);
|
|
34326
|
+
server.unref();
|
|
34327
|
+
await new Promise((resolve, reject) => {
|
|
34328
|
+
server.once("error", reject);
|
|
34329
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
34330
|
+
});
|
|
34331
|
+
this.server = server;
|
|
34332
|
+
const timer = setTimeout(() => this.stop(server), this.timeoutMs);
|
|
34333
|
+
timer.unref();
|
|
34334
|
+
server.on("close", () => clearTimeout(timer));
|
|
34335
|
+
const url2 = `http://127.0.0.1:${server.address().port}/?key=${key}`;
|
|
34336
|
+
const opened = await this.openUrl(url2).then(
|
|
34337
|
+
() => true,
|
|
34338
|
+
() => false
|
|
34339
|
+
);
|
|
34340
|
+
return { url: url2, expires_at: new Date(Date.now() + this.timeoutMs).toISOString(), opened };
|
|
34341
|
+
}
|
|
34342
|
+
close() {
|
|
34343
|
+
if (this.server) this.stop(this.server);
|
|
34344
|
+
}
|
|
34345
|
+
stop(server) {
|
|
34346
|
+
if (this.server === server) this.server = null;
|
|
34347
|
+
server.close();
|
|
34348
|
+
server.closeAllConnections();
|
|
34349
|
+
}
|
|
34350
|
+
async handle(req, res, key, server) {
|
|
34351
|
+
const url2 = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
34352
|
+
const html = (status, body) => res.writeHead(status, {
|
|
34353
|
+
"content-type": "text/html; charset=utf-8",
|
|
34354
|
+
"cache-control": "no-store",
|
|
34355
|
+
"referrer-policy": "no-referrer"
|
|
34356
|
+
}).end(page(body));
|
|
34357
|
+
const denied = () => html(403, "<p>Use the link from Claude.</p>");
|
|
34358
|
+
if (req.method === "GET" && url2.pathname === "/") {
|
|
34359
|
+
if (!sameKey(url2.searchParams.get("key"), key)) return void denied();
|
|
34360
|
+
return void html(200, await this.form(key));
|
|
34361
|
+
}
|
|
34362
|
+
if (req.method !== "POST") return void res.writeHead(404).end();
|
|
34363
|
+
let form;
|
|
34364
|
+
try {
|
|
34365
|
+
form = new URLSearchParams(await readBody(req));
|
|
34366
|
+
} catch (err) {
|
|
34367
|
+
if (!(err instanceof BodyTooLarge)) throw err;
|
|
34368
|
+
return void res.writeHead(413, { connection: "close" }).end();
|
|
34369
|
+
}
|
|
34370
|
+
if (!sameKey(form.get("key"), key)) return void denied();
|
|
34371
|
+
try {
|
|
34372
|
+
if (url2.pathname === "/save") {
|
|
34373
|
+
await this.deps.secrets.update((d) => {
|
|
34374
|
+
for (const s of ["inara", "edsm"]) {
|
|
34375
|
+
if (form.get(`clear_${s}`) === "on") {
|
|
34376
|
+
delete d[s];
|
|
34377
|
+
continue;
|
|
34378
|
+
}
|
|
34379
|
+
const next = entry(form, s, d[s]);
|
|
34380
|
+
if (next) d[s] = next;
|
|
34381
|
+
}
|
|
34382
|
+
});
|
|
34383
|
+
} else if (url2.pathname === "/reencrypt") {
|
|
34384
|
+
await this.deps.secrets.reencrypt(form.get("old_password") ?? "");
|
|
34385
|
+
} else if (url2.pathname === "/reset") {
|
|
34386
|
+
await this.deps.secrets.reset();
|
|
34387
|
+
} else {
|
|
34388
|
+
return void res.writeHead(404).end();
|
|
34389
|
+
}
|
|
34390
|
+
} catch (err) {
|
|
34391
|
+
if (!(err instanceof UserError)) throw err;
|
|
34392
|
+
const text = err instanceof WrongPasswordError && url2.pathname === "/reencrypt" ? "The old password is wrong." : err.message;
|
|
34393
|
+
return void html(400, `<p>${esc2(text)}</p><p>Go back and try again.</p>`);
|
|
34394
|
+
}
|
|
34395
|
+
res.once("finish", () => this.stop(server));
|
|
34396
|
+
html(200, "<p>Saved. You can close this tab and go back to Claude.</p>");
|
|
34397
|
+
}
|
|
34398
|
+
async form(key) {
|
|
34399
|
+
const k = `<input type="hidden" name="key" value="${key}">`;
|
|
34400
|
+
const shared = this.inSyncFolder ? "<p><strong>This changes the secrets for every computer that shares the sync folder.</strong></p>" : "";
|
|
34401
|
+
let stored;
|
|
34402
|
+
try {
|
|
34403
|
+
stored = await this.deps.secrets.get();
|
|
34404
|
+
} catch (err) {
|
|
34405
|
+
if (!(err instanceof SecretsError)) throw err;
|
|
34406
|
+
return `<h1>ed-mcp settings</h1><p>${esc2(err.message)}</p><form method="post" action="/reencrypt">${k}<fieldset><legend>Re-encrypt with the current password</legend>${shared}<label>Old password <input type="password" name="old_password" autocomplete="off"></label><button>Re-encrypt</button></fieldset></form><form method="post" action="/reset">${k}<fieldset><legend>Start fresh</legend>${shared}<p>Removes the saved keys and the Frontier login. Plans and preferences are not affected.</p><button>Start fresh</button></fieldset></form>`;
|
|
34407
|
+
}
|
|
34408
|
+
const sources = await this.deps.settings.sources();
|
|
34409
|
+
const help = {
|
|
34410
|
+
inara: `Get your key: log in at <a href="https://inara.cz/elite/inara-api/" target="_blank" rel="noopener noreferrer">inara.cz</a>, click the cog next to "Personal content", open API settings and choose "Generate new key". The commander name is your in-game name.`,
|
|
34411
|
+
edsm: `Get your key: log in at <a href="https://www.edsm.net/" target="_blank" rel="noopener noreferrer">edsm.net</a>, open the user menu and choose "My API Key". Use your EDSM commander name, which may differ from your in-game name.`
|
|
34412
|
+
};
|
|
34413
|
+
const section = (s, title, info) => `<fieldset><legend>${title}</legend><p><small>${help[s]}</small></p><p>In use: ${info.source === "environment" ? "from the environment (it wins on this computer)" : info.source}. Saved key: ${mask(stored[s]?.apiKey)}</p><label>${title} API key <input type="password" name="${s}_key" autocomplete="off" placeholder="leave empty to keep"></label><label>Commander name <input type="text" name="${s}_cmdr" value="${esc2(stored[s]?.cmdr ?? "")}"></label><label><input type="checkbox" name="clear_${s}"> Forget the saved ${title} key</label></fieldset>`;
|
|
34414
|
+
return `<h1>ed-mcp settings</h1><form method="post" action="/save">${k}${shared}` + section("inara", "Inara", sources.inara) + section("edsm", "EDSM", sources.edsm) + `<button>Save</button></form>`;
|
|
34415
|
+
}
|
|
34416
|
+
};
|
|
34417
|
+
function sameKey(given, key) {
|
|
34418
|
+
const a = Buffer.from(given ?? "");
|
|
34419
|
+
const b = Buffer.from(key);
|
|
34420
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
34421
|
+
}
|
|
34422
|
+
function entry(form, s, current) {
|
|
34423
|
+
const apiKey = form.get(`${s}_key`)?.trim() || current?.apiKey;
|
|
34424
|
+
const cmdr = form.get(`${s}_cmdr`)?.trim() || current?.cmdr;
|
|
34425
|
+
if (!!apiKey !== !!cmdr) {
|
|
34426
|
+
const name = s === "inara" ? "Inara" : "EDSM";
|
|
34427
|
+
throw new UserError(`Give both the API key and the commander name for ${name}.`);
|
|
34428
|
+
}
|
|
34429
|
+
return apiKey && cmdr ? { apiKey, cmdr } : void 0;
|
|
34430
|
+
}
|
|
34431
|
+
function readBody(req) {
|
|
34432
|
+
return new Promise((resolve, reject) => {
|
|
34433
|
+
let size = 0;
|
|
34434
|
+
let failed = false;
|
|
34435
|
+
const chunks = [];
|
|
34436
|
+
req.on("data", (c) => {
|
|
34437
|
+
if (failed) return;
|
|
34438
|
+
size += c.length;
|
|
34439
|
+
if (size > MAX_BODY) {
|
|
34440
|
+
failed = true;
|
|
34441
|
+
reject(new BodyTooLarge());
|
|
34442
|
+
} else chunks.push(c);
|
|
34443
|
+
});
|
|
34444
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
34445
|
+
req.on("error", reject);
|
|
34446
|
+
});
|
|
34447
|
+
}
|
|
34448
|
+
|
|
34449
|
+
// src/version/check.ts
|
|
34450
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
34451
|
+
import path12 from "path";
|
|
34452
|
+
var RELEASE_URL = "https://api.github.com/repos/pilotso11/ed-mcp/releases/latest";
|
|
34453
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
34454
|
+
var RETRY_MS = 60 * 60 * 1e3;
|
|
34455
|
+
var RELEASE_PAGE = "https://github.com/pilotso11/ed-mcp/releases/tag";
|
|
34456
|
+
var STRICT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
34457
|
+
var releaseSchema = external_exports.object({
|
|
34458
|
+
tag_name: external_exports.string().transform((t) => t.replace(/^v/, "")).pipe(external_exports.string().regex(STRICT_VERSION))
|
|
34459
|
+
});
|
|
34460
|
+
var cacheSchema = external_exports.object({
|
|
34461
|
+
checked_at: external_exports.number(),
|
|
34462
|
+
latest: external_exports.string().regex(STRICT_VERSION)
|
|
34463
|
+
});
|
|
34464
|
+
function parse3(version2) {
|
|
34465
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(version2.trim());
|
|
34466
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
34467
|
+
}
|
|
34468
|
+
function isNewer(latest, current) {
|
|
34469
|
+
const a = parse3(latest);
|
|
34470
|
+
const b = parse3(current);
|
|
34471
|
+
if (!a || !b) return false;
|
|
34472
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
34473
|
+
return false;
|
|
34474
|
+
}
|
|
34475
|
+
var VersionCheck = class {
|
|
34476
|
+
constructor(opts) {
|
|
34477
|
+
this.opts = opts;
|
|
34478
|
+
this.file = path12.join(opts.cacheDir, "version-check.json");
|
|
34479
|
+
this.now = opts.now ?? Date.now;
|
|
34480
|
+
}
|
|
34481
|
+
opts;
|
|
34482
|
+
file;
|
|
34483
|
+
now;
|
|
34484
|
+
running = Promise.resolve();
|
|
34485
|
+
inFlight = false;
|
|
34486
|
+
stopped = false;
|
|
34487
|
+
timer;
|
|
34488
|
+
/**
|
|
34489
|
+
* Checks in the background, then again every 24 hours (every hour after a failure), so a
|
|
34490
|
+
* server that runs for days still learns of a new release. It never throws.
|
|
34491
|
+
*/
|
|
34492
|
+
start() {
|
|
34493
|
+
this.stopped = false;
|
|
34494
|
+
if (this.timer || this.inFlight) return;
|
|
34495
|
+
this.run();
|
|
34496
|
+
}
|
|
34497
|
+
/** Cancels the next scheduled check. */
|
|
34498
|
+
stop() {
|
|
34499
|
+
this.stopped = true;
|
|
34500
|
+
clearTimeout(this.timer);
|
|
34501
|
+
this.timer = void 0;
|
|
34502
|
+
}
|
|
34503
|
+
/** Resolves when the check in progress is done. */
|
|
34504
|
+
whenIdle() {
|
|
34505
|
+
return this.running;
|
|
34506
|
+
}
|
|
34507
|
+
/** Reads the saved result only. It never makes a request or writes. */
|
|
34508
|
+
async notice() {
|
|
34509
|
+
try {
|
|
34510
|
+
const cached2 = await this.read();
|
|
34511
|
+
if (!cached2 || !isNewer(cached2.latest, this.opts.current)) return null;
|
|
34512
|
+
return `ed-mcp ${cached2.latest} is available (you have ${this.opts.current}): ${RELEASE_PAGE}/v${cached2.latest}`;
|
|
34513
|
+
} catch {
|
|
34514
|
+
return null;
|
|
34515
|
+
}
|
|
34516
|
+
}
|
|
34517
|
+
run() {
|
|
34518
|
+
clearTimeout(this.timer);
|
|
34519
|
+
this.timer = void 0;
|
|
34520
|
+
if (this.inFlight) return;
|
|
34521
|
+
this.inFlight = true;
|
|
34522
|
+
this.running = this.check().then(
|
|
34523
|
+
() => true,
|
|
34524
|
+
(err) => {
|
|
34525
|
+
if (!(err instanceof UserError || err instanceof external_exports.ZodError || err instanceof SyntaxError)) {
|
|
34526
|
+
console.error(
|
|
34527
|
+
`ed-mcp version check failed: ${err instanceof Error ? err.message : err}`
|
|
34528
|
+
);
|
|
34529
|
+
}
|
|
34530
|
+
return false;
|
|
34531
|
+
}
|
|
34532
|
+
).then((ok2) => {
|
|
34533
|
+
this.inFlight = false;
|
|
34534
|
+
if (this.stopped) return;
|
|
34535
|
+
clearTimeout(this.timer);
|
|
34536
|
+
this.timer = setTimeout(
|
|
34537
|
+
() => {
|
|
34538
|
+
this.timer = void 0;
|
|
34539
|
+
this.run();
|
|
34540
|
+
},
|
|
34541
|
+
ok2 ? DAY_MS : RETRY_MS
|
|
34542
|
+
);
|
|
34543
|
+
this.timer.unref();
|
|
34544
|
+
});
|
|
34545
|
+
}
|
|
34546
|
+
async read() {
|
|
34547
|
+
try {
|
|
34548
|
+
return cacheSchema.parse(JSON.parse(await readFile6(this.file, "utf8")));
|
|
34549
|
+
} catch {
|
|
34550
|
+
return null;
|
|
34551
|
+
}
|
|
34552
|
+
}
|
|
34553
|
+
// A clock set back would keep a stale entry fresh for too long, so a future time counts as old.
|
|
34554
|
+
isStale(cached2) {
|
|
34555
|
+
if (!cached2) return true;
|
|
34556
|
+
const age = this.now() - cached2.checked_at;
|
|
34557
|
+
return age < 0 || age >= DAY_MS;
|
|
34558
|
+
}
|
|
34559
|
+
async check() {
|
|
34560
|
+
if (!this.isStale(await this.read())) return;
|
|
34561
|
+
const release = releaseSchema.parse(await this.opts.http.json(RELEASE_URL));
|
|
34562
|
+
const entry2 = {
|
|
34563
|
+
checked_at: this.now(),
|
|
34564
|
+
latest: release.tag_name
|
|
34565
|
+
};
|
|
34566
|
+
await lockedWrite(
|
|
34567
|
+
this.file,
|
|
34568
|
+
{ timeoutMs: 2e3, lockedMessage: "version-check.json is locked" },
|
|
34569
|
+
async () => ({ content: JSON.stringify(entry2), result: void 0 })
|
|
34570
|
+
);
|
|
34571
|
+
}
|
|
34572
|
+
};
|
|
34573
|
+
|
|
33581
34574
|
// src/deps.ts
|
|
34575
|
+
var envKey = (c) => c ? { apiKey: c.apiKey, cmdr: c.cmdr } : void 0;
|
|
33582
34576
|
function buildDeps(config2, opts = {}) {
|
|
33583
34577
|
const caches = new CacheRegistry();
|
|
33584
34578
|
const http = new HttpClient({ userAgent: config2.userAgent, fetchFn: opts.fetchFn });
|
|
33585
34579
|
const catalog = new CatalogService(http, config2.cacheDir);
|
|
33586
34580
|
const ships2 = new ShipDataService({ cacheDir: config2.cacheDir, http });
|
|
34581
|
+
const sync = config2.syncDir ? new SyncFolder(config2.syncDir) : void 0;
|
|
34582
|
+
const guardFor = (file2) => sync?.contains(file2) ? () => sync.check() : void 0;
|
|
34583
|
+
const lockFor = (file2) => sync?.contains(file2) ? config2.locksDir : void 0;
|
|
34584
|
+
const secrets = new SecretStore(config2.secretsFile, {
|
|
34585
|
+
password: config2.syncPassword,
|
|
34586
|
+
lockDir: lockFor(config2.secretsFile),
|
|
34587
|
+
guard: guardFor(config2.secretsFile)
|
|
34588
|
+
});
|
|
34589
|
+
const settings = new Settings(
|
|
34590
|
+
{ inara: envKey(config2.inara), edsm: envKey(config2.edsm) },
|
|
34591
|
+
config2.inaraApp,
|
|
34592
|
+
secrets
|
|
34593
|
+
);
|
|
34594
|
+
const plans = new PlanStore(config2.plansFile, void 0, {
|
|
34595
|
+
lockDir: lockFor(config2.plansFile),
|
|
34596
|
+
guard: guardFor(config2.plansFile),
|
|
34597
|
+
markdownDir: config2.plansMarkdownDir
|
|
34598
|
+
});
|
|
34599
|
+
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;
|
|
34600
|
+
const syncWarnings = sync ? () => sync.warnings([config2.plansFile, config2.prefsFile, config2.secretsFile]) : void 0;
|
|
33587
34601
|
const inaraCache = new TtlCache("inara", 6e4);
|
|
33588
34602
|
caches.register(inaraCache);
|
|
33589
|
-
const inara = new InaraClient(http,
|
|
34603
|
+
const inara = new InaraClient(http, () => settings.inara(), inaraCache);
|
|
33590
34604
|
const edsmCache = new TtlCache("edsm", 6e4);
|
|
33591
34605
|
caches.register(edsmCache);
|
|
33592
|
-
const edsm = new EdsmClient(http,
|
|
34606
|
+
const edsm = new EdsmClient(http, () => settings.edsm(), edsmCache);
|
|
33593
34607
|
const spanshCache = new TtlCache("spansh", 3e5);
|
|
33594
34608
|
caches.register(spanshCache);
|
|
33595
34609
|
const spanshSystemCache = new TtlCache("spansh-systems", 864e5);
|
|
@@ -33603,12 +34617,9 @@ function buildDeps(config2, opts = {}) {
|
|
|
33603
34617
|
const capiHttp = new HttpClient({ userAgent: `EDCD-EdMcp-${VERSION}`, fetchFn: opts.fetchFn });
|
|
33604
34618
|
const capiCache = new TtlCache("capi", 6e4);
|
|
33605
34619
|
caches.register(capiCache);
|
|
33606
|
-
const capiAuth = new CapiAuth(
|
|
33607
|
-
|
|
33608
|
-
|
|
33609
|
-
new TokenStore(path10.join(config2.configDir, "capi-token.json")),
|
|
33610
|
-
{ onSessionChange: () => void capiCache.clear() }
|
|
33611
|
-
);
|
|
34620
|
+
const capiAuth = new CapiAuth(capiHttp, config2.capi, new TokenStore(secrets), {
|
|
34621
|
+
onSessionChange: () => void capiCache.clear()
|
|
34622
|
+
});
|
|
33612
34623
|
const capi = new CapiClient(capiHttp, capiAuth, capiCache);
|
|
33613
34624
|
const journal = opts.journal ?? new JournalService(config2.journalDir);
|
|
33614
34625
|
return {
|
|
@@ -33628,8 +34639,30 @@ function buildDeps(config2, opts = {}) {
|
|
|
33628
34639
|
},
|
|
33629
34640
|
jumpRange: async () => (await journal.freshShip())?.max_jump_range ?? null,
|
|
33630
34641
|
journal,
|
|
33631
|
-
|
|
33632
|
-
|
|
34642
|
+
settings,
|
|
34643
|
+
secrets,
|
|
34644
|
+
settingsPage: new SettingsPage(
|
|
34645
|
+
{ secrets, settings },
|
|
34646
|
+
{ inSyncFolder: !!sync?.contains(config2.secretsFile) }
|
|
34647
|
+
),
|
|
34648
|
+
plans,
|
|
34649
|
+
planWarnings: async () => {
|
|
34650
|
+
const problem = await plans.markdownProblem();
|
|
34651
|
+
return [
|
|
34652
|
+
...await syncWarnings?.() ?? [],
|
|
34653
|
+
...problem ? [problem] : [],
|
|
34654
|
+
...legacyPlans ? [legacyPlans] : []
|
|
34655
|
+
];
|
|
34656
|
+
},
|
|
34657
|
+
prefs: new PreferencesStore(config2.prefsFile, {
|
|
34658
|
+
lockDir: lockFor(config2.prefsFile),
|
|
34659
|
+
guard: guardFor(config2.prefsFile)
|
|
34660
|
+
}),
|
|
34661
|
+
sync,
|
|
34662
|
+
syncWarnings,
|
|
34663
|
+
versionCheck: new VersionCheck({ http, cacheDir: config2.cacheDir, current: VERSION }),
|
|
34664
|
+
notes: legacyPlans ? [legacyPlans] : [],
|
|
34665
|
+
localDir: config2.configDir
|
|
33633
34666
|
};
|
|
33634
34667
|
}
|
|
33635
34668
|
|
|
@@ -34007,8 +35040,8 @@ function getErrorMap2() {
|
|
|
34007
35040
|
|
|
34008
35041
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
34009
35042
|
var makeIssue = (params) => {
|
|
34010
|
-
const { data, path:
|
|
34011
|
-
const fullPath = [...
|
|
35043
|
+
const { data, path: path15, errorMaps, issueData } = params;
|
|
35044
|
+
const fullPath = [...path15, ...issueData.path || []];
|
|
34012
35045
|
const fullIssue = {
|
|
34013
35046
|
...issueData,
|
|
34014
35047
|
path: fullPath
|
|
@@ -34123,11 +35156,11 @@ var errorUtil;
|
|
|
34123
35156
|
|
|
34124
35157
|
// node_modules/zod/v3/types.js
|
|
34125
35158
|
var ParseInputLazyPath = class {
|
|
34126
|
-
constructor(parent, value,
|
|
35159
|
+
constructor(parent, value, path15, key) {
|
|
34127
35160
|
this._cachedPath = [];
|
|
34128
35161
|
this.parent = parent;
|
|
34129
35162
|
this.data = value;
|
|
34130
|
-
this._path =
|
|
35163
|
+
this._path = path15;
|
|
34131
35164
|
this._key = key;
|
|
34132
35165
|
}
|
|
34133
35166
|
get path() {
|
|
@@ -36139,10 +37172,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36139
37172
|
catchall: index
|
|
36140
37173
|
});
|
|
36141
37174
|
}
|
|
36142
|
-
pick(
|
|
37175
|
+
pick(mask2) {
|
|
36143
37176
|
const shape = {};
|
|
36144
|
-
for (const key of util.objectKeys(
|
|
36145
|
-
if (
|
|
37177
|
+
for (const key of util.objectKeys(mask2)) {
|
|
37178
|
+
if (mask2[key] && this.shape[key]) {
|
|
36146
37179
|
shape[key] = this.shape[key];
|
|
36147
37180
|
}
|
|
36148
37181
|
}
|
|
@@ -36151,10 +37184,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36151
37184
|
shape: () => shape
|
|
36152
37185
|
});
|
|
36153
37186
|
}
|
|
36154
|
-
omit(
|
|
37187
|
+
omit(mask2) {
|
|
36155
37188
|
const shape = {};
|
|
36156
37189
|
for (const key of util.objectKeys(this.shape)) {
|
|
36157
|
-
if (!
|
|
37190
|
+
if (!mask2[key]) {
|
|
36158
37191
|
shape[key] = this.shape[key];
|
|
36159
37192
|
}
|
|
36160
37193
|
}
|
|
@@ -36169,11 +37202,11 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36169
37202
|
deepPartial() {
|
|
36170
37203
|
return deepPartialify(this);
|
|
36171
37204
|
}
|
|
36172
|
-
partial(
|
|
37205
|
+
partial(mask2) {
|
|
36173
37206
|
const newShape = {};
|
|
36174
37207
|
for (const key of util.objectKeys(this.shape)) {
|
|
36175
37208
|
const fieldSchema = this.shape[key];
|
|
36176
|
-
if (
|
|
37209
|
+
if (mask2 && !mask2[key]) {
|
|
36177
37210
|
newShape[key] = fieldSchema;
|
|
36178
37211
|
} else {
|
|
36179
37212
|
newShape[key] = fieldSchema.optional();
|
|
@@ -36184,10 +37217,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36184
37217
|
shape: () => newShape
|
|
36185
37218
|
});
|
|
36186
37219
|
}
|
|
36187
|
-
required(
|
|
37220
|
+
required(mask2) {
|
|
36188
37221
|
const newShape = {};
|
|
36189
37222
|
for (const key of util.objectKeys(this.shape)) {
|
|
36190
|
-
if (
|
|
37223
|
+
if (mask2 && !mask2[key]) {
|
|
36191
37224
|
newShape[key] = this.shape[key];
|
|
36192
37225
|
} else {
|
|
36193
37226
|
const fieldSchema = this.shape[key];
|
|
@@ -37678,11 +38711,11 @@ function normalizeObjectSchema(schema) {
|
|
|
37678
38711
|
}
|
|
37679
38712
|
return void 0;
|
|
37680
38713
|
}
|
|
37681
|
-
function getDotPath(
|
|
37682
|
-
if (
|
|
38714
|
+
function getDotPath(path15) {
|
|
38715
|
+
if (path15.length === 0) {
|
|
37683
38716
|
return "object root";
|
|
37684
38717
|
}
|
|
37685
|
-
return
|
|
38718
|
+
return path15.reduce((acc, seg, index) => {
|
|
37686
38719
|
if (index === 0) {
|
|
37687
38720
|
return String(seg);
|
|
37688
38721
|
}
|
|
@@ -39675,7 +40708,7 @@ var Protocol = class {
|
|
|
39675
40708
|
}
|
|
39676
40709
|
};
|
|
39677
40710
|
}
|
|
39678
|
-
const cancel = (
|
|
40711
|
+
const cancel = (reason2) => {
|
|
39679
40712
|
this._responseHandlers.delete(messageId);
|
|
39680
40713
|
this._progressHandlers.delete(messageId);
|
|
39681
40714
|
this._cleanupTimeout(messageId);
|
|
@@ -39684,10 +40717,10 @@ var Protocol = class {
|
|
|
39684
40717
|
method: "notifications/cancelled",
|
|
39685
40718
|
params: {
|
|
39686
40719
|
requestId: messageId,
|
|
39687
|
-
reason: String(
|
|
40720
|
+
reason: String(reason2)
|
|
39688
40721
|
}
|
|
39689
40722
|
}, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error63) => this._onerror(new Error(`Failed to send cancellation: ${error63}`)));
|
|
39690
|
-
const error62 =
|
|
40723
|
+
const error62 = reason2 instanceof McpError ? reason2 : new McpError(ErrorCode.RequestTimeout, String(reason2));
|
|
39691
40724
|
reject(error62);
|
|
39692
40725
|
};
|
|
39693
40726
|
this._responseHandlers.set(messageId, (response) => {
|
|
@@ -41966,7 +42999,7 @@ function toJournalModifiers(module, modifiers) {
|
|
|
41966
42999
|
return out;
|
|
41967
43000
|
}
|
|
41968
43001
|
function bestRoll(blueprint, grade, module, catalog) {
|
|
41969
|
-
const features = catalog.blueprintFeatures(blueprint, grade);
|
|
43002
|
+
const features = catalog.blueprintFeatures(blueprint, grade, module.grp);
|
|
41970
43003
|
if (!features) {
|
|
41971
43004
|
throw new UserError(`Blueprint ${blueprint} has no grade ${grade}.`);
|
|
41972
43005
|
}
|
|
@@ -42980,6 +44013,27 @@ function wrap(fn) {
|
|
|
42980
44013
|
}
|
|
42981
44014
|
};
|
|
42982
44015
|
}
|
|
44016
|
+
function wrapWarn(fn, warnings) {
|
|
44017
|
+
const inner = wrap(fn);
|
|
44018
|
+
return async (args) => {
|
|
44019
|
+
const result = await inner(args);
|
|
44020
|
+
const list2 = warnings ? await warnings().catch(() => []) : [];
|
|
44021
|
+
if (!list2.length) return result;
|
|
44022
|
+
return {
|
|
44023
|
+
...result,
|
|
44024
|
+
content: [
|
|
44025
|
+
...result.content,
|
|
44026
|
+
{ type: "text", text: `Warnings:
|
|
44027
|
+
${list2.map((w) => `- ${w}`).join("\n")}` }
|
|
44028
|
+
]
|
|
44029
|
+
};
|
|
44030
|
+
};
|
|
44031
|
+
}
|
|
44032
|
+
function reason(err) {
|
|
44033
|
+
if (err instanceof UserError) return err.message;
|
|
44034
|
+
console.error(err);
|
|
44035
|
+
return err instanceof Error ? err.message : String(err);
|
|
44036
|
+
}
|
|
42983
44037
|
|
|
42984
44038
|
// src/tools/build.ts
|
|
42985
44039
|
var SCO_NOTE = "Jump range uses the drive's normal values, not supercruise overcharge.";
|
|
@@ -43006,6 +44060,13 @@ function registerBuildStatsTool(server, deps) {
|
|
|
43006
44060
|
server.registerTool(
|
|
43007
44061
|
"build_stats",
|
|
43008
44062
|
{
|
|
44063
|
+
annotations: {
|
|
44064
|
+
title: "Build Stats",
|
|
44065
|
+
readOnlyHint: true,
|
|
44066
|
+
destructiveHint: false,
|
|
44067
|
+
idempotentHint: true,
|
|
44068
|
+
openWorldHint: true
|
|
44069
|
+
},
|
|
43009
44070
|
description: "Model a ship build: mass, fuel, jump range, power, speed, shields, armour and weapon damage. The build comes from the latest journal Loadout (the default), the CAPI ship, a SLEF build or a plan's target_build. `changes` apply what-if changes in order: swap, engineer or remove modules. With changes, `diff` lists only the values that changed, and `build` is the changed build as SLEF. Save it with plan_upsert (target_build), in a file or in an artifact. Real builds use their real engineering values; engineering from changes assumes the best roll for the blueprint and grade.",
|
|
43010
44071
|
inputSchema: {
|
|
43011
44072
|
build: BuildInput.optional().describe("Where to load the build. Default: the journal."),
|
|
@@ -43144,12 +44205,12 @@ function diff(before, after) {
|
|
|
43144
44205
|
const a = flatten(before, "", /* @__PURE__ */ new Map());
|
|
43145
44206
|
const b = flatten(after, "", /* @__PURE__ */ new Map());
|
|
43146
44207
|
const out = {};
|
|
43147
|
-
for (const
|
|
43148
|
-
const x = a.get(
|
|
43149
|
-
const y = b.get(
|
|
44208
|
+
for (const path15 of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) {
|
|
44209
|
+
const x = a.get(path15) ?? null;
|
|
44210
|
+
const y = b.get(path15) ?? null;
|
|
43150
44211
|
if (x === y) continue;
|
|
43151
44212
|
const change = typeof x === "number" && typeof y === "number" ? Math.round((y - x) * 1e4) / 1e4 : null;
|
|
43152
|
-
out[
|
|
44213
|
+
out[path15] = { before: x, after: y, change };
|
|
43153
44214
|
}
|
|
43154
44215
|
return out;
|
|
43155
44216
|
}
|
|
@@ -43159,6 +44220,13 @@ function registerBuildTools(server, deps) {
|
|
|
43159
44220
|
server.registerTool(
|
|
43160
44221
|
"build_parse_slef",
|
|
43161
44222
|
{
|
|
44223
|
+
annotations: {
|
|
44224
|
+
title: "Parse SLEF Build",
|
|
44225
|
+
readOnlyHint: true,
|
|
44226
|
+
destructiveHint: false,
|
|
44227
|
+
idempotentHint: true,
|
|
44228
|
+
openWorldHint: true
|
|
44229
|
+
},
|
|
43162
44230
|
description: "Parse a SLEF ship build (export from EDSY, Coriolis or Inara) into slots, modules, blueprints, grades and experimental effects. Module names, class and rating are resolved from the game data catalog when available. Accepts SLEF JSON only. An EDSY link (edsy.org/#/L=...) is not SLEF: open it in a browser, click Export..., read the export_slef text input and pass that value, or ask the user to paste the SLEF.",
|
|
43163
44231
|
inputSchema: { slef: external_exports.string().describe("SLEF JSON text") }
|
|
43164
44232
|
},
|
|
@@ -43208,6 +44276,13 @@ function registerCacheTools(server, deps) {
|
|
|
43208
44276
|
server.registerTool(
|
|
43209
44277
|
"cache_refresh",
|
|
43210
44278
|
{
|
|
44279
|
+
annotations: {
|
|
44280
|
+
title: "Refresh Cache",
|
|
44281
|
+
readOnlyHint: false,
|
|
44282
|
+
destructiveHint: false,
|
|
44283
|
+
idempotentHint: true,
|
|
44284
|
+
openWorldHint: true
|
|
44285
|
+
},
|
|
43211
44286
|
description: "Clear cached data so the next call fetches fresh data. source: a source name or 'all' (default). For 'catalog' and 'ships' this also checks upstream for new game data.",
|
|
43212
44287
|
inputSchema: { source: external_exports.string().optional() }
|
|
43213
44288
|
},
|
|
@@ -43243,6 +44318,13 @@ function registerCacheTools(server, deps) {
|
|
|
43243
44318
|
server.registerTool(
|
|
43244
44319
|
"cache_status",
|
|
43245
44320
|
{
|
|
44321
|
+
annotations: {
|
|
44322
|
+
title: "Cache Status",
|
|
44323
|
+
readOnlyHint: true,
|
|
44324
|
+
destructiveHint: false,
|
|
44325
|
+
idempotentHint: true,
|
|
44326
|
+
openWorldHint: false
|
|
44327
|
+
},
|
|
43246
44328
|
description: "Show cached entries and their age per source, and the catalog and ship data versions."
|
|
43247
44329
|
},
|
|
43248
44330
|
wrap(async () => ({
|
|
@@ -43257,6 +44339,13 @@ function registerCapiTools(server, deps) {
|
|
|
43257
44339
|
server.registerTool(
|
|
43258
44340
|
"capi_login",
|
|
43259
44341
|
{
|
|
44342
|
+
annotations: {
|
|
44343
|
+
title: "Log In to Frontier",
|
|
44344
|
+
readOnlyHint: false,
|
|
44345
|
+
destructiveHint: false,
|
|
44346
|
+
idempotentHint: false,
|
|
44347
|
+
openWorldHint: true
|
|
44348
|
+
},
|
|
43260
44349
|
description: "Log in to Frontier's Companion API in the browser. Returns immediately with the login URL; the user finishes in the browser. Call again to check the state; pass force: true to log in again. Logins last 25 days.",
|
|
43261
44350
|
inputSchema: { force: external_exports.boolean().optional() }
|
|
43262
44351
|
},
|
|
@@ -43272,13 +44361,29 @@ function registerCapiTools(server, deps) {
|
|
|
43272
44361
|
server.registerTool(
|
|
43273
44362
|
"capi_profile",
|
|
43274
44363
|
{
|
|
44364
|
+
annotations: {
|
|
44365
|
+
title: "Frontier Profile",
|
|
44366
|
+
readOnlyHint: true,
|
|
44367
|
+
destructiveHint: false,
|
|
44368
|
+
idempotentHint: true,
|
|
44369
|
+
openWorldHint: true
|
|
44370
|
+
},
|
|
43275
44371
|
description: "Live commander data from Frontier: current system and docked station, current ship with every module and its engineering (blueprint, grade, engineer, experimental), and all owned ships with locations. Does not include materials (use edsm_commander_materials). Cached 60 s."
|
|
43276
44372
|
},
|
|
43277
44373
|
wrap(async () => deps.capi.profile())
|
|
43278
44374
|
);
|
|
43279
44375
|
server.registerTool(
|
|
43280
44376
|
"capi_logout",
|
|
43281
|
-
{
|
|
44377
|
+
{
|
|
44378
|
+
annotations: {
|
|
44379
|
+
title: "Log Out of Frontier",
|
|
44380
|
+
readOnlyHint: false,
|
|
44381
|
+
destructiveHint: true,
|
|
44382
|
+
idempotentHint: true,
|
|
44383
|
+
openWorldHint: false
|
|
44384
|
+
},
|
|
44385
|
+
description: "Forget the stored Frontier login. With a sync password, this logs out every computer that shares the sync folder."
|
|
44386
|
+
},
|
|
43282
44387
|
wrap(async () => {
|
|
43283
44388
|
await deps.capiAuth.logout();
|
|
43284
44389
|
return { state: "logged_out" };
|
|
@@ -43292,6 +44397,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43292
44397
|
server.registerTool(
|
|
43293
44398
|
"catalog_blueprint",
|
|
43294
44399
|
{
|
|
44400
|
+
annotations: {
|
|
44401
|
+
title: "Look Up Blueprint",
|
|
44402
|
+
readOnlyHint: true,
|
|
44403
|
+
destructiveHint: false,
|
|
44404
|
+
idempotentHint: true,
|
|
44405
|
+
openWorldHint: true
|
|
44406
|
+
},
|
|
43295
44407
|
description: "Engineering blueprint by journal id (e.g. FSD_LongRange) or name (e.g. 'Increased range'): materials per grade 1-5 with material symbol, grade and trade group, and the experimental effects available for the module. Give module when the name is shared by several modules.",
|
|
43296
44408
|
inputSchema: { name: external_exports.string(), module: external_exports.string().optional() }
|
|
43297
44409
|
},
|
|
@@ -43300,6 +44412,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43300
44412
|
server.registerTool(
|
|
43301
44413
|
"catalog_engineers",
|
|
43302
44414
|
{
|
|
44415
|
+
annotations: {
|
|
44416
|
+
title: "Look Up Engineers",
|
|
44417
|
+
readOnlyHint: true,
|
|
44418
|
+
destructiveHint: false,
|
|
44419
|
+
idempotentHint: true,
|
|
44420
|
+
openWorldHint: true
|
|
44421
|
+
},
|
|
43303
44422
|
description: "With blueprint (and optional module): engineers who apply it, their max grade, location and invite/unlock requirements, best first. With engineer: that engineer's location, requirements and every blueprint they offer.",
|
|
43304
44423
|
inputSchema: {
|
|
43305
44424
|
blueprint: external_exports.string().optional(),
|
|
@@ -43320,6 +44439,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43320
44439
|
server.registerTool(
|
|
43321
44440
|
"catalog_material",
|
|
43322
44441
|
{
|
|
44442
|
+
annotations: {
|
|
44443
|
+
title: "Look Up Material",
|
|
44444
|
+
readOnlyHint: true,
|
|
44445
|
+
destructiveHint: false,
|
|
44446
|
+
idempotentHint: true,
|
|
44447
|
+
openWorldHint: true
|
|
44448
|
+
},
|
|
43323
44449
|
description: "Engineering material by name or symbol: kind, grade, trade group, other materials in the same trade group, and material trader exchange rules.",
|
|
43324
44450
|
inputSchema: { name: external_exports.string() }
|
|
43325
44451
|
},
|
|
@@ -43328,6 +44454,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43328
44454
|
server.registerTool(
|
|
43329
44455
|
"catalog_module",
|
|
43330
44456
|
{
|
|
44457
|
+
annotations: {
|
|
44458
|
+
title: "Look Up Module",
|
|
44459
|
+
readOnlyHint: true,
|
|
44460
|
+
destructiveHint: false,
|
|
44461
|
+
idempotentHint: true,
|
|
44462
|
+
openWorldHint: true
|
|
44463
|
+
},
|
|
43331
44464
|
description: "Outfitting module by journal symbol (e.g. Int_Hyperdrive_Size5_Class5) or name: class, rating, mount and engineering group. Returns total matches and at most 20 items.",
|
|
43332
44465
|
inputSchema: { query: external_exports.string() }
|
|
43333
44466
|
},
|
|
@@ -43738,23 +44871,23 @@ async function screenPermits(a) {
|
|
|
43738
44871
|
if (kept.length >= a.limit) break;
|
|
43739
44872
|
const refs = a.systemsOf(item).filter((r) => !exempt.has(r.name.toLowerCase()));
|
|
43740
44873
|
let needsPermit = false;
|
|
43741
|
-
let
|
|
44874
|
+
let open4 = true;
|
|
43742
44875
|
let missed = false;
|
|
43743
44876
|
for (const ref of refs) {
|
|
43744
44877
|
if (!lookup2.has(ref.name)) stop ??= stopReason();
|
|
43745
44878
|
if (stop && !lookup2.has(ref.name)) {
|
|
43746
44879
|
missed = true;
|
|
43747
|
-
|
|
44880
|
+
open4 = false;
|
|
43748
44881
|
continue;
|
|
43749
44882
|
}
|
|
43750
44883
|
const permit = await within(lookup2.check(ref), deadline - now());
|
|
43751
44884
|
if (permit === TIMED_OUT) {
|
|
43752
44885
|
stop = "time limit";
|
|
43753
44886
|
missed = true;
|
|
43754
|
-
|
|
44887
|
+
open4 = false;
|
|
43755
44888
|
continue;
|
|
43756
44889
|
}
|
|
43757
|
-
if (permit === null)
|
|
44890
|
+
if (permit === null) open4 = false;
|
|
43758
44891
|
if (permit === true) {
|
|
43759
44892
|
needsPermit = true;
|
|
43760
44893
|
if (!locked.has(ref.name.toLowerCase())) locked.set(ref.name.toLowerCase(), ref.name);
|
|
@@ -43771,7 +44904,7 @@ async function screenPermits(a) {
|
|
|
43771
44904
|
}
|
|
43772
44905
|
} else {
|
|
43773
44906
|
kept.push(item);
|
|
43774
|
-
if (
|
|
44907
|
+
if (open4) checkedOpen.push(item);
|
|
43775
44908
|
}
|
|
43776
44909
|
}
|
|
43777
44910
|
const notes = [];
|
|
@@ -43846,20 +44979,20 @@ function credits(n) {
|
|
|
43846
44979
|
function recommendStation(ranked, t) {
|
|
43847
44980
|
const best = ranked[0];
|
|
43848
44981
|
if (!best) return null;
|
|
43849
|
-
let
|
|
44982
|
+
let reason2;
|
|
43850
44983
|
if (best.effective_cost === null) {
|
|
43851
|
-
|
|
44984
|
+
reason2 = `fastest, about ${best.travel_min} min`;
|
|
43852
44985
|
} else {
|
|
43853
44986
|
const fastest = [...ranked].sort((a, b) => a.travel_s - b.travel_s)[0];
|
|
43854
44987
|
if (fastest !== best && fastest.price && best.price && fastest.price > best.price) {
|
|
43855
44988
|
const extra = best.travel_s - fastest.travel_s;
|
|
43856
44989
|
const more = extra < 60 ? `${extra} more seconds` : `${(extra / 60).toFixed(1)} more minutes`;
|
|
43857
|
-
|
|
44990
|
+
reason2 = `${credits(fastest.price - best.price)} cr cheaper than ${fastest.name} (${fastest.system}) for ${more} at ${credits(t.credits_per_hour)} cr/h`;
|
|
43858
44991
|
} else {
|
|
43859
|
-
|
|
44992
|
+
reason2 = `lowest cost including travel time, about ${best.travel_min} min`;
|
|
43860
44993
|
}
|
|
43861
44994
|
}
|
|
43862
|
-
return { name: best.name, system: best.system, reason };
|
|
44995
|
+
return { name: best.name, system: best.system, reason: reason2 };
|
|
43863
44996
|
}
|
|
43864
44997
|
|
|
43865
44998
|
// src/stations/search.ts
|
|
@@ -43930,20 +45063,20 @@ async function searchStations(spansh, query, rules, limit, permits = {}) {
|
|
|
43930
45063
|
spansh,
|
|
43931
45064
|
...permits
|
|
43932
45065
|
});
|
|
43933
|
-
const
|
|
45066
|
+
const page2 = screen.kept.map(
|
|
43934
45067
|
(h) => screen.locked(h.system) ? { ...h, needs_permit: true } : h
|
|
43935
45068
|
);
|
|
43936
|
-
const
|
|
45069
|
+
const open4 = mode === "exclude" ? ranked.filter((h) => !screen.locked(h.system)) : ranked;
|
|
43937
45070
|
const recommended = screen.checkedOpen.length ? recommendStation(screen.checkedOpen, rules.travel) : null;
|
|
43938
45071
|
return {
|
|
43939
45072
|
near: source.near,
|
|
43940
45073
|
total: source.total,
|
|
43941
|
-
matched:
|
|
45074
|
+
matched: open4.length,
|
|
43942
45075
|
total_capped: source.total_capped,
|
|
43943
|
-
stations:
|
|
45076
|
+
stations: page2,
|
|
43944
45077
|
recommended,
|
|
43945
45078
|
// Only the returned page counts, so every listed rule is broken by a station shown.
|
|
43946
|
-
relaxed: relaxAttempted ? brokenRules(
|
|
45079
|
+
relaxed: relaxAttempted ? brokenRules(page2, rules) : [],
|
|
43947
45080
|
relax_attempted: relaxAttempted,
|
|
43948
45081
|
applied_preferences: describeRules(rules),
|
|
43949
45082
|
notes: screen.notes
|
|
@@ -43984,6 +45117,13 @@ function registerMaterialsTools(server, deps) {
|
|
|
43984
45117
|
server.registerTool(
|
|
43985
45118
|
"materials_plan",
|
|
43986
45119
|
{
|
|
45120
|
+
annotations: {
|
|
45121
|
+
title: "Plan Materials",
|
|
45122
|
+
readOnlyHint: true,
|
|
45123
|
+
destructiveHint: false,
|
|
45124
|
+
idempotentHint: true,
|
|
45125
|
+
openWorldHint: true
|
|
45126
|
+
},
|
|
43987
45127
|
description: "Plan engineering materials: needs for blueprints (grades, rolls, experimentals) and extra materials, compared with the commander's inventory (journal or EDSM), trades from surplus at exact material trader rates, what is still short, and the best trader of each type near the commander (travel preferences apply).",
|
|
43988
45128
|
inputSchema: {
|
|
43989
45129
|
blueprints: external_exports.array(
|
|
@@ -44185,17 +45325,17 @@ var better = (l1, t1, l2, t2) => l1 < l2 || l1 === l2 && t1 < t2;
|
|
|
44185
45325
|
var lateAt = (deadlines, t) => deadlines.filter((d) => t > d).length;
|
|
44186
45326
|
function findCycle(after) {
|
|
44187
45327
|
const state = [];
|
|
44188
|
-
const
|
|
45328
|
+
const path15 = [];
|
|
44189
45329
|
const visit2 = (i) => {
|
|
44190
45330
|
if (state[i] === 2) return null;
|
|
44191
|
-
if (state[i] === 1) return
|
|
45331
|
+
if (state[i] === 1) return path15.slice(path15.indexOf(i));
|
|
44192
45332
|
state[i] = 1;
|
|
44193
|
-
|
|
45333
|
+
path15.push(i);
|
|
44194
45334
|
for (const j of after[i] ?? []) {
|
|
44195
45335
|
const cycle = visit2(j);
|
|
44196
45336
|
if (cycle) return cycle;
|
|
44197
45337
|
}
|
|
44198
|
-
|
|
45338
|
+
path15.pop();
|
|
44199
45339
|
state[i] = 2;
|
|
44200
45340
|
return null;
|
|
44201
45341
|
};
|
|
@@ -44274,14 +45414,14 @@ function exactOrder(p) {
|
|
|
44274
45414
|
parent: null
|
|
44275
45415
|
});
|
|
44276
45416
|
}
|
|
44277
|
-
for (let
|
|
45417
|
+
for (let mask2 = 1; mask2 < 1 << n; mask2++) {
|
|
44278
45418
|
for (let last = 0; last < n; last++) {
|
|
44279
|
-
for (const label of fronts[
|
|
45419
|
+
for (const label of fronts[mask2][last]) {
|
|
44280
45420
|
for (let next = 0; next < n; next++) {
|
|
44281
|
-
if (
|
|
45421
|
+
if (mask2 & 1 << next || (needs[next] & mask2) !== needs[next]) continue;
|
|
44282
45422
|
const time3 = label.time + between[last][next];
|
|
44283
45423
|
const late2 = label.late + lateAt(p.deadlines[next] ?? [], time3);
|
|
44284
|
-
insert(fronts[
|
|
45424
|
+
insert(fronts[mask2 | 1 << next][next], { late: late2, time: time3, last: next, parent: label });
|
|
44285
45425
|
}
|
|
44286
45426
|
}
|
|
44287
45427
|
}
|
|
@@ -44405,6 +45545,13 @@ function registerMissionTools(server, deps) {
|
|
|
44405
45545
|
server.registerTool(
|
|
44406
45546
|
"missions_route",
|
|
44407
45547
|
{
|
|
45548
|
+
annotations: {
|
|
45549
|
+
title: "Plan Mission Route",
|
|
45550
|
+
readOnlyHint: true,
|
|
45551
|
+
destructiveHint: false,
|
|
45552
|
+
idempotentHint: true,
|
|
45553
|
+
openWorldHint: true
|
|
45554
|
+
},
|
|
44408
45555
|
description: "Order the stops for accepted missions (from the game journal): passengers, deliveries and hand-ins for completed missions. Uses Spansh coordinates and the travel-time model to minimise time while meeting deadlines; lists late and unroutable missions. The order is exact up to 12 stops and 24 missions with an expiry; above that it is a nearest-first heuristic and deadline handling is best-effort. Planetary stations are inferred from the Spansh station type. `from` and `return_to` must be systems Spansh knows. The first call for systems not seen in the last day can take tens of seconds, because Spansh lookups are rate limited.",
|
|
44409
45556
|
inputSchema: {
|
|
44410
45557
|
from: external_exports.string().optional().describe("Start system; default the journal location"),
|
|
@@ -44457,8 +45604,8 @@ function registerMissionTools(server, deps) {
|
|
|
44457
45604
|
places.push(await place2(stop.system, stop.station));
|
|
44458
45605
|
stops.push(stop);
|
|
44459
45606
|
} catch (e) {
|
|
44460
|
-
const
|
|
44461
|
-
for (const m of stop.missions) unroutable.push({ id: m.id, name: m.name, reason });
|
|
45607
|
+
const reason2 = e instanceof UserError ? e.message : `Spansh lookup failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
45608
|
+
for (const m of stop.missions) unroutable.push({ id: m.id, name: m.name, reason: reason2 });
|
|
44462
45609
|
}
|
|
44463
45610
|
}
|
|
44464
45611
|
const returnPlace = a.return_to ? await endpoint("return_to", a.return_to.system, a.return_to.station ?? null) : void 0;
|
|
@@ -44548,13 +45695,31 @@ function registerJournalTools(server, deps) {
|
|
|
44548
45695
|
server.registerTool(
|
|
44549
45696
|
"journal_status",
|
|
44550
45697
|
{
|
|
44551
|
-
|
|
45698
|
+
annotations: {
|
|
45699
|
+
title: "Journal Status",
|
|
45700
|
+
readOnlyHint: true,
|
|
45701
|
+
destructiveHint: false,
|
|
45702
|
+
idempotentHint: true,
|
|
45703
|
+
openWorldHint: false
|
|
45704
|
+
},
|
|
45705
|
+
description: "Check the Elite Dangerous journal folder: whether it exists, the newest journal and how old its last event is, the commander and game version. Use it first when journal data looks wrong or out of date; warnings explain missing folders and stalled Syncthing sync, and also mention a newer ed-mcp release when one is available."
|
|
44552
45706
|
},
|
|
44553
|
-
wrap(async () =>
|
|
45707
|
+
wrap(async () => {
|
|
45708
|
+
const status = await deps.journal.status();
|
|
45709
|
+
const update = await deps.update?.();
|
|
45710
|
+
return update ? { ...status, warnings: [...status.warnings, update] } : status;
|
|
45711
|
+
})
|
|
44554
45712
|
);
|
|
44555
45713
|
server.registerTool(
|
|
44556
45714
|
"journal_commander",
|
|
44557
45715
|
{
|
|
45716
|
+
annotations: {
|
|
45717
|
+
title: "Journal Commander",
|
|
45718
|
+
readOnlyHint: true,
|
|
45719
|
+
destructiveHint: false,
|
|
45720
|
+
idempotentHint: true,
|
|
45721
|
+
openWorldHint: false
|
|
45722
|
+
},
|
|
44558
45723
|
description: "The commander's current state from the local game journal: name, credits, location (system, station, docked), ship (type, name, jump range, cargo and fuel capacity) and cargo. " + FRESHNESS_TEXT
|
|
44559
45724
|
},
|
|
44560
45725
|
wrap(async () => {
|
|
@@ -44572,6 +45737,13 @@ function registerJournalTools(server, deps) {
|
|
|
44572
45737
|
server.registerTool(
|
|
44573
45738
|
"journal_materials",
|
|
44574
45739
|
{
|
|
45740
|
+
annotations: {
|
|
45741
|
+
title: "Journal Materials",
|
|
45742
|
+
readOnlyHint: true,
|
|
45743
|
+
destructiveHint: false,
|
|
45744
|
+
idempotentHint: true,
|
|
45745
|
+
openWorldHint: false
|
|
45746
|
+
},
|
|
44575
45747
|
description: "Engineering materials the commander holds, from the local game journal, by kind (raw, manufactured, encoded) with counts. Pass kinds to limit the result. " + FRESHNESS_TEXT,
|
|
44576
45748
|
inputSchema: { kinds: external_exports.array(external_exports.enum(["raw", "manufactured", "encoded"])).optional() }
|
|
44577
45749
|
},
|
|
@@ -44597,6 +45769,13 @@ function registerJournalTools(server, deps) {
|
|
|
44597
45769
|
server.registerTool(
|
|
44598
45770
|
"journal_missions",
|
|
44599
45771
|
{
|
|
45772
|
+
annotations: {
|
|
45773
|
+
title: "Journal Missions",
|
|
45774
|
+
readOnlyHint: true,
|
|
45775
|
+
destructiveHint: false,
|
|
45776
|
+
idempotentHint: true,
|
|
45777
|
+
openWorldHint: false
|
|
45778
|
+
},
|
|
44600
45779
|
description: "Accepted missions (active and complete) from the local game journal: destination, expiry, reward and passenger or cargo details, soonest expiry first. status is active or complete (objectives met, not handed in). origin_system and origin_station are where the mission was accepted (station is null when not docked). expired is true once the expiry has passed. details_missing marks missions accepted before the 14-day journal window (name and expiry only). " + FRESHNESS_TEXT
|
|
44601
45780
|
},
|
|
44602
45781
|
wrap(async () => {
|
|
@@ -44612,6 +45791,13 @@ function registerJournalTools(server, deps) {
|
|
|
44612
45791
|
server.registerTool(
|
|
44613
45792
|
"journal_engineers",
|
|
44614
45793
|
{
|
|
45794
|
+
annotations: {
|
|
45795
|
+
title: "Journal Engineers",
|
|
45796
|
+
readOnlyHint: true,
|
|
45797
|
+
destructiveHint: false,
|
|
45798
|
+
idempotentHint: true,
|
|
45799
|
+
openWorldHint: false
|
|
45800
|
+
},
|
|
44615
45801
|
description: "Engineer progress from the local game journal: invited, unlocked or known, with rank and rank progress. " + FRESHNESS_TEXT
|
|
44616
45802
|
},
|
|
44617
45803
|
wrap(async () => {
|
|
@@ -44831,6 +46017,13 @@ function registerLoopTools(server, deps) {
|
|
|
44831
46017
|
server.registerTool(
|
|
44832
46018
|
"trade_loops",
|
|
44833
46019
|
{
|
|
46020
|
+
annotations: {
|
|
46021
|
+
title: "Find Trade Loops",
|
|
46022
|
+
readOnlyHint: true,
|
|
46023
|
+
destructiveHint: false,
|
|
46024
|
+
idempotentHint: true,
|
|
46025
|
+
openWorldHint: true
|
|
46026
|
+
},
|
|
44834
46027
|
description: `Find repeatable trade loops: two or three stations, where a leg can run empty. start "here" (default) finds loops through your docked station; start "any" finds the best loops anywhere within radius_ly of near (default: where you are). Ranked by sustained profit per hour: thin markets that cannot refill a full hold every lap rank low. Uses Spansh market data (check each price's age). Cargo and capital default to the journal. Loops through permit-locked systems are left out unless permit_systems is allow. Legality is not checked.`,
|
|
44835
46028
|
inputSchema: {
|
|
44836
46029
|
start: external_exports.enum(["here", "any"]).default("here"),
|
|
@@ -44978,25 +46171,25 @@ function registerLoopTools(server, deps) {
|
|
|
44978
46171
|
);
|
|
44979
46172
|
if (!startMarket) {
|
|
44980
46173
|
const info = await spansh.system(centre.system, STATION_LIMIT);
|
|
44981
|
-
const
|
|
44982
|
-
if (
|
|
46174
|
+
const entry2 = info.stations.find((st) => same2(st.name, name));
|
|
46175
|
+
if (entry2?.market_id == null) {
|
|
44983
46176
|
throw new UserError(`Spansh has no market for ${name} in ${info.name}.`);
|
|
44984
46177
|
}
|
|
44985
|
-
const id2 =
|
|
46178
|
+
const id2 = entry2.market_id;
|
|
44986
46179
|
const fresh = journalMarket?.market_id === id2 && localHours !== null && localHours <= a.max_price_age_days * 24;
|
|
44987
46180
|
const rows = fresh ? renamedLocalRows() : null;
|
|
44988
46181
|
startMarket = rows ? fromJournal2(
|
|
44989
46182
|
{
|
|
44990
|
-
name:
|
|
46183
|
+
name: entry2.name,
|
|
44991
46184
|
system: info.name,
|
|
44992
46185
|
system_id64: info.id64,
|
|
44993
46186
|
market_id: id2,
|
|
44994
46187
|
distance_ly: null,
|
|
44995
|
-
arrival_ls:
|
|
46188
|
+
arrival_ls: entry2.arrival_ls,
|
|
44996
46189
|
coords: info.coords,
|
|
44997
|
-
pad:
|
|
44998
|
-
type:
|
|
44999
|
-
planetary: PLANETARY.test(
|
|
46190
|
+
pad: entry2.pad,
|
|
46191
|
+
type: entry2.type,
|
|
46192
|
+
planetary: PLANETARY.test(entry2.type ?? ""),
|
|
45000
46193
|
market_age_hours: null,
|
|
45001
46194
|
market: []
|
|
45002
46195
|
},
|
|
@@ -45022,15 +46215,15 @@ function registerLoopTools(server, deps) {
|
|
|
45022
46215
|
break;
|
|
45023
46216
|
}
|
|
45024
46217
|
let market = null;
|
|
45025
|
-
let
|
|
46218
|
+
let failure3 = "Spansh has no market for it.";
|
|
45026
46219
|
try {
|
|
45027
46220
|
market = await includeMarket(inc);
|
|
45028
46221
|
} catch (err) {
|
|
45029
46222
|
if (!(err instanceof UserError)) throw err;
|
|
45030
|
-
|
|
46223
|
+
failure3 = err.message;
|
|
45031
46224
|
}
|
|
45032
46225
|
if (!market) {
|
|
45033
|
-
notes.push(`Cannot include ${inc.station}: ${
|
|
46226
|
+
notes.push(`Cannot include ${inc.station}: ${failure3}`);
|
|
45034
46227
|
} else if (!candidates.some((c) => c.market_id === market.market_id)) {
|
|
45035
46228
|
candidates.push(market);
|
|
45036
46229
|
}
|
|
@@ -45181,6 +46374,13 @@ function registerInaraTools(server, deps) {
|
|
|
45181
46374
|
server.registerTool(
|
|
45182
46375
|
"inara_commander_profile",
|
|
45183
46376
|
{
|
|
46377
|
+
annotations: {
|
|
46378
|
+
title: "Inara Commander Profile",
|
|
46379
|
+
readOnlyHint: true,
|
|
46380
|
+
destructiveHint: false,
|
|
46381
|
+
idempotentHint: true,
|
|
46382
|
+
openWorldHint: true
|
|
46383
|
+
},
|
|
45184
46384
|
description: "Commander profile from Inara: ranks with progress, allegiance, power, main ship type, squadron and Inara URL. Omit name for your own commander. Inara does not expose location, loadout or materials; use EDSM or CAPI tools for those. Cached 60 s.",
|
|
45185
46385
|
inputSchema: { name: external_exports.string().optional() }
|
|
45186
46386
|
},
|
|
@@ -45188,7 +46388,16 @@ function registerInaraTools(server, deps) {
|
|
|
45188
46388
|
);
|
|
45189
46389
|
server.registerTool(
|
|
45190
46390
|
"inara_community_goals",
|
|
45191
|
-
{
|
|
46391
|
+
{
|
|
46392
|
+
annotations: {
|
|
46393
|
+
title: "Community Goals",
|
|
46394
|
+
readOnlyHint: true,
|
|
46395
|
+
destructiveHint: false,
|
|
46396
|
+
idempotentHint: true,
|
|
46397
|
+
openWorldHint: true
|
|
46398
|
+
},
|
|
46399
|
+
description: "Recent and active community goals from Inara. Cached 60 s."
|
|
46400
|
+
},
|
|
45192
46401
|
wrap(async () => deps.inara.communityGoals())
|
|
45193
46402
|
);
|
|
45194
46403
|
}
|
|
@@ -45199,6 +46408,13 @@ function registerEdsmTools(server, deps) {
|
|
|
45199
46408
|
server.registerTool(
|
|
45200
46409
|
"edsm_commander_location",
|
|
45201
46410
|
{
|
|
46411
|
+
annotations: {
|
|
46412
|
+
title: "Commander Location",
|
|
46413
|
+
readOnlyHint: true,
|
|
46414
|
+
destructiveHint: false,
|
|
46415
|
+
idempotentHint: true,
|
|
46416
|
+
openWorldHint: true
|
|
46417
|
+
},
|
|
45202
46418
|
description: "Commander's last known system, coordinates, docked station and ship, from EDSM. Cached 60 s."
|
|
45203
46419
|
},
|
|
45204
46420
|
wrap(async () => deps.edsm.position())
|
|
@@ -45206,6 +46422,13 @@ function registerEdsmTools(server, deps) {
|
|
|
45206
46422
|
server.registerTool(
|
|
45207
46423
|
"edsm_commander_materials",
|
|
45208
46424
|
{
|
|
46425
|
+
annotations: {
|
|
46426
|
+
title: "EDSM Commander Materials",
|
|
46427
|
+
readOnlyHint: true,
|
|
46428
|
+
destructiveHint: false,
|
|
46429
|
+
idempotentHint: true,
|
|
46430
|
+
openWorldHint: true
|
|
46431
|
+
},
|
|
45209
46432
|
description: "Commander's engineering materials (raw and manufactured), encoded data, and cargo, from EDSM. kinds defaults to all three. Cached 60 s.",
|
|
45210
46433
|
inputSchema: { kinds: external_exports.array(Kind).min(1).optional() }
|
|
45211
46434
|
},
|
|
@@ -45284,13 +46507,13 @@ function buildPool(a, catalog) {
|
|
|
45284
46507
|
const byEngineer = /* @__PURE__ */ new Map();
|
|
45285
46508
|
for (const item of a.plan.items) {
|
|
45286
46509
|
if (!item.engineer || item.status === "done" || item.status === "skipped") continue;
|
|
45287
|
-
const
|
|
46510
|
+
const entry2 = byEngineer.get(lc(item.engineer)) ?? {
|
|
45288
46511
|
id: item.id,
|
|
45289
46512
|
name: item.engineer,
|
|
45290
46513
|
descriptions: []
|
|
45291
46514
|
};
|
|
45292
|
-
|
|
45293
|
-
byEngineer.set(lc(item.engineer),
|
|
46515
|
+
entry2.descriptions.push(item.description);
|
|
46516
|
+
byEngineer.set(lc(item.engineer), entry2);
|
|
45294
46517
|
}
|
|
45295
46518
|
const baseFor = /* @__PURE__ */ new Map();
|
|
45296
46519
|
if (catalog) {
|
|
@@ -45526,6 +46749,13 @@ function registerRouteTools(server, deps) {
|
|
|
45526
46749
|
server.registerTool(
|
|
45527
46750
|
"route_stops",
|
|
45528
46751
|
{
|
|
46752
|
+
annotations: {
|
|
46753
|
+
title: "Plan Route Stops",
|
|
46754
|
+
readOnlyHint: false,
|
|
46755
|
+
destructiveHint: false,
|
|
46756
|
+
idempotentHint: true,
|
|
46757
|
+
openWorldHint: true
|
|
46758
|
+
},
|
|
45529
46759
|
description: "Order the stops for a build: shops, material traders and engineers, from a plan (plan_id) or a list (stops). Uses Spansh coordinates and the travel-time model. For each engineer stop, set `after` to the shops and traders that supply what that engineer needs (the module to buy, the materials to trade). Give `after: []` to an engineer who needs nothing from the other stops, so they can come first. Without your rules, a plan puts every engineer after every shop and trader. Use `one_of` for alternatives, for example two engineers who both do the work; the tool tries every combination (at most 16) and keeps the fastest. `only` and `skip` choose stops from the plan, so one plan can serve several trips. One place (the same engineer, or the same system and station) can be an option in several groups; it is visited once, and each stop's `meets` lists the groups it covers. Leg times are estimates from straight-line jumps; pass plot: true to plot the chosen route with Spansh.",
|
|
45530
46760
|
inputSchema: {
|
|
45531
46761
|
plan_id: external_exports.string().optional(),
|
|
@@ -45567,8 +46797,8 @@ function registerRouteTools(server, deps) {
|
|
|
45567
46797
|
);
|
|
45568
46798
|
const cycle = findCycle(graph);
|
|
45569
46799
|
if (cycle) {
|
|
45570
|
-
const
|
|
45571
|
-
throw new UserError(`The after rules form a cycle: ${
|
|
46800
|
+
const path15 = [...cycle, cycle[0]].map((i) => names(all[i]).join("+"));
|
|
46801
|
+
throw new UserError(`The after rules form a cycle: ${path15.join(" -> ")}`);
|
|
45572
46802
|
}
|
|
45573
46803
|
const rules = await resolveSearchRules(deps, a.preferences, a.use_preferences);
|
|
45574
46804
|
const travel = rules.travel;
|
|
@@ -45813,19 +47043,23 @@ function registerRouteTools(server, deps) {
|
|
|
45813
47043
|
const total = min2(totalSeconds);
|
|
45814
47044
|
let saved = false;
|
|
45815
47045
|
if (a.save_to_plan && a.plan_id) {
|
|
45816
|
-
|
|
45817
|
-
|
|
45818
|
-
|
|
45819
|
-
|
|
45820
|
-
|
|
45821
|
-
|
|
45822
|
-
|
|
45823
|
-
|
|
45824
|
-
|
|
45825
|
-
|
|
45826
|
-
|
|
45827
|
-
|
|
45828
|
-
|
|
47046
|
+
try {
|
|
47047
|
+
await deps.plans.setRoute(a.plan_id, {
|
|
47048
|
+
computed_at: new Date(now()).toISOString(),
|
|
47049
|
+
from: startPlace.system,
|
|
47050
|
+
stops: [
|
|
47051
|
+
...solved.order.map(optionLabel),
|
|
47052
|
+
...returnPlace ? [
|
|
47053
|
+
`return to ${returnPlace.system}${returnPlace.station ? ` \u2014 ${returnPlace.station}` : ""}`
|
|
47054
|
+
] : []
|
|
47055
|
+
],
|
|
47056
|
+
total_min: total,
|
|
47057
|
+
plotted: a.plot && allPlotted
|
|
47058
|
+
});
|
|
47059
|
+
saved = true;
|
|
47060
|
+
} catch (err) {
|
|
47061
|
+
notes.add(`The route was not saved to plan "${a.plan_id}": ${reason(err)}`);
|
|
47062
|
+
}
|
|
45829
47063
|
}
|
|
45830
47064
|
return {
|
|
45831
47065
|
start: { system: startPlace.system, station: startPlace.station, source },
|
|
@@ -45851,66 +47085,26 @@ function registerRouteTools(server, deps) {
|
|
|
45851
47085
|
|
|
45852
47086
|
// src/tools/plans.ts
|
|
45853
47087
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
45854
|
-
import { mkdir as
|
|
45855
|
-
import
|
|
47088
|
+
import { mkdir as mkdir6, readFile as readFile7, rename as rename5, stat as stat7, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
47089
|
+
import path13 from "path";
|
|
47090
|
+
var MAX_ECHOED_PLAN = 100 * 1024;
|
|
47091
|
+
function registerPlanTools(server, deps) {
|
|
47092
|
+
const { plans, warnings } = deps;
|
|
47093
|
+
const saveOrReturnJson = async (input2, tooLargeAdvice, opts = {}) => {
|
|
47094
|
+
const parsed = PlanInputSchema.safeParse(input2);
|
|
47095
|
+
if (!parsed.success) throw new UserError(`Invalid plan: ${external_exports.prettifyError(parsed.error)}`);
|
|
47096
|
+
try {
|
|
47097
|
+
return await plans.upsert(parsed.data, opts);
|
|
47098
|
+
} catch (err) {
|
|
47099
|
+
if (err instanceof PlanExistsError) throw err;
|
|
47100
|
+
const json3 = JSON.stringify(parsed.data, null, 2);
|
|
47101
|
+
const tail = json3.length > MAX_ECHOED_PLAN ? `The plan was not saved, and it is too large to include here. ${tooLargeAdvice}` : `The plan was not saved. Save this JSON and load it later with plan_import:
|
|
47102
|
+
${json3}`;
|
|
47103
|
+
throw new UserError(`${reason(err)}
|
|
45856
47104
|
|
|
45857
|
-
|
|
45858
|
-
var box = {
|
|
45859
|
-
todo: "[ ]",
|
|
45860
|
-
in_progress: "[~]",
|
|
45861
|
-
done: "[x]",
|
|
45862
|
-
skipped: "[-]"
|
|
45863
|
-
};
|
|
45864
|
-
function planToMarkdown(plan) {
|
|
45865
|
-
const done = plan.items.filter((i) => i.status === "done" || i.status === "skipped").length;
|
|
45866
|
-
const out = [
|
|
45867
|
-
`# ${plan.name}`,
|
|
45868
|
-
"",
|
|
45869
|
-
`Plan \`${plan.id}\` \xB7 updated ${plan.updated_at} \xB7 ${done}/${plan.items.length} done`,
|
|
45870
|
-
""
|
|
45871
|
-
];
|
|
45872
|
-
if (plan.notes) out.push(plan.notes, "");
|
|
45873
|
-
if (plan.items.length) {
|
|
45874
|
-
out.push("## Items", "");
|
|
45875
|
-
for (const i of plan.items) {
|
|
45876
|
-
const where = [
|
|
45877
|
-
i.station ? `at ${i.station}` : void 0,
|
|
45878
|
-
i.engineer ? `engineer ${i.engineer}` : void 0
|
|
45879
|
-
].filter(Boolean);
|
|
45880
|
-
out.push(
|
|
45881
|
-
`- ${box[i.status]} **${i.kind}** ${i.description}${where.length ? ` \u2014 ${where.join(", ")}` : ""}`
|
|
45882
|
-
);
|
|
45883
|
-
if (i.materials?.length) {
|
|
45884
|
-
out.push(` - Materials: ${i.materials.map((m) => `${m.count}\xD7 ${m.name}`).join(", ")}`);
|
|
45885
|
-
}
|
|
45886
|
-
if (i.notes) out.push(` - Notes: ${i.notes}`);
|
|
47105
|
+
${tail}`);
|
|
45887
47106
|
}
|
|
45888
|
-
|
|
45889
|
-
}
|
|
45890
|
-
if (plan.stops.length) {
|
|
45891
|
-
out.push("## Stops", "");
|
|
45892
|
-
plan.stops.forEach((s, n) => {
|
|
45893
|
-
out.push(`${n + 1}. ${s.system}${s.station ? ` \u2014 ${s.station}` : ""}: ${s.purpose}`);
|
|
45894
|
-
});
|
|
45895
|
-
out.push("");
|
|
45896
|
-
}
|
|
45897
|
-
if (plan.route) {
|
|
45898
|
-
out.push("## Route", "");
|
|
45899
|
-
plan.route.stops.forEach((s, n) => out.push(`${n + 1}. ${s}`));
|
|
45900
|
-
const r = plan.route;
|
|
45901
|
-
const kind2 = r.plotted ? "plotted" : "estimate";
|
|
45902
|
-
out.push(
|
|
45903
|
-
"",
|
|
45904
|
-
`From ${r.from}; ${kind2}. Total ${r.total_min} min, computed ${r.computed_at}`,
|
|
45905
|
-
""
|
|
45906
|
-
);
|
|
45907
|
-
}
|
|
45908
|
-
return out.join("\n");
|
|
45909
|
-
}
|
|
45910
|
-
|
|
45911
|
-
// src/tools/plans.ts
|
|
45912
|
-
function registerPlanTools(server, deps) {
|
|
45913
|
-
const { plans } = deps;
|
|
47107
|
+
};
|
|
45914
47108
|
const exports = /* @__PURE__ */ new Map();
|
|
45915
47109
|
const serialise = (target, fn) => {
|
|
45916
47110
|
const key = process.platform === "linux" ? target : target.toLowerCase();
|
|
@@ -45923,25 +47117,61 @@ function registerPlanTools(server, deps) {
|
|
|
45923
47117
|
};
|
|
45924
47118
|
server.registerTool(
|
|
45925
47119
|
"plan_list",
|
|
45926
|
-
{
|
|
45927
|
-
|
|
47120
|
+
{
|
|
47121
|
+
annotations: {
|
|
47122
|
+
title: "List Plans",
|
|
47123
|
+
readOnlyHint: true,
|
|
47124
|
+
destructiveHint: false,
|
|
47125
|
+
idempotentHint: true,
|
|
47126
|
+
openWorldHint: false
|
|
47127
|
+
},
|
|
47128
|
+
description: "List saved build plans with item counts and progress."
|
|
47129
|
+
},
|
|
47130
|
+
wrapWarn(async () => plans.list(), warnings)
|
|
45928
47131
|
);
|
|
45929
47132
|
server.registerTool(
|
|
45930
47133
|
"plan_get",
|
|
45931
|
-
{
|
|
45932
|
-
|
|
47134
|
+
{
|
|
47135
|
+
annotations: {
|
|
47136
|
+
title: "Get Plan",
|
|
47137
|
+
readOnlyHint: true,
|
|
47138
|
+
destructiveHint: false,
|
|
47139
|
+
idempotentHint: true,
|
|
47140
|
+
openWorldHint: false
|
|
47141
|
+
},
|
|
47142
|
+
description: "Get a saved plan by id.",
|
|
47143
|
+
inputSchema: { id: external_exports.string() }
|
|
47144
|
+
},
|
|
47145
|
+
wrapWarn(async ({ id }) => plans.get(id), warnings)
|
|
45933
47146
|
);
|
|
45934
47147
|
server.registerTool(
|
|
45935
47148
|
"plan_upsert",
|
|
45936
47149
|
{
|
|
47150
|
+
annotations: {
|
|
47151
|
+
title: "Save Plan",
|
|
47152
|
+
readOnlyHint: false,
|
|
47153
|
+
destructiveHint: true,
|
|
47154
|
+
idempotentHint: true,
|
|
47155
|
+
openWorldHint: false
|
|
47156
|
+
},
|
|
45937
47157
|
description: "Create or replace a build plan. Items are things to buy, engineer, unlock, gather or trade, plus refits (moving modules between slots, storing modules, or a fitting decision); stops are the places to visit (optionally with label, kind and after rules); route is the saved visiting order. Replacing keeps created_at.",
|
|
45938
47158
|
inputSchema: { plan: PlanInputSchema }
|
|
45939
47159
|
},
|
|
45940
|
-
|
|
47160
|
+
wrapWarn(
|
|
47161
|
+
async ({ plan }) => saveOrReturnJson(plan, "Ask Claude to save it again later."),
|
|
47162
|
+
warnings
|
|
47163
|
+
)
|
|
45941
47164
|
);
|
|
45942
47165
|
server.registerTool(
|
|
45943
47166
|
"plan_update_item",
|
|
45944
47167
|
{
|
|
47168
|
+
annotations: {
|
|
47169
|
+
title: "Update Plan Item",
|
|
47170
|
+
readOnlyHint: false,
|
|
47171
|
+
destructiveHint: false,
|
|
47172
|
+
idempotentHint: true,
|
|
47173
|
+
openWorldHint: false
|
|
47174
|
+
},
|
|
45945
47175
|
description: "Set the status and/or notes of one plan item.",
|
|
45946
47176
|
inputSchema: {
|
|
45947
47177
|
plan_id: external_exports.string(),
|
|
@@ -45950,26 +47180,57 @@ function registerPlanTools(server, deps) {
|
|
|
45950
47180
|
notes: external_exports.string().optional()
|
|
45951
47181
|
}
|
|
45952
47182
|
},
|
|
45953
|
-
|
|
47183
|
+
wrapWarn(
|
|
45954
47184
|
async (a) => {
|
|
45955
47185
|
if (a.status === void 0 && a.notes === void 0) {
|
|
45956
47186
|
throw new UserError("Give status or notes to update");
|
|
45957
47187
|
}
|
|
45958
|
-
|
|
45959
|
-
|
|
47188
|
+
try {
|
|
47189
|
+
return await plans.updateItem(a.plan_id, a.item_id, {
|
|
47190
|
+
status: a.status,
|
|
47191
|
+
notes: a.notes
|
|
47192
|
+
});
|
|
47193
|
+
} catch (err) {
|
|
47194
|
+
const changes = [
|
|
47195
|
+
...a.status !== void 0 ? [`status: ${a.status}`] : [],
|
|
47196
|
+
...a.notes !== void 0 ? ["notes changed"] : []
|
|
47197
|
+
].join(", ");
|
|
47198
|
+
throw new UserError(
|
|
47199
|
+
`Item "${a.item_id}" in plan "${a.plan_id}" was not updated (${changes}). ${reason(err)}`
|
|
47200
|
+
);
|
|
47201
|
+
}
|
|
47202
|
+
},
|
|
47203
|
+
warnings
|
|
45960
47204
|
)
|
|
45961
47205
|
);
|
|
45962
47206
|
server.registerTool(
|
|
45963
47207
|
"plan_delete",
|
|
45964
|
-
{
|
|
45965
|
-
|
|
47208
|
+
{
|
|
47209
|
+
annotations: {
|
|
47210
|
+
title: "Delete Plan",
|
|
47211
|
+
readOnlyHint: false,
|
|
47212
|
+
destructiveHint: true,
|
|
47213
|
+
idempotentHint: true,
|
|
47214
|
+
openWorldHint: false
|
|
47215
|
+
},
|
|
47216
|
+
description: "Delete a saved plan.",
|
|
47217
|
+
inputSchema: { id: external_exports.string() }
|
|
47218
|
+
},
|
|
47219
|
+
wrapWarn(async ({ id }) => {
|
|
45966
47220
|
await plans.delete(id);
|
|
45967
47221
|
return { deleted: id };
|
|
45968
|
-
})
|
|
47222
|
+
}, warnings)
|
|
45969
47223
|
);
|
|
45970
47224
|
server.registerTool(
|
|
45971
47225
|
"plan_export",
|
|
45972
47226
|
{
|
|
47227
|
+
annotations: {
|
|
47228
|
+
title: "Export Plan",
|
|
47229
|
+
readOnlyHint: false,
|
|
47230
|
+
destructiveHint: true,
|
|
47231
|
+
idempotentHint: true,
|
|
47232
|
+
openWorldHint: false
|
|
47233
|
+
},
|
|
45973
47234
|
description: "Export a plan as Markdown (default) or JSON. Without path, returns the text (markdown) or the plan (json). With path (absolute or ~/...), writes the file. A JSON file can be loaded again with plan_import.",
|
|
45974
47235
|
inputSchema: {
|
|
45975
47236
|
id: external_exports.string(),
|
|
@@ -45978,7 +47239,7 @@ function registerPlanTools(server, deps) {
|
|
|
45978
47239
|
overwrite: external_exports.boolean().optional()
|
|
45979
47240
|
}
|
|
45980
47241
|
},
|
|
45981
|
-
|
|
47242
|
+
wrapWarn(
|
|
45982
47243
|
async (a) => {
|
|
45983
47244
|
const plan = await plans.get(a.id);
|
|
45984
47245
|
const json3 = a.format === "json";
|
|
@@ -45992,7 +47253,7 @@ function registerPlanTools(server, deps) {
|
|
|
45992
47253
|
if (!a.overwrite && await exists(target)) {
|
|
45993
47254
|
throw new UserError(`${target} exists; pass overwrite: true to replace it`);
|
|
45994
47255
|
}
|
|
45995
|
-
await
|
|
47256
|
+
await mkdir6(path13.dirname(target), { recursive: true });
|
|
45996
47257
|
const tmp = `${target}.${process.pid}.${randomUUID2()}.tmp`;
|
|
45997
47258
|
try {
|
|
45998
47259
|
await writeFile5(tmp, text);
|
|
@@ -46003,12 +47264,20 @@ function registerPlanTools(server, deps) {
|
|
|
46003
47264
|
}
|
|
46004
47265
|
});
|
|
46005
47266
|
return { path: target, bytes: Buffer.byteLength(text) };
|
|
46006
|
-
}
|
|
47267
|
+
},
|
|
47268
|
+
warnings
|
|
46007
47269
|
)
|
|
46008
47270
|
);
|
|
46009
47271
|
server.registerTool(
|
|
46010
47272
|
"plan_import",
|
|
46011
47273
|
{
|
|
47274
|
+
annotations: {
|
|
47275
|
+
title: "Import Plan",
|
|
47276
|
+
readOnlyHint: false,
|
|
47277
|
+
destructiveHint: true,
|
|
47278
|
+
idempotentHint: true,
|
|
47279
|
+
openWorldHint: false
|
|
47280
|
+
},
|
|
46012
47281
|
description: "Load a plan from a JSON file written by plan_export (absolute or ~/... path). id replaces the id in the file. Refuses to replace an existing plan unless overwrite is true.",
|
|
46013
47282
|
inputSchema: {
|
|
46014
47283
|
path: external_exports.string(),
|
|
@@ -46016,14 +47285,14 @@ function registerPlanTools(server, deps) {
|
|
|
46016
47285
|
overwrite: external_exports.boolean().optional()
|
|
46017
47286
|
}
|
|
46018
47287
|
},
|
|
46019
|
-
|
|
47288
|
+
wrapWarn(async (a) => {
|
|
46020
47289
|
const source = resolveUserPath(a.path);
|
|
46021
47290
|
let raw;
|
|
46022
47291
|
try {
|
|
46023
|
-
if ((await
|
|
47292
|
+
if ((await stat7(source)).isDirectory()) {
|
|
46024
47293
|
throw new UserError(`${source} is a directory, not a plan file`);
|
|
46025
47294
|
}
|
|
46026
|
-
raw = await
|
|
47295
|
+
raw = await readFile7(source, "utf8");
|
|
46027
47296
|
} catch (err) {
|
|
46028
47297
|
if (err instanceof UserError) throw err;
|
|
46029
47298
|
if (err.code === "ENOENT") {
|
|
@@ -46042,13 +47311,15 @@ function registerPlanTools(server, deps) {
|
|
|
46042
47311
|
}
|
|
46043
47312
|
const parsed = PlanInputSchema.safeParse(json3);
|
|
46044
47313
|
if (!parsed.success) throw new UserError(`Invalid plan: ${external_exports.prettifyError(parsed.error)}`);
|
|
46045
|
-
return
|
|
46046
|
-
|
|
47314
|
+
return saveOrReturnJson(parsed.data, "Keep your original file and try again later.", {
|
|
47315
|
+
overwrite: a.overwrite === true
|
|
47316
|
+
});
|
|
47317
|
+
}, warnings)
|
|
46047
47318
|
);
|
|
46048
47319
|
}
|
|
46049
47320
|
async function exists(p) {
|
|
46050
47321
|
try {
|
|
46051
|
-
await
|
|
47322
|
+
await stat7(p);
|
|
46052
47323
|
return true;
|
|
46053
47324
|
} catch (err) {
|
|
46054
47325
|
if (err.code === "ENOENT") return false;
|
|
@@ -46061,17 +47332,31 @@ function registerPrefsTools(server, deps) {
|
|
|
46061
47332
|
server.registerTool(
|
|
46062
47333
|
"prefs_get",
|
|
46063
47334
|
{
|
|
47335
|
+
annotations: {
|
|
47336
|
+
title: "Get Preferences",
|
|
47337
|
+
readOnlyHint: true,
|
|
47338
|
+
destructiveHint: false,
|
|
47339
|
+
idempotentHint: true,
|
|
47340
|
+
openWorldHint: false
|
|
47341
|
+
},
|
|
46064
47342
|
description: "Get the commander's stored travel preferences used by station searches: maximum arrival distance (soft/hard/off), planetary, fleet carrier and megaship rules, minimum pad (auto = current ship), permit-locked systems (allow/exclude), and the travel-time model (jump range and the share of the journal's unladen range that auto uses, seconds per jump, supercruise curve, docking, planetary time, value of time in cr/h)."
|
|
46065
47343
|
},
|
|
46066
|
-
|
|
47344
|
+
wrapWarn(async () => deps.prefs.get(), deps.warnings)
|
|
46067
47345
|
);
|
|
46068
47346
|
server.registerTool(
|
|
46069
47347
|
"prefs_set",
|
|
46070
47348
|
{
|
|
47349
|
+
annotations: {
|
|
47350
|
+
title: "Set Preferences",
|
|
47351
|
+
readOnlyHint: false,
|
|
47352
|
+
destructiveHint: false,
|
|
47353
|
+
idempotentHint: true,
|
|
47354
|
+
openWorldHint: false
|
|
47355
|
+
},
|
|
46071
47356
|
description: 'Change stored travel preferences with a partial patch, for example {"stations":{"max_arrival_ls":{"value":2000}}}. Saved for future sessions; returns the full preferences. For a one-off change pass `preferences` to the search tool instead.',
|
|
46072
47357
|
inputSchema: { patch: PrefsPatchSchema }
|
|
46073
47358
|
},
|
|
46074
|
-
|
|
47359
|
+
wrapWarn(async ({ patch }) => deps.prefs.set(patch), deps.warnings)
|
|
46075
47360
|
);
|
|
46076
47361
|
}
|
|
46077
47362
|
|
|
@@ -46169,6 +47454,13 @@ function registerStationTools(server, deps) {
|
|
|
46169
47454
|
server.registerTool(
|
|
46170
47455
|
"station_info",
|
|
46171
47456
|
{
|
|
47457
|
+
annotations: {
|
|
47458
|
+
title: "Station Info",
|
|
47459
|
+
readOnlyHint: true,
|
|
47460
|
+
destructiveHint: false,
|
|
47461
|
+
idempotentHint: true,
|
|
47462
|
+
openWorldHint: false
|
|
47463
|
+
},
|
|
46172
47464
|
description: "What the station you last opened a screen at sells, from the game's own files in the journal folder (Market.json, Outfitting.json, Shipyard.json). Complete and current, but only for the latest station of each kind. part: market (commodities sold and bought, each with price, stock or demand and vs_mean_pct), outfitting (modules by group with price), shipyard (ships by price) or all (a short summary of each, the default). commodity filters the market by name; search filters modules by name, group, symbol or class and rating (for example '5A'). Notes flag data that is old or from a station other than the docked one.",
|
|
46173
47465
|
inputSchema: {
|
|
46174
47466
|
part: external_exports.enum(["all", "market", "outfitting", "shipyard"]).optional(),
|
|
@@ -46264,6 +47556,13 @@ function registerSpanshTools(server, deps) {
|
|
|
46264
47556
|
server.registerTool(
|
|
46265
47557
|
"spansh_find_stations",
|
|
46266
47558
|
{
|
|
47559
|
+
annotations: {
|
|
47560
|
+
title: "Find Stations",
|
|
47561
|
+
readOnlyHint: true,
|
|
47562
|
+
destructiveHint: false,
|
|
47563
|
+
idempotentHint: true,
|
|
47564
|
+
openWorldHint: true
|
|
47565
|
+
},
|
|
46267
47566
|
description: "Find the best stations near a system, ranked by travel time and cost, that sell a module or ship, or offer a service (material trader, tech broker, interstellar factors). Give exactly one of module, ship, service. Module names are generic (e.g. 'Frame Shift Drive') with class 1-8 and rating A-I; give class and rating to compare prices. Applies the commander's stored travel preferences (arrival distance, station types, pad size from the current ship, travel-time model); override any of them for this call with `preferences` (same shape as prefs_set's patch) or ignore them with use_preferences=false. Returns total (Spansh matches before preferences; 10000 means 10000 or more), matched (matches after preferences among the stations fetched: the nearest 50, or up to 100 when the arrival limit was relaxed). Stations are ranked by estimated travel time (jumps plus supercruise, docking and planetary time) and, for purchases, by effective cost = price + travel time \xD7 the commander's credits_per_hour. Each station has jumps, travel_s, travel_min, arrival_assumed, effective_cost and discount_pct (versus the median price); recommended explains the trade-off. Stations in permit-locked systems are left out unless permit_systems is allow, which keeps them with needs_permit: true. Also returns any relaxed rules, the applied preferences and notes. Cached 5 min.",
|
|
46268
47567
|
inputSchema: {
|
|
46269
47568
|
near: external_exports.string().describe("Exact reference system name"),
|
|
@@ -46304,6 +47603,13 @@ function registerSpanshTools(server, deps) {
|
|
|
46304
47603
|
server.registerTool(
|
|
46305
47604
|
"spansh_system",
|
|
46306
47605
|
{
|
|
47606
|
+
annotations: {
|
|
47607
|
+
title: "Look Up System",
|
|
47608
|
+
readOnlyHint: true,
|
|
47609
|
+
destructiveHint: false,
|
|
47610
|
+
idempotentHint: true,
|
|
47611
|
+
openWorldHint: true
|
|
47612
|
+
},
|
|
46307
47613
|
description: "Look up a system by exact name: coordinates, allegiance, permit, and its stations sorted by distance from arrival (limit applies to stations). Cached 24 h.",
|
|
46308
47614
|
inputSchema: { name: external_exports.string(), limit: Limit }
|
|
46309
47615
|
},
|
|
@@ -46312,6 +47618,13 @@ function registerSpanshTools(server, deps) {
|
|
|
46312
47618
|
server.registerTool(
|
|
46313
47619
|
"spansh_route",
|
|
46314
47620
|
{
|
|
47621
|
+
annotations: {
|
|
47622
|
+
title: "Plot Route",
|
|
47623
|
+
readOnlyHint: true,
|
|
47624
|
+
destructiveHint: false,
|
|
47625
|
+
idempotentHint: true,
|
|
47626
|
+
openWorldHint: true
|
|
47627
|
+
},
|
|
46315
47628
|
description: "Plot a neutron-boosted route between two systems for a laden jump range in LY. Returns waypoints with jumps needed to reach each. May take a while; if it says still plotting, call again with the same arguments. Cached 5 min.",
|
|
46316
47629
|
inputSchema: {
|
|
46317
47630
|
from: external_exports.string(),
|
|
@@ -46422,7 +47735,7 @@ function recommendCommodityStation(stations, mode, maxAgeHours = Infinity) {
|
|
|
46422
47735
|
const best = stations.find(
|
|
46423
47736
|
(h) => h.market_age_hours === null || h.market_age_hours <= maxAgeHours
|
|
46424
47737
|
);
|
|
46425
|
-
const
|
|
47738
|
+
const reason2 = (b) => {
|
|
46426
47739
|
const trip = `about ${b.travel_min} min`;
|
|
46427
47740
|
if (b.value === null) {
|
|
46428
47741
|
return `${mode === "sell" ? "highest" : "lowest"} price, ${credits2(b.price ?? 0)} cr, ${trip}`;
|
|
@@ -46432,7 +47745,7 @@ function recommendCommodityStation(stations, mode, maxAgeHours = Infinity) {
|
|
|
46432
47745
|
}
|
|
46433
47746
|
return `best ${mode === "sell" ? "net return" : "total cost"} after travel time: ${credits2(b.value)} cr, ${trip}`;
|
|
46434
47747
|
};
|
|
46435
|
-
return best ? { name: best.name, system: best.system, reason:
|
|
47748
|
+
return best ? { name: best.name, system: best.system, reason: reason2(best) } : null;
|
|
46436
47749
|
}
|
|
46437
47750
|
|
|
46438
47751
|
// src/tools/trade.ts
|
|
@@ -46490,6 +47803,13 @@ function registerTradeTools(server, deps) {
|
|
|
46490
47803
|
server.registerTool(
|
|
46491
47804
|
"trade_route",
|
|
46492
47805
|
{
|
|
47806
|
+
annotations: {
|
|
47807
|
+
title: "Find Trade Route",
|
|
47808
|
+
readOnlyHint: true,
|
|
47809
|
+
destructiveHint: false,
|
|
47810
|
+
idempotentHint: true,
|
|
47811
|
+
openWorldHint: true
|
|
47812
|
+
},
|
|
46493
47813
|
description: "Plan a multi-hop trade run with the Spansh trade planner and rank it by profit per hour. Starts at from_system/from_station, or the docked station from the game journal. Cargo and capital default to the journal's ship and credits (else 7 t and 1000 cr). Pad size, planetary stations and fleet carriers and a hard arrival-distance limit come from the commander's travel preferences. Returns hops with commodities to buy and sell, profit, travel_min, total_profit, total_travel_min and profit_per_hour (from the travel-time model), plus the resolved inputs. Spansh takes one to several minutes: if the result is status pending, call again with the inputs it shows to resume. Finished routes are cached 5 min.",
|
|
46494
47814
|
inputSchema: {
|
|
46495
47815
|
from_system: external_exports.string().optional(),
|
|
@@ -46631,6 +47951,13 @@ function registerTradeTools(server, deps) {
|
|
|
46631
47951
|
server.registerTool(
|
|
46632
47952
|
"commodity_prices",
|
|
46633
47953
|
{
|
|
47954
|
+
annotations: {
|
|
47955
|
+
title: "Commodity Prices",
|
|
47956
|
+
readOnlyHint: true,
|
|
47957
|
+
destructiveHint: false,
|
|
47958
|
+
idempotentHint: true,
|
|
47959
|
+
openWorldHint: true
|
|
47960
|
+
},
|
|
46634
47961
|
description: "Find the best stations near a system to sell or buy one commodity, from Spansh market data, filtered and ranked with the commander's travel preferences. Stations show price, demand (sell) or supply (buy), market_age_hours and travel_min. Give amount (tonnes) to rank by value = price \xD7 amount \u2213 travel time \xD7 credits_per_hour (minus when selling, plus when buying); otherwise ranked by price. Searches within max_distance_ly of near (default the journal's current system) and only markets updated within max_price_age_days. Stations must have demand or supply of at least amount (else 1). Stations in permit-locked systems are left out unless permit_systems is allow. recommended names the best station and why. Cached 5 min.",
|
|
46635
47962
|
inputSchema: {
|
|
46636
47963
|
commodity: external_exports.string().describe("Exact English commodity name, e.g. Gold"),
|
|
@@ -46687,7 +48014,7 @@ function registerTradeTools(server, deps) {
|
|
|
46687
48014
|
now
|
|
46688
48015
|
});
|
|
46689
48016
|
notes.push(...screen.notes);
|
|
46690
|
-
const
|
|
48017
|
+
const open4 = mode === "exclude" ? ranked.stations.filter((h) => !screen.locked(h.system)) : ranked.stations;
|
|
46691
48018
|
const applied_preferences = describeRules(rules);
|
|
46692
48019
|
if (!found.stations.length) {
|
|
46693
48020
|
notes.push("No stations found; check the exact English commodity name");
|
|
@@ -46705,7 +48032,7 @@ function registerTradeTools(server, deps) {
|
|
|
46705
48032
|
return {
|
|
46706
48033
|
near: found.near,
|
|
46707
48034
|
total: found.total,
|
|
46708
|
-
matched:
|
|
48035
|
+
matched: open4.length,
|
|
46709
48036
|
stations: screen.kept.map((st) => ({
|
|
46710
48037
|
...st,
|
|
46711
48038
|
...screen.locked(st.system) ? { needs_permit: true } : {},
|
|
@@ -46735,14 +48062,150 @@ function registerTradeTools(server, deps) {
|
|
|
46735
48062
|
);
|
|
46736
48063
|
}
|
|
46737
48064
|
|
|
48065
|
+
// src/tools/settings.ts
|
|
48066
|
+
import { access as access2 } from "fs/promises";
|
|
48067
|
+
import path14 from "path";
|
|
48068
|
+
async function exists2(file2) {
|
|
48069
|
+
return access2(file2).then(
|
|
48070
|
+
() => true,
|
|
48071
|
+
() => false
|
|
48072
|
+
);
|
|
48073
|
+
}
|
|
48074
|
+
async function existing(dir, names) {
|
|
48075
|
+
const found = [];
|
|
48076
|
+
for (const n of names) if (await exists2(path14.join(dir, n))) found.push(n);
|
|
48077
|
+
return found;
|
|
48078
|
+
}
|
|
48079
|
+
function registerSettingsTools(server, deps) {
|
|
48080
|
+
server.registerTool(
|
|
48081
|
+
"settings_open",
|
|
48082
|
+
{
|
|
48083
|
+
annotations: {
|
|
48084
|
+
title: "Open Settings Page",
|
|
48085
|
+
// Not read-only: it starts a local page that saves keys.
|
|
48086
|
+
readOnlyHint: false,
|
|
48087
|
+
destructiveHint: false,
|
|
48088
|
+
idempotentHint: false,
|
|
48089
|
+
openWorldHint: false
|
|
48090
|
+
},
|
|
48091
|
+
description: "Open the ed-mcp settings page in the browser, where the user enters or clears Inara and EDSM API keys and commander names. Keys entered there apply at once, without a restart, and sync (encrypted) when a sync folder and password are set. Never ask the user to type a key into the chat; use this page."
|
|
48092
|
+
},
|
|
48093
|
+
wrap(async () => {
|
|
48094
|
+
const { url: url2, expires_at, opened } = await deps.page.open();
|
|
48095
|
+
return opened ? {
|
|
48096
|
+
opened: true,
|
|
48097
|
+
expires_at,
|
|
48098
|
+
next: "Ask the user to enter their keys in the browser tab that opened and press Save."
|
|
48099
|
+
} : {
|
|
48100
|
+
opened: false,
|
|
48101
|
+
url: url2,
|
|
48102
|
+
expires_at,
|
|
48103
|
+
next: "Ask the user to open this link in their browser, enter their keys and press Save."
|
|
48104
|
+
};
|
|
48105
|
+
})
|
|
48106
|
+
);
|
|
48107
|
+
server.registerTool(
|
|
48108
|
+
"settings_status",
|
|
48109
|
+
{
|
|
48110
|
+
annotations: {
|
|
48111
|
+
title: "Settings Status",
|
|
48112
|
+
// Not read-only: a status call can copy environment keys into the store once (seed-once).
|
|
48113
|
+
readOnlyHint: false,
|
|
48114
|
+
destructiveHint: false,
|
|
48115
|
+
idempotentHint: true,
|
|
48116
|
+
openWorldHint: false
|
|
48117
|
+
},
|
|
48118
|
+
description: "Where ed-mcp keeps its data: the sync folder and whether it is available, whether secrets are encrypted, where each API key comes from (environment or saved), whether a Frontier login is saved, any sync conflict copies, and notes on settings that look mismatched between Claude apps. An `update` line says when a newer ed-mcp release is available. Never returns key values."
|
|
48119
|
+
},
|
|
48120
|
+
wrap(async () => {
|
|
48121
|
+
let syncFolder = null;
|
|
48122
|
+
if (deps.sync) {
|
|
48123
|
+
try {
|
|
48124
|
+
await deps.sync.check();
|
|
48125
|
+
syncFolder = { path: deps.sync.dir, available: true };
|
|
48126
|
+
} catch (err) {
|
|
48127
|
+
if (!(err instanceof UserError)) throw err;
|
|
48128
|
+
syncFolder = { path: deps.sync.dir, available: false, error: err.message };
|
|
48129
|
+
}
|
|
48130
|
+
}
|
|
48131
|
+
let readable = true;
|
|
48132
|
+
let secretsError;
|
|
48133
|
+
let frontier = false;
|
|
48134
|
+
try {
|
|
48135
|
+
frontier = !!(await deps.secrets.get()).capi;
|
|
48136
|
+
} catch (err) {
|
|
48137
|
+
if (!(err instanceof UserError)) throw err;
|
|
48138
|
+
readable = false;
|
|
48139
|
+
secretsError = err.message;
|
|
48140
|
+
}
|
|
48141
|
+
const sources = await deps.settings.sources();
|
|
48142
|
+
const notes = [...deps.notes ?? []];
|
|
48143
|
+
if (deps.sync && deps.localDir && path14.resolve(deps.localDir) !== path14.resolve(deps.sync.dir)) {
|
|
48144
|
+
const own2 = path14.resolve(deps.secrets.file);
|
|
48145
|
+
const localDir = deps.localDir;
|
|
48146
|
+
const files = (await existing(localDir, [
|
|
48147
|
+
"plans.json",
|
|
48148
|
+
"preferences.json",
|
|
48149
|
+
"secrets.json",
|
|
48150
|
+
"secrets.enc"
|
|
48151
|
+
])).filter((n) => path14.resolve(localDir, n) !== own2);
|
|
48152
|
+
if (files.length > 0) {
|
|
48153
|
+
notes.push(
|
|
48154
|
+
`The settings folder ${deps.localDir} also holds ${files.join(", ")}. Another Claude app on this computer may not have the sync folder set. Give every Claude app the same ED_MCP_SYNC_DIR and ED_MCP_SYNC_PASSWORD.`
|
|
48155
|
+
);
|
|
48156
|
+
}
|
|
48157
|
+
}
|
|
48158
|
+
if (deps.secrets.encrypted) {
|
|
48159
|
+
const dir = path14.dirname(deps.secrets.file);
|
|
48160
|
+
if (await exists2(path14.join(dir, "secrets.json"))) {
|
|
48161
|
+
notes.push(
|
|
48162
|
+
`${dir} holds both secrets.enc and secrets.json. A Claude app without ED_MCP_SYNC_PASSWORD uses secrets.json. Give every Claude app the same password, then delete secrets.json.`
|
|
48163
|
+
);
|
|
48164
|
+
}
|
|
48165
|
+
}
|
|
48166
|
+
return {
|
|
48167
|
+
sync_folder: syncFolder,
|
|
48168
|
+
secrets: {
|
|
48169
|
+
file: deps.secrets.file,
|
|
48170
|
+
encrypted: deps.secrets.encrypted,
|
|
48171
|
+
readable,
|
|
48172
|
+
...secretsError ? { error: secretsError } : {}
|
|
48173
|
+
},
|
|
48174
|
+
inara: sources.inara,
|
|
48175
|
+
edsm: sources.edsm,
|
|
48176
|
+
frontier_login: frontier,
|
|
48177
|
+
conflicts: await deps.syncWarnings?.() ?? [],
|
|
48178
|
+
notes,
|
|
48179
|
+
update: await deps.update?.() ?? null
|
|
48180
|
+
};
|
|
48181
|
+
})
|
|
48182
|
+
);
|
|
48183
|
+
}
|
|
48184
|
+
|
|
46738
48185
|
// src/server.ts
|
|
46739
|
-
function
|
|
48186
|
+
function createServer3(deps) {
|
|
46740
48187
|
const server = new McpServer({ name: "ed-mcp", version: VERSION });
|
|
46741
48188
|
if (deps.caches) {
|
|
46742
48189
|
registerCacheTools(server, { caches: deps.caches, refreshables: deps.refreshables });
|
|
46743
48190
|
}
|
|
46744
|
-
if (deps.plans)
|
|
46745
|
-
|
|
48191
|
+
if (deps.plans)
|
|
48192
|
+
registerPlanTools(server, {
|
|
48193
|
+
plans: deps.plans,
|
|
48194
|
+
warnings: deps.planWarnings ?? deps.syncWarnings
|
|
48195
|
+
});
|
|
48196
|
+
if (deps.prefs) registerPrefsTools(server, { prefs: deps.prefs, warnings: deps.syncWarnings });
|
|
48197
|
+
if (deps.settingsPage && deps.settings && deps.secrets) {
|
|
48198
|
+
registerSettingsTools(server, {
|
|
48199
|
+
page: deps.settingsPage,
|
|
48200
|
+
settings: deps.settings,
|
|
48201
|
+
secrets: deps.secrets,
|
|
48202
|
+
sync: deps.sync,
|
|
48203
|
+
syncWarnings: deps.syncWarnings,
|
|
48204
|
+
localDir: deps.localDir,
|
|
48205
|
+
notes: deps.notes,
|
|
48206
|
+
update: deps.versionCheck ? () => deps.versionCheck.notice() : void 0
|
|
48207
|
+
});
|
|
48208
|
+
}
|
|
46746
48209
|
if (deps.catalog) registerCatalogTools(server, { catalog: deps.catalog });
|
|
46747
48210
|
if (deps.inara) registerInaraTools(server, { inara: deps.inara });
|
|
46748
48211
|
if (deps.edsm) registerEdsmTools(server, { edsm: deps.edsm });
|
|
@@ -46803,7 +48266,12 @@ function createServer2(deps) {
|
|
|
46803
48266
|
if (deps.capiAuth && deps.capi) {
|
|
46804
48267
|
registerCapiTools(server, { capiAuth: deps.capiAuth, capi: deps.capi });
|
|
46805
48268
|
}
|
|
46806
|
-
if (deps.journal)
|
|
48269
|
+
if (deps.journal)
|
|
48270
|
+
registerJournalTools(server, {
|
|
48271
|
+
journal: deps.journal,
|
|
48272
|
+
catalog: deps.catalog,
|
|
48273
|
+
update: deps.versionCheck ? () => deps.versionCheck.notice() : void 0
|
|
48274
|
+
});
|
|
46807
48275
|
if (deps.journal) registerStationTools(server, { journal: deps.journal, catalog: deps.catalog });
|
|
46808
48276
|
registerBuildTools(server, { catalog: deps.catalog });
|
|
46809
48277
|
if (deps.ships) {
|
|
@@ -46821,7 +48289,9 @@ function createServer2(deps) {
|
|
|
46821
48289
|
// src/index.ts
|
|
46822
48290
|
async function main() {
|
|
46823
48291
|
const config2 = loadConfig(process.env, defaultDirs());
|
|
46824
|
-
const
|
|
48292
|
+
const deps = buildDeps(config2);
|
|
48293
|
+
deps.versionCheck?.start();
|
|
48294
|
+
const server = createServer3(deps);
|
|
46825
48295
|
await server.connect(new StdioServerTransport());
|
|
46826
48296
|
}
|
|
46827
48297
|
main().catch((err) => {
|