moodle-cli 0.7.0-alpha.6 → 0.7.0-alpha.7

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/ONBOARDING.md CHANGED
@@ -19,7 +19,7 @@ Wait for the answer before doing setup work. Do not ask the student to find the
19
19
 
20
20
  Once the student shares a URL, complete the setup on their computer:
21
21
 
22
- - Check for Node.js 22+ and Bun. Install `moodle-cli` with the runtime available on the computer. Use `bunx --bun moodle-cli` as the command prefix when Bun is the only runtime.
22
+ - Check for Node.js 22.13+ and Bun (older Node cannot read browser cookies). Install `moodle-cli` with the runtime available on the computer. Use `bunx --bun moodle-cli` as the command prefix when Bun is the only runtime.
23
23
  - Follow redirects and resolve the URL to the final Moodle origin in the form `https://host`. Remove the path, query, and fragment. Confirm that the origin serves Moodle before saving it.
24
24
  - Read `~/.config/moodle-cli/config.yaml` if it exists. Set `base_url` to the verified origin and preserve the other settings.
25
25
  - Tell the student that Moodle may open in their browser and that you will wait while they complete their university sign-in. Run `moodle auth login`. If a browser opens, let the student finish SSO there, then continue when the command returns.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Let it keep up with deadlines and grades, fetch course files, and search forum d
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/moodle-cli?logo=npm)](https://www.npmjs.com/package/moodle-cli)
8
8
  [![CI](https://github.com/bunizao/moodle-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/bunizao/moodle-cli/actions/workflows/ci.yml)
9
- [![Node.js 22+](https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
9
+ [![Node.js 22.13+](https://img.shields.io/badge/Node.js-22.13%2B-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
10
10
  [![Bun](https://img.shields.io/badge/Bun-supported-fbf0df?logo=bun&logoColor=black)](https://bun.sh/)
11
11
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
12
 
@@ -32,7 +32,7 @@ Your agent asks for your Moodle URL and opens your university's sign-in page whe
32
32
 
33
33
  ### Install and sign in manually
34
34
 
35
- Use Node.js 22+ or Bun:
35
+ Use Node.js 22.13+ (needed for `node:sqlite`, which reads browser cookies) or Bun:
36
36
 
37
37
  ```bash
38
38
  # npm
@@ -176,7 +176,7 @@ moodle mcp bridge
176
176
 
177
177
  The default client connection uses `moodle mcp bridge`, which keeps the Bearer token out of client configuration. Use `moodle mcp connect CLIENT --mode remote` for clients that support authenticated remote MCP headers.
178
178
 
179
- Alpha version `0.7.0-alpha.6` supports MCP `2026-07-28` and a stateless compatibility lane for `2025-11-25` clients.
179
+ Alpha version `0.7.0-alpha.7` supports MCP `2026-07-28` and a stateless compatibility lane for `2025-11-25` clients.
180
180
 
181
181
  ### Configuration
182
182
 
package/dist/moodle.js CHANGED
@@ -406,15 +406,22 @@ async function loadSessionsFromOktaCli(baseUrl, options = {}, forceLogin = false
406
406
  return refreshed.length ? refreshed : stored;
407
407
  }
408
408
  var COOKIE_ACCESS_DENIED = /EPERM|EACCES|operation not permitted|permission denied/i;
409
+ var COOKIE_SQLITE_UNAVAILABLE = /No such built-in module: node:sqlite/i;
410
+ var MINIMUM_NODE_FOR_BROWSER_COOKIES = "22.13.0";
409
411
  function cookieAccessBlocked(warnings) {
410
- return warnings.some((warning) => COOKIE_ACCESS_DENIED.test(warning));
412
+ return warnings.some((warning) => COOKIE_ACCESS_DENIED.test(warning) || COOKIE_SQLITE_UNAVAILABLE.test(warning));
411
413
  }
412
414
  function cookieAccessHint(warnings, platform = process.platform) {
413
415
  const grant = platform === "darwin" ? "Grant Full Disk Access to the application running this command (System Settings > Privacy & Security > Full Disk Access), then restart it." : "Run this command as the user that owns the browser profile, or grant it read access to the browser cookie store.";
416
+ const remedy = warnings.some((warning) => COOKIE_SQLITE_UNAVAILABLE.test(warning)) ? [
417
+ `This Node.js runtime has no node:sqlite, which is needed to read browser cookies. Use Node.js ${MINIMUM_NODE_FOR_BROWSER_COOKIES} or newer, or run the CLI with Bun (bunx --bun moodle-cli).`
418
+ ] : [
419
+ "If this runs inside a sandboxed app (an IDE or agent terminal), rerun it from a regular terminal first.",
420
+ grant
421
+ ];
414
422
  return [
415
423
  "The browser cookie store could not be read, so the session could not be detected.",
416
- "If this runs inside a sandboxed app (an IDE or agent terminal), rerun it from a regular terminal first.",
417
- grant,
424
+ ...remedy,
418
425
  `Alternatively set ${ENV_MOODLE_SESSION} to a valid MoodleSession cookie value.`,
419
426
  "",
420
427
  "Cookie store diagnostics:",
@@ -3880,7 +3887,7 @@ function escapeXml(value) {
3880
3887
  }
3881
3888
 
3882
3889
  // src/version.ts
3883
- var VERSION = "0.7.0-alpha.6";
3890
+ var VERSION = "0.7.0-alpha.7";
3884
3891
 
3885
3892
  // src/forum.ts
3886
3893
  function parseDiscussionReference(value) {
@@ -5347,8 +5354,8 @@ var DeploymentPlanError = class extends Error {
5347
5354
  code;
5348
5355
  };
5349
5356
  var DeploymentApplyError = class extends Error {
5350
- constructor(code, message) {
5351
- super(message);
5357
+ constructor(code, message, options) {
5358
+ super(message, options);
5352
5359
  this.code = code;
5353
5360
  this.name = "DeploymentApplyError";
5354
5361
  }
@@ -5377,7 +5384,7 @@ var ManagedMcpDeployment = class {
5377
5384
  const receipt = replacingExisting || remote === null ? null : matchingReceipt;
5378
5385
  const existing = remote && receipt ? { ...remote, productionEndpoint: receipt.productionEndpoint, releaseDigest: receipt.releaseDigest } : remote;
5379
5386
  const rotate = intent.rotateToken === true && credentials !== null;
5380
- const releaseChanged = existing?.releaseDigest !== intent.releaseDigest;
5387
+ const releaseChanged = existing?.releaseDigest !== intent.releaseDigest || receipt?.restoredRelease === true;
5381
5388
  const uploadCandidate = !existing || replacingExisting || releaseChanged || intent.repair === true || rotate;
5382
5389
  return {
5383
5390
  intent: { ...intent },
@@ -5424,20 +5431,13 @@ var ManagedMcpDeployment = class {
5424
5431
  yield completed(activeStage);
5425
5432
  activeStage = "upload_private_credentials";
5426
5433
  yield started(activeStage);
5427
- if (plan.uploadCandidate) {
5428
- if (plan.operation === "create") {
5429
- initializedWorker = await this.dependencies.wrangler.initializeWorker({
5430
- accountId: plan.intent.accountId,
5431
- workerName: plan.intent.workerName,
5432
- configPath: prepared.wranglerConfigPath,
5433
- releaseDigest: plan.intent.releaseDigest
5434
- });
5435
- }
5436
- await this.dependencies.wrangler.uploadSecrets({
5434
+ if (plan.uploadCandidate && plan.operation === "create") {
5435
+ initializedWorker = await this.dependencies.wrangler.initializeWorker({
5437
5436
  accountId: plan.intent.accountId,
5438
5437
  workerName: plan.intent.workerName,
5439
5438
  configPath: prepared.wranglerConfigPath,
5440
- secretsFilePath: prepared.secretsFilePath
5439
+ secretsFilePath: prepared.secretsFilePath,
5440
+ releaseDigest: plan.intent.releaseDigest
5441
5441
  });
5442
5442
  secretsUploaded = true;
5443
5443
  }
@@ -5453,9 +5453,11 @@ var ManagedMcpDeployment = class {
5453
5453
  accountId: plan.intent.accountId,
5454
5454
  workerName: plan.intent.workerName,
5455
5455
  configPath: prepared.wranglerConfigPath,
5456
+ secretsFilePath: prepared.secretsFilePath,
5456
5457
  releaseDigest: plan.intent.releaseDigest,
5457
5458
  productionEndpoint: productionEndpoint2
5458
5459
  });
5460
+ secretsUploaded = true;
5459
5461
  if (!candidate.previewEndpoint) {
5460
5462
  await this.dependencies.wrangler.promote({
5461
5463
  accountId: plan.intent.accountId,
@@ -5605,10 +5607,9 @@ var ManagedMcpDeployment = class {
5605
5607
  let readinessReasonCode = null;
5606
5608
  let sessionRevision = null;
5607
5609
  if (worker && credentials) {
5608
- const remoteReadiness = await this.dependencies.worker.getReadiness({
5609
- endpoint: receipt.productionEndpoint,
5610
- sessionSyncToken: credentials.sessionSyncToken
5611
- });
5610
+ const target = { endpoint: receipt.productionEndpoint, sessionSyncToken: credentials.sessionSyncToken };
5611
+ await this.dependencies.worker.touchSession(target);
5612
+ const remoteReadiness = await this.dependencies.worker.getReadiness(target);
5612
5613
  readiness = remoteReadiness.status;
5613
5614
  readinessReasonCode = remoteReadiness.reasonCode;
5614
5615
  sessionRevision = remoteReadiness.revision;
@@ -5676,6 +5677,7 @@ var ManagedMcpDeployment = class {
5676
5677
  await this.dependencies.receipts.write({
5677
5678
  ...receipt,
5678
5679
  productionVersionId: worker.previousHealthyVersionId,
5680
+ ...swappedDigests(receipt),
5679
5681
  sessionRevision: upload.revision
5680
5682
  });
5681
5683
  await this.reconcileLocalIntegrations(profile);
@@ -5722,7 +5724,8 @@ var ManagedMcpDeployment = class {
5722
5724
  }
5723
5725
  await this.dependencies.receipts.write({
5724
5726
  ...receipt,
5725
- productionVersionId: previousVersionId
5727
+ productionVersionId: previousVersionId,
5728
+ ...swappedDigests(receipt)
5726
5729
  });
5727
5730
  return { status: "restored", versionId: previousVersionId };
5728
5731
  }
@@ -5829,6 +5832,7 @@ function makeReceipt(plan, candidate, sessionRevision) {
5829
5832
  productionEndpoint: candidate.productionEndpoint,
5830
5833
  productionVersionId: candidate.versionId,
5831
5834
  releaseDigest: plan.intent.releaseDigest,
5835
+ ...previousDigest(plan.existing?.releaseDigest),
5832
5836
  sessionRevision
5833
5837
  };
5834
5838
  }
@@ -5844,9 +5848,16 @@ function makeReceipt(plan, candidate, sessionRevision) {
5844
5848
  productionEndpoint: plan.existing.productionEndpoint,
5845
5849
  productionVersionId: plan.existing.productionVersionId,
5846
5850
  releaseDigest: plan.existing.releaseDigest,
5851
+ ...previousDigest(plan.receipt?.previousReleaseDigest),
5847
5852
  sessionRevision
5848
5853
  };
5849
5854
  }
5855
+ function previousDigest(digest2) {
5856
+ return digest2 === void 0 ? {} : { previousReleaseDigest: digest2 };
5857
+ }
5858
+ function swappedDigests(receipt) {
5859
+ return { releaseDigest: receipt.previousReleaseDigest ?? "", previousReleaseDigest: receipt.releaseDigest, restoredRelease: true };
5860
+ }
5850
5861
  function started(stageId) {
5851
5862
  return event(stageId, "started");
5852
5863
  }
@@ -5867,7 +5878,8 @@ function asDeploymentError(error) {
5867
5878
  if (error instanceof DeploymentApplyError) {
5868
5879
  return error;
5869
5880
  }
5870
- return new DeploymentApplyError("DEPLOYMENT_FAILED", "The managed Moodle MCP deployment failed");
5881
+ const detail = error instanceof Error && error.message ? `: ${error.message}` : "";
5882
+ return new DeploymentApplyError("DEPLOYMENT_FAILED", `The managed Moodle MCP deployment failed${detail}`, { cause: error });
5871
5883
  }
5872
5884
 
5873
5885
  // src/mcp/deployment/node-adapters.ts
@@ -6015,6 +6027,7 @@ function macOSPlan(options, intervalMinutes) {
6015
6027
  const label = `com.moodle-cli.mcp-renewal.${options.profile}`;
6016
6028
  const path4 = `${trimEnd(options.homeDirectory, "/")}/Library/LaunchAgents/${label}.plist`;
6017
6029
  const target = `gui/${options.uid}`;
6030
+ const logPath = `${trimEnd(options.homeDirectory, "/")}/Library/Logs/${label}.log`;
6018
6031
  const plist = [
6019
6032
  '<?xml version="1.0" encoding="UTF-8"?>',
6020
6033
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
@@ -6025,6 +6038,8 @@ function macOSPlan(options, intervalMinutes) {
6025
6038
  "</array>",
6026
6039
  `<key>StartInterval</key><integer>${intervalMinutes * 60}</integer>`,
6027
6040
  "<key>RunAtLoad</key><true/>",
6041
+ `<key>StandardOutPath</key><string>${xml(logPath)}</string>`,
6042
+ `<key>StandardErrorPath</key><string>${xml(logPath)}</string>`,
6028
6043
  "</dict></plist>",
6029
6044
  ""
6030
6045
  ].join("\n");
@@ -6319,7 +6334,7 @@ var NodeDeploymentCommandRunner = class {
6319
6334
  };
6320
6335
  var WranglerCommandError = class extends Error {
6321
6336
  constructor(exitCode, stdout, stderr) {
6322
- super("Packaged Wrangler command failed");
6337
+ super(wranglerFailureMessage(stderr, stdout));
6323
6338
  this.exitCode = exitCode;
6324
6339
  this.stdout = stdout;
6325
6340
  this.stderr = stderr;
@@ -6329,6 +6344,13 @@ var WranglerCommandError = class extends Error {
6329
6344
  stdout;
6330
6345
  stderr;
6331
6346
  };
6347
+ function wranglerFailureMessage(stderr, stdout) {
6348
+ const lines = `${stderr}
6349
+ ${stdout}`.replace(/\u001B\[[0-9;]*m/gu, "").split(/\r?\n/u).map((line) => line.trim());
6350
+ const errorIndex = lines.findIndex((line) => line.includes("[ERROR]"));
6351
+ const detail = errorIndex >= 0 ? lines.slice(errorIndex).filter((line) => line && !/^(To learn more|If you think this is a bug|Logs were written)/u.test(line)).slice(0, 3).join(" ").replace(/^.*\[ERROR\]\s*/u, "") : "";
6352
+ return detail ? `Packaged Wrangler command failed: ${detail.slice(0, 600)}` : "Packaged Wrangler command failed";
6353
+ }
6332
6354
  var NodeWranglerDeploymentAdapter = class {
6333
6355
  wranglerBinPath;
6334
6356
  runner;
@@ -6371,8 +6393,8 @@ ${error.stderr}`)) {
6371
6393
  throw error;
6372
6394
  }
6373
6395
  const document = parseJsonOutput(result.stdout);
6374
- const versionIds = deploymentVersionIds(document);
6375
- if (!versionIds.length) {
6396
+ const [current, previous] = deploymentHistory(document);
6397
+ if (!current) {
6376
6398
  return null;
6377
6399
  }
6378
6400
  const productionEndpoint = firstWorkersDevUrl(document) ?? `https://${workerName}.workers.dev`;
@@ -6383,22 +6405,11 @@ ${error.stderr}`)) {
6383
6405
  deploymentId: deploymentId2,
6384
6406
  ownershipTag: deploymentId2,
6385
6407
  productionEndpoint,
6386
- productionVersionId: versionIds[0],
6387
- previousHealthyVersionId: versionIds[1] ?? null,
6388
- releaseDigest: releaseDigestFromDocument(document) ?? ""
6408
+ productionVersionId: current.versionId,
6409
+ previousHealthyVersionId: previous?.versionId ?? null,
6410
+ releaseDigest: releaseDigestFromMessage(current.message) ?? ""
6389
6411
  };
6390
6412
  }
6391
- async uploadSecrets(input) {
6392
- await this.wrangler([
6393
- "secret",
6394
- "bulk",
6395
- input.secretsFilePath,
6396
- "--name",
6397
- input.workerName,
6398
- "--config",
6399
- input.configPath
6400
- ], input.accountId);
6401
- }
6402
6413
  async initializeWorker(input) {
6403
6414
  let result = null;
6404
6415
  try {
@@ -6408,6 +6419,8 @@ ${error.stderr}`)) {
6408
6419
  input.workerName,
6409
6420
  "--config",
6410
6421
  input.configPath,
6422
+ "--secrets-file",
6423
+ input.secretsFilePath,
6411
6424
  "--message",
6412
6425
  `moodle-cli-bootstrap:${input.releaseDigest}`
6413
6426
  ], input.accountId);
@@ -6442,6 +6455,8 @@ ${error.stderr}`)) {
6442
6455
  input.workerName,
6443
6456
  "--config",
6444
6457
  input.configPath,
6458
+ "--secrets-file",
6459
+ input.secretsFilePath,
6445
6460
  "--preview-alias",
6446
6461
  "moodle-cli-candidate",
6447
6462
  "--message",
@@ -6568,7 +6583,7 @@ var DefaultMoodleSessionSource = class {
6568
6583
  }
6569
6584
  options;
6570
6585
  async loadValidated(_profile, moodleOrigin) {
6571
- const session = this.options.interactive === false ? await getAuthenticatedSession(moodleOrigin, { ...this.options, nonInteractive: true }) : await getAuthenticatedSessionWithBrowserFallback(moodleOrigin, this.options);
6586
+ const session = this.options.interactive === false ? await getAuthenticatedSession(moodleOrigin, { ...this.options, nonInteractive: true, noCache: true }) : await getAuthenticatedSessionWithBrowserFallback(moodleOrigin, this.options);
6572
6587
  return {
6573
6588
  moodleOrigin,
6574
6589
  cookieName: session.cookie.name,
@@ -6610,7 +6625,7 @@ var FetchManagedWorkerClient = class {
6610
6625
  return { revision: body.revision };
6611
6626
  }
6612
6627
  const code = isRecord8(body) && typeof body.code === "string" ? body.code : "SESSION_UPLOAD_FAILED";
6613
- throw new DeploymentApplyError(code, "The Worker rejected the Moodle session update");
6628
+ throw new DeploymentApplyError(code, `The Worker rejected the Moodle session update (${code})`);
6614
6629
  }
6615
6630
  async getReadiness(input) {
6616
6631
  const response = await this.fetchWithRetry(endpointUrl(input.endpoint, "/readyz"), {
@@ -6628,6 +6643,12 @@ var FetchManagedWorkerClient = class {
6628
6643
  }
6629
6644
  return { status: "fail", reasonCode: null, revision: null };
6630
6645
  }
6646
+ async touchSession(input) {
6647
+ await this.fetchWithRetry(endpointUrl(input.endpoint, "/session/touch"), {
6648
+ method: "POST",
6649
+ headers: { authorization: `Bearer ${input.sessionSyncToken}` }
6650
+ }, isRetryableWorkerRouting);
6651
+ }
6631
6652
  async runSmoke(input) {
6632
6653
  const health = await this.fetchWithRetry(
6633
6654
  endpointUrl(input.endpoint, "/healthz"),
@@ -6819,6 +6840,9 @@ function isRetryableSessionUpload(status) {
6819
6840
  function isRetryableWorkerPropagation(status) {
6820
6841
  return status === 404 || status === 429 || status >= 500;
6821
6842
  }
6843
+ function isRetryableWorkerRouting(status) {
6844
+ return status === 404 || status === 429;
6845
+ }
6822
6846
  async function safeJson(response) {
6823
6847
  try {
6824
6848
  return await response.json();
@@ -6871,30 +6895,35 @@ async function readWranglerVersionUpload(outputFilePath, workerName) {
6871
6895
  }
6872
6896
  throw new DeploymentApplyError("CANDIDATE_UPLOAD_INVALID", "Wrangler wrote unsupported candidate metadata");
6873
6897
  }
6874
- function collectStrings(value, keys) {
6875
- const result = [];
6876
- visit(value, (key, item) => {
6877
- if (keys.has(key) && typeof item === "string") {
6878
- result.push(item);
6898
+ function deploymentHistory(value) {
6899
+ const entries = [];
6900
+ visit(value, (_key, item) => {
6901
+ if (!isRecord8(item) || !Array.isArray(item.versions)) {
6902
+ return;
6879
6903
  }
6904
+ const versionId = activeVersionId(item.versions);
6905
+ if (!versionId) {
6906
+ return;
6907
+ }
6908
+ const createdOn = typeof item.created_on === "string" ? Date.parse(item.created_on) : Number.NaN;
6909
+ const message = isRecord8(item.annotations) ? item.annotations["workers/message"] : void 0;
6910
+ entries.push({
6911
+ versionId,
6912
+ message: typeof message === "string" ? message : null,
6913
+ createdOn: Number.isNaN(createdOn) ? 0 : createdOn,
6914
+ index: entries.length
6915
+ });
6880
6916
  });
6881
- return result;
6917
+ return entries.sort((a, b) => b.createdOn - a.createdOn || b.index - a.index).map(({ versionId, message }) => ({ versionId, message }));
6882
6918
  }
6883
- function deploymentVersionIds(value) {
6884
- const ids = collectStrings(value, /* @__PURE__ */ new Set(["version_id", "versionId"]));
6885
- return [...new Set(ids)];
6919
+ function activeVersionId(versions) {
6920
+ const records = versions.filter(isRecord8);
6921
+ const active = records.find((version) => version.percentage === 100) ?? records[0];
6922
+ const id = active?.version_id ?? active?.versionId;
6923
+ return typeof id === "string" ? id : null;
6886
6924
  }
6887
- function releaseDigestFromDocument(value) {
6888
- let digestValue = null;
6889
- visit(value, (_key, item) => {
6890
- if (!digestValue && typeof item === "string") {
6891
- const match = item.match(/moodle-cli-release:([a-zA-Z0-9._-]+)/u);
6892
- if (match?.[1]) {
6893
- digestValue = match[1];
6894
- }
6895
- }
6896
- });
6897
- return digestValue;
6925
+ function releaseDigestFromMessage(message) {
6926
+ return message?.match(/moodle-cli-release:([a-zA-Z0-9._-]+)/u)?.[1] ?? null;
6898
6927
  }
6899
6928
  function collectAccountObjects(value) {
6900
6929
  const accounts = [];
@@ -8062,10 +8091,9 @@ var DefaultMcpCommandService = class {
8062
8091
  throw new UsageError(`No managed Moodle MCP deployment exists for profile ${profile}.`);
8063
8092
  }
8064
8093
  let receipt = storedReceipt;
8065
- const readiness = await this.worker.getReadiness({
8066
- endpoint: receipt.productionEndpoint,
8067
- sessionSyncToken: credentials.sessionSyncToken
8068
- });
8094
+ const target = { endpoint: receipt.productionEndpoint, sessionSyncToken: credentials.sessionSyncToken };
8095
+ await this.worker.touchSession(target);
8096
+ const readiness = await this.worker.getReadiness(target);
8069
8097
  if (readiness.revision !== null && readiness.revision !== receipt.sessionRevision) {
8070
8098
  receipt = await this.writeRenewalRevision(receipt, readiness.revision);
8071
8099
  }
@@ -8077,6 +8105,7 @@ var DefaultMcpCommandService = class {
8077
8105
  agentInstalled: await this.renewal.inspect(profile)
8078
8106
  };
8079
8107
  let replacement = null;
8108
+ let signInDetail;
8080
8109
  if (snapshot.remote === "expiring" || snapshot.remote === "expired") {
8081
8110
  try {
8082
8111
  replacement = await this.sessions.loadValidated(profile, receipt.moodleOrigin);
@@ -8086,6 +8115,7 @@ var DefaultMcpCommandService = class {
8086
8115
  throw error;
8087
8116
  }
8088
8117
  snapshot.replacement = { source: "mfa_required" };
8118
+ signInDetail = [error.message, error.hint].filter(Boolean).join(" ");
8089
8119
  }
8090
8120
  }
8091
8121
  let uploaded = false;
@@ -8138,9 +8168,10 @@ var DefaultMcpCommandService = class {
8138
8168
  text: "Moodle MCP session renewed."
8139
8169
  };
8140
8170
  }
8171
+ const detail = decision.state === "needs_sign_in" && signInDetail ? { detail: signInDetail } : {};
8141
8172
  return {
8142
- data: { profile, state: decision.state, reasonCode: decision.reasonCode, revision: receipt.sessionRevision },
8143
- text: renewalResultText(decision)
8173
+ data: { profile, state: decision.state, reasonCode: decision.reasonCode, revision: receipt.sessionRevision, ...detail },
8174
+ text: [renewalResultText(decision), signInDetail].filter(Boolean).join("\n")
8144
8175
  };
8145
8176
  }
8146
8177
  async writeRenewalRevision(receipt, revision) {
@@ -20597,7 +20597,7 @@ function stringValue(value) {
20597
20597
  }
20598
20598
 
20599
20599
  // src/version.ts
20600
- var VERSION = "0.7.0-alpha.6";
20600
+ var VERSION = "0.7.0-alpha.7";
20601
20601
 
20602
20602
  // src/worker/http.ts
20603
20603
  var HEALTH_PATH = "/healthz";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moodle-cli",
3
- "version": "0.7.0-alpha.6",
3
+ "version": "0.7.0-alpha.7",
4
4
  "description": "Terminal-first CLI for Moodle LMS",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,7 +47,7 @@
47
47
  "vitest": "^3.2.4"
48
48
  },
49
49
  "engines": {
50
- "node": ">=22"
50
+ "node": ">=22.13"
51
51
  },
52
52
  "repository": {
53
53
  "type": "git",