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/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, constants) {
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, constants);
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, constants) {
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, constants);
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, constants) {
497
- this.code = optimizeExpr(this.code, names, constants);
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, constants) {
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, constants))
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, constants) {
584
+ optimizeNames(names, constants3) {
585
585
  var _a3;
586
- this.else = (_a3 = this.else) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
587
- if (!(super.optimizeNames(names, constants) || this.else))
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, constants);
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, constants) {
613
- if (!super.optimizeNames(names, constants))
612
+ optimizeNames(names, constants3) {
613
+ if (!super.optimizeNames(names, constants3))
614
614
  return;
615
- this.iteration = optimizeExpr(this.iteration, names, constants);
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, constants) {
652
- if (!super.optimizeNames(names, constants))
651
+ optimizeNames(names, constants3) {
652
+ if (!super.optimizeNames(names, constants3))
653
653
  return;
654
- this.iterable = optimizeExpr(this.iterable, names, constants);
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, constants) {
696
+ optimizeNames(names, constants3) {
697
697
  var _a3, _b;
698
- super.optimizeNames(names, constants);
699
- (_a3 = this.catch) === null || _a3 === void 0 ? void 0 : _a3.optimizeNames(names, constants);
700
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants);
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, constants) {
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 = constants[n.str];
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 && constants[c.str] !== void 0);
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(path12) {
3264
- let input2 = path12;
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 path12 = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3674
- wsComponent.path = path12 && path12 !== "/" ? path12 : void 0;
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
- parse3(serialize(uri, options), options);
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 = parse3(serialize(base2, options), options);
3879
- relative = parse3(serialize(relative, options), options);
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 parse3(uri, opts) {
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: parse3
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, path12) {
8021
- if (!path12)
8020
+ function getElementAtPath(obj, path15) {
8021
+ if (!path15)
8022
8022
  return obj;
8023
- return path12.reduce((acc, key) => acc?.[key], obj);
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, mask) {
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, mask));
8248
+ mirrorShape(newShape, schema, maskedKeys(schema, mask2));
8249
8249
  return clone(schema, mergeDefs(currDef, { shape: newShape, checks: [] }));
8250
8250
  }
8251
- function maskedKeys(schema, mask) {
8251
+ function maskedKeys(schema, mask2) {
8252
8252
  const raw = sourceShape(schema);
8253
8253
  const keys = [];
8254
- for (const key of Reflect.ownKeys(mask)) {
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 (mask[key])
8258
+ if (mask2[key])
8259
8259
  keys.push(key);
8260
8260
  }
8261
8261
  return keys;
8262
8262
  }
8263
- function omit(schema, mask) {
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, mask));
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, mask, name = "partial") {
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 = mask ? new Set(maskedKeys(schema, mask)) : void 0;
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, mask) {
8335
- const selected = mask ? new Set(maskedKeys(schema, mask)) : void 0;
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(path12, issues) {
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(path12);
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, path12 = []) => {
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 }, [...path12, ...issue2.path]));
8832
+ issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
8833
8833
  } else if (issue2.code === "invalid_key") {
8834
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8834
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8835
8835
  } else if (issue2.code === "invalid_element") {
8836
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8836
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8837
8837
  } else {
8838
- const fullpath = [...path12, ...issue2.path];
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, path12 = []) => {
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 }, [...path12, ...issue2.path]));
8881
+ issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
8882
8882
  } else if (issue2.code === "invalid_key") {
8883
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8883
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8884
8884
  } else if (issue2.code === "invalid_element") {
8885
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
8885
+ processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8886
8886
  } else {
8887
- const fullpath = [...path12, ...issue2.path];
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 path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8927
- for (const seg of path12) {
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 parse3 = _parse(_Err);
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 parse3(schema, value, ctx, finalizeParams(fn, _params));
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 parse3 = _parse(_Err);
9070
+ const parse4 = _parse(_Err);
9071
9071
  const fn = (schema, value, _ctx, _params) => {
9072
- return parse3(schema, value, _ctx, finalizeParams(fn, _params));
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 parse3 = inst._zod.parse;
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 parse3(payload, ctx);
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 entry = { value: empty, issues: null };
12427
- bucket.set(payload.value, entry);
12428
- open.push(entry);
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 entry = open.length > depth ? open.pop() : void 0;
12502
+ const entry2 = open.length > depth ? open.pop() : void 0;
12503
12503
  if (result instanceof Promise) {
12504
12504
  return result.then((r) => {
12505
- if (entry)
12506
- entry.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
12505
+ if (entry2)
12506
+ entry2.issues = r.issues.length ? cloneIssues(r.issues) : NO_ISSUES;
12507
12507
  return r;
12508
12508
  });
12509
12509
  }
12510
- if (entry)
12511
- entry.issues = result.issues.length ? cloneIssues(result.issues) : NO_ISSUES;
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 existing = globalRegistry.get(inst) ?? {};
22723
- globalRegistry.add(inst, { ...existing, description });
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 existing = globalRegistry.get(inst) ?? {};
22736
- globalRegistry.add(inst, { ...existing, ...metadata });
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 entry of ctx.seen.entries()) {
22926
- const id = ctx.metadataRegistry.get(entry[0])?.id;
22925
+ for (const entry2 of ctx.seen.entries()) {
22926
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
22927
22927
  if (id) {
22928
- const existing = idToSchema.get(id);
22929
- if (existing && existing !== entry[0]) {
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, entry[0]);
22932
+ idToSchema.set(id, entry2[0]);
22933
22933
  }
22934
22934
  }
22935
- const makeURI = (entry) => {
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(entry[0])?.id;
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 = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
22944
- entry[1].defId = id;
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 (entry[1] === root && !entry[1].schema.id) {
22949
+ if (entry2[1] === root && !entry2[1].schema.id) {
22950
22950
  return { ref: uriPrefix };
22951
22951
  }
22952
- const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
22952
+ const defId = entry2[1].schema.id ?? `__schema${ctx.counter++}`;
22953
22953
  return { defId, ref: defUriPrefix + encodeJSONPointerSegment(defId) };
22954
22954
  };
22955
- const extractToDef = (entry) => {
22956
- if (entry[1].schema.$ref) {
22955
+ const extractToDef = (entry2) => {
22956
+ if (entry2[1].schema.$ref) {
22957
22957
  return;
22958
22958
  }
22959
- const seen = entry[1];
22960
- const { ref, defId } = makeURI(entry);
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 entry of ctx.seen.entries()) {
22972
- const seen = entry[1];
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 entry of ctx.seen.entries()) {
22981
- const seen = entry[1];
22982
- if (schema === entry[0]) {
22983
- extractToDef(entry);
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(entry[0])?.id;
22988
- if (schema !== entry[0] && ext) {
22989
- extractToDef(entry);
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(entry[0])?.id;
22993
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
22994
22994
  if (id) {
22995
- extractToDef(entry);
22995
+ extractToDef(entry2);
22996
22996
  continue;
22997
22997
  }
22998
22998
  if (seen.cycle) {
22999
- extractToDef(entry);
22999
+ extractToDef(entry2);
23000
23000
  continue;
23001
23001
  }
23002
23002
  if (seen.count > 1) {
23003
23003
  if (ctx.reused === "ref") {
23004
- extractToDef(entry);
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 entry of [...ctx.seen.entries()].reverse()) {
23187
- flattenRef(entry[0]);
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 entry of ctx.seen.entries()) {
23191
- compactTypeUnion(entry[1].def ?? entry[1].schema);
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 existing = carriers.get(allOf);
23204
- if (existing)
23205
- existing.push(json3);
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 entry of ctx.seen.entries()) {
23239
- const seen = entry[1];
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 entry of ctx.seen.values()) {
23852
- if (entry.def && !bySchema.has(entry.schema))
23853
- bySchema.set(entry.schema, entry);
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 entry of ctx.seen.values()) {
23868
- for (const carrier of [entry.schema, entry.def]) {
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 entry of registry2._idmap.entries()) {
24063
- const [_, schema] = entry;
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 entry of registry2._idmap.entries()) {
24074
- const [key, schema] = entry;
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(mask) {
25296
- return util_exports.pick(this, mask);
25295
+ pick(mask2) {
25296
+ return util_exports.pick(this, mask2);
25297
25297
  },
25298
- omit(mask) {
25299
- return util_exports.omit(this, mask);
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 path12 = ref.slice(1).split("/").filter(Boolean);
26066
- if (path12.length === 0) {
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 (path12[0] === defsKey) {
26071
- const key = path12[1] === void 0 ? void 0 : decodeJSONPointerSegment(path12[1]);
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.3.1";
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: get(env2, "INARA_APP_NAME") ?? "ed-mcp",
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
- plansFile: userPath(env2, "ED_MCP_PLANS") ?? path3.join(dirs.config, "plans.json"),
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, { detached: true, stdio: "ignore" });
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
- return this.refresh(cfg, latest, true);
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}). Call capi_login to log in again.`
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(entry) {
29205
- const o = entry;
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(path12) {
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}${path12}`;
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(file2) {
29360
- this.file = file2;
29404
+ constructor(secrets) {
29405
+ this.secrets = secrets;
29361
29406
  }
29362
- file;
29407
+ secrets;
29363
29408
  async load() {
29364
- let json3;
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 mkdir(path4.dirname(this.file), { recursive: true });
29378
- const tmp = `${this.file}.${process.pid}.tmp`;
29379
- await writeFile(tmp, JSON.stringify(t), { mode: 384 });
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 rm(this.file, { force: true });
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 as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
29395
- import path5 from "path";
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 = path5.join(cacheDir, "catalog");
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 readFile2(path5.join(this.dir, "meta.json"), "utf8"));
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 = path5.join(this.dir, `${k}.raw`);
31011
- const text = await readFile2(file2, "utf8").catch((err) => {
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 mkdir2(this.dir, { recursive: true });
31107
+ await mkdir(this.dir, { recursive: true });
31077
31108
  for (const k of changed)
31078
- await atomicWrite(path5.join(this.dir, `${k}.raw`), next[k]);
31109
+ await atomicWrite(path4.join(this.dir, `${k}.raw`), next[k]);
31079
31110
  const meta3 = { version: 1, checked_at: stamp, files };
31080
- await atomicWrite(path5.join(this.dir, "meta.json"), JSON.stringify(meta3, null, 2));
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 writeFile2(tmp, text);
31096
- await rename2(tmp, file2);
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 readFile4, stat as stat4 } from "fs/promises";
31200
- import path7 from "path";
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 path6 from "path";
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 = path6.join(dir, n.name);
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 readFile3, stat as stat3 } from "fs/promises";
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 readFile3(file2.path, "utf8")).split(/\r?\n/)) {
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 entry = target.get(symbol2) ?? { symbol: symbol2, name: symbol2, count: 0 };
31389
+ const entry2 = target.get(symbol2) ?? { symbol: symbol2, name: symbol2, count: 0 };
31359
31390
  const label = str2(localised);
31360
- if (label) entry.name = label;
31361
- entry.count = Math.max(0, entry.count + delta);
31362
- if (entry.count === 0) target.delete(symbol2);
31363
- else target.set(symbol2, entry);
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 readFile4(path7.join(dir, "Cargo.json"), "utf8"));
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 readFile4(path7.join(this.requireDir(), STATION_FILES[kind2]), "utf8"));
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(path7.join(dir, "Options")).then(
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 "${name ?? this.cfg?.cmdr}"`);
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.cfg;
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 page = 0; loaded < q.maxStations; page++) {
32443
- if (page > 0 && deadline !== void 0 && this.now() >= deadline) {
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(path12) {
32555
- const { status, body } = await this.raw(path12);
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(path12, body, contentType, opts) {
32598
+ async post(path15, body, contentType, opts) {
32560
32599
  const res = await this.raw(
32561
- path12,
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(path12, init = {}, opts) {
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}${path12}`, init, opts);
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 { readFile as readFile6 } from "fs/promises";
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 { link, mkdir as mkdir3, open as open2, readFile as readFile5, rename as rename3, stat as stat5, unlink, writeFile as writeFile3 } from "fs/promises";
32624
- import path8 from "path";
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
- async function acquireLock(file2, timeoutMs, lockedMessage) {
32629
- const lock = `${file2}.lock`;
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 readFile5(lock, "utf8").catch(() => void 0);
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 mkdir3(path8.dirname(file2), { recursive: true });
32686
- const release = await acquireLock(file2, opts.timeoutMs, opts.lockedMessage);
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 writeFile3(tmp, content);
32692
- await rename3(tmp, file2);
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 rename3(lock, aside);
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 UserError(`Plan ${parsed.data.id} exists; pass overwrite: true to replace it`);
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
- let raw;
32851
- try {
32852
- raw = await readFile6(this.file, "utf8");
32853
- } catch (err) {
32854
- if (err.code === "ENOENT") return { version: 1, plans: [] };
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
- () => lockedWrite(
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
- lockedMessage: "Plan file is locked by another process; try again"
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
- return { content: JSON.stringify(data, null, 2), result };
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
- let raw;
33025
- try {
33026
- raw = await readFile7(this.file, "utf8");
33027
- } catch (err) {
33028
- if (err.code === "ENOENT") return DEFAULT_PREFS;
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
- return { content: JSON.stringify(next, null, 2), result: next };
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 readFile8, rename as rename4, writeFile as writeFile4 } from "fs/promises";
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 path12 = str3(h, 0, 100);
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) path12 = `${prefix}/${path12}`;
33568
+ if (prefix) path15 = `${prefix}/${path15}`;
33315
33569
  }
33316
- if (nextPath !== null) path12 = nextPath;
33570
+ if (nextPath !== null) path15 = nextPath;
33317
33571
  nextPath = null;
33318
- if (type === "0" || type === "\0") files.push({ path: path12, data });
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 readFile8(this.file, "utf8"));
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, config2.inara, inaraCache);
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, config2.edsm, edsmCache);
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
- capiHttp,
33630
- config2.capi,
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
- plans: new PlanStore(config2.plansFile),
33654
- prefs: new PreferencesStore(path10.join(config2.configDir, "preferences.json"))
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: path12, errorMaps, issueData } = params;
34033
- const fullPath = [...path12, ...issueData.path || []];
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, path12, key) {
35159
+ constructor(parent, value, path15, key) {
34149
35160
  this._cachedPath = [];
34150
35161
  this.parent = parent;
34151
35162
  this.data = value;
34152
- this._path = path12;
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(mask) {
37175
+ pick(mask2) {
36165
37176
  const shape = {};
36166
- for (const key of util.objectKeys(mask)) {
36167
- if (mask[key] && this.shape[key]) {
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(mask) {
37187
+ omit(mask2) {
36177
37188
  const shape = {};
36178
37189
  for (const key of util.objectKeys(this.shape)) {
36179
- if (!mask[key]) {
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(mask) {
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 (mask && !mask[key]) {
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(mask) {
37220
+ required(mask2) {
36210
37221
  const newShape = {};
36211
37222
  for (const key of util.objectKeys(this.shape)) {
36212
- if (mask && !mask[key]) {
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(path12) {
37704
- if (path12.length === 0) {
38714
+ function getDotPath(path15) {
38715
+ if (path15.length === 0) {
37705
38716
  return "object root";
37706
38717
  }
37707
- return path12.reduce((acc, seg, index) => {
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 = (reason) => {
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(reason)
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 = reason instanceof McpError ? reason : new McpError(ErrorCode.RequestTimeout, String(reason));
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 path12 of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) {
43170
- const x = a.get(path12) ?? null;
43171
- const y = b.get(path12) ?? null;
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[path12] = { before: x, after: y, change };
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
- { description: "Forget the stored Frontier login on this machine." },
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 open3 = true;
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
- open3 = false;
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
- open3 = false;
44887
+ open4 = false;
43777
44888
  continue;
43778
44889
  }
43779
- if (permit === null) open3 = false;
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 (open3) checkedOpen.push(item);
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 reason;
44982
+ let reason2;
43872
44983
  if (best.effective_cost === null) {
43873
- reason = `fastest, about ${best.travel_min} min`;
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
- reason = `${credits(fastest.price - best.price)} cr cheaper than ${fastest.name} (${fastest.system}) for ${more} at ${credits(t.credits_per_hour)} cr/h`;
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
- reason = `lowest cost including travel time, about ${best.travel_min} min`;
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 page = screen.kept.map(
45066
+ const page2 = screen.kept.map(
43956
45067
  (h) => screen.locked(h.system) ? { ...h, needs_permit: true } : h
43957
45068
  );
43958
- const open3 = mode === "exclude" ? ranked.filter((h) => !screen.locked(h.system)) : ranked;
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: open3.length,
45074
+ matched: open4.length,
43964
45075
  total_capped: source.total_capped,
43965
- stations: page,
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(page, rules) : [],
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 path12 = [];
45328
+ const path15 = [];
44211
45329
  const visit2 = (i) => {
44212
45330
  if (state[i] === 2) return null;
44213
- if (state[i] === 1) return path12.slice(path12.indexOf(i));
45331
+ if (state[i] === 1) return path15.slice(path15.indexOf(i));
44214
45332
  state[i] = 1;
44215
- path12.push(i);
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
- path12.pop();
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 mask = 1; mask < 1 << n; mask++) {
45417
+ for (let mask2 = 1; mask2 < 1 << n; mask2++) {
44300
45418
  for (let last = 0; last < n; last++) {
44301
- for (const label of fronts[mask][last]) {
45419
+ for (const label of fronts[mask2][last]) {
44302
45420
  for (let next = 0; next < n; next++) {
44303
- if (mask & 1 << next || (needs[next] & mask) !== needs[next]) continue;
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[mask | 1 << next][next], { late: late2, time: time3, last: next, parent: label });
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 reason = e instanceof UserError ? e.message : `Spansh lookup failed: ${e instanceof Error ? e.message : String(e)}`;
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
- 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."
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 () => deps.journal.status())
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 entry = info.stations.find((st) => same2(st.name, name));
45004
- if (entry?.market_id == null) {
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 = entry.market_id;
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: entry.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: entry.arrival_ls,
46188
+ arrival_ls: entry2.arrival_ls,
45018
46189
  coords: info.coords,
45019
- pad: entry.pad,
45020
- type: entry.type,
45021
- planetary: PLANETARY.test(entry.type ?? ""),
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 failure2 = "Spansh has no market for it.";
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
- failure2 = err.message;
46223
+ failure3 = err.message;
45053
46224
  }
45054
46225
  if (!market) {
45055
- notes.push(`Cannot include ${inc.station}: ${failure2}`);
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
- { description: "Recent and active community goals from Inara. Cached 60 s." },
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 entry = byEngineer.get(lc(item.engineer)) ?? {
46510
+ const entry2 = byEngineer.get(lc(item.engineer)) ?? {
45310
46511
  id: item.id,
45311
46512
  name: item.engineer,
45312
46513
  descriptions: []
45313
46514
  };
45314
- entry.descriptions.push(item.description);
45315
- byEngineer.set(lc(item.engineer), entry);
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 path12 = [...cycle, cycle[0]].map((i) => names(all[i]).join("+"));
45593
- throw new UserError(`The after rules form a cycle: ${path12.join(" -> ")}`);
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
- await deps.plans.setRoute(a.plan_id, {
45839
- computed_at: new Date(now()).toISOString(),
45840
- from: startPlace.system,
45841
- stops: [
45842
- ...solved.order.map(optionLabel),
45843
- ...returnPlace ? [
45844
- `return to ${returnPlace.system}${returnPlace.station ? ` \u2014 ${returnPlace.station}` : ""}`
45845
- ] : []
45846
- ],
45847
- total_min: total,
45848
- plotted: a.plot && allPlotted
45849
- });
45850
- saved = true;
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 mkdir5, readFile as readFile9, rename as rename5, stat as stat6, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
45877
- import path11 from "path";
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
- // src/plans/markdown.ts
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
- out.push("");
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
- { description: "List saved build plans with item counts and progress." },
45949
- wrap(async () => plans.list())
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
- { description: "Get a saved plan by id.", inputSchema: { id: external_exports.string() } },
45954
- wrap(async ({ id }) => plans.get(id))
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
- wrap(async ({ plan }) => plans.upsert(plan))
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
- wrap(
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
- return plans.updateItem(a.plan_id, a.item_id, { status: a.status, notes: a.notes });
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
- { description: "Delete a saved plan.", inputSchema: { id: external_exports.string() } },
45987
- wrap(async ({ id }) => {
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
- wrap(
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 mkdir5(path11.dirname(target), { recursive: true });
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
- wrap(async (a) => {
47288
+ wrapWarn(async (a) => {
46042
47289
  const source = resolveUserPath(a.path);
46043
47290
  let raw;
46044
47291
  try {
46045
- if ((await stat6(source)).isDirectory()) {
47292
+ if ((await stat7(source)).isDirectory()) {
46046
47293
  throw new UserError(`${source} is a directory, not a plan file`);
46047
47294
  }
46048
- raw = await readFile9(source, "utf8");
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 plans.upsert(parsed.data, { overwrite: a.overwrite === true });
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 stat6(p);
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
- wrap(async () => deps.prefs.get())
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
- wrap(async ({ patch }) => deps.prefs.set(patch))
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 reason = (b) => {
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: reason(best) } : null;
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 open3 = mode === "exclude" ? ranked.stations.filter((h) => !screen.locked(h.system)) : ranked.stations;
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: open3.length,
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 createServer2(deps) {
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) registerPlanTools(server, { plans: deps.plans });
46767
- if (deps.prefs) registerPrefsTools(server, { prefs: deps.prefs });
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) registerJournalTools(server, { journal: deps.journal, catalog: deps.catalog });
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 server = createServer2(buildDeps(config2));
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) => {