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

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
@@ -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.5` supports MCP `2026-07-28` and a stateless compatibility lane for `2025-11-25` clients.
179
+ Alpha version `0.7.0-alpha.6` 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
@@ -222,8 +222,16 @@ async function getAuthenticatedSession(baseUrl, options = {}) {
222
222
  if (cached) {
223
223
  return cached;
224
224
  }
225
+ const cookieWarnings = [];
226
+ const providerOptions = {
227
+ ...options,
228
+ onCookieWarnings: (warnings) => {
229
+ cookieWarnings.push(...warnings);
230
+ options.onCookieWarnings?.(warnings);
231
+ }
232
+ };
225
233
  const browserProvider = options.browserCookieProvider ?? defaultBrowserCookieProvider;
226
- const browserCookies = matchingMoodleSessionCookies(await browserProvider(baseUrl, options), baseUrl);
234
+ const browserCookies = matchingMoodleSessionCookies(await browserProvider(baseUrl, providerOptions), baseUrl);
227
235
  const browserSession = await firstValidSession(baseUrl, browserCookies, validate);
228
236
  if (browserSession) {
229
237
  await refreshSessionCache(baseUrl, browserSession.cookie, browserSession.context, options);
@@ -247,10 +255,22 @@ async function getAuthenticatedSession(baseUrl, options = {}) {
247
255
  return { baseUrl, cookie: refreshedSession.cookie, ...refreshedSession.context, fromCache: false };
248
256
  }
249
257
  }
250
- throw new AuthError(`No usable MoodleSession found for ${baseUrl}.`, authFailureHint(baseUrl));
258
+ throw new AuthError(
259
+ `No usable MoodleSession found for ${baseUrl}.`,
260
+ authFailureHint(baseUrl, cookieWarnings, options.platform)
261
+ );
251
262
  }
252
263
  async function getAuthenticatedSessionWithBrowserFallback(baseUrl, options = {}) {
253
- const authOptions = { ...options, noCache: true, nonInteractive: true };
264
+ const cookieWarnings = [];
265
+ const authOptions = {
266
+ ...options,
267
+ noCache: true,
268
+ nonInteractive: true,
269
+ onCookieWarnings: (warnings) => {
270
+ cookieWarnings.push(...warnings);
271
+ options.onCookieWarnings?.(warnings);
272
+ }
273
+ };
254
274
  const browserAuthOptions = {
255
275
  ...authOptions,
256
276
  env: { ...options.env ?? process.env, [ENV_MOODLE_SESSION]: void 0 }
@@ -271,6 +291,12 @@ async function getAuthenticatedSessionWithBrowserFallback(baseUrl, options = {})
271
291
  }
272
292
  }
273
293
  }
294
+ if (cookieAccessBlocked(cookieWarnings)) {
295
+ throw new AuthError(
296
+ `Cannot read browser cookies for ${baseUrl}.`,
297
+ cookieAccessHint(cookieWarnings, options.platform)
298
+ );
299
+ }
274
300
  const url = loginUrl(baseUrl);
275
301
  await (options.openBrowser ?? ((target) => openSystemBrowser(target, options)))(url);
276
302
  options.onBrowserOpened?.(url);
@@ -330,7 +356,11 @@ async function defaultBrowserCookieProvider(baseUrl, options = {}) {
330
356
  mode: "merge"
331
357
  });
332
358
  const braveProfiles = await braveProfilePaths(options);
333
- const brave = braveProfiles.length ? await getCookies({ url: baseUrl, browsers: ["chrome"], chromeProfile: braveProfiles, mode: "merge" }) : { cookies: [] };
359
+ const brave = braveProfiles.length ? await getCookies({ url: baseUrl, browsers: ["chrome"], chromeProfile: braveProfiles, mode: "merge" }) : { cookies: [], warnings: [] };
360
+ const warnings = [...primary.warnings ?? [], ...brave.warnings ?? []];
361
+ if (warnings.length) {
362
+ options.onCookieWarnings?.(warnings);
363
+ }
334
364
  return [...primary.cookies, ...brave.cookies].map((cookie) => ({
335
365
  name: cookie.name,
336
366
  value: cookie.value,
@@ -345,7 +375,7 @@ async function braveProfilePaths(options = {}) {
345
375
  const roots = platform === "linux" ? [
346
376
  join2(home, ".config/BraveSoftware/Brave-Browser"),
347
377
  join2(home, ".var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser")
348
- ] : platform === "win32" ? [join2(home, "AppData/Local/BraveSoftware/Brave-Browser/User Data")] : [];
378
+ ] : platform === "win32" ? [join2(home, "AppData/Local/BraveSoftware/Brave-Browser/User Data")] : platform === "darwin" ? [join2(home, "Library/Application Support/BraveSoftware/Brave-Browser")] : [];
349
379
  const profiles = [];
350
380
  for (const root of roots) {
351
381
  try {
@@ -375,13 +405,36 @@ async function loadSessionsFromOktaCli(baseUrl, options = {}, forceLogin = false
375
405
  const refreshed = await readOktaCookies(executable, baseUrl, execFile2);
376
406
  return refreshed.length ? refreshed : stored;
377
407
  }
378
- function authFailureHint(baseUrl) {
408
+ var COOKIE_ACCESS_DENIED = /EPERM|EACCES|operation not permitted|permission denied/i;
409
+ function cookieAccessBlocked(warnings) {
410
+ return warnings.some((warning) => COOKIE_ACCESS_DENIED.test(warning));
411
+ }
412
+ function cookieAccessHint(warnings, platform = process.platform) {
413
+ 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.";
379
414
  return [
415
+ "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,
418
+ `Alternatively set ${ENV_MOODLE_SESSION} to a valid MoodleSession cookie value.`,
419
+ "",
420
+ "Cookie store diagnostics:",
421
+ ...warnings.map((warning) => ` - ${warning}`)
422
+ ].join("\n");
423
+ }
424
+ function authFailureHint(baseUrl, cookieWarnings = [], platform = process.platform) {
425
+ if (cookieAccessBlocked(cookieWarnings)) {
426
+ return cookieAccessHint(cookieWarnings, platform);
427
+ }
428
+ const lines = [
380
429
  `Log in to ${loginUrl(baseUrl)} in your browser, then rerun the command.`,
381
430
  `Or set ${ENV_MOODLE_SESSION} to a valid MoodleSession cookie value.`,
382
431
  `For automatic login, install okta-auth: ${OKTA_AUTH_INSTALL_COMMAND}, then run ${OKTA_AUTH_CONFIG_COMMAND}.`,
383
432
  `okta-auth: ${OKTA_AUTH_URL}`
384
- ].join("\n");
433
+ ];
434
+ if (cookieWarnings.length) {
435
+ lines.push("", "Cookie store diagnostics:", ...cookieWarnings.map((warning) => ` - ${warning}`));
436
+ }
437
+ return lines.join("\n");
385
438
  }
386
439
  async function invalidateCachedSession(baseUrl, options = {}) {
387
440
  await deleteCachedSession(baseUrl, cacheOptions(options));
@@ -3745,8 +3798,8 @@ function keepaliveProgramArguments(execPath = process.execPath, argv1 = process.
3745
3798
  }
3746
3799
  return [execPath, resolvedArgv1, ...tail];
3747
3800
  }
3748
- function buildKeepalivePlist(programArguments, intervalMinutes, logPath) {
3749
- const args = programArguments.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
3801
+ function buildKeepalivePlist(programArguments2, intervalMinutes, logPath) {
3802
+ const args = programArguments2.map((arg) => ` <string>${escapeXml(arg)}</string>`).join("\n");
3750
3803
  return [
3751
3804
  '<?xml version="1.0" encoding="UTF-8"?>',
3752
3805
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
@@ -3827,7 +3880,7 @@ function escapeXml(value) {
3827
3880
  }
3828
3881
 
3829
3882
  // src/version.ts
3830
- var VERSION = "0.7.0-alpha.5";
3883
+ var VERSION = "0.7.0-alpha.6";
3831
3884
 
3832
3885
  // src/forum.ts
3833
3886
  function parseDiscussionReference(value) {
@@ -4412,7 +4465,7 @@ function jsonCodec(container) {
4412
4465
  };
4413
4466
  }
4414
4467
  function jsonRegistration(connection) {
4415
- return connection.mode === "bridge" ? { command: connection.command, args: ["mcp", "bridge", "--profile", connection.profile] } : {
4468
+ return connection.mode === "bridge" ? { command: connection.command, args: connection.args } : {
4416
4469
  type: "http",
4417
4470
  url: connection.endpoint,
4418
4471
  headers: { Authorization: `Bearer ${connection.accessToken}` }
@@ -4426,7 +4479,7 @@ function tomlBlock(registration, connection) {
4426
4479
  if (connection.mode === "bridge") {
4427
4480
  lines.push(
4428
4481
  `command = ${JSON.stringify(connection.command)}`,
4429
- `args = ${JSON.stringify(["mcp", "bridge", "--profile", connection.profile])}`
4482
+ `args = ${JSON.stringify(connection.args)}`
4430
4483
  );
4431
4484
  } else {
4432
4485
  lines.push(
@@ -4478,7 +4531,11 @@ function validateProfile(profile) {
4478
4531
  }
4479
4532
  function resolveConnection(options) {
4480
4533
  if (options.mode !== "remote") {
4481
- return { mode: "bridge", command: options.command ?? "moodle", profile: options.profile };
4534
+ return {
4535
+ mode: "bridge",
4536
+ command: options.command ?? "moodle",
4537
+ args: [...options.commandArgs ?? [], "mcp", "bridge", "--profile", options.profile]
4538
+ };
4482
4539
  }
4483
4540
  if (!options.endpoint || !options.accessToken) {
4484
4541
  throw new Error("Remote MCP connection requires an endpoint and access token");
@@ -4498,6 +4555,15 @@ function resolveConnection(options) {
4498
4555
  return { mode: "remote", endpoint: endpoint.toString(), accessToken: options.accessToken };
4499
4556
  }
4500
4557
 
4558
+ // src/mcp/self-command.ts
4559
+ function selfCommand(argv = process.argv, execPath = process.execPath) {
4560
+ const script = argv[1];
4561
+ return script && script !== execPath ? { command: execPath, args: [script] } : { command: execPath, args: [] };
4562
+ }
4563
+ function runtimeCommand(command, args) {
4564
+ return command ? { command, args: [...args ?? []] } : selfCommand();
4565
+ }
4566
+
4501
4567
  // src/mcp/connectors/node-connectors.ts
4502
4568
  import { chmod as chmod2, mkdir as mkdir4, readFile as readFile3, rm as rm3, stat as stat2, writeFile as writeFile4 } from "fs/promises";
4503
4569
  import { homedir as homedir5 } from "os";
@@ -4530,9 +4596,11 @@ function createDefaultClientConnectors(profile, options = {}) {
4530
4596
  const home = options.homeDirectory ?? homedir5();
4531
4597
  const platform = options.platform ?? process.platform;
4532
4598
  const fileSystem = options.fileSystem ?? new NodeConnectorFileSystem();
4599
+ const runtime = runtimeCommand(options.command, options.commandArgs);
4533
4600
  const shared = {
4534
4601
  profile,
4535
- command: options.command,
4602
+ command: runtime.command,
4603
+ commandArgs: runtime.args,
4536
4604
  mode: options.mode,
4537
4605
  endpoint: options.endpoint,
4538
4606
  accessToken: options.accessToken
@@ -5392,7 +5460,8 @@ var ManagedMcpDeployment = class {
5392
5460
  await this.dependencies.wrangler.promote({
5393
5461
  accountId: plan.intent.accountId,
5394
5462
  workerName: plan.intent.workerName,
5395
- versionId: candidate.versionId
5463
+ versionId: candidate.versionId,
5464
+ releaseDigest: plan.intent.releaseDigest
5396
5465
  });
5397
5466
  promoted = true;
5398
5467
  }
@@ -5434,7 +5503,8 @@ var ManagedMcpDeployment = class {
5434
5503
  await this.dependencies.wrangler.promote({
5435
5504
  accountId: plan.intent.accountId,
5436
5505
  workerName: plan.intent.workerName,
5437
- versionId: candidate.versionId
5506
+ versionId: candidate.versionId,
5507
+ releaseDigest: plan.intent.releaseDigest
5438
5508
  });
5439
5509
  promoted = true;
5440
5510
  }
@@ -5543,7 +5613,7 @@ var ManagedMcpDeployment = class {
5543
5613
  readinessReasonCode = remoteReadiness.reasonCode;
5544
5614
  sessionRevision = remoteReadiness.revision;
5545
5615
  }
5546
- const resolvedWorker = worker ? { ...worker, productionEndpoint: receipt.productionEndpoint } : null;
5616
+ const resolvedWorker = worker ? { ...worker, productionEndpoint: receipt.productionEndpoint, releaseDigest: worker.releaseDigest || receipt.releaseDigest } : null;
5547
5617
  return {
5548
5618
  profile,
5549
5619
  worker: resolvedWorker,
@@ -5945,14 +6015,13 @@ function macOSPlan(options, intervalMinutes) {
5945
6015
  const label = `com.moodle-cli.mcp-renewal.${options.profile}`;
5946
6016
  const path4 = `${trimEnd(options.homeDirectory, "/")}/Library/LaunchAgents/${label}.plist`;
5947
6017
  const target = `gui/${options.uid}`;
5948
- const args = renewalArgs(options.profile);
5949
6018
  const plist = [
5950
6019
  '<?xml version="1.0" encoding="UTF-8"?>',
5951
6020
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
5952
6021
  '<plist version="1.0"><dict>',
5953
6022
  `<key>Label</key><string>${xml(label)}</string>`,
5954
6023
  "<key>ProgramArguments</key><array>",
5955
- ...[options.executable, ...args].map((arg) => `<string>${xml(arg)}</string>`),
6024
+ ...programArguments(options).map((arg) => `<string>${xml(arg)}</string>`),
5956
6025
  "</array>",
5957
6026
  `<key>StartInterval</key><integer>${intervalMinutes * 60}</integer>`,
5958
6027
  "<key>RunAtLoad</key><true/>",
@@ -5976,7 +6045,7 @@ function linuxPlan(options, intervalMinutes) {
5976
6045
  const directory = `${trimEnd(options.homeDirectory, "/")}/.config/systemd/user`;
5977
6046
  const servicePath = `${directory}/${label}.service`;
5978
6047
  const timerPath = `${directory}/${label}.timer`;
5979
- const command = [options.executable, ...renewalArgs(options.profile)].map(systemdQuote).join(" ");
6048
+ const command = programArguments(options).map(systemdQuote).join(" ");
5980
6049
  const service = [
5981
6050
  "[Unit]",
5982
6051
  `Description=Moodle MCP session renewal (${options.profile})`,
@@ -6020,7 +6089,7 @@ function linuxPlan(options, intervalMinutes) {
6020
6089
  function windowsPlan(options, intervalMinutes) {
6021
6090
  const label = `Moodle CLI MCP Renewal (${options.profile})`;
6022
6091
  const path4 = `${trimEnd(options.homeDirectory, "\\/")}\\AppData\\Local\\moodle-cli\\renewal\\${options.profile}.xml`;
6023
- const argumentsText = renewalArgs(options.profile).map(windowsArgument).join(" ");
6092
+ const argumentsText = [...options.executableArgs ?? [], ...renewalArgs(options.profile)].map(windowsArgument).join(" ");
6024
6093
  const task = [
6025
6094
  '<?xml version="1.0" encoding="UTF-8"?>',
6026
6095
  '<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
@@ -6047,6 +6116,9 @@ function windowsPlan(options, intervalMinutes) {
6047
6116
  function renewalArgs(profile) {
6048
6117
  return ["mcp", "renewal", "run", "--profile", profile, "--json"];
6049
6118
  }
6119
+ function programArguments(options) {
6120
+ return [options.executable, ...options.executableArgs ?? [], ...renewalArgs(options.profile)];
6121
+ }
6050
6122
  function validateOptions(options) {
6051
6123
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(options.profile)) {
6052
6124
  throw new Error("Invalid renewal profile name");
@@ -6054,6 +6126,9 @@ function validateOptions(options) {
6054
6126
  if (!options.executable || /[\r\n]/.test(options.executable)) {
6055
6127
  throw new Error("Invalid renewal executable path");
6056
6128
  }
6129
+ if ((options.executableArgs ?? []).some((arg) => /[\r\n]/.test(arg))) {
6130
+ throw new Error("Invalid renewal executable arguments");
6131
+ }
6057
6132
  if (!options.homeDirectory || /[\r\n]/.test(options.homeDirectory)) {
6058
6133
  throw new Error("Invalid renewal home directory");
6059
6134
  }
@@ -6128,10 +6203,12 @@ function createDefaultRenewalInstaller(profile, options = {}) {
6128
6203
  if (!isSupportedPlatform(platform)) {
6129
6204
  throw new Error(`Moodle MCP renewal is not supported on ${platform}`);
6130
6205
  }
6206
+ const runtime = runtimeCommand(options.executable, options.executableArgs);
6131
6207
  const plan = buildRenewalInstallPlan({
6132
6208
  platform,
6133
6209
  profile,
6134
- executable: options.executable ?? process.argv[1] ?? process.execPath,
6210
+ executable: runtime.command,
6211
+ executableArgs: runtime.args,
6135
6212
  homeDirectory: options.homeDirectory ?? homedir7(),
6136
6213
  uid: options.uid ?? (typeof process.getuid === "function" ? process.getuid() : void 0),
6137
6214
  intervalMinutes: options.intervalMinutes
@@ -6386,6 +6463,8 @@ ${error.stderr}`)) {
6386
6463
  await rm6(outputFilePath, { force: true });
6387
6464
  }
6388
6465
  }
6466
+ // restoreProduction re-deploys an older version whose digest is unknown, so the
6467
+ // release annotation is only written when the caller knows it.
6389
6468
  async promote(input) {
6390
6469
  await this.wrangler([
6391
6470
  "versions",
@@ -6393,7 +6472,8 @@ ${error.stderr}`)) {
6393
6472
  `${input.versionId}@100`,
6394
6473
  "--name",
6395
6474
  input.workerName,
6396
- "--yes"
6475
+ "--yes",
6476
+ ...input.releaseDigest ? ["--message", `moodle-cli-release:${input.releaseDigest}`] : []
6397
6477
  ], input.accountId);
6398
6478
  }
6399
6479
  async restoreProduction(input) {
@@ -6657,7 +6737,7 @@ var PrivateDeploymentReceiptStore = class {
6657
6737
  function createDefaultManagedDeployment(options) {
6658
6738
  const homeDirectory = options.homeDirectory ?? homedir8();
6659
6739
  const platform = options.platform ?? process.platform;
6660
- const executable = options.executable ?? process.argv[1] ?? process.execPath;
6740
+ const runtime = runtimeCommand(options.executable, options.executableArgs);
6661
6741
  const defaults = {
6662
6742
  wrangler: new NodeWranglerDeploymentAdapter({ wranglerBinPath: options.wranglerBinPath }),
6663
6743
  materializer: new NodeReleaseMaterializer({
@@ -6671,14 +6751,16 @@ function createDefaultManagedDeployment(options) {
6671
6751
  ...options.renewal,
6672
6752
  platform,
6673
6753
  homeDirectory,
6674
- executable,
6754
+ executable: runtime.command,
6755
+ executableArgs: runtime.args,
6675
6756
  uid: options.uid
6676
6757
  }),
6677
6758
  clients: new DefaultClientIntegration({
6678
6759
  ...options.connector,
6679
6760
  platform,
6680
6761
  homeDirectory,
6681
- command: executable
6762
+ command: runtime.command,
6763
+ commandArgs: runtime.args
6682
6764
  }),
6683
6765
  receipts: new PrivateDeploymentReceiptStore(join7(homeDirectory, ".config", "moodle-cli", "mcp", "deployments")),
6684
6766
  createToken: () => randomBytes(32).toString("base64url")
@@ -7744,8 +7826,7 @@ var DefaultMcpCommandService = class {
7744
7826
  this.worker = options.worker ?? new FetchManagedWorkerClient(options.fetchImpl);
7745
7827
  this.renewal = options.renewal ?? new DefaultRenewalIntegration({
7746
7828
  platform: process.platform,
7747
- homeDirectory: this.homeDirectory,
7748
- executable: process.argv[1] ?? process.execPath
7829
+ homeDirectory: this.homeDirectory
7749
7830
  });
7750
7831
  this.sessions = options.sessions ?? createBackgroundMoodleSessionSource({
7751
7832
  env: options.env,
@@ -7908,7 +7989,6 @@ var DefaultMcpCommandService = class {
7908
7989
  const connectors = createDefaultClientConnectors(profile, {
7909
7990
  homeDirectory: this.homeDirectory,
7910
7991
  platform: process.platform,
7911
- command: process.argv[1] ?? "moodle",
7912
7992
  mode: input.mode,
7913
7993
  ...input.mode === "remote" ? { endpoint, accessToken: credentials.mcpAccessToken } : {}
7914
7994
  });
@@ -8111,7 +8191,6 @@ var DefaultMcpCommandService = class {
8111
8191
  compatibilityDate: this.options.compatibilityDate ?? WORKER_COMPATIBILITY_DATE,
8112
8192
  homeDirectory: this.homeDirectory,
8113
8193
  platform: process.platform,
8114
- executable: process.argv[1] ?? process.execPath,
8115
8194
  fetch: this.options.fetchImpl,
8116
8195
  auth: {
8117
8196
  env: this.options.env,
@@ -8183,8 +8262,7 @@ Selection: `)).trim());
8183
8262
  async connectedClientNames(profile) {
8184
8263
  const connectors = createDefaultClientConnectors(profile, {
8185
8264
  homeDirectory: this.homeDirectory,
8186
- platform: process.platform,
8187
- command: process.argv[1] ?? "moodle"
8265
+ platform: process.platform
8188
8266
  });
8189
8267
  const clients = [];
8190
8268
  for (const connector of connectors) {
@@ -20597,7 +20597,7 @@ function stringValue(value) {
20597
20597
  }
20598
20598
 
20599
20599
  // src/version.ts
20600
- var VERSION = "0.7.0-alpha.5";
20600
+ var VERSION = "0.7.0-alpha.6";
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.5",
3
+ "version": "0.7.0-alpha.6",
4
4
  "description": "Terminal-first CLI for Moodle LMS",
5
5
  "license": "MIT",
6
6
  "type": "module",