ed-mcp 0.3.1 → 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 +1899 -451
- 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);
|
|
@@ -30942,7 +30973,7 @@ var DEFAULT_STATICS = {
|
|
|
30942
30973
|
var CatalogService = class {
|
|
30943
30974
|
constructor(http, cacheDir, opts = {}) {
|
|
30944
30975
|
this.http = http;
|
|
30945
|
-
this.dir =
|
|
30976
|
+
this.dir = path4.join(cacheDir, "catalog");
|
|
30946
30977
|
this.statics = opts.statics ?? DEFAULT_STATICS;
|
|
30947
30978
|
this.now = opts.now ?? Date.now;
|
|
30948
30979
|
this.maxAgeMs = opts.maxAgeMs ?? 7 * 864e5;
|
|
@@ -31004,11 +31035,11 @@ var CatalogService = class {
|
|
|
31004
31035
|
}
|
|
31005
31036
|
async loadDisk() {
|
|
31006
31037
|
try {
|
|
31007
|
-
const meta3 = JSON.parse(await
|
|
31038
|
+
const meta3 = JSON.parse(await readFile(path4.join(this.dir, "meta.json"), "utf8"));
|
|
31008
31039
|
const texts = {};
|
|
31009
31040
|
for (const k of KEYS) {
|
|
31010
|
-
const file2 =
|
|
31011
|
-
const text = await
|
|
31041
|
+
const file2 = path4.join(this.dir, `${k}.raw`);
|
|
31042
|
+
const text = await readFile(file2, "utf8").catch((err) => {
|
|
31012
31043
|
if (OPTIONAL.has(k) && err.code === "ENOENT") return void 0;
|
|
31013
31044
|
throw err;
|
|
31014
31045
|
});
|
|
@@ -31073,11 +31104,11 @@ var CatalogService = class {
|
|
|
31073
31104
|
changed.push(k);
|
|
31074
31105
|
}
|
|
31075
31106
|
const built = buildCatalog(next, this.statics, this.minimums);
|
|
31076
|
-
await
|
|
31107
|
+
await mkdir(this.dir, { recursive: true });
|
|
31077
31108
|
for (const k of changed)
|
|
31078
|
-
await atomicWrite(
|
|
31109
|
+
await atomicWrite(path4.join(this.dir, `${k}.raw`), next[k]);
|
|
31079
31110
|
const meta3 = { version: 1, checked_at: stamp, files };
|
|
31080
|
-
await atomicWrite(
|
|
31111
|
+
await atomicWrite(path4.join(this.dir, "meta.json"), JSON.stringify(meta3, null, 2));
|
|
31081
31112
|
this.catalog = built.catalog;
|
|
31082
31113
|
this.warnings = [...missing, ...built.warnings];
|
|
31083
31114
|
this.texts = next;
|
|
@@ -31092,8 +31123,8 @@ var CatalogService = class {
|
|
|
31092
31123
|
};
|
|
31093
31124
|
async function atomicWrite(file2, text) {
|
|
31094
31125
|
const tmp = `${file2}.${process.pid}.tmp`;
|
|
31095
|
-
await
|
|
31096
|
-
await
|
|
31126
|
+
await writeFile(tmp, text);
|
|
31127
|
+
await rename(tmp, file2);
|
|
31097
31128
|
}
|
|
31098
31129
|
|
|
31099
31130
|
// src/http/cache.ts
|
|
@@ -31196,12 +31227,12 @@ var CacheRegistry = class {
|
|
|
31196
31227
|
};
|
|
31197
31228
|
|
|
31198
31229
|
// src/journal/service.ts
|
|
31199
|
-
import { readFile as
|
|
31200
|
-
import
|
|
31230
|
+
import { readFile as readFile3, stat as stat4 } from "fs/promises";
|
|
31231
|
+
import path6 from "path";
|
|
31201
31232
|
|
|
31202
31233
|
// src/journal/files.ts
|
|
31203
31234
|
import { readdir, stat as stat2 } from "fs/promises";
|
|
31204
|
-
import
|
|
31235
|
+
import path5 from "path";
|
|
31205
31236
|
var NEW = /^Journal\.(\d{4})-(\d{2})-(\d{2})T(\d{2})(\d{2})(\d{2})\.(\d{2})\.log$/;
|
|
31206
31237
|
var OLD = /^Journal\.(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{2})\.log$/;
|
|
31207
31238
|
function parseJournalName(name) {
|
|
@@ -31225,7 +31256,7 @@ async function listJournals(dir, opts = {}) {
|
|
|
31225
31256
|
}
|
|
31226
31257
|
const files = await Promise.all(
|
|
31227
31258
|
candidates.map(async (n) => {
|
|
31228
|
-
const file2 =
|
|
31259
|
+
const file2 = path5.join(dir, n.name);
|
|
31229
31260
|
const mtimeMs = await stat2(file2).then(
|
|
31230
31261
|
(st) => st.mtimeMs,
|
|
31231
31262
|
() => null
|
|
@@ -31242,7 +31273,7 @@ async function listJournals(dir, opts = {}) {
|
|
|
31242
31273
|
}
|
|
31243
31274
|
|
|
31244
31275
|
// src/journal/reader.ts
|
|
31245
|
-
import { readFile as
|
|
31276
|
+
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
31246
31277
|
var JournalReader = class {
|
|
31247
31278
|
keep;
|
|
31248
31279
|
cache = /* @__PURE__ */ new Map();
|
|
@@ -31256,7 +31287,7 @@ var JournalReader = class {
|
|
|
31256
31287
|
if (hit && hit.size === st.size && hit.mtimeMs === st.mtimeMs) return hit.events;
|
|
31257
31288
|
const events = [];
|
|
31258
31289
|
let last;
|
|
31259
|
-
for (const line of (await
|
|
31290
|
+
for (const line of (await readFile2(file2.path, "utf8")).split(/\r?\n/)) {
|
|
31260
31291
|
if (!line.trim()) continue;
|
|
31261
31292
|
try {
|
|
31262
31293
|
const e = JSON.parse(line);
|
|
@@ -31355,12 +31386,12 @@ function buildState(events) {
|
|
|
31355
31386
|
const symbol2 = name.toLowerCase();
|
|
31356
31387
|
const target = kind2 ? materials.get(kind2) : KINDS.map((k) => materials.get(k)).find((m) => m.has(symbol2));
|
|
31357
31388
|
if (!target) return;
|
|
31358
|
-
const
|
|
31389
|
+
const entry2 = target.get(symbol2) ?? { symbol: symbol2, name: symbol2, count: 0 };
|
|
31359
31390
|
const label = str2(localised);
|
|
31360
|
-
if (label)
|
|
31361
|
-
|
|
31362
|
-
if (
|
|
31363
|
-
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);
|
|
31364
31395
|
};
|
|
31365
31396
|
const setLocation = (e, docked) => {
|
|
31366
31397
|
location.system = str2(e.StarSystem) ?? location.system;
|
|
@@ -31765,7 +31796,7 @@ var JournalService = class {
|
|
|
31765
31796
|
// deliver the logs before this file, so a file older than the newest Cargo event is ignored.
|
|
31766
31797
|
async readCargoFile(dir, newestCargoEvent) {
|
|
31767
31798
|
try {
|
|
31768
|
-
const j = JSON.parse(await
|
|
31799
|
+
const j = JSON.parse(await readFile3(path6.join(dir, "Cargo.json"), "utf8"));
|
|
31769
31800
|
if (!Array.isArray(j.Inventory) || j.Vessel !== void 0 && j.Vessel !== "Ship") {
|
|
31770
31801
|
return null;
|
|
31771
31802
|
}
|
|
@@ -31781,7 +31812,7 @@ var JournalService = class {
|
|
|
31781
31812
|
async stationFile(kind2) {
|
|
31782
31813
|
let j;
|
|
31783
31814
|
try {
|
|
31784
|
-
j = JSON.parse(await
|
|
31815
|
+
j = JSON.parse(await readFile3(path6.join(this.requireDir(), STATION_FILES[kind2]), "utf8"));
|
|
31785
31816
|
} catch (err) {
|
|
31786
31817
|
if (err instanceof NotConfiguredError) throw err;
|
|
31787
31818
|
return null;
|
|
@@ -31845,7 +31876,7 @@ function marketDemand(item) {
|
|
|
31845
31876
|
return demand > 1 ? demand : 0;
|
|
31846
31877
|
}
|
|
31847
31878
|
async function isSettingsFolder(dir) {
|
|
31848
|
-
return stat4(
|
|
31879
|
+
return stat4(path6.join(dir, "Options")).then(
|
|
31849
31880
|
(s) => s.isDirectory(),
|
|
31850
31881
|
() => false
|
|
31851
31882
|
);
|
|
@@ -31867,12 +31898,14 @@ var InaraClient = class {
|
|
|
31867
31898
|
endpoint;
|
|
31868
31899
|
now;
|
|
31869
31900
|
async profile(name) {
|
|
31901
|
+
const cfg = await this.requireConfig();
|
|
31902
|
+
const who = name ?? cfg.cmdr;
|
|
31870
31903
|
const ev = await this.cache.getOrLoad(
|
|
31871
|
-
`profile:${name ?? ""}`,
|
|
31872
|
-
() => this.send("getCommanderProfile", name ? { searchName: name } : {})
|
|
31904
|
+
`profile:${cfg.cmdr}:${name ?? ""}`,
|
|
31905
|
+
() => this.send(cfg, "getCommanderProfile", name ? { searchName: name } : {})
|
|
31873
31906
|
);
|
|
31874
31907
|
if (ev.eventStatus === 204) {
|
|
31875
|
-
throw new UserError(`Inara: no commander found for "${
|
|
31908
|
+
throw new UserError(`Inara: no commander found for "${who}"`);
|
|
31876
31909
|
}
|
|
31877
31910
|
const d = ev.eventData ?? {};
|
|
31878
31911
|
const ship = d.commanderMainShip;
|
|
@@ -31906,7 +31939,7 @@ var InaraClient = class {
|
|
|
31906
31939
|
async communityGoals() {
|
|
31907
31940
|
const ev = await this.cache.getOrLoad(
|
|
31908
31941
|
"cgs",
|
|
31909
|
-
() => this.send("getCommunityGoalsRecent", [])
|
|
31942
|
+
async () => this.send(await this.requireConfig(), "getCommunityGoalsRecent", [])
|
|
31910
31943
|
);
|
|
31911
31944
|
if (ev.eventStatus === 204) return [];
|
|
31912
31945
|
const goals = ev.eventData ?? [];
|
|
@@ -31925,12 +31958,15 @@ var InaraClient = class {
|
|
|
31925
31958
|
description: String(g.goalDescriptionText ?? "").slice(0, 300)
|
|
31926
31959
|
}));
|
|
31927
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
|
+
}
|
|
31928
31968
|
// Inara asks clients not to hammer the API, so requests run one at a time.
|
|
31929
|
-
send(eventName, eventData) {
|
|
31930
|
-
const cfg = this.cfg;
|
|
31931
|
-
if (!cfg) {
|
|
31932
|
-
return Promise.reject(new NotConfiguredError("Inara", ["INARA_API_KEY", "INARA_CMDR"]));
|
|
31933
|
-
}
|
|
31969
|
+
send(cfg, eventName, eventData) {
|
|
31934
31970
|
const run = this.queue.then(async () => {
|
|
31935
31971
|
const body = {
|
|
31936
31972
|
header: {
|
|
@@ -31956,7 +31992,7 @@ var InaraClient = class {
|
|
|
31956
31992
|
});
|
|
31957
31993
|
if (res.header?.eventStatus === 400) {
|
|
31958
31994
|
throw new UserError(
|
|
31959
|
-
`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.`
|
|
31960
31996
|
);
|
|
31961
31997
|
}
|
|
31962
31998
|
const ev = res.events?.[0];
|
|
@@ -32006,8 +32042,11 @@ var EdsmClient = class {
|
|
|
32006
32042
|
}
|
|
32007
32043
|
return out;
|
|
32008
32044
|
}
|
|
32045
|
+
config() {
|
|
32046
|
+
return typeof this.cfg === "function" ? this.cfg() : Promise.resolve(this.cfg);
|
|
32047
|
+
}
|
|
32009
32048
|
async call(endpoint, params) {
|
|
32010
|
-
const cfg = this.
|
|
32049
|
+
const cfg = await this.config();
|
|
32011
32050
|
if (!cfg) throw new NotConfiguredError("EDSM", ["EDSM_API_KEY", "EDSM_CMDR"]);
|
|
32012
32051
|
const url2 = new URL(`${this.baseUrl}/${endpoint}`);
|
|
32013
32052
|
url2.search = new URLSearchParams({
|
|
@@ -32029,7 +32068,7 @@ function check2(b) {
|
|
|
32029
32068
|
if (b.msgnum === 100) return;
|
|
32030
32069
|
if (b.msgnum === 203) {
|
|
32031
32070
|
throw new UserError(
|
|
32032
|
-
"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."
|
|
32033
32072
|
);
|
|
32034
32073
|
}
|
|
32035
32074
|
throw new UserError(`EDSM error ${b.msgnum}: ${b.msg}`);
|
|
@@ -32439,8 +32478,8 @@ var SpanshClient = class {
|
|
|
32439
32478
|
let droppedOld = 0;
|
|
32440
32479
|
let loaded = 0;
|
|
32441
32480
|
let partial2 = false;
|
|
32442
|
-
for (let
|
|
32443
|
-
if (
|
|
32481
|
+
for (let page2 = 0; loaded < q.maxStations; page2++) {
|
|
32482
|
+
if (page2 > 0 && deadline !== void 0 && this.now() >= deadline) {
|
|
32444
32483
|
partial2 = true;
|
|
32445
32484
|
break;
|
|
32446
32485
|
}
|
|
@@ -32448,7 +32487,7 @@ var SpanshClient = class {
|
|
|
32448
32487
|
filters,
|
|
32449
32488
|
sort: [{ distance: { direction: "asc" } }],
|
|
32450
32489
|
size: SEARCH_PAGE_SIZE,
|
|
32451
|
-
page,
|
|
32490
|
+
page: page2,
|
|
32452
32491
|
reference_system: q.near
|
|
32453
32492
|
};
|
|
32454
32493
|
let res;
|
|
@@ -32551,14 +32590,14 @@ var SpanshClient = class {
|
|
|
32551
32590
|
cached(key, load) {
|
|
32552
32591
|
return this.cache.getOrLoad(key, load);
|
|
32553
32592
|
}
|
|
32554
|
-
async get(
|
|
32555
|
-
const { status, body } = await this.raw(
|
|
32593
|
+
async get(path15) {
|
|
32594
|
+
const { status, body } = await this.raw(path15);
|
|
32556
32595
|
if (status >= 400) throw new SpanshError(status, body);
|
|
32557
32596
|
return body;
|
|
32558
32597
|
}
|
|
32559
|
-
async post(
|
|
32598
|
+
async post(path15, body, contentType, opts) {
|
|
32560
32599
|
const res = await this.raw(
|
|
32561
|
-
|
|
32600
|
+
path15,
|
|
32562
32601
|
{ method: "POST", body, headers: { "content-type": contentType } },
|
|
32563
32602
|
opts
|
|
32564
32603
|
);
|
|
@@ -32566,7 +32605,7 @@ var SpanshClient = class {
|
|
|
32566
32605
|
return res.body;
|
|
32567
32606
|
}
|
|
32568
32607
|
// Spansh publishes no quota; spacing requests keeps us a polite client.
|
|
32569
|
-
async raw(
|
|
32608
|
+
async raw(path15, init = {}, opts) {
|
|
32570
32609
|
const turn = this.gate.then(async () => {
|
|
32571
32610
|
const wait = this.lastStart + this.minIntervalMs - Date.now();
|
|
32572
32611
|
if (wait > 0) await this.sleep(wait);
|
|
@@ -32574,7 +32613,7 @@ var SpanshClient = class {
|
|
|
32574
32613
|
});
|
|
32575
32614
|
this.gate = turn.catch(() => void 0);
|
|
32576
32615
|
await turn;
|
|
32577
|
-
const { status, text } = await this.http.text(`${this.baseUrl}${
|
|
32616
|
+
const { status, text } = await this.http.text(`${this.baseUrl}${path15}`, init, opts);
|
|
32578
32617
|
let body;
|
|
32579
32618
|
try {
|
|
32580
32619
|
body = text ? parseSpanshJson(text) : void 0;
|
|
@@ -32616,17 +32655,34 @@ function mapRoute(r) {
|
|
|
32616
32655
|
}
|
|
32617
32656
|
|
|
32618
32657
|
// src/plans/store.ts
|
|
32619
|
-
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";
|
|
32620
32661
|
|
|
32621
32662
|
// src/fs/lock.ts
|
|
32622
|
-
import { randomUUID } from "crypto";
|
|
32623
|
-
import {
|
|
32624
|
-
|
|
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";
|
|
32625
32676
|
var LOCK_RETRY_MS = 50;
|
|
32626
32677
|
var LOCK_STALE_MS = 3e4;
|
|
32627
32678
|
var PENDING_DELETE_MS = 1e3;
|
|
32628
|
-
|
|
32629
|
-
|
|
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);
|
|
32630
32686
|
const deadline = Date.now() + timeoutMs;
|
|
32631
32687
|
let pendingSince;
|
|
32632
32688
|
const waitForDelete = async (err) => {
|
|
@@ -32663,7 +32719,7 @@ async function acquireLock(file2, timeoutMs, lockedMessage) {
|
|
|
32663
32719
|
}
|
|
32664
32720
|
const token = randomUUID();
|
|
32665
32721
|
const release = async () => {
|
|
32666
|
-
const held = await
|
|
32722
|
+
const held = await readFile4(lock, "utf8").catch(() => void 0);
|
|
32667
32723
|
if (held === token) await unlink(lock).catch(() => void 0);
|
|
32668
32724
|
};
|
|
32669
32725
|
try {
|
|
@@ -32682,18 +32738,21 @@ function pendingDelete(err) {
|
|
|
32682
32738
|
return process.platform === "win32" && (code === "EPERM" || code === "EACCES");
|
|
32683
32739
|
}
|
|
32684
32740
|
async function lockedWrite(file2, opts, update) {
|
|
32685
|
-
await
|
|
32686
|
-
|
|
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);
|
|
32687
32744
|
try {
|
|
32688
32745
|
const { content, result } = await update();
|
|
32689
32746
|
const tmp = `${file2}.${process.pid}.${randomUUID()}.tmp`;
|
|
32690
32747
|
try {
|
|
32691
|
-
await
|
|
32692
|
-
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);
|
|
32693
32751
|
} catch (err) {
|
|
32694
32752
|
await unlink(tmp).catch(() => void 0);
|
|
32695
32753
|
throw err;
|
|
32696
32754
|
}
|
|
32755
|
+
await opts.afterWrite?.();
|
|
32697
32756
|
return result;
|
|
32698
32757
|
} finally {
|
|
32699
32758
|
await release();
|
|
@@ -32702,7 +32761,7 @@ async function lockedWrite(file2, opts, update) {
|
|
|
32702
32761
|
async function breakStaleLock(lock) {
|
|
32703
32762
|
const aside = `${lock}.${process.pid}.${Date.now()}.stale`;
|
|
32704
32763
|
try {
|
|
32705
|
-
await
|
|
32764
|
+
await rename2(lock, aside);
|
|
32706
32765
|
} catch (err) {
|
|
32707
32766
|
if (err.code === "ENOENT") return true;
|
|
32708
32767
|
throw err;
|
|
@@ -32717,6 +32776,105 @@ async function breakStaleLock(lock) {
|
|
|
32717
32776
|
return true;
|
|
32718
32777
|
}
|
|
32719
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
|
+
|
|
32720
32878
|
// src/plans/schema.ts
|
|
32721
32879
|
var ItemKind = external_exports.enum([
|
|
32722
32880
|
"buy",
|
|
@@ -32774,16 +32932,28 @@ var PlanFileSchema = external_exports.object({
|
|
|
32774
32932
|
|
|
32775
32933
|
// src/plans/store.ts
|
|
32776
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
|
+
};
|
|
32777
32940
|
var PlanStore = class {
|
|
32778
32941
|
constructor(file2, now = () => /* @__PURE__ */ new Date(), opts = {}) {
|
|
32779
32942
|
this.file = file2;
|
|
32780
32943
|
this.now = now;
|
|
32781
32944
|
this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
|
|
32945
|
+
this.lockDir = opts.lockDir;
|
|
32946
|
+
this.guard = opts.guard;
|
|
32947
|
+
this.markdownDir = opts.markdownDir;
|
|
32782
32948
|
}
|
|
32783
32949
|
file;
|
|
32784
32950
|
now;
|
|
32951
|
+
lastCopyError = null;
|
|
32785
32952
|
queue = Promise.resolve();
|
|
32786
32953
|
lockTimeoutMs;
|
|
32954
|
+
lockDir;
|
|
32955
|
+
guard;
|
|
32956
|
+
markdownDir;
|
|
32787
32957
|
async list() {
|
|
32788
32958
|
const { plans } = await this.read();
|
|
32789
32959
|
return plans.map((p) => ({
|
|
@@ -32806,7 +32976,7 @@ var PlanStore = class {
|
|
|
32806
32976
|
const stamp = this.now().toISOString();
|
|
32807
32977
|
const idx = data.plans.findIndex((p) => p.id === parsed.data.id);
|
|
32808
32978
|
if (idx >= 0 && opts.overwrite === false) {
|
|
32809
|
-
throw new
|
|
32979
|
+
throw new PlanExistsError(parsed.data.id);
|
|
32810
32980
|
}
|
|
32811
32981
|
const plan = {
|
|
32812
32982
|
...parsed.data,
|
|
@@ -32847,13 +33017,13 @@ var PlanStore = class {
|
|
|
32847
33017
|
});
|
|
32848
33018
|
}
|
|
32849
33019
|
async read() {
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
|
|
32855
|
-
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}`);
|
|
32856
33025
|
}
|
|
33026
|
+
const raw = read2.text;
|
|
32857
33027
|
let json3;
|
|
32858
33028
|
try {
|
|
32859
33029
|
json3 = JSON.parse(raw);
|
|
@@ -32869,27 +33039,99 @@ var PlanStore = class {
|
|
|
32869
33039
|
// Read-modify-write runs one at a time so concurrent tool calls cannot lose updates.
|
|
32870
33040
|
// The lock file extends that across processes (e.g. Claude Desktop and Claude Code).
|
|
32871
33041
|
mutate(fn) {
|
|
32872
|
-
const run = this.queue.then(
|
|
32873
|
-
()
|
|
33042
|
+
const run = this.queue.then(async () => {
|
|
33043
|
+
await this.guard?.();
|
|
33044
|
+
let latest = null;
|
|
33045
|
+
let before = [];
|
|
33046
|
+
return lockedWrite(
|
|
32874
33047
|
this.file,
|
|
32875
33048
|
{
|
|
32876
33049
|
timeoutMs: this.lockTimeoutMs,
|
|
32877
|
-
|
|
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
|
+
}
|
|
32878
33056
|
},
|
|
32879
33057
|
async () => {
|
|
32880
33058
|
const data = await this.read();
|
|
33059
|
+
before = data.plans.map((p) => p.id);
|
|
32881
33060
|
const result = fn(data);
|
|
32882
|
-
|
|
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 };
|
|
32883
33069
|
}
|
|
32884
|
-
)
|
|
32885
|
-
);
|
|
33070
|
+
);
|
|
33071
|
+
});
|
|
32886
33072
|
this.queue = run.catch(() => void 0);
|
|
32887
33073
|
return run;
|
|
32888
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
|
+
}
|
|
32889
33133
|
};
|
|
32890
|
-
|
|
32891
|
-
// src/prefs/store.ts
|
|
32892
|
-
import { readFile as readFile7 } from "fs/promises";
|
|
33134
|
+
var PRUNABLE = /^[a-z0-9][a-z0-9-]{0,63}\.md$/;
|
|
32893
33135
|
|
|
32894
33136
|
// src/prefs/schema.ts
|
|
32895
33137
|
var Mode = external_exports.enum(["soft", "hard", "off"]);
|
|
@@ -33016,18 +33258,22 @@ var PreferencesStore = class {
|
|
|
33016
33258
|
constructor(file2, opts = {}) {
|
|
33017
33259
|
this.file = file2;
|
|
33018
33260
|
this.lockTimeoutMs = opts.lockTimeoutMs ?? LOCK_TIMEOUT_MS2;
|
|
33261
|
+
this.lockDir = opts.lockDir;
|
|
33262
|
+
this.guard = opts.guard;
|
|
33019
33263
|
}
|
|
33020
33264
|
file;
|
|
33021
33265
|
queue = Promise.resolve();
|
|
33022
33266
|
lockTimeoutMs;
|
|
33267
|
+
lockDir;
|
|
33268
|
+
guard;
|
|
33023
33269
|
async get() {
|
|
33024
|
-
|
|
33025
|
-
|
|
33026
|
-
|
|
33027
|
-
|
|
33028
|
-
|
|
33029
|
-
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}`);
|
|
33030
33275
|
}
|
|
33276
|
+
const raw = read2.text;
|
|
33031
33277
|
let json3;
|
|
33032
33278
|
try {
|
|
33033
33279
|
json3 = JSON.parse(raw);
|
|
@@ -33049,22 +33295,30 @@ var PreferencesStore = class {
|
|
|
33049
33295
|
}
|
|
33050
33296
|
async write(patch) {
|
|
33051
33297
|
const parsed = parsePatch(patch);
|
|
33298
|
+
await this.guard?.();
|
|
33052
33299
|
return lockedWrite(
|
|
33053
33300
|
this.file,
|
|
33054
33301
|
{
|
|
33055
33302
|
timeoutMs: this.lockTimeoutMs,
|
|
33303
|
+
lockDir: this.lockDir,
|
|
33056
33304
|
lockedMessage: "Preferences file is locked by another process; try again"
|
|
33057
33305
|
},
|
|
33058
33306
|
async () => {
|
|
33059
33307
|
const next = applyPatch(await this.get(), parsed);
|
|
33060
|
-
|
|
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 };
|
|
33061
33315
|
}
|
|
33062
33316
|
);
|
|
33063
33317
|
}
|
|
33064
33318
|
};
|
|
33065
33319
|
|
|
33066
33320
|
// src/ships/data.ts
|
|
33067
|
-
import { mkdir as mkdir4, readFile as
|
|
33321
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
33068
33322
|
import path9 from "path";
|
|
33069
33323
|
import { gunzipSync } from "zlib";
|
|
33070
33324
|
|
|
@@ -33308,14 +33562,14 @@ function readTar(buf) {
|
|
|
33308
33562
|
comment = paxFields(data).comment ?? comment;
|
|
33309
33563
|
continue;
|
|
33310
33564
|
}
|
|
33311
|
-
let
|
|
33565
|
+
let path15 = str3(h, 0, 100);
|
|
33312
33566
|
if (str3(h, 257, 6) === "ustar") {
|
|
33313
33567
|
const prefix = str3(h, 345, 155);
|
|
33314
|
-
if (prefix)
|
|
33568
|
+
if (prefix) path15 = `${prefix}/${path15}`;
|
|
33315
33569
|
}
|
|
33316
|
-
if (nextPath !== null)
|
|
33570
|
+
if (nextPath !== null) path15 = nextPath;
|
|
33317
33571
|
nextPath = null;
|
|
33318
|
-
if (type === "0" || type === "\0") files.push({ path:
|
|
33572
|
+
if (type === "0" || type === "\0") files.push({ path: path15, data });
|
|
33319
33573
|
}
|
|
33320
33574
|
return { files, comment };
|
|
33321
33575
|
}
|
|
@@ -33581,7 +33835,7 @@ var ShipDataService = class {
|
|
|
33581
33835
|
}
|
|
33582
33836
|
async loadDisk() {
|
|
33583
33837
|
try {
|
|
33584
|
-
const snap = JSON.parse(await
|
|
33838
|
+
const snap = JSON.parse(await readFile5(this.file, "utf8"));
|
|
33585
33839
|
if (snap.version !== 1 || typeof snap.commit !== "string") return;
|
|
33586
33840
|
const data = buildShipData(snap.commit, snap.files);
|
|
33587
33841
|
if (this.data) return;
|
|
@@ -33600,18 +33854,756 @@ var ShipDataService = class {
|
|
|
33600
33854
|
};
|
|
33601
33855
|
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
33602
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
|
+
|
|
33603
34574
|
// src/deps.ts
|
|
34575
|
+
var envKey = (c) => c ? { apiKey: c.apiKey, cmdr: c.cmdr } : void 0;
|
|
33604
34576
|
function buildDeps(config2, opts = {}) {
|
|
33605
34577
|
const caches = new CacheRegistry();
|
|
33606
34578
|
const http = new HttpClient({ userAgent: config2.userAgent, fetchFn: opts.fetchFn });
|
|
33607
34579
|
const catalog = new CatalogService(http, config2.cacheDir);
|
|
33608
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;
|
|
33609
34601
|
const inaraCache = new TtlCache("inara", 6e4);
|
|
33610
34602
|
caches.register(inaraCache);
|
|
33611
|
-
const inara = new InaraClient(http,
|
|
34603
|
+
const inara = new InaraClient(http, () => settings.inara(), inaraCache);
|
|
33612
34604
|
const edsmCache = new TtlCache("edsm", 6e4);
|
|
33613
34605
|
caches.register(edsmCache);
|
|
33614
|
-
const edsm = new EdsmClient(http,
|
|
34606
|
+
const edsm = new EdsmClient(http, () => settings.edsm(), edsmCache);
|
|
33615
34607
|
const spanshCache = new TtlCache("spansh", 3e5);
|
|
33616
34608
|
caches.register(spanshCache);
|
|
33617
34609
|
const spanshSystemCache = new TtlCache("spansh-systems", 864e5);
|
|
@@ -33625,12 +34617,9 @@ function buildDeps(config2, opts = {}) {
|
|
|
33625
34617
|
const capiHttp = new HttpClient({ userAgent: `EDCD-EdMcp-${VERSION}`, fetchFn: opts.fetchFn });
|
|
33626
34618
|
const capiCache = new TtlCache("capi", 6e4);
|
|
33627
34619
|
caches.register(capiCache);
|
|
33628
|
-
const capiAuth = new CapiAuth(
|
|
33629
|
-
|
|
33630
|
-
|
|
33631
|
-
new TokenStore(path10.join(config2.configDir, "capi-token.json")),
|
|
33632
|
-
{ onSessionChange: () => void capiCache.clear() }
|
|
33633
|
-
);
|
|
34620
|
+
const capiAuth = new CapiAuth(capiHttp, config2.capi, new TokenStore(secrets), {
|
|
34621
|
+
onSessionChange: () => void capiCache.clear()
|
|
34622
|
+
});
|
|
33634
34623
|
const capi = new CapiClient(capiHttp, capiAuth, capiCache);
|
|
33635
34624
|
const journal = opts.journal ?? new JournalService(config2.journalDir);
|
|
33636
34625
|
return {
|
|
@@ -33650,8 +34639,30 @@ function buildDeps(config2, opts = {}) {
|
|
|
33650
34639
|
},
|
|
33651
34640
|
jumpRange: async () => (await journal.freshShip())?.max_jump_range ?? null,
|
|
33652
34641
|
journal,
|
|
33653
|
-
|
|
33654
|
-
|
|
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
|
|
33655
34666
|
};
|
|
33656
34667
|
}
|
|
33657
34668
|
|
|
@@ -34029,8 +35040,8 @@ function getErrorMap2() {
|
|
|
34029
35040
|
|
|
34030
35041
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
34031
35042
|
var makeIssue = (params) => {
|
|
34032
|
-
const { data, path:
|
|
34033
|
-
const fullPath = [...
|
|
35043
|
+
const { data, path: path15, errorMaps, issueData } = params;
|
|
35044
|
+
const fullPath = [...path15, ...issueData.path || []];
|
|
34034
35045
|
const fullIssue = {
|
|
34035
35046
|
...issueData,
|
|
34036
35047
|
path: fullPath
|
|
@@ -34145,11 +35156,11 @@ var errorUtil;
|
|
|
34145
35156
|
|
|
34146
35157
|
// node_modules/zod/v3/types.js
|
|
34147
35158
|
var ParseInputLazyPath = class {
|
|
34148
|
-
constructor(parent, value,
|
|
35159
|
+
constructor(parent, value, path15, key) {
|
|
34149
35160
|
this._cachedPath = [];
|
|
34150
35161
|
this.parent = parent;
|
|
34151
35162
|
this.data = value;
|
|
34152
|
-
this._path =
|
|
35163
|
+
this._path = path15;
|
|
34153
35164
|
this._key = key;
|
|
34154
35165
|
}
|
|
34155
35166
|
get path() {
|
|
@@ -36161,10 +37172,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36161
37172
|
catchall: index
|
|
36162
37173
|
});
|
|
36163
37174
|
}
|
|
36164
|
-
pick(
|
|
37175
|
+
pick(mask2) {
|
|
36165
37176
|
const shape = {};
|
|
36166
|
-
for (const key of util.objectKeys(
|
|
36167
|
-
if (
|
|
37177
|
+
for (const key of util.objectKeys(mask2)) {
|
|
37178
|
+
if (mask2[key] && this.shape[key]) {
|
|
36168
37179
|
shape[key] = this.shape[key];
|
|
36169
37180
|
}
|
|
36170
37181
|
}
|
|
@@ -36173,10 +37184,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36173
37184
|
shape: () => shape
|
|
36174
37185
|
});
|
|
36175
37186
|
}
|
|
36176
|
-
omit(
|
|
37187
|
+
omit(mask2) {
|
|
36177
37188
|
const shape = {};
|
|
36178
37189
|
for (const key of util.objectKeys(this.shape)) {
|
|
36179
|
-
if (!
|
|
37190
|
+
if (!mask2[key]) {
|
|
36180
37191
|
shape[key] = this.shape[key];
|
|
36181
37192
|
}
|
|
36182
37193
|
}
|
|
@@ -36191,11 +37202,11 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36191
37202
|
deepPartial() {
|
|
36192
37203
|
return deepPartialify(this);
|
|
36193
37204
|
}
|
|
36194
|
-
partial(
|
|
37205
|
+
partial(mask2) {
|
|
36195
37206
|
const newShape = {};
|
|
36196
37207
|
for (const key of util.objectKeys(this.shape)) {
|
|
36197
37208
|
const fieldSchema = this.shape[key];
|
|
36198
|
-
if (
|
|
37209
|
+
if (mask2 && !mask2[key]) {
|
|
36199
37210
|
newShape[key] = fieldSchema;
|
|
36200
37211
|
} else {
|
|
36201
37212
|
newShape[key] = fieldSchema.optional();
|
|
@@ -36206,10 +37217,10 @@ var ZodObject2 = class _ZodObject extends ZodType2 {
|
|
|
36206
37217
|
shape: () => newShape
|
|
36207
37218
|
});
|
|
36208
37219
|
}
|
|
36209
|
-
required(
|
|
37220
|
+
required(mask2) {
|
|
36210
37221
|
const newShape = {};
|
|
36211
37222
|
for (const key of util.objectKeys(this.shape)) {
|
|
36212
|
-
if (
|
|
37223
|
+
if (mask2 && !mask2[key]) {
|
|
36213
37224
|
newShape[key] = this.shape[key];
|
|
36214
37225
|
} else {
|
|
36215
37226
|
const fieldSchema = this.shape[key];
|
|
@@ -37700,11 +38711,11 @@ function normalizeObjectSchema(schema) {
|
|
|
37700
38711
|
}
|
|
37701
38712
|
return void 0;
|
|
37702
38713
|
}
|
|
37703
|
-
function getDotPath(
|
|
37704
|
-
if (
|
|
38714
|
+
function getDotPath(path15) {
|
|
38715
|
+
if (path15.length === 0) {
|
|
37705
38716
|
return "object root";
|
|
37706
38717
|
}
|
|
37707
|
-
return
|
|
38718
|
+
return path15.reduce((acc, seg, index) => {
|
|
37708
38719
|
if (index === 0) {
|
|
37709
38720
|
return String(seg);
|
|
37710
38721
|
}
|
|
@@ -39697,7 +40708,7 @@ var Protocol = class {
|
|
|
39697
40708
|
}
|
|
39698
40709
|
};
|
|
39699
40710
|
}
|
|
39700
|
-
const cancel = (
|
|
40711
|
+
const cancel = (reason2) => {
|
|
39701
40712
|
this._responseHandlers.delete(messageId);
|
|
39702
40713
|
this._progressHandlers.delete(messageId);
|
|
39703
40714
|
this._cleanupTimeout(messageId);
|
|
@@ -39706,10 +40717,10 @@ var Protocol = class {
|
|
|
39706
40717
|
method: "notifications/cancelled",
|
|
39707
40718
|
params: {
|
|
39708
40719
|
requestId: messageId,
|
|
39709
|
-
reason: String(
|
|
40720
|
+
reason: String(reason2)
|
|
39710
40721
|
}
|
|
39711
40722
|
}, { relatedRequestId, resumptionToken, onresumptiontoken }).catch((error63) => this._onerror(new Error(`Failed to send cancellation: ${error63}`)));
|
|
39712
|
-
const error62 =
|
|
40723
|
+
const error62 = reason2 instanceof McpError ? reason2 : new McpError(ErrorCode.RequestTimeout, String(reason2));
|
|
39713
40724
|
reject(error62);
|
|
39714
40725
|
};
|
|
39715
40726
|
this._responseHandlers.set(messageId, (response) => {
|
|
@@ -43002,6 +44013,27 @@ function wrap(fn) {
|
|
|
43002
44013
|
}
|
|
43003
44014
|
};
|
|
43004
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
|
+
}
|
|
43005
44037
|
|
|
43006
44038
|
// src/tools/build.ts
|
|
43007
44039
|
var SCO_NOTE = "Jump range uses the drive's normal values, not supercruise overcharge.";
|
|
@@ -43028,6 +44060,13 @@ function registerBuildStatsTool(server, deps) {
|
|
|
43028
44060
|
server.registerTool(
|
|
43029
44061
|
"build_stats",
|
|
43030
44062
|
{
|
|
44063
|
+
annotations: {
|
|
44064
|
+
title: "Build Stats",
|
|
44065
|
+
readOnlyHint: true,
|
|
44066
|
+
destructiveHint: false,
|
|
44067
|
+
idempotentHint: true,
|
|
44068
|
+
openWorldHint: true
|
|
44069
|
+
},
|
|
43031
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.",
|
|
43032
44071
|
inputSchema: {
|
|
43033
44072
|
build: BuildInput.optional().describe("Where to load the build. Default: the journal."),
|
|
@@ -43166,12 +44205,12 @@ function diff(before, after) {
|
|
|
43166
44205
|
const a = flatten(before, "", /* @__PURE__ */ new Map());
|
|
43167
44206
|
const b = flatten(after, "", /* @__PURE__ */ new Map());
|
|
43168
44207
|
const out = {};
|
|
43169
|
-
for (const
|
|
43170
|
-
const x = a.get(
|
|
43171
|
-
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;
|
|
43172
44211
|
if (x === y) continue;
|
|
43173
44212
|
const change = typeof x === "number" && typeof y === "number" ? Math.round((y - x) * 1e4) / 1e4 : null;
|
|
43174
|
-
out[
|
|
44213
|
+
out[path15] = { before: x, after: y, change };
|
|
43175
44214
|
}
|
|
43176
44215
|
return out;
|
|
43177
44216
|
}
|
|
@@ -43181,6 +44220,13 @@ function registerBuildTools(server, deps) {
|
|
|
43181
44220
|
server.registerTool(
|
|
43182
44221
|
"build_parse_slef",
|
|
43183
44222
|
{
|
|
44223
|
+
annotations: {
|
|
44224
|
+
title: "Parse SLEF Build",
|
|
44225
|
+
readOnlyHint: true,
|
|
44226
|
+
destructiveHint: false,
|
|
44227
|
+
idempotentHint: true,
|
|
44228
|
+
openWorldHint: true
|
|
44229
|
+
},
|
|
43184
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.",
|
|
43185
44231
|
inputSchema: { slef: external_exports.string().describe("SLEF JSON text") }
|
|
43186
44232
|
},
|
|
@@ -43230,6 +44276,13 @@ function registerCacheTools(server, deps) {
|
|
|
43230
44276
|
server.registerTool(
|
|
43231
44277
|
"cache_refresh",
|
|
43232
44278
|
{
|
|
44279
|
+
annotations: {
|
|
44280
|
+
title: "Refresh Cache",
|
|
44281
|
+
readOnlyHint: false,
|
|
44282
|
+
destructiveHint: false,
|
|
44283
|
+
idempotentHint: true,
|
|
44284
|
+
openWorldHint: true
|
|
44285
|
+
},
|
|
43233
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.",
|
|
43234
44287
|
inputSchema: { source: external_exports.string().optional() }
|
|
43235
44288
|
},
|
|
@@ -43265,6 +44318,13 @@ function registerCacheTools(server, deps) {
|
|
|
43265
44318
|
server.registerTool(
|
|
43266
44319
|
"cache_status",
|
|
43267
44320
|
{
|
|
44321
|
+
annotations: {
|
|
44322
|
+
title: "Cache Status",
|
|
44323
|
+
readOnlyHint: true,
|
|
44324
|
+
destructiveHint: false,
|
|
44325
|
+
idempotentHint: true,
|
|
44326
|
+
openWorldHint: false
|
|
44327
|
+
},
|
|
43268
44328
|
description: "Show cached entries and their age per source, and the catalog and ship data versions."
|
|
43269
44329
|
},
|
|
43270
44330
|
wrap(async () => ({
|
|
@@ -43279,6 +44339,13 @@ function registerCapiTools(server, deps) {
|
|
|
43279
44339
|
server.registerTool(
|
|
43280
44340
|
"capi_login",
|
|
43281
44341
|
{
|
|
44342
|
+
annotations: {
|
|
44343
|
+
title: "Log In to Frontier",
|
|
44344
|
+
readOnlyHint: false,
|
|
44345
|
+
destructiveHint: false,
|
|
44346
|
+
idempotentHint: false,
|
|
44347
|
+
openWorldHint: true
|
|
44348
|
+
},
|
|
43282
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.",
|
|
43283
44350
|
inputSchema: { force: external_exports.boolean().optional() }
|
|
43284
44351
|
},
|
|
@@ -43294,13 +44361,29 @@ function registerCapiTools(server, deps) {
|
|
|
43294
44361
|
server.registerTool(
|
|
43295
44362
|
"capi_profile",
|
|
43296
44363
|
{
|
|
44364
|
+
annotations: {
|
|
44365
|
+
title: "Frontier Profile",
|
|
44366
|
+
readOnlyHint: true,
|
|
44367
|
+
destructiveHint: false,
|
|
44368
|
+
idempotentHint: true,
|
|
44369
|
+
openWorldHint: true
|
|
44370
|
+
},
|
|
43297
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."
|
|
43298
44372
|
},
|
|
43299
44373
|
wrap(async () => deps.capi.profile())
|
|
43300
44374
|
);
|
|
43301
44375
|
server.registerTool(
|
|
43302
44376
|
"capi_logout",
|
|
43303
|
-
{
|
|
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
|
+
},
|
|
43304
44387
|
wrap(async () => {
|
|
43305
44388
|
await deps.capiAuth.logout();
|
|
43306
44389
|
return { state: "logged_out" };
|
|
@@ -43314,6 +44397,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43314
44397
|
server.registerTool(
|
|
43315
44398
|
"catalog_blueprint",
|
|
43316
44399
|
{
|
|
44400
|
+
annotations: {
|
|
44401
|
+
title: "Look Up Blueprint",
|
|
44402
|
+
readOnlyHint: true,
|
|
44403
|
+
destructiveHint: false,
|
|
44404
|
+
idempotentHint: true,
|
|
44405
|
+
openWorldHint: true
|
|
44406
|
+
},
|
|
43317
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.",
|
|
43318
44408
|
inputSchema: { name: external_exports.string(), module: external_exports.string().optional() }
|
|
43319
44409
|
},
|
|
@@ -43322,6 +44412,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43322
44412
|
server.registerTool(
|
|
43323
44413
|
"catalog_engineers",
|
|
43324
44414
|
{
|
|
44415
|
+
annotations: {
|
|
44416
|
+
title: "Look Up Engineers",
|
|
44417
|
+
readOnlyHint: true,
|
|
44418
|
+
destructiveHint: false,
|
|
44419
|
+
idempotentHint: true,
|
|
44420
|
+
openWorldHint: true
|
|
44421
|
+
},
|
|
43325
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.",
|
|
43326
44423
|
inputSchema: {
|
|
43327
44424
|
blueprint: external_exports.string().optional(),
|
|
@@ -43342,6 +44439,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43342
44439
|
server.registerTool(
|
|
43343
44440
|
"catalog_material",
|
|
43344
44441
|
{
|
|
44442
|
+
annotations: {
|
|
44443
|
+
title: "Look Up Material",
|
|
44444
|
+
readOnlyHint: true,
|
|
44445
|
+
destructiveHint: false,
|
|
44446
|
+
idempotentHint: true,
|
|
44447
|
+
openWorldHint: true
|
|
44448
|
+
},
|
|
43345
44449
|
description: "Engineering material by name or symbol: kind, grade, trade group, other materials in the same trade group, and material trader exchange rules.",
|
|
43346
44450
|
inputSchema: { name: external_exports.string() }
|
|
43347
44451
|
},
|
|
@@ -43350,6 +44454,13 @@ function registerCatalogTools(server, deps) {
|
|
|
43350
44454
|
server.registerTool(
|
|
43351
44455
|
"catalog_module",
|
|
43352
44456
|
{
|
|
44457
|
+
annotations: {
|
|
44458
|
+
title: "Look Up Module",
|
|
44459
|
+
readOnlyHint: true,
|
|
44460
|
+
destructiveHint: false,
|
|
44461
|
+
idempotentHint: true,
|
|
44462
|
+
openWorldHint: true
|
|
44463
|
+
},
|
|
43353
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.",
|
|
43354
44465
|
inputSchema: { query: external_exports.string() }
|
|
43355
44466
|
},
|
|
@@ -43760,23 +44871,23 @@ async function screenPermits(a) {
|
|
|
43760
44871
|
if (kept.length >= a.limit) break;
|
|
43761
44872
|
const refs = a.systemsOf(item).filter((r) => !exempt.has(r.name.toLowerCase()));
|
|
43762
44873
|
let needsPermit = false;
|
|
43763
|
-
let
|
|
44874
|
+
let open4 = true;
|
|
43764
44875
|
let missed = false;
|
|
43765
44876
|
for (const ref of refs) {
|
|
43766
44877
|
if (!lookup2.has(ref.name)) stop ??= stopReason();
|
|
43767
44878
|
if (stop && !lookup2.has(ref.name)) {
|
|
43768
44879
|
missed = true;
|
|
43769
|
-
|
|
44880
|
+
open4 = false;
|
|
43770
44881
|
continue;
|
|
43771
44882
|
}
|
|
43772
44883
|
const permit = await within(lookup2.check(ref), deadline - now());
|
|
43773
44884
|
if (permit === TIMED_OUT) {
|
|
43774
44885
|
stop = "time limit";
|
|
43775
44886
|
missed = true;
|
|
43776
|
-
|
|
44887
|
+
open4 = false;
|
|
43777
44888
|
continue;
|
|
43778
44889
|
}
|
|
43779
|
-
if (permit === null)
|
|
44890
|
+
if (permit === null) open4 = false;
|
|
43780
44891
|
if (permit === true) {
|
|
43781
44892
|
needsPermit = true;
|
|
43782
44893
|
if (!locked.has(ref.name.toLowerCase())) locked.set(ref.name.toLowerCase(), ref.name);
|
|
@@ -43793,7 +44904,7 @@ async function screenPermits(a) {
|
|
|
43793
44904
|
}
|
|
43794
44905
|
} else {
|
|
43795
44906
|
kept.push(item);
|
|
43796
|
-
if (
|
|
44907
|
+
if (open4) checkedOpen.push(item);
|
|
43797
44908
|
}
|
|
43798
44909
|
}
|
|
43799
44910
|
const notes = [];
|
|
@@ -43868,20 +44979,20 @@ function credits(n) {
|
|
|
43868
44979
|
function recommendStation(ranked, t) {
|
|
43869
44980
|
const best = ranked[0];
|
|
43870
44981
|
if (!best) return null;
|
|
43871
|
-
let
|
|
44982
|
+
let reason2;
|
|
43872
44983
|
if (best.effective_cost === null) {
|
|
43873
|
-
|
|
44984
|
+
reason2 = `fastest, about ${best.travel_min} min`;
|
|
43874
44985
|
} else {
|
|
43875
44986
|
const fastest = [...ranked].sort((a, b) => a.travel_s - b.travel_s)[0];
|
|
43876
44987
|
if (fastest !== best && fastest.price && best.price && fastest.price > best.price) {
|
|
43877
44988
|
const extra = best.travel_s - fastest.travel_s;
|
|
43878
44989
|
const more = extra < 60 ? `${extra} more seconds` : `${(extra / 60).toFixed(1)} more minutes`;
|
|
43879
|
-
|
|
44990
|
+
reason2 = `${credits(fastest.price - best.price)} cr cheaper than ${fastest.name} (${fastest.system}) for ${more} at ${credits(t.credits_per_hour)} cr/h`;
|
|
43880
44991
|
} else {
|
|
43881
|
-
|
|
44992
|
+
reason2 = `lowest cost including travel time, about ${best.travel_min} min`;
|
|
43882
44993
|
}
|
|
43883
44994
|
}
|
|
43884
|
-
return { name: best.name, system: best.system, reason };
|
|
44995
|
+
return { name: best.name, system: best.system, reason: reason2 };
|
|
43885
44996
|
}
|
|
43886
44997
|
|
|
43887
44998
|
// src/stations/search.ts
|
|
@@ -43952,20 +45063,20 @@ async function searchStations(spansh, query, rules, limit, permits = {}) {
|
|
|
43952
45063
|
spansh,
|
|
43953
45064
|
...permits
|
|
43954
45065
|
});
|
|
43955
|
-
const
|
|
45066
|
+
const page2 = screen.kept.map(
|
|
43956
45067
|
(h) => screen.locked(h.system) ? { ...h, needs_permit: true } : h
|
|
43957
45068
|
);
|
|
43958
|
-
const
|
|
45069
|
+
const open4 = mode === "exclude" ? ranked.filter((h) => !screen.locked(h.system)) : ranked;
|
|
43959
45070
|
const recommended = screen.checkedOpen.length ? recommendStation(screen.checkedOpen, rules.travel) : null;
|
|
43960
45071
|
return {
|
|
43961
45072
|
near: source.near,
|
|
43962
45073
|
total: source.total,
|
|
43963
|
-
matched:
|
|
45074
|
+
matched: open4.length,
|
|
43964
45075
|
total_capped: source.total_capped,
|
|
43965
|
-
stations:
|
|
45076
|
+
stations: page2,
|
|
43966
45077
|
recommended,
|
|
43967
45078
|
// Only the returned page counts, so every listed rule is broken by a station shown.
|
|
43968
|
-
relaxed: relaxAttempted ? brokenRules(
|
|
45079
|
+
relaxed: relaxAttempted ? brokenRules(page2, rules) : [],
|
|
43969
45080
|
relax_attempted: relaxAttempted,
|
|
43970
45081
|
applied_preferences: describeRules(rules),
|
|
43971
45082
|
notes: screen.notes
|
|
@@ -44006,6 +45117,13 @@ function registerMaterialsTools(server, deps) {
|
|
|
44006
45117
|
server.registerTool(
|
|
44007
45118
|
"materials_plan",
|
|
44008
45119
|
{
|
|
45120
|
+
annotations: {
|
|
45121
|
+
title: "Plan Materials",
|
|
45122
|
+
readOnlyHint: true,
|
|
45123
|
+
destructiveHint: false,
|
|
45124
|
+
idempotentHint: true,
|
|
45125
|
+
openWorldHint: true
|
|
45126
|
+
},
|
|
44009
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).",
|
|
44010
45128
|
inputSchema: {
|
|
44011
45129
|
blueprints: external_exports.array(
|
|
@@ -44207,17 +45325,17 @@ var better = (l1, t1, l2, t2) => l1 < l2 || l1 === l2 && t1 < t2;
|
|
|
44207
45325
|
var lateAt = (deadlines, t) => deadlines.filter((d) => t > d).length;
|
|
44208
45326
|
function findCycle(after) {
|
|
44209
45327
|
const state = [];
|
|
44210
|
-
const
|
|
45328
|
+
const path15 = [];
|
|
44211
45329
|
const visit2 = (i) => {
|
|
44212
45330
|
if (state[i] === 2) return null;
|
|
44213
|
-
if (state[i] === 1) return
|
|
45331
|
+
if (state[i] === 1) return path15.slice(path15.indexOf(i));
|
|
44214
45332
|
state[i] = 1;
|
|
44215
|
-
|
|
45333
|
+
path15.push(i);
|
|
44216
45334
|
for (const j of after[i] ?? []) {
|
|
44217
45335
|
const cycle = visit2(j);
|
|
44218
45336
|
if (cycle) return cycle;
|
|
44219
45337
|
}
|
|
44220
|
-
|
|
45338
|
+
path15.pop();
|
|
44221
45339
|
state[i] = 2;
|
|
44222
45340
|
return null;
|
|
44223
45341
|
};
|
|
@@ -44296,14 +45414,14 @@ function exactOrder(p) {
|
|
|
44296
45414
|
parent: null
|
|
44297
45415
|
});
|
|
44298
45416
|
}
|
|
44299
|
-
for (let
|
|
45417
|
+
for (let mask2 = 1; mask2 < 1 << n; mask2++) {
|
|
44300
45418
|
for (let last = 0; last < n; last++) {
|
|
44301
|
-
for (const label of fronts[
|
|
45419
|
+
for (const label of fronts[mask2][last]) {
|
|
44302
45420
|
for (let next = 0; next < n; next++) {
|
|
44303
|
-
if (
|
|
45421
|
+
if (mask2 & 1 << next || (needs[next] & mask2) !== needs[next]) continue;
|
|
44304
45422
|
const time3 = label.time + between[last][next];
|
|
44305
45423
|
const late2 = label.late + lateAt(p.deadlines[next] ?? [], time3);
|
|
44306
|
-
insert(fronts[
|
|
45424
|
+
insert(fronts[mask2 | 1 << next][next], { late: late2, time: time3, last: next, parent: label });
|
|
44307
45425
|
}
|
|
44308
45426
|
}
|
|
44309
45427
|
}
|
|
@@ -44427,6 +45545,13 @@ function registerMissionTools(server, deps) {
|
|
|
44427
45545
|
server.registerTool(
|
|
44428
45546
|
"missions_route",
|
|
44429
45547
|
{
|
|
45548
|
+
annotations: {
|
|
45549
|
+
title: "Plan Mission Route",
|
|
45550
|
+
readOnlyHint: true,
|
|
45551
|
+
destructiveHint: false,
|
|
45552
|
+
idempotentHint: true,
|
|
45553
|
+
openWorldHint: true
|
|
45554
|
+
},
|
|
44430
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.",
|
|
44431
45556
|
inputSchema: {
|
|
44432
45557
|
from: external_exports.string().optional().describe("Start system; default the journal location"),
|
|
@@ -44479,8 +45604,8 @@ function registerMissionTools(server, deps) {
|
|
|
44479
45604
|
places.push(await place2(stop.system, stop.station));
|
|
44480
45605
|
stops.push(stop);
|
|
44481
45606
|
} catch (e) {
|
|
44482
|
-
const
|
|
44483
|
-
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 });
|
|
44484
45609
|
}
|
|
44485
45610
|
}
|
|
44486
45611
|
const returnPlace = a.return_to ? await endpoint("return_to", a.return_to.system, a.return_to.station ?? null) : void 0;
|
|
@@ -44570,13 +45695,31 @@ function registerJournalTools(server, deps) {
|
|
|
44570
45695
|
server.registerTool(
|
|
44571
45696
|
"journal_status",
|
|
44572
45697
|
{
|
|
44573
|
-
|
|
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."
|
|
44574
45706
|
},
|
|
44575
|
-
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
|
+
})
|
|
44576
45712
|
);
|
|
44577
45713
|
server.registerTool(
|
|
44578
45714
|
"journal_commander",
|
|
44579
45715
|
{
|
|
45716
|
+
annotations: {
|
|
45717
|
+
title: "Journal Commander",
|
|
45718
|
+
readOnlyHint: true,
|
|
45719
|
+
destructiveHint: false,
|
|
45720
|
+
idempotentHint: true,
|
|
45721
|
+
openWorldHint: false
|
|
45722
|
+
},
|
|
44580
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
|
|
44581
45724
|
},
|
|
44582
45725
|
wrap(async () => {
|
|
@@ -44594,6 +45737,13 @@ function registerJournalTools(server, deps) {
|
|
|
44594
45737
|
server.registerTool(
|
|
44595
45738
|
"journal_materials",
|
|
44596
45739
|
{
|
|
45740
|
+
annotations: {
|
|
45741
|
+
title: "Journal Materials",
|
|
45742
|
+
readOnlyHint: true,
|
|
45743
|
+
destructiveHint: false,
|
|
45744
|
+
idempotentHint: true,
|
|
45745
|
+
openWorldHint: false
|
|
45746
|
+
},
|
|
44597
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,
|
|
44598
45748
|
inputSchema: { kinds: external_exports.array(external_exports.enum(["raw", "manufactured", "encoded"])).optional() }
|
|
44599
45749
|
},
|
|
@@ -44619,6 +45769,13 @@ function registerJournalTools(server, deps) {
|
|
|
44619
45769
|
server.registerTool(
|
|
44620
45770
|
"journal_missions",
|
|
44621
45771
|
{
|
|
45772
|
+
annotations: {
|
|
45773
|
+
title: "Journal Missions",
|
|
45774
|
+
readOnlyHint: true,
|
|
45775
|
+
destructiveHint: false,
|
|
45776
|
+
idempotentHint: true,
|
|
45777
|
+
openWorldHint: false
|
|
45778
|
+
},
|
|
44622
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
|
|
44623
45780
|
},
|
|
44624
45781
|
wrap(async () => {
|
|
@@ -44634,6 +45791,13 @@ function registerJournalTools(server, deps) {
|
|
|
44634
45791
|
server.registerTool(
|
|
44635
45792
|
"journal_engineers",
|
|
44636
45793
|
{
|
|
45794
|
+
annotations: {
|
|
45795
|
+
title: "Journal Engineers",
|
|
45796
|
+
readOnlyHint: true,
|
|
45797
|
+
destructiveHint: false,
|
|
45798
|
+
idempotentHint: true,
|
|
45799
|
+
openWorldHint: false
|
|
45800
|
+
},
|
|
44637
45801
|
description: "Engineer progress from the local game journal: invited, unlocked or known, with rank and rank progress. " + FRESHNESS_TEXT
|
|
44638
45802
|
},
|
|
44639
45803
|
wrap(async () => {
|
|
@@ -44853,6 +46017,13 @@ function registerLoopTools(server, deps) {
|
|
|
44853
46017
|
server.registerTool(
|
|
44854
46018
|
"trade_loops",
|
|
44855
46019
|
{
|
|
46020
|
+
annotations: {
|
|
46021
|
+
title: "Find Trade Loops",
|
|
46022
|
+
readOnlyHint: true,
|
|
46023
|
+
destructiveHint: false,
|
|
46024
|
+
idempotentHint: true,
|
|
46025
|
+
openWorldHint: true
|
|
46026
|
+
},
|
|
44856
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.`,
|
|
44857
46028
|
inputSchema: {
|
|
44858
46029
|
start: external_exports.enum(["here", "any"]).default("here"),
|
|
@@ -45000,25 +46171,25 @@ function registerLoopTools(server, deps) {
|
|
|
45000
46171
|
);
|
|
45001
46172
|
if (!startMarket) {
|
|
45002
46173
|
const info = await spansh.system(centre.system, STATION_LIMIT);
|
|
45003
|
-
const
|
|
45004
|
-
if (
|
|
46174
|
+
const entry2 = info.stations.find((st) => same2(st.name, name));
|
|
46175
|
+
if (entry2?.market_id == null) {
|
|
45005
46176
|
throw new UserError(`Spansh has no market for ${name} in ${info.name}.`);
|
|
45006
46177
|
}
|
|
45007
|
-
const id2 =
|
|
46178
|
+
const id2 = entry2.market_id;
|
|
45008
46179
|
const fresh = journalMarket?.market_id === id2 && localHours !== null && localHours <= a.max_price_age_days * 24;
|
|
45009
46180
|
const rows = fresh ? renamedLocalRows() : null;
|
|
45010
46181
|
startMarket = rows ? fromJournal2(
|
|
45011
46182
|
{
|
|
45012
|
-
name:
|
|
46183
|
+
name: entry2.name,
|
|
45013
46184
|
system: info.name,
|
|
45014
46185
|
system_id64: info.id64,
|
|
45015
46186
|
market_id: id2,
|
|
45016
46187
|
distance_ly: null,
|
|
45017
|
-
arrival_ls:
|
|
46188
|
+
arrival_ls: entry2.arrival_ls,
|
|
45018
46189
|
coords: info.coords,
|
|
45019
|
-
pad:
|
|
45020
|
-
type:
|
|
45021
|
-
planetary: PLANETARY.test(
|
|
46190
|
+
pad: entry2.pad,
|
|
46191
|
+
type: entry2.type,
|
|
46192
|
+
planetary: PLANETARY.test(entry2.type ?? ""),
|
|
45022
46193
|
market_age_hours: null,
|
|
45023
46194
|
market: []
|
|
45024
46195
|
},
|
|
@@ -45044,15 +46215,15 @@ function registerLoopTools(server, deps) {
|
|
|
45044
46215
|
break;
|
|
45045
46216
|
}
|
|
45046
46217
|
let market = null;
|
|
45047
|
-
let
|
|
46218
|
+
let failure3 = "Spansh has no market for it.";
|
|
45048
46219
|
try {
|
|
45049
46220
|
market = await includeMarket(inc);
|
|
45050
46221
|
} catch (err) {
|
|
45051
46222
|
if (!(err instanceof UserError)) throw err;
|
|
45052
|
-
|
|
46223
|
+
failure3 = err.message;
|
|
45053
46224
|
}
|
|
45054
46225
|
if (!market) {
|
|
45055
|
-
notes.push(`Cannot include ${inc.station}: ${
|
|
46226
|
+
notes.push(`Cannot include ${inc.station}: ${failure3}`);
|
|
45056
46227
|
} else if (!candidates.some((c) => c.market_id === market.market_id)) {
|
|
45057
46228
|
candidates.push(market);
|
|
45058
46229
|
}
|
|
@@ -45203,6 +46374,13 @@ function registerInaraTools(server, deps) {
|
|
|
45203
46374
|
server.registerTool(
|
|
45204
46375
|
"inara_commander_profile",
|
|
45205
46376
|
{
|
|
46377
|
+
annotations: {
|
|
46378
|
+
title: "Inara Commander Profile",
|
|
46379
|
+
readOnlyHint: true,
|
|
46380
|
+
destructiveHint: false,
|
|
46381
|
+
idempotentHint: true,
|
|
46382
|
+
openWorldHint: true
|
|
46383
|
+
},
|
|
45206
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.",
|
|
45207
46385
|
inputSchema: { name: external_exports.string().optional() }
|
|
45208
46386
|
},
|
|
@@ -45210,7 +46388,16 @@ function registerInaraTools(server, deps) {
|
|
|
45210
46388
|
);
|
|
45211
46389
|
server.registerTool(
|
|
45212
46390
|
"inara_community_goals",
|
|
45213
|
-
{
|
|
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
|
+
},
|
|
45214
46401
|
wrap(async () => deps.inara.communityGoals())
|
|
45215
46402
|
);
|
|
45216
46403
|
}
|
|
@@ -45221,6 +46408,13 @@ function registerEdsmTools(server, deps) {
|
|
|
45221
46408
|
server.registerTool(
|
|
45222
46409
|
"edsm_commander_location",
|
|
45223
46410
|
{
|
|
46411
|
+
annotations: {
|
|
46412
|
+
title: "Commander Location",
|
|
46413
|
+
readOnlyHint: true,
|
|
46414
|
+
destructiveHint: false,
|
|
46415
|
+
idempotentHint: true,
|
|
46416
|
+
openWorldHint: true
|
|
46417
|
+
},
|
|
45224
46418
|
description: "Commander's last known system, coordinates, docked station and ship, from EDSM. Cached 60 s."
|
|
45225
46419
|
},
|
|
45226
46420
|
wrap(async () => deps.edsm.position())
|
|
@@ -45228,6 +46422,13 @@ function registerEdsmTools(server, deps) {
|
|
|
45228
46422
|
server.registerTool(
|
|
45229
46423
|
"edsm_commander_materials",
|
|
45230
46424
|
{
|
|
46425
|
+
annotations: {
|
|
46426
|
+
title: "EDSM Commander Materials",
|
|
46427
|
+
readOnlyHint: true,
|
|
46428
|
+
destructiveHint: false,
|
|
46429
|
+
idempotentHint: true,
|
|
46430
|
+
openWorldHint: true
|
|
46431
|
+
},
|
|
45231
46432
|
description: "Commander's engineering materials (raw and manufactured), encoded data, and cargo, from EDSM. kinds defaults to all three. Cached 60 s.",
|
|
45232
46433
|
inputSchema: { kinds: external_exports.array(Kind).min(1).optional() }
|
|
45233
46434
|
},
|
|
@@ -45306,13 +46507,13 @@ function buildPool(a, catalog) {
|
|
|
45306
46507
|
const byEngineer = /* @__PURE__ */ new Map();
|
|
45307
46508
|
for (const item of a.plan.items) {
|
|
45308
46509
|
if (!item.engineer || item.status === "done" || item.status === "skipped") continue;
|
|
45309
|
-
const
|
|
46510
|
+
const entry2 = byEngineer.get(lc(item.engineer)) ?? {
|
|
45310
46511
|
id: item.id,
|
|
45311
46512
|
name: item.engineer,
|
|
45312
46513
|
descriptions: []
|
|
45313
46514
|
};
|
|
45314
|
-
|
|
45315
|
-
byEngineer.set(lc(item.engineer),
|
|
46515
|
+
entry2.descriptions.push(item.description);
|
|
46516
|
+
byEngineer.set(lc(item.engineer), entry2);
|
|
45316
46517
|
}
|
|
45317
46518
|
const baseFor = /* @__PURE__ */ new Map();
|
|
45318
46519
|
if (catalog) {
|
|
@@ -45548,6 +46749,13 @@ function registerRouteTools(server, deps) {
|
|
|
45548
46749
|
server.registerTool(
|
|
45549
46750
|
"route_stops",
|
|
45550
46751
|
{
|
|
46752
|
+
annotations: {
|
|
46753
|
+
title: "Plan Route Stops",
|
|
46754
|
+
readOnlyHint: false,
|
|
46755
|
+
destructiveHint: false,
|
|
46756
|
+
idempotentHint: true,
|
|
46757
|
+
openWorldHint: true
|
|
46758
|
+
},
|
|
45551
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.",
|
|
45552
46760
|
inputSchema: {
|
|
45553
46761
|
plan_id: external_exports.string().optional(),
|
|
@@ -45589,8 +46797,8 @@ function registerRouteTools(server, deps) {
|
|
|
45589
46797
|
);
|
|
45590
46798
|
const cycle = findCycle(graph);
|
|
45591
46799
|
if (cycle) {
|
|
45592
|
-
const
|
|
45593
|
-
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(" -> ")}`);
|
|
45594
46802
|
}
|
|
45595
46803
|
const rules = await resolveSearchRules(deps, a.preferences, a.use_preferences);
|
|
45596
46804
|
const travel = rules.travel;
|
|
@@ -45835,19 +47043,23 @@ function registerRouteTools(server, deps) {
|
|
|
45835
47043
|
const total = min2(totalSeconds);
|
|
45836
47044
|
let saved = false;
|
|
45837
47045
|
if (a.save_to_plan && a.plan_id) {
|
|
45838
|
-
|
|
45839
|
-
|
|
45840
|
-
|
|
45841
|
-
|
|
45842
|
-
|
|
45843
|
-
|
|
45844
|
-
|
|
45845
|
-
|
|
45846
|
-
|
|
45847
|
-
|
|
45848
|
-
|
|
45849
|
-
|
|
45850
|
-
|
|
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
|
+
}
|
|
45851
47063
|
}
|
|
45852
47064
|
return {
|
|
45853
47065
|
start: { system: startPlace.system, station: startPlace.station, source },
|
|
@@ -45873,66 +47085,26 @@ function registerRouteTools(server, deps) {
|
|
|
45873
47085
|
|
|
45874
47086
|
// src/tools/plans.ts
|
|
45875
47087
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
45876
|
-
import { mkdir as
|
|
45877
|
-
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)}
|
|
45878
47104
|
|
|
45879
|
-
|
|
45880
|
-
var box = {
|
|
45881
|
-
todo: "[ ]",
|
|
45882
|
-
in_progress: "[~]",
|
|
45883
|
-
done: "[x]",
|
|
45884
|
-
skipped: "[-]"
|
|
45885
|
-
};
|
|
45886
|
-
function planToMarkdown(plan) {
|
|
45887
|
-
const done = plan.items.filter((i) => i.status === "done" || i.status === "skipped").length;
|
|
45888
|
-
const out = [
|
|
45889
|
-
`# ${plan.name}`,
|
|
45890
|
-
"",
|
|
45891
|
-
`Plan \`${plan.id}\` \xB7 updated ${plan.updated_at} \xB7 ${done}/${plan.items.length} done`,
|
|
45892
|
-
""
|
|
45893
|
-
];
|
|
45894
|
-
if (plan.notes) out.push(plan.notes, "");
|
|
45895
|
-
if (plan.items.length) {
|
|
45896
|
-
out.push("## Items", "");
|
|
45897
|
-
for (const i of plan.items) {
|
|
45898
|
-
const where = [
|
|
45899
|
-
i.station ? `at ${i.station}` : void 0,
|
|
45900
|
-
i.engineer ? `engineer ${i.engineer}` : void 0
|
|
45901
|
-
].filter(Boolean);
|
|
45902
|
-
out.push(
|
|
45903
|
-
`- ${box[i.status]} **${i.kind}** ${i.description}${where.length ? ` \u2014 ${where.join(", ")}` : ""}`
|
|
45904
|
-
);
|
|
45905
|
-
if (i.materials?.length) {
|
|
45906
|
-
out.push(` - Materials: ${i.materials.map((m) => `${m.count}\xD7 ${m.name}`).join(", ")}`);
|
|
45907
|
-
}
|
|
45908
|
-
if (i.notes) out.push(` - Notes: ${i.notes}`);
|
|
47105
|
+
${tail}`);
|
|
45909
47106
|
}
|
|
45910
|
-
|
|
45911
|
-
}
|
|
45912
|
-
if (plan.stops.length) {
|
|
45913
|
-
out.push("## Stops", "");
|
|
45914
|
-
plan.stops.forEach((s, n) => {
|
|
45915
|
-
out.push(`${n + 1}. ${s.system}${s.station ? ` \u2014 ${s.station}` : ""}: ${s.purpose}`);
|
|
45916
|
-
});
|
|
45917
|
-
out.push("");
|
|
45918
|
-
}
|
|
45919
|
-
if (plan.route) {
|
|
45920
|
-
out.push("## Route", "");
|
|
45921
|
-
plan.route.stops.forEach((s, n) => out.push(`${n + 1}. ${s}`));
|
|
45922
|
-
const r = plan.route;
|
|
45923
|
-
const kind2 = r.plotted ? "plotted" : "estimate";
|
|
45924
|
-
out.push(
|
|
45925
|
-
"",
|
|
45926
|
-
`From ${r.from}; ${kind2}. Total ${r.total_min} min, computed ${r.computed_at}`,
|
|
45927
|
-
""
|
|
45928
|
-
);
|
|
45929
|
-
}
|
|
45930
|
-
return out.join("\n");
|
|
45931
|
-
}
|
|
45932
|
-
|
|
45933
|
-
// src/tools/plans.ts
|
|
45934
|
-
function registerPlanTools(server, deps) {
|
|
45935
|
-
const { plans } = deps;
|
|
47107
|
+
};
|
|
45936
47108
|
const exports = /* @__PURE__ */ new Map();
|
|
45937
47109
|
const serialise = (target, fn) => {
|
|
45938
47110
|
const key = process.platform === "linux" ? target : target.toLowerCase();
|
|
@@ -45945,25 +47117,61 @@ function registerPlanTools(server, deps) {
|
|
|
45945
47117
|
};
|
|
45946
47118
|
server.registerTool(
|
|
45947
47119
|
"plan_list",
|
|
45948
|
-
{
|
|
45949
|
-
|
|
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)
|
|
45950
47131
|
);
|
|
45951
47132
|
server.registerTool(
|
|
45952
47133
|
"plan_get",
|
|
45953
|
-
{
|
|
45954
|
-
|
|
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)
|
|
45955
47146
|
);
|
|
45956
47147
|
server.registerTool(
|
|
45957
47148
|
"plan_upsert",
|
|
45958
47149
|
{
|
|
47150
|
+
annotations: {
|
|
47151
|
+
title: "Save Plan",
|
|
47152
|
+
readOnlyHint: false,
|
|
47153
|
+
destructiveHint: true,
|
|
47154
|
+
idempotentHint: true,
|
|
47155
|
+
openWorldHint: false
|
|
47156
|
+
},
|
|
45959
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.",
|
|
45960
47158
|
inputSchema: { plan: PlanInputSchema }
|
|
45961
47159
|
},
|
|
45962
|
-
|
|
47160
|
+
wrapWarn(
|
|
47161
|
+
async ({ plan }) => saveOrReturnJson(plan, "Ask Claude to save it again later."),
|
|
47162
|
+
warnings
|
|
47163
|
+
)
|
|
45963
47164
|
);
|
|
45964
47165
|
server.registerTool(
|
|
45965
47166
|
"plan_update_item",
|
|
45966
47167
|
{
|
|
47168
|
+
annotations: {
|
|
47169
|
+
title: "Update Plan Item",
|
|
47170
|
+
readOnlyHint: false,
|
|
47171
|
+
destructiveHint: false,
|
|
47172
|
+
idempotentHint: true,
|
|
47173
|
+
openWorldHint: false
|
|
47174
|
+
},
|
|
45967
47175
|
description: "Set the status and/or notes of one plan item.",
|
|
45968
47176
|
inputSchema: {
|
|
45969
47177
|
plan_id: external_exports.string(),
|
|
@@ -45972,26 +47180,57 @@ function registerPlanTools(server, deps) {
|
|
|
45972
47180
|
notes: external_exports.string().optional()
|
|
45973
47181
|
}
|
|
45974
47182
|
},
|
|
45975
|
-
|
|
47183
|
+
wrapWarn(
|
|
45976
47184
|
async (a) => {
|
|
45977
47185
|
if (a.status === void 0 && a.notes === void 0) {
|
|
45978
47186
|
throw new UserError("Give status or notes to update");
|
|
45979
47187
|
}
|
|
45980
|
-
|
|
45981
|
-
|
|
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
|
|
45982
47204
|
)
|
|
45983
47205
|
);
|
|
45984
47206
|
server.registerTool(
|
|
45985
47207
|
"plan_delete",
|
|
45986
|
-
{
|
|
45987
|
-
|
|
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 }) => {
|
|
45988
47220
|
await plans.delete(id);
|
|
45989
47221
|
return { deleted: id };
|
|
45990
|
-
})
|
|
47222
|
+
}, warnings)
|
|
45991
47223
|
);
|
|
45992
47224
|
server.registerTool(
|
|
45993
47225
|
"plan_export",
|
|
45994
47226
|
{
|
|
47227
|
+
annotations: {
|
|
47228
|
+
title: "Export Plan",
|
|
47229
|
+
readOnlyHint: false,
|
|
47230
|
+
destructiveHint: true,
|
|
47231
|
+
idempotentHint: true,
|
|
47232
|
+
openWorldHint: false
|
|
47233
|
+
},
|
|
45995
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.",
|
|
45996
47235
|
inputSchema: {
|
|
45997
47236
|
id: external_exports.string(),
|
|
@@ -46000,7 +47239,7 @@ function registerPlanTools(server, deps) {
|
|
|
46000
47239
|
overwrite: external_exports.boolean().optional()
|
|
46001
47240
|
}
|
|
46002
47241
|
},
|
|
46003
|
-
|
|
47242
|
+
wrapWarn(
|
|
46004
47243
|
async (a) => {
|
|
46005
47244
|
const plan = await plans.get(a.id);
|
|
46006
47245
|
const json3 = a.format === "json";
|
|
@@ -46014,7 +47253,7 @@ function registerPlanTools(server, deps) {
|
|
|
46014
47253
|
if (!a.overwrite && await exists(target)) {
|
|
46015
47254
|
throw new UserError(`${target} exists; pass overwrite: true to replace it`);
|
|
46016
47255
|
}
|
|
46017
|
-
await
|
|
47256
|
+
await mkdir6(path13.dirname(target), { recursive: true });
|
|
46018
47257
|
const tmp = `${target}.${process.pid}.${randomUUID2()}.tmp`;
|
|
46019
47258
|
try {
|
|
46020
47259
|
await writeFile5(tmp, text);
|
|
@@ -46025,12 +47264,20 @@ function registerPlanTools(server, deps) {
|
|
|
46025
47264
|
}
|
|
46026
47265
|
});
|
|
46027
47266
|
return { path: target, bytes: Buffer.byteLength(text) };
|
|
46028
|
-
}
|
|
47267
|
+
},
|
|
47268
|
+
warnings
|
|
46029
47269
|
)
|
|
46030
47270
|
);
|
|
46031
47271
|
server.registerTool(
|
|
46032
47272
|
"plan_import",
|
|
46033
47273
|
{
|
|
47274
|
+
annotations: {
|
|
47275
|
+
title: "Import Plan",
|
|
47276
|
+
readOnlyHint: false,
|
|
47277
|
+
destructiveHint: true,
|
|
47278
|
+
idempotentHint: true,
|
|
47279
|
+
openWorldHint: false
|
|
47280
|
+
},
|
|
46034
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.",
|
|
46035
47282
|
inputSchema: {
|
|
46036
47283
|
path: external_exports.string(),
|
|
@@ -46038,14 +47285,14 @@ function registerPlanTools(server, deps) {
|
|
|
46038
47285
|
overwrite: external_exports.boolean().optional()
|
|
46039
47286
|
}
|
|
46040
47287
|
},
|
|
46041
|
-
|
|
47288
|
+
wrapWarn(async (a) => {
|
|
46042
47289
|
const source = resolveUserPath(a.path);
|
|
46043
47290
|
let raw;
|
|
46044
47291
|
try {
|
|
46045
|
-
if ((await
|
|
47292
|
+
if ((await stat7(source)).isDirectory()) {
|
|
46046
47293
|
throw new UserError(`${source} is a directory, not a plan file`);
|
|
46047
47294
|
}
|
|
46048
|
-
raw = await
|
|
47295
|
+
raw = await readFile7(source, "utf8");
|
|
46049
47296
|
} catch (err) {
|
|
46050
47297
|
if (err instanceof UserError) throw err;
|
|
46051
47298
|
if (err.code === "ENOENT") {
|
|
@@ -46064,13 +47311,15 @@ function registerPlanTools(server, deps) {
|
|
|
46064
47311
|
}
|
|
46065
47312
|
const parsed = PlanInputSchema.safeParse(json3);
|
|
46066
47313
|
if (!parsed.success) throw new UserError(`Invalid plan: ${external_exports.prettifyError(parsed.error)}`);
|
|
46067
|
-
return
|
|
46068
|
-
|
|
47314
|
+
return saveOrReturnJson(parsed.data, "Keep your original file and try again later.", {
|
|
47315
|
+
overwrite: a.overwrite === true
|
|
47316
|
+
});
|
|
47317
|
+
}, warnings)
|
|
46069
47318
|
);
|
|
46070
47319
|
}
|
|
46071
47320
|
async function exists(p) {
|
|
46072
47321
|
try {
|
|
46073
|
-
await
|
|
47322
|
+
await stat7(p);
|
|
46074
47323
|
return true;
|
|
46075
47324
|
} catch (err) {
|
|
46076
47325
|
if (err.code === "ENOENT") return false;
|
|
@@ -46083,17 +47332,31 @@ function registerPrefsTools(server, deps) {
|
|
|
46083
47332
|
server.registerTool(
|
|
46084
47333
|
"prefs_get",
|
|
46085
47334
|
{
|
|
47335
|
+
annotations: {
|
|
47336
|
+
title: "Get Preferences",
|
|
47337
|
+
readOnlyHint: true,
|
|
47338
|
+
destructiveHint: false,
|
|
47339
|
+
idempotentHint: true,
|
|
47340
|
+
openWorldHint: false
|
|
47341
|
+
},
|
|
46086
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)."
|
|
46087
47343
|
},
|
|
46088
|
-
|
|
47344
|
+
wrapWarn(async () => deps.prefs.get(), deps.warnings)
|
|
46089
47345
|
);
|
|
46090
47346
|
server.registerTool(
|
|
46091
47347
|
"prefs_set",
|
|
46092
47348
|
{
|
|
47349
|
+
annotations: {
|
|
47350
|
+
title: "Set Preferences",
|
|
47351
|
+
readOnlyHint: false,
|
|
47352
|
+
destructiveHint: false,
|
|
47353
|
+
idempotentHint: true,
|
|
47354
|
+
openWorldHint: false
|
|
47355
|
+
},
|
|
46093
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.',
|
|
46094
47357
|
inputSchema: { patch: PrefsPatchSchema }
|
|
46095
47358
|
},
|
|
46096
|
-
|
|
47359
|
+
wrapWarn(async ({ patch }) => deps.prefs.set(patch), deps.warnings)
|
|
46097
47360
|
);
|
|
46098
47361
|
}
|
|
46099
47362
|
|
|
@@ -46191,6 +47454,13 @@ function registerStationTools(server, deps) {
|
|
|
46191
47454
|
server.registerTool(
|
|
46192
47455
|
"station_info",
|
|
46193
47456
|
{
|
|
47457
|
+
annotations: {
|
|
47458
|
+
title: "Station Info",
|
|
47459
|
+
readOnlyHint: true,
|
|
47460
|
+
destructiveHint: false,
|
|
47461
|
+
idempotentHint: true,
|
|
47462
|
+
openWorldHint: false
|
|
47463
|
+
},
|
|
46194
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.",
|
|
46195
47465
|
inputSchema: {
|
|
46196
47466
|
part: external_exports.enum(["all", "market", "outfitting", "shipyard"]).optional(),
|
|
@@ -46286,6 +47556,13 @@ function registerSpanshTools(server, deps) {
|
|
|
46286
47556
|
server.registerTool(
|
|
46287
47557
|
"spansh_find_stations",
|
|
46288
47558
|
{
|
|
47559
|
+
annotations: {
|
|
47560
|
+
title: "Find Stations",
|
|
47561
|
+
readOnlyHint: true,
|
|
47562
|
+
destructiveHint: false,
|
|
47563
|
+
idempotentHint: true,
|
|
47564
|
+
openWorldHint: true
|
|
47565
|
+
},
|
|
46289
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.",
|
|
46290
47567
|
inputSchema: {
|
|
46291
47568
|
near: external_exports.string().describe("Exact reference system name"),
|
|
@@ -46326,6 +47603,13 @@ function registerSpanshTools(server, deps) {
|
|
|
46326
47603
|
server.registerTool(
|
|
46327
47604
|
"spansh_system",
|
|
46328
47605
|
{
|
|
47606
|
+
annotations: {
|
|
47607
|
+
title: "Look Up System",
|
|
47608
|
+
readOnlyHint: true,
|
|
47609
|
+
destructiveHint: false,
|
|
47610
|
+
idempotentHint: true,
|
|
47611
|
+
openWorldHint: true
|
|
47612
|
+
},
|
|
46329
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.",
|
|
46330
47614
|
inputSchema: { name: external_exports.string(), limit: Limit }
|
|
46331
47615
|
},
|
|
@@ -46334,6 +47618,13 @@ function registerSpanshTools(server, deps) {
|
|
|
46334
47618
|
server.registerTool(
|
|
46335
47619
|
"spansh_route",
|
|
46336
47620
|
{
|
|
47621
|
+
annotations: {
|
|
47622
|
+
title: "Plot Route",
|
|
47623
|
+
readOnlyHint: true,
|
|
47624
|
+
destructiveHint: false,
|
|
47625
|
+
idempotentHint: true,
|
|
47626
|
+
openWorldHint: true
|
|
47627
|
+
},
|
|
46337
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.",
|
|
46338
47629
|
inputSchema: {
|
|
46339
47630
|
from: external_exports.string(),
|
|
@@ -46444,7 +47735,7 @@ function recommendCommodityStation(stations, mode, maxAgeHours = Infinity) {
|
|
|
46444
47735
|
const best = stations.find(
|
|
46445
47736
|
(h) => h.market_age_hours === null || h.market_age_hours <= maxAgeHours
|
|
46446
47737
|
);
|
|
46447
|
-
const
|
|
47738
|
+
const reason2 = (b) => {
|
|
46448
47739
|
const trip = `about ${b.travel_min} min`;
|
|
46449
47740
|
if (b.value === null) {
|
|
46450
47741
|
return `${mode === "sell" ? "highest" : "lowest"} price, ${credits2(b.price ?? 0)} cr, ${trip}`;
|
|
@@ -46454,7 +47745,7 @@ function recommendCommodityStation(stations, mode, maxAgeHours = Infinity) {
|
|
|
46454
47745
|
}
|
|
46455
47746
|
return `best ${mode === "sell" ? "net return" : "total cost"} after travel time: ${credits2(b.value)} cr, ${trip}`;
|
|
46456
47747
|
};
|
|
46457
|
-
return best ? { name: best.name, system: best.system, reason:
|
|
47748
|
+
return best ? { name: best.name, system: best.system, reason: reason2(best) } : null;
|
|
46458
47749
|
}
|
|
46459
47750
|
|
|
46460
47751
|
// src/tools/trade.ts
|
|
@@ -46512,6 +47803,13 @@ function registerTradeTools(server, deps) {
|
|
|
46512
47803
|
server.registerTool(
|
|
46513
47804
|
"trade_route",
|
|
46514
47805
|
{
|
|
47806
|
+
annotations: {
|
|
47807
|
+
title: "Find Trade Route",
|
|
47808
|
+
readOnlyHint: true,
|
|
47809
|
+
destructiveHint: false,
|
|
47810
|
+
idempotentHint: true,
|
|
47811
|
+
openWorldHint: true
|
|
47812
|
+
},
|
|
46515
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.",
|
|
46516
47814
|
inputSchema: {
|
|
46517
47815
|
from_system: external_exports.string().optional(),
|
|
@@ -46653,6 +47951,13 @@ function registerTradeTools(server, deps) {
|
|
|
46653
47951
|
server.registerTool(
|
|
46654
47952
|
"commodity_prices",
|
|
46655
47953
|
{
|
|
47954
|
+
annotations: {
|
|
47955
|
+
title: "Commodity Prices",
|
|
47956
|
+
readOnlyHint: true,
|
|
47957
|
+
destructiveHint: false,
|
|
47958
|
+
idempotentHint: true,
|
|
47959
|
+
openWorldHint: true
|
|
47960
|
+
},
|
|
46656
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.",
|
|
46657
47962
|
inputSchema: {
|
|
46658
47963
|
commodity: external_exports.string().describe("Exact English commodity name, e.g. Gold"),
|
|
@@ -46709,7 +48014,7 @@ function registerTradeTools(server, deps) {
|
|
|
46709
48014
|
now
|
|
46710
48015
|
});
|
|
46711
48016
|
notes.push(...screen.notes);
|
|
46712
|
-
const
|
|
48017
|
+
const open4 = mode === "exclude" ? ranked.stations.filter((h) => !screen.locked(h.system)) : ranked.stations;
|
|
46713
48018
|
const applied_preferences = describeRules(rules);
|
|
46714
48019
|
if (!found.stations.length) {
|
|
46715
48020
|
notes.push("No stations found; check the exact English commodity name");
|
|
@@ -46727,7 +48032,7 @@ function registerTradeTools(server, deps) {
|
|
|
46727
48032
|
return {
|
|
46728
48033
|
near: found.near,
|
|
46729
48034
|
total: found.total,
|
|
46730
|
-
matched:
|
|
48035
|
+
matched: open4.length,
|
|
46731
48036
|
stations: screen.kept.map((st) => ({
|
|
46732
48037
|
...st,
|
|
46733
48038
|
...screen.locked(st.system) ? { needs_permit: true } : {},
|
|
@@ -46757,14 +48062,150 @@ function registerTradeTools(server, deps) {
|
|
|
46757
48062
|
);
|
|
46758
48063
|
}
|
|
46759
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
|
+
|
|
46760
48185
|
// src/server.ts
|
|
46761
|
-
function
|
|
48186
|
+
function createServer3(deps) {
|
|
46762
48187
|
const server = new McpServer({ name: "ed-mcp", version: VERSION });
|
|
46763
48188
|
if (deps.caches) {
|
|
46764
48189
|
registerCacheTools(server, { caches: deps.caches, refreshables: deps.refreshables });
|
|
46765
48190
|
}
|
|
46766
|
-
if (deps.plans)
|
|
46767
|
-
|
|
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
|
+
}
|
|
46768
48209
|
if (deps.catalog) registerCatalogTools(server, { catalog: deps.catalog });
|
|
46769
48210
|
if (deps.inara) registerInaraTools(server, { inara: deps.inara });
|
|
46770
48211
|
if (deps.edsm) registerEdsmTools(server, { edsm: deps.edsm });
|
|
@@ -46825,7 +48266,12 @@ function createServer2(deps) {
|
|
|
46825
48266
|
if (deps.capiAuth && deps.capi) {
|
|
46826
48267
|
registerCapiTools(server, { capiAuth: deps.capiAuth, capi: deps.capi });
|
|
46827
48268
|
}
|
|
46828
|
-
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
|
+
});
|
|
46829
48275
|
if (deps.journal) registerStationTools(server, { journal: deps.journal, catalog: deps.catalog });
|
|
46830
48276
|
registerBuildTools(server, { catalog: deps.catalog });
|
|
46831
48277
|
if (deps.ships) {
|
|
@@ -46843,7 +48289,9 @@ function createServer2(deps) {
|
|
|
46843
48289
|
// src/index.ts
|
|
46844
48290
|
async function main() {
|
|
46845
48291
|
const config2 = loadConfig(process.env, defaultDirs());
|
|
46846
|
-
const
|
|
48292
|
+
const deps = buildDeps(config2);
|
|
48293
|
+
deps.versionCheck?.start();
|
|
48294
|
+
const server = createServer3(deps);
|
|
46847
48295
|
await server.connect(new StdioServerTransport());
|
|
46848
48296
|
}
|
|
46849
48297
|
main().catch((err) => {
|