bavitutor 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,13 @@ npx bavitutor
16
16
  ```
17
17
 
18
18
  That's all: it downloads BaviTutor, serves it on <http://localhost:4100> and
19
- opens your browser. Data stays on your machine in `~/.bavitutor`.
19
+ opens your browser. Data stays on your machine in `~/.bavitutor`. If 4100 is
20
+ taken it moves to the next free port, and if BaviTutor is already running it
21
+ opens that one.
22
+
23
+ npx and vpx reuse a version they downloaded before (pnpm for a day), so when a
24
+ newer one is out BaviTutor says so on start, with the exact command:
25
+ `npx bavitutor@<version>` always fetches that version.
20
26
 
21
27
  Requirements: Node 24+, [Vite+](https://viteplus.dev) (`vp`), and a logged-in
22
28
  [Claude Code](https://claude.com/claude-code) install (the Claude Agent SDK
@@ -25052,7 +25052,7 @@ const number = /*#__PURE__*/ new Number$2();
25052
25052
  * @category constructors
25053
25053
  * @since 4.0.0
25054
25054
  */
25055
- const Boolean$2 = class extends ASTNodeImpl {
25055
+ const Boolean$3 = class extends ASTNodeImpl {
25056
25056
  _tag = "Boolean";
25057
25057
  /** @internal */
25058
25058
  getParser() {
@@ -25077,7 +25077,7 @@ const Boolean$2 = class extends ASTNodeImpl {
25077
25077
  * @category constructors
25078
25078
  * @since 4.0.0
25079
25079
  */
25080
- const boolean = /*#__PURE__*/ new Boolean$2();
25080
+ const boolean = /*#__PURE__*/ new Boolean$3();
25081
25081
  /**
25082
25082
  * Constructs a {@link Arrays}.
25083
25083
  *
@@ -30776,7 +30776,7 @@ const InetV6Proto = {
30776
30776
  return this.toJSON();
30777
30777
  }
30778
30778
  };
30779
- const checkPort$1 = (port) => Number.isInteger(port) && port >= 0 && port <= 65535;
30779
+ const checkPort = (port) => Number.isInteger(port) && port >= 0 && port <= 65535;
30780
30780
  /**
30781
30781
  * Creates a checked IPv4 internet address.
30782
30782
  *
@@ -30785,7 +30785,7 @@ const checkPort$1 = (port) => Number.isInteger(port) && port >= 0 && port <= 655
30785
30785
  * @since 4.0.0
30786
30786
  */
30787
30787
  const inetAddressV4 = (address, port) => {
30788
- if (!checkPort$1(port)) return addressError(address, "port must be an integer from 0 through 65535");
30788
+ if (!checkPort(port)) return addressError(address, "port must be an integer from 0 through 65535");
30789
30789
  const self = Object.create(InetV4Proto);
30790
30790
  self.address = address;
30791
30791
  self.port = port;
@@ -30799,7 +30799,7 @@ const inetAddressV4 = (address, port) => {
30799
30799
  * @since 4.0.0
30800
30800
  */
30801
30801
  const inetAddressV6 = (address, port, options) => {
30802
- if (!checkPort$1(port)) return addressError(address, "port must be an integer from 0 through 65535");
30802
+ if (!checkPort(port)) return addressError(address, "port must be an integer from 0 through 65535");
30803
30803
  const scopeId = options?.scopeId ?? 0;
30804
30804
  if (!Number.isInteger(scopeId) || scopeId < 0 || scopeId > 4294967295) return addressError(address, "scopeId must be an unsigned 32-bit integer");
30805
30805
  const self = Object.create(InetV6Proto);
@@ -31552,7 +31552,7 @@ const Number$1 = /*#__PURE__*/ make$34(number);
31552
31552
  * @category schemas
31553
31553
  * @since 4.0.0
31554
31554
  */
31555
- const Boolean$1 = /*#__PURE__*/ make$34(boolean);
31555
+ const Boolean$2 = /*#__PURE__*/ make$34(boolean);
31556
31556
  /**
31557
31557
  * Schema for a TypeScript `void` return value.
31558
31558
  *
@@ -31761,7 +31761,7 @@ const FalseLiterals = /*#__PURE__*/ Literals([
31761
31761
  "n"
31762
31762
  ]);
31763
31763
  /** @internal */
31764
- const BooleanLiterals = /*#__PURE__*/ Literals([...TrueLiterals.literals, ...FalseLiterals.literals]).pipe(/*#__PURE__*/ decodeTo(Boolean$1, /*#__PURE__*/ transform({
31764
+ const BooleanLiterals = /*#__PURE__*/ Literals([...TrueLiterals.literals, ...FalseLiterals.literals]).pipe(/*#__PURE__*/ decodeTo(Boolean$2, /*#__PURE__*/ transform({
31765
31765
  decode: (value) => value === "true" || value === "yes" || value === "on" || value === "1" || value === "y",
31766
31766
  encode: (value) => value ? "true" : "false"
31767
31767
  })));
@@ -38187,7 +38187,7 @@ function Int(name) {
38187
38187
  * @category constructors
38188
38188
  * @since 2.0.0
38189
38189
  */
38190
- function Boolean(name) {
38190
+ function Boolean$1(name) {
38191
38191
  return schema(BooleanLiterals, name);
38192
38192
  }
38193
38193
  //#endregion
@@ -46583,7 +46583,8 @@ const codeByLiteral = {
46583
46583
  const fromLiteral = (literal) => codeByLiteral[literal];
46584
46584
  //#endregion
46585
46585
  //#region package.json
46586
- var version$1 = "0.1.0";
46586
+ var name = "bavitutor";
46587
+ var version$1 = "0.1.2";
46587
46588
  /**
46588
46589
  * Default Server-Sent Events encoder.
46589
46590
  *
@@ -48740,7 +48741,7 @@ const Concept = Struct({
48740
48741
  estimatedMinutes: Number$1,
48741
48742
  difficulty: Number$1,
48742
48743
  depth: Number$1,
48743
- isGoal: Boolean$1,
48744
+ isGoal: Boolean$2,
48744
48745
  sourceIds: ArraySchema(SourceId)
48745
48746
  });
48746
48747
  /** `from` is the prerequisite, `to` is the concept that depends on it. */
@@ -49031,7 +49032,7 @@ const ReviewResult = Struct({
49031
49032
  nextDueAt: Number$1,
49032
49033
  nextDueInHours: Number$1,
49033
49034
  /** Whether this attempt updated the memory model (first attempt of the day). */
49034
- counted: Boolean$1,
49035
+ counted: Boolean$2,
49035
49036
  /** Prerequisites whose next review was postponed by this success. */
49036
49037
  creditedConcepts: ArraySchema(String$2),
49037
49038
  /** After a lapse: quick checks on the weakest prerequisites (R3.3). */
@@ -49044,7 +49045,7 @@ const ReplacementAnswer = Struct({
49044
49045
  goalId: GoalId,
49045
49046
  conceptId: ConceptId,
49046
49047
  verdict: Verdict,
49047
- isMultipleChoice: Boolean$1
49048
+ isMultipleChoice: Boolean$2
49048
49049
  });
49049
49050
  const ReplacementSummary = Struct({
49050
49051
  /** Answered well: processed as successful reviews. */
@@ -49097,7 +49098,7 @@ const StreakInfo = Struct({
49097
49098
  freezesUsed: Number$1,
49098
49099
  freezesLeft: Number$1,
49099
49100
  /** A freeze kept the streak alive within the last week. */
49100
- isSavedByFreeze: Boolean$1
49101
+ isSavedByFreeze: Boolean$2
49101
49102
  });
49102
49103
  const Today = Struct({
49103
49104
  plan: DayPlan,
@@ -49312,7 +49313,7 @@ const TeachBackResult = Struct({
49312
49313
  });
49313
49314
  const LessonOutcome = Struct({
49314
49315
  mastery: Mastery,
49315
- criterionMet: Boolean$1,
49316
+ criterionMet: Boolean$2,
49316
49317
  maybeNextReviewAt: OptionFromNullOr(Number$1),
49317
49318
  message: String$2
49318
49319
  });
@@ -49329,7 +49330,7 @@ const AnswerContext = Literals([
49329
49330
  const AskTutorPayload = Struct({
49330
49331
  question: String$2,
49331
49332
  stepContext: String$2,
49332
- isGradedStep: Boolean$1,
49333
+ isGradedStep: Boolean$2,
49333
49334
  history: ArraySchema(Struct({
49334
49335
  role: Literals(["Learner", "Tutor"]),
49335
49336
  text: String$2
@@ -49364,7 +49365,7 @@ const PlacementBelief = Struct({
49364
49365
  "ToLearn"
49365
49366
  ]),
49366
49367
  /** True when the learner answered this concept directly. */
49367
- wasAsked: Boolean$1
49368
+ wasAsked: Boolean$2
49368
49369
  });
49369
49370
  const PlacementAnswerReview = Struct({
49370
49371
  conceptId: ConceptId,
@@ -49390,8 +49391,8 @@ const PlacementView = Union([
49390
49391
  conceptTitle: String$2,
49391
49392
  number: Number$1,
49392
49393
  questionsLeft: Number$1,
49393
- canFinishEarly: Boolean$1,
49394
- isWarmup: Boolean$1
49394
+ canFinishEarly: Boolean$2,
49395
+ isWarmup: Boolean$2
49395
49396
  }),
49396
49397
  TaggedStruct("Summary", {
49397
49398
  beliefs: ArraySchema(PlacementBelief),
@@ -49461,8 +49462,8 @@ const Calibration = Struct({
49461
49462
  readings: ArraySchema(CalibrationReading),
49462
49463
  /** Fewer answers than this in a bucket are too few to read. */
49463
49464
  minimumAnswers: Number$1,
49464
- isOverconfident: Boolean$1,
49465
- isUnderconfident: Boolean$1
49465
+ isOverconfident: Boolean$2,
49466
+ isUnderconfident: Boolean$2
49466
49467
  });
49467
49468
  const Activity = Struct({
49468
49469
  /** Whole weeks from a Sunday, oldest first, ending today. */
@@ -49538,7 +49539,7 @@ const ModelChoice = Struct({
49538
49539
  /** An alias that follows the newest version (`opus`) or a pinned model id. */
49539
49540
  model: String$2,
49540
49541
  reasoning: Reasoning,
49541
- fastMode: Boolean$1
49542
+ fastMode: Boolean$2
49542
49543
  });
49543
49544
  const ModelChoices = Struct({
49544
49545
  deep: ModelChoice,
@@ -49552,16 +49553,16 @@ const ModelOption = Struct({
49552
49553
  displayName: String$2,
49553
49554
  description: String$2,
49554
49555
  /** Aliases like `opus` move to each new version; ids stay pinned. */
49555
- isLatest: Boolean$1,
49556
+ isLatest: Boolean$2,
49556
49557
  reasoningLevels: ArraySchema(EffortLevel),
49557
- supportsFastMode: Boolean$1
49558
+ supportsFastMode: Boolean$2
49558
49559
  });
49559
49560
  /**
49560
49561
  * A daily nudge at a fixed time (R14.6: a stable cue helps habits form).
49561
49562
  * Shown as a browser notification while BaviTutor is open in a tab.
49562
49563
  */
49563
49564
  const Reminder = Struct({
49564
- isOn: Boolean$1,
49565
+ isOn: Boolean$2,
49565
49566
  /** Local time of day, "HH:MM". */
49566
49567
  time: String$2
49567
49568
  });
@@ -49579,7 +49580,7 @@ const Settings = Struct({
49579
49580
  desiredRetention: Number$1,
49580
49581
  /** Hard cap on new concepts per day: 3 by default, 5 for "intensive" (R12). */
49581
49582
  maxNewLessonsPerDay: Number$1,
49582
- showVisuals: Boolean$1,
49583
+ showVisuals: Boolean$2,
49583
49584
  reminder: Reminder,
49584
49585
  models: ModelChoices
49585
49586
  });
@@ -53464,8 +53465,8 @@ const askSdk = tryPromise({
53464
53465
  const ask = async (attempt, best) => {
53465
53466
  const models = await session.supportedModels();
53466
53467
  const better = models.length >= best.length ? models : best;
53467
- if (isComplete(better) || attempt >= 3) return better;
53468
- await new Promise((done) => setTimeout(done, 1500));
53468
+ if (isComplete(better) || attempt >= 5) return better;
53469
+ await new Promise((done) => setTimeout(done, 2e3));
53469
53470
  return ask(attempt + 1, better);
53470
53471
  };
53471
53472
  return await ask(1, []);
@@ -57715,7 +57716,7 @@ const AdapterChoice = Struct({
57715
57716
  })).annotate({ description: "Achieve the Core coherence map (K-12 maths prerequisite graph). Use for school maths goals; null otherwise" }),
57716
57717
  openStaxBooks: ArraySchema(String$2).annotate({ description: "OpenStax textbook titles to read the table of contents of (0-2), e.g. [\"Algebra 1\", \"Prealgebra 2e\"]" }),
57717
57718
  exercismTracks: ArraySchema(String$2).annotate({ description: "Exercism language track slugs (0-2), e.g. [\"python\"], for programming-language goals" }),
57718
- ossu: Boolean$1.annotate({ description: "read the OSSU computer-science curriculum (CS goals)" }),
57719
+ ossu: Boolean$2.annotate({ description: "read the OSSU computer-science curriculum (CS goals)" }),
57719
57720
  ocwCourses: ArraySchema(String$2).annotate({ description: "MIT OpenCourseWare course numbers or exact titles you are confident exist (0-2), e.g. [\"6.100L\"]" }),
57720
57721
  roadmapSh: ArraySchema(String$2).annotate({ description: "roadmap.sh roadmap slugs (0-2) for software and tech goals, the goal's own roadmap first, e.g. [\"backend\"], [\"react\"], [\"docker\"]" })
57721
57722
  });
@@ -57764,7 +57765,7 @@ const DiscoveryReport = Struct({
57764
57765
  scores: JudgedScores,
57765
57766
  fit: Confidence.annotate({ description: "goal fit: 0, 0.25, 0.5, 0.75 or 1" }),
57766
57767
  flags: ArraySchema(GraderFlag),
57767
- complete: Boolean$1.annotate({ description: "false if you could only read part of the outline" }),
57768
+ complete: Boolean$2.annotate({ description: "false if you could only read part of the outline" }),
57768
57769
  topics: ArraySchema(SourceTopic).annotate({ description: "the outline in the order the source teaches it (up to 60 items)" })
57769
57770
  })),
57770
57771
  gaps: String$2.annotate({ description: "one sentence on what you looked for but could not find" })
@@ -57796,7 +57797,7 @@ const DraftConcept = Struct({
57796
57797
  objectives: ArraySchema(String$2).annotate({ description: "2-3 testable objectives starting with a verb (\"Solve…\", \"Explain…\")" }),
57797
57798
  estimatedMinutes: Int$1,
57798
57799
  difficulty: Score,
57799
- isGoal: Boolean$1.annotate({ description: "true for the concepts that together constitute reaching the learner goal" }),
57800
+ isGoal: Boolean$2.annotate({ description: "true for the concepts that together constitute reaching the learner goal" }),
57800
57801
  sourceRefs: ArraySchema(String$2).annotate({ description: "source tags like \"S1\" that teach it" }),
57801
57802
  prerequisites: ArraySchema(DraftPrerequisite).annotate({ description: "direct prerequisites only (keys of other concepts), never transitive ones" })
57802
57803
  });
@@ -58480,7 +58481,7 @@ const Difficulty = Int$1.check(isBetween$1({
58480
58481
  }));
58481
58482
  const DraftOption = Struct({
58482
58483
  text: String$2.annotate({ description: "a bare claim or value, no justification, same grammatical skeleton and length as the other options" }),
58483
- isCorrect: Boolean$1,
58484
+ isCorrect: Boolean$2,
58484
58485
  misconception: String$2.annotate({ description: "for a distractor: the specific misconception that produces it (one short phrase). Empty for the correct option" })
58485
58486
  });
58486
58487
  const DiagnosticBatch = Struct({ questions: ArraySchema(Struct({
@@ -58998,7 +58999,7 @@ const decodeFrameEvent = decodeUnknownOption(Union([
58998
58999
  index: Number$1,
58999
59000
  count: Number$1,
59000
59001
  caption: String$2,
59001
- playing: Boolean$1
59002
+ playing: Boolean$2
59002
59003
  }),
59003
59004
  Struct({
59004
59005
  type: Literal("bv:audit"),
@@ -59392,7 +59393,7 @@ var Visuals = class Visuals extends Service()("bavitutor/Visuals") {
59392
59393
  const claude = yield* Claude;
59393
59394
  const renderer = yield* Renderer;
59394
59395
  const goals = yield* Goals;
59395
- const isEnabled = yield* Boolean("BAVI_VISUALS").pipe(withDefault$1(true));
59396
+ const isEnabled = yield* Boolean$1("BAVI_VISUALS").pipe(withDefault$1(true));
59396
59397
  const running = yield* make$18();
59397
59398
  const slots = yield* make$50(CONCURRENT_GENERATIONS);
59398
59399
  const visualRow = (goalId, visualId) => sql`
@@ -61874,13 +61875,91 @@ const isBaviTutor = async (port) => {
61874
61875
  }
61875
61876
  };
61876
61877
  /**
61877
- * Fails with a clear message if the port is taken. Also probes loopback: on
61878
+ * Whether something already listens on the port. Also probes loopback: on
61878
61879
  * Windows a second server can bind 127.0.0.1 next to one on all interfaces.
61879
61880
  */
61880
- const checkPort = async (port, host) => {
61881
+ const isPortTaken = async (port, host) => {
61881
61882
  const error = await canListen(port, host);
61882
- if (error?.code === "EADDRINUSE" || error === void 0 && (await answers(port, "127.0.0.1") || await answers(port, "::1"))) fail([`Port ${port} is already in use${await isBaviTutor(port) ? " by a BaviTutor server" : ""}.`, `Pick another one: bavitutor --port ${port === 65535 ? port - 1 : port + 1}`]);
61883
- if (error !== void 0) fail([`Can't listen on ${host}:${port}: ${error.message}`]);
61883
+ if (error !== void 0 && error.code !== "EADDRINUSE") fail([`Can't listen on ${host}:${port}: ${error.message}`]);
61884
+ return error !== void 0 || await answers(port, "127.0.0.1") || await answers(port, "::1");
61885
+ };
61886
+ /** A whole BaviTutor (web app included) is serving on this port already. */
61887
+ const servesBaviTutorApp = async (port) => {
61888
+ try {
61889
+ const response = await fetch(`http://127.0.0.1:${port}/`, {
61890
+ headers: { accept: "text/html" },
61891
+ signal: AbortSignal.timeout(1500)
61892
+ });
61893
+ return response.ok && (await response.text()).includes("BaviTutor");
61894
+ } catch {
61895
+ return false;
61896
+ }
61897
+ };
61898
+ const SPARE_PORTS = 20;
61899
+ /**
61900
+ * The port to serve on. A port the learner asked for must be free. The
61901
+ * default moves on to the next free one, unless a BaviTutor app already runs
61902
+ * on one of them, which is then opened instead of starting a second copy.
61903
+ */
61904
+ const choosePort = async (requested, isExplicit, host) => {
61905
+ if (!await isPortTaken(requested, host)) return {
61906
+ port: requested,
61907
+ isRunning: false
61908
+ };
61909
+ if (await servesBaviTutorApp(requested)) return {
61910
+ port: requested,
61911
+ isRunning: true
61912
+ };
61913
+ if (isExplicit) return fail([`Port ${requested} is already in use${await isBaviTutor(requested) ? " by a BaviTutor server" : ""}.`, `Pick another one: bavitutor --port ${requested === 65535 ? requested - 1 : requested + 1}`]);
61914
+ const candidates = Array.from({ length: SPARE_PORTS }, (_, offset) => requested + 1 + offset).filter((candidate) => candidate <= 65535);
61915
+ for (const candidate of candidates) {
61916
+ if (!await isPortTaken(candidate, host)) {
61917
+ say(` port ${requested} is busy, so using ${candidate}`);
61918
+ return {
61919
+ port: candidate,
61920
+ isRunning: false
61921
+ };
61922
+ }
61923
+ if (await servesBaviTutorApp(candidate)) return {
61924
+ port: candidate,
61925
+ isRunning: true
61926
+ };
61927
+ }
61928
+ return fail([`Ports ${requested}-${requested + SPARE_PORTS} are all in use.`, "Pick a free one: bavitutor --port <port>"]);
61929
+ };
61930
+ /** The version npm calls latest, or undefined when npm doesn't answer quickly. */
61931
+ const latestVersion = async () => {
61932
+ if (process.env["NO_UPDATE_NOTIFIER"]) return;
61933
+ try {
61934
+ const response = await fetch(`https://registry.npmjs.org/${name}/latest`, { signal: AbortSignal.timeout(3e3) });
61935
+ const body = await response.json();
61936
+ return response.ok && body !== null && typeof body === "object" && "version" in body && typeof body.version === "string" ? body.version : void 0;
61937
+ } catch {
61938
+ return;
61939
+ }
61940
+ };
61941
+ const isNewer = (candidate, current) => {
61942
+ const parts = (version) => (version.split("-")[0] ?? "").split(".").map(Number);
61943
+ const [next, now] = [parts(candidate), parts(current)];
61944
+ const difference = [
61945
+ 0,
61946
+ 1,
61947
+ 2
61948
+ ].map((i) => (next[i] ?? 0) - (now[i] ?? 0)).find((delta) => delta !== 0);
61949
+ return difference !== void 0 && difference > 0;
61950
+ };
61951
+ /**
61952
+ * npx and vpx (through pnpm dlx) reuse what they downloaded before, so a bare
61953
+ * `npx bavitutor` can keep starting an old version. An exact version is
61954
+ * always fetched, so the notice names one.
61955
+ */
61956
+ const sayIfOutdated = (latest) => {
61957
+ if (latest !== void 0 && isNewer(latest, version$1)) {
61958
+ say(`BaviTutor ${latest} is out (this is ${version$1}). To update,`);
61959
+ say(`stop this one and run: npx bavitutor@${latest}`);
61960
+ say(` or: vpx bavitutor@${latest}`);
61961
+ say();
61962
+ }
61884
61963
  };
61885
61964
  const openBrowser = (url) => {
61886
61965
  const [command, args] = process.platform === "win32" ? ["cmd.exe", [
@@ -61913,20 +61992,22 @@ const main = async () => {
61913
61992
  say(version$1);
61914
61993
  return;
61915
61994
  }
61995
+ const latest = latestVersion();
61916
61996
  const dataDir = resolve(expandHome(options["data-dir"] ?? join(homedir(), ".bavitutor")));
61917
61997
  mkdirSync(dataDir, { recursive: true });
61918
61998
  const envFile = join(dataDir, ".env");
61919
61999
  if (existsSync(envFile)) process.loadEnvFile(envFile);
61920
- const port = options.port !== void 0 ? parsePort(options.port, "--port") : process.env["PORT"] ? parsePort(process.env["PORT"], "PORT") : DEFAULT_PORT;
61921
- process.env["PORT"] = String(port);
62000
+ const isExplicitPort = options.port !== void 0 || Boolean(process.env["PORT"]);
62001
+ const requestedPort = options.port !== void 0 ? parsePort(options.port, "--port") : process.env["PORT"] ? parsePort(process.env["PORT"], "PORT") : DEFAULT_PORT;
61922
62002
  const host = options.host ?? DEFAULT_HOST;
61923
- const url = `http://${[
62003
+ const isLocal = [
61924
62004
  "127.0.0.1",
61925
62005
  "localhost",
61926
62006
  "::1",
61927
62007
  "0.0.0.0",
61928
62008
  "::"
61929
- ].includes(host) ? "localhost" : host}:${port}`;
62009
+ ].includes(host);
62010
+ const urlFor = (port) => `http://${isLocal ? "localhost" : host}:${port}`;
61930
62011
  const staticDir = fileURLToPath(new URL("web", import.meta.url));
61931
62012
  if (!existsSync(join(staticDir, "index.html"))) fail([`The web app isn't built (no ${join(staticDir, "index.html")}).`, "Build it from the repository with `vp run build`."]);
61932
62013
  say(`BaviTutor ${version$1}`);
@@ -61948,13 +62029,25 @@ const main = async () => {
61948
62029
  ]);
61949
62030
  else say(` visuals ${chrome}`);
61950
62031
  }
61951
- await checkPort(port, host);
62032
+ const choice = await choosePort(requestedPort, isExplicitPort, host);
62033
+ if (choice.isRunning) {
62034
+ say();
62035
+ say(`BaviTutor is already running at ${urlFor(choice.port)}`);
62036
+ say();
62037
+ if (options.open) openBrowser(urlFor(choice.port));
62038
+ sayIfOutdated(await latest);
62039
+ return;
62040
+ }
62041
+ const port = choice.port;
62042
+ process.env["PORT"] = String(port);
62043
+ const url = urlFor(port);
61952
62044
  effectDiscard(gen(function* () {
61953
62045
  yield* HttpServer;
61954
62046
  say();
61955
62047
  say(`BaviTutor is running at ${url} (Ctrl+C to stop)`);
61956
62048
  say();
61957
62049
  if (options.open) openBrowser(url);
62050
+ latest.then(sayIfOutdated);
61958
62051
  })).pipe(provide(makeServer({
61959
62052
  port,
61960
62053
  host,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bavitutor",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Describe what you want to learn; BaviTutor researches it, maps it and teaches it.",
5
5
  "keywords": [
6
6
  "claude",