moodle-cli 0.7.0-alpha.1 → 0.7.0-alpha.3

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
@@ -174,7 +174,7 @@ moodle mcp bridge
174
174
 
175
175
  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.
176
176
 
177
- Alpha version `0.7.0-alpha.1` supports MCP `2026-07-28` and a stateless compatibility lane for `2025-11-25` clients.
177
+ Alpha version `0.7.0-alpha.3` supports MCP `2026-07-28` and a stateless compatibility lane for `2025-11-25` clients.
178
178
 
179
179
  ### Configuration
180
180
 
package/dist/moodle.js CHANGED
@@ -1861,7 +1861,7 @@ var MoodleClientCore = class {
1861
1861
  constructor(baseUrl, options) {
1862
1862
  this.baseUrl = baseUrl.replace(/\/$/, "");
1863
1863
  const resolvedOptions = typeof options === "string" ? { cookie: { name: "MoodleSession", value: options } } : options;
1864
- this.fetchImpl = resolvedOptions.fetchImpl ?? fetch;
1864
+ this.fetchImpl = resolvedOptions.fetchImpl ?? ((input, init) => fetch(input, init));
1865
1865
  this.cookie = resolvedOptions.cookie;
1866
1866
  this.sesskey = resolvedOptions.pageContext?.sesskey ?? resolvedOptions.sesskey ?? null;
1867
1867
  this.userid = resolvedOptions.pageContext?.user_info.userid ?? resolvedOptions.userid ?? null;
@@ -3700,7 +3700,7 @@ function escapeXml(value) {
3700
3700
  }
3701
3701
 
3702
3702
  // src/version.ts
3703
- var VERSION = "0.7.0-alpha.1";
3703
+ var VERSION = "0.7.0-alpha.3";
3704
3704
 
3705
3705
  // src/forum.ts
3706
3706
  function parseDiscussionReference(value) {
@@ -4519,6 +4519,61 @@ import { chmod as chmod3, mkdir as mkdir5, readFile as readFile4, rename as rena
4519
4519
  import { homedir as homedir6 } from "os";
4520
4520
  import { dirname as dirname5, join as join6, win32 as windowsPath } from "path";
4521
4521
  var SERVICE = "moodle-cli-mcp";
4522
+ var MACOS_KEYCHAIN_SCRIPT = `
4523
+ ObjC.import("Foundation")
4524
+ ObjC.import("Security")
4525
+
4526
+ function main() {
4527
+ const input = $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile
4528
+ const text = $.NSString.alloc.initWithDataEncoding(input, $.NSUTF8StringEncoding)
4529
+ const payload = JSON.parse(ObjC.unwrap(text))
4530
+ const keys = {
4531
+ class: "class",
4532
+ genericPassword: "genp",
4533
+ service: "svce",
4534
+ account: "acct",
4535
+ valueData: "v_Data",
4536
+ returnData: "r_Data",
4537
+ }
4538
+ const query = $.NSMutableDictionary.alloc.init
4539
+ query.setObjectForKey(keys.genericPassword, keys.class)
4540
+ query.setObjectForKey(payload.service, keys.service)
4541
+ query.setObjectForKey(payload.profile, keys.account)
4542
+
4543
+ if (payload.operation === "read") {
4544
+ query.setObjectForKey(true, keys.returnData)
4545
+ const result = $()
4546
+ const status = Number($.SecItemCopyMatching(query, result))
4547
+ if (status === -25300) return
4548
+ if (status !== 0) throw new Error("Keychain read failed: " + status)
4549
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(result)
4550
+ return
4551
+ }
4552
+
4553
+ if (payload.operation === "write") {
4554
+ const value = $(payload.credentials).dataUsingEncoding($.NSUTF8StringEncoding)
4555
+ const attributes = $.NSMutableDictionary.alloc.init
4556
+ attributes.setObjectForKey(value, keys.valueData)
4557
+ let status = Number($.SecItemUpdate(query, attributes))
4558
+ if (status === -25300) {
4559
+ query.setObjectForKey(value, keys.valueData)
4560
+ status = Number($.SecItemAdd(query, null))
4561
+ }
4562
+ if (status !== 0) throw new Error("Keychain write failed: " + status)
4563
+ return
4564
+ }
4565
+
4566
+ if (payload.operation === "delete") {
4567
+ const status = Number($.SecItemDelete(query))
4568
+ if (status !== 0 && status !== -25300) throw new Error("Keychain delete failed: " + status)
4569
+ return
4570
+ }
4571
+
4572
+ throw new Error("Unsupported Keychain operation")
4573
+ }
4574
+
4575
+ main()
4576
+ `;
4522
4577
  var WINDOWS_CREDENTIAL_READ = `
4523
4578
  $ErrorActionPreference = "Stop"
4524
4579
  [Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
@@ -4623,24 +4678,30 @@ var MacOSKeychainCredentialBackend = class {
4623
4678
  name = "macOS Login Keychain";
4624
4679
  async read(profile) {
4625
4680
  try {
4626
- const result = await this.runner.run("security", ["find-generic-password", "-s", SERVICE, "-a", profile, "-w"]);
4627
- return parseCredentials(result.stdout.trim());
4681
+ const result = await this.runner.run(
4682
+ "osascript",
4683
+ ["-l", "JavaScript", "-e", MACOS_KEYCHAIN_SCRIPT],
4684
+ macosKeychainInput("read", profile)
4685
+ );
4686
+ const value = result.stdout.trim();
4687
+ if (value) {
4688
+ return parseCredentials(value);
4689
+ }
4690
+ await this.deleteEmptyLegacyEntry(profile);
4691
+ return null;
4628
4692
  } catch (error) {
4629
4693
  if (commandNotFound(error)) {
4630
4694
  throw new CredentialBackendUnavailableError(this.name, error);
4631
4695
  }
4632
- if (commandExitCode(error) === 44) {
4633
- return null;
4634
- }
4635
4696
  throw error;
4636
4697
  }
4637
4698
  }
4638
4699
  async write(profile, credentials) {
4639
4700
  try {
4640
4701
  await this.runner.run(
4641
- "security",
4642
- ["add-generic-password", "-s", SERVICE, "-a", profile, "-U", "-w"],
4643
- JSON.stringify(credentials)
4702
+ "osascript",
4703
+ ["-l", "JavaScript", "-e", MACOS_KEYCHAIN_SCRIPT],
4704
+ macosKeychainInput("write", profile, credentials)
4644
4705
  );
4645
4706
  } catch (error) {
4646
4707
  if (commandNotFound(error)) {
@@ -4651,14 +4712,26 @@ var MacOSKeychainCredentialBackend = class {
4651
4712
  }
4652
4713
  async delete(profile) {
4653
4714
  try {
4654
- await this.runner.run("security", ["delete-generic-password", "-s", SERVICE, "-a", profile]);
4715
+ await this.runner.run(
4716
+ "osascript",
4717
+ ["-l", "JavaScript", "-e", MACOS_KEYCHAIN_SCRIPT],
4718
+ macosKeychainInput("delete", profile)
4719
+ );
4655
4720
  } catch (error) {
4656
4721
  if (commandNotFound(error)) {
4657
4722
  throw new CredentialBackendUnavailableError(this.name, error);
4658
4723
  }
4659
- if (commandExitCode(error) !== 44) {
4660
- throw error;
4724
+ throw error;
4725
+ }
4726
+ }
4727
+ async deleteEmptyLegacyEntry(profile) {
4728
+ try {
4729
+ await this.runner.run("security", ["delete-generic-password", "-s", SERVICE, "-a", profile]);
4730
+ } catch (error) {
4731
+ if (commandNotFound(error) || commandExitCode(error) === 44) {
4732
+ return;
4661
4733
  }
4734
+ throw error;
4662
4735
  }
4663
4736
  }
4664
4737
  };
@@ -4883,6 +4956,14 @@ function parseCredentials(value) {
4883
4956
  function powershellArgs(script) {
4884
4957
  return ["-NoProfile", "-NonInteractive", "-Command", script];
4885
4958
  }
4959
+ function macosKeychainInput(operation, profile, credentials) {
4960
+ return JSON.stringify({
4961
+ operation,
4962
+ service: SERVICE,
4963
+ profile,
4964
+ ...credentials ? { credentials: JSON.stringify(credentials) } : {}
4965
+ });
4966
+ }
4886
4967
  function windowsCredentialInput(profile, credentials) {
4887
4968
  return JSON.stringify({
4888
4969
  service: SERVICE,
@@ -5073,7 +5154,7 @@ var ManagedMcpDeployment = class {
5073
5154
  `Worker ${intent.workerName} is not owned by Moodle MCP profile ${intent.profile}`
5074
5155
  );
5075
5156
  }
5076
- const receipt = replacingExisting ? null : matchingReceipt;
5157
+ const receipt = replacingExisting || remote === null ? null : matchingReceipt;
5077
5158
  const existing = remote && receipt ? { ...remote, productionEndpoint: receipt.productionEndpoint, releaseDigest: receipt.releaseDigest } : remote;
5078
5159
  const rotate = intent.rotateToken === true && credentials !== null;
5079
5160
  const releaseChanged = existing?.releaseDigest !== intent.releaseDigest;
@@ -5098,8 +5179,10 @@ var ManagedMcpDeployment = class {
5098
5179
  let credentials = null;
5099
5180
  let credentialsBefore = null;
5100
5181
  let secretsUploaded = false;
5182
+ let initializedWorker = null;
5101
5183
  let candidate = null;
5102
5184
  let appliedReceipt = null;
5185
+ let productionRestored = false;
5103
5186
  let moodleUser = null;
5104
5187
  try {
5105
5188
  yield started(activeStage);
@@ -5122,6 +5205,14 @@ var ManagedMcpDeployment = class {
5122
5205
  activeStage = "upload_private_credentials";
5123
5206
  yield started(activeStage);
5124
5207
  if (plan.uploadCandidate) {
5208
+ if (plan.operation === "create") {
5209
+ initializedWorker = await this.dependencies.wrangler.initializeWorker({
5210
+ accountId: plan.intent.accountId,
5211
+ workerName: plan.intent.workerName,
5212
+ configPath: prepared.wranglerConfigPath,
5213
+ releaseDigest: plan.intent.releaseDigest
5214
+ });
5215
+ }
5125
5216
  await this.dependencies.wrangler.uploadSecrets({
5126
5217
  accountId: plan.intent.accountId,
5127
5218
  workerName: plan.intent.workerName,
@@ -5134,17 +5225,30 @@ var ManagedMcpDeployment = class {
5134
5225
  activeStage = "deploy_candidate_version";
5135
5226
  yield started(activeStage);
5136
5227
  if (plan.uploadCandidate) {
5228
+ const productionEndpoint2 = initializedWorker?.productionEndpoint ?? plan.existing?.productionEndpoint;
5229
+ if (!productionEndpoint2) {
5230
+ throw new DeploymentApplyError("MISSING_ENDPOINT", "The Worker production endpoint is unavailable");
5231
+ }
5137
5232
  candidate = await this.dependencies.wrangler.uploadCandidate({
5138
5233
  accountId: plan.intent.accountId,
5139
5234
  workerName: plan.intent.workerName,
5140
5235
  configPath: prepared.wranglerConfigPath,
5141
- releaseDigest: plan.intent.releaseDigest
5236
+ releaseDigest: plan.intent.releaseDigest,
5237
+ productionEndpoint: productionEndpoint2
5142
5238
  });
5239
+ if (!candidate.previewEndpoint) {
5240
+ await this.dependencies.wrangler.promote({
5241
+ accountId: plan.intent.accountId,
5242
+ workerName: plan.intent.workerName,
5243
+ versionId: candidate.versionId
5244
+ });
5245
+ promoted = true;
5246
+ }
5143
5247
  }
5144
5248
  yield completed(activeStage);
5145
5249
  activeStage = "upload_moodle_session";
5146
5250
  yield started(activeStage);
5147
- const sessionEndpoint = candidate?.previewEndpoint ?? plan.existing?.productionEndpoint;
5251
+ const sessionEndpoint = candidate?.previewEndpoint ?? candidate?.productionEndpoint ?? plan.existing?.productionEndpoint;
5148
5252
  if (!sessionEndpoint) {
5149
5253
  throw new DeploymentApplyError("MISSING_ENDPOINT", "The Worker did not provide a session endpoint");
5150
5254
  }
@@ -5162,7 +5266,7 @@ var ManagedMcpDeployment = class {
5162
5266
  yield completed(activeStage);
5163
5267
  activeStage = "run_release_checks";
5164
5268
  yield started(activeStage);
5165
- if (candidate) {
5269
+ if (candidate?.previewEndpoint) {
5166
5270
  try {
5167
5271
  await this.dependencies.worker.runSmoke({
5168
5272
  endpoint: candidate.previewEndpoint,
@@ -5197,6 +5301,9 @@ var ManagedMcpDeployment = class {
5197
5301
  if (!promoted) {
5198
5302
  throw new DeploymentApplyError("PRODUCTION_VALIDATION_FAILED", "The existing Worker failed validation");
5199
5303
  }
5304
+ if (plan.operation === "create") {
5305
+ throw new DeploymentApplyError("PRODUCTION_VALIDATION_FAILED", "The new Worker failed production validation");
5306
+ }
5200
5307
  candidateRevision = await this.rollbackProduction(
5201
5308
  plan,
5202
5309
  credentials,
@@ -5204,6 +5311,7 @@ var ManagedMcpDeployment = class {
5204
5311
  productionEndpoint,
5205
5312
  candidateRevision
5206
5313
  );
5314
+ productionRestored = true;
5207
5315
  throw new DeploymentApplyError(
5208
5316
  "PRODUCTION_VALIDATION_FAILED_RESTORED",
5209
5317
  "The previous healthy release was restored with the current credentials and Moodle session"
@@ -5221,7 +5329,25 @@ var ManagedMcpDeployment = class {
5221
5329
  if (plan.intent.rotateToken && credentialsBefore && !secretsUploaded) {
5222
5330
  await this.dependencies.credentials.write(plan.intent.profile, credentialsBefore);
5223
5331
  }
5224
- if (!appliedReceipt && plan.receipt && candidateRevision !== null) {
5332
+ if (promoted && plan.existing && !productionRestored && !appliedReceipt) {
5333
+ await this.dependencies.wrangler.restoreProduction({
5334
+ accountId: plan.intent.accountId,
5335
+ workerName: plan.intent.workerName,
5336
+ previousVersionId: plan.existing.productionVersionId
5337
+ });
5338
+ productionRestored = true;
5339
+ if (plan.intent.rotateToken && credentialsBefore) {
5340
+ await this.dependencies.credentials.write(plan.intent.profile, credentialsBefore);
5341
+ }
5342
+ }
5343
+ if (initializedWorker && !appliedReceipt) {
5344
+ await this.dependencies.wrangler.removeWorker({
5345
+ accountId: plan.intent.accountId,
5346
+ workerName: plan.intent.workerName,
5347
+ deploymentId: deploymentId(plan.intent.accountId, plan.intent.workerName)
5348
+ });
5349
+ await this.removeLocalState(plan.intent.profile);
5350
+ } else if (!appliedReceipt && plan.receipt && candidateRevision !== null) {
5225
5351
  await this.dependencies.receipts.write({ ...plan.receipt, sessionRevision: candidateRevision });
5226
5352
  }
5227
5353
  const safe = asDeploymentError(error);
@@ -5458,6 +5584,9 @@ function validateIntent(intent) {
5458
5584
  throw new DeploymentPlanError("INVALID_INTENT", "Cloudflare account and release digest are required");
5459
5585
  }
5460
5586
  }
5587
+ function deploymentId(accountId, workerName) {
5588
+ return `moodle-cli:${accountId}:${workerName}`;
5589
+ }
5461
5590
  function isOwnedByProfile(worker, receipt, profile) {
5462
5591
  return receipt ? receipt.accountId === worker.accountId && receipt.workerName === worker.workerName && receipt.deploymentId === worker.ownershipTag : worker.ownershipTag === `moodle-cli:${profile}`;
5463
5592
  }
@@ -5930,6 +6059,8 @@ function isMissing3(error) {
5930
6059
 
5931
6060
  // src/mcp/deployment/node-adapters.ts
5932
6061
  var MODERN_MCP_VERSION = "2026-07-28";
6062
+ var WORKER_PROPAGATION_ATTEMPTS = 10;
6063
+ var WORKER_PROPAGATION_MAX_DELAY_MS = 4e3;
5933
6064
  var NodeDeploymentCommandRunner = class {
5934
6065
  async run(command, args, environment = {}) {
5935
6066
  return new Promise((resolve, reject) => {
@@ -6016,12 +6147,12 @@ ${error.stderr}`)) {
6016
6147
  return null;
6017
6148
  }
6018
6149
  const productionEndpoint = firstWorkersDevUrl(document) ?? `https://${workerName}.workers.dev`;
6019
- const deploymentId = ownershipId(accountId, workerName);
6150
+ const deploymentId2 = ownershipId(accountId, workerName);
6020
6151
  return {
6021
6152
  accountId,
6022
6153
  workerName,
6023
- deploymentId,
6024
- ownershipTag: deploymentId,
6154
+ deploymentId: deploymentId2,
6155
+ ownershipTag: deploymentId2,
6025
6156
  productionEndpoint,
6026
6157
  productionVersionId: versionIds[0],
6027
6158
  previousHealthyVersionId: versionIds[1] ?? null,
@@ -6039,6 +6170,36 @@ ${error.stderr}`)) {
6039
6170
  input.configPath
6040
6171
  ], input.accountId);
6041
6172
  }
6173
+ async initializeWorker(input) {
6174
+ let result = null;
6175
+ try {
6176
+ result = await this.wrangler([
6177
+ "deploy",
6178
+ "--name",
6179
+ input.workerName,
6180
+ "--config",
6181
+ input.configPath,
6182
+ "--message",
6183
+ `moodle-cli-bootstrap:${input.releaseDigest}`
6184
+ ], input.accountId);
6185
+ const worker = await this.inspect(input.accountId, input.workerName);
6186
+ if (!worker) {
6187
+ throw new DeploymentApplyError("INITIAL_WORKER_INVALID", "Wrangler did not return the initialized Worker");
6188
+ }
6189
+ const productionEndpoint = firstWorkersDevUrl([result.stdout, result.stderr]);
6190
+ return productionEndpoint ? { ...worker, productionEndpoint } : worker;
6191
+ } catch (error) {
6192
+ const worker = await this.inspect(input.accountId, input.workerName).catch(() => null);
6193
+ if (worker || result) {
6194
+ await this.removeWorker({
6195
+ accountId: input.accountId,
6196
+ workerName: input.workerName,
6197
+ deploymentId: ownershipId(input.accountId, input.workerName)
6198
+ }).catch(() => void 0);
6199
+ }
6200
+ throw error;
6201
+ }
6202
+ }
6042
6203
  async uploadCandidate(input) {
6043
6204
  const outputFilePath = join7(dirname7(input.configPath), "wrangler-version-upload.jsonl");
6044
6205
  await rm6(outputFilePath, { force: true });
@@ -6058,13 +6219,13 @@ ${error.stderr}`)) {
6058
6219
  const document = await readWranglerVersionUpload(outputFilePath, input.workerName);
6059
6220
  const versionId = typeof document.version_id === "string" ? document.version_id : null;
6060
6221
  const previewEndpoint = firstWorkersDevUrl(document.preview_alias_url) ?? firstWorkersDevUrl(document.preview_url);
6061
- if (!versionId || !previewEndpoint) {
6062
- throw new DeploymentApplyError("CANDIDATE_UPLOAD_INVALID", "Wrangler did not return a candidate version and preview endpoint");
6222
+ if (!versionId) {
6223
+ throw new DeploymentApplyError("CANDIDATE_UPLOAD_INVALID", "Wrangler did not return a candidate version");
6063
6224
  }
6064
6225
  return {
6065
6226
  versionId,
6066
- previewEndpoint,
6067
- productionEndpoint: productionEndpointFromPreview(previewEndpoint, input.workerName),
6227
+ previewEndpoint: previewEndpoint ?? null,
6228
+ productionEndpoint: input.productionEndpoint,
6068
6229
  deploymentId: ownershipId(input.accountId, input.workerName)
6069
6230
  };
6070
6231
  } finally {
@@ -6130,6 +6291,7 @@ var NodeReleaseMaterializer = class {
6130
6291
  account_id: plan.intent.accountId,
6131
6292
  main: `./${basename(workerFile)}`,
6132
6293
  compatibility_date: this.options.compatibilityDate,
6294
+ preview_urls: true,
6133
6295
  vars: { MOODLE_ORIGIN: plan.intent.moodleOrigin },
6134
6296
  durable_objects: {
6135
6297
  bindings: [{ name: "SESSION_BROKER", class_name: "SessionBroker" }]
@@ -6185,12 +6347,14 @@ function createBackgroundMoodleSessionSource(options = {}) {
6185
6347
  return new DefaultMoodleSessionSource({ ...options, interactive: false });
6186
6348
  }
6187
6349
  var FetchManagedWorkerClient = class {
6188
- constructor(fetchImpl = fetch) {
6189
- this.fetchImpl = fetchImpl;
6350
+ constructor(fetchImpl, sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))) {
6351
+ this.sleep = sleep;
6352
+ this.fetchImpl = fetchImpl ?? ((input, init) => fetch(input, init));
6190
6353
  }
6354
+ sleep;
6191
6355
  fetchImpl;
6192
6356
  async putSession(input) {
6193
- const response = await this.fetchImpl(endpointUrl(input.endpoint, "/session"), {
6357
+ const response = await this.fetchWithRetry(endpointUrl(input.endpoint, "/session"), {
6194
6358
  method: "PUT",
6195
6359
  headers: {
6196
6360
  authorization: `Bearer ${input.sessionSyncToken}`,
@@ -6202,18 +6366,18 @@ var FetchManagedWorkerClient = class {
6202
6366
  cookieValue: input.session.cookieValue,
6203
6367
  expectedRevision: input.expectedRevision
6204
6368
  })
6205
- });
6369
+ }, isRetryableSessionUpload);
6206
6370
  const body = await safeJson(response);
6207
- if (!response.ok || !isRecord8(body) || typeof body.revision !== "number") {
6208
- const code = isRecord8(body) && typeof body.code === "string" ? body.code : "SESSION_UPLOAD_FAILED";
6209
- throw new DeploymentApplyError(code, "The Worker rejected the Moodle session update");
6371
+ if (response.ok && isRecord8(body) && typeof body.revision === "number") {
6372
+ return { revision: body.revision };
6210
6373
  }
6211
- return { revision: body.revision };
6374
+ const code = isRecord8(body) && typeof body.code === "string" ? body.code : "SESSION_UPLOAD_FAILED";
6375
+ throw new DeploymentApplyError(code, "The Worker rejected the Moodle session update");
6212
6376
  }
6213
6377
  async getReadiness(input) {
6214
- const response = await this.fetchImpl(endpointUrl(input.endpoint, "/readyz"), {
6378
+ const response = await this.fetchWithRetry(endpointUrl(input.endpoint, "/readyz"), {
6215
6379
  headers: { authorization: `Bearer ${input.sessionSyncToken}` }
6216
- });
6380
+ }, isRetryableWorkerPropagation);
6217
6381
  const body = await safeJson(response);
6218
6382
  if (isRecord8(body) && (body.status === "pass" || body.status === "warn" || body.status === "fail")) {
6219
6383
  const session = firstHealthCheck(body, "moodle:session");
@@ -6227,7 +6391,11 @@ var FetchManagedWorkerClient = class {
6227
6391
  return { status: "fail", reasonCode: null, revision: null };
6228
6392
  }
6229
6393
  async runSmoke(input) {
6230
- const health = await this.fetchImpl(endpointUrl(input.endpoint, "/healthz"));
6394
+ const health = await this.fetchWithRetry(
6395
+ endpointUrl(input.endpoint, "/healthz"),
6396
+ void 0,
6397
+ isRetryableWorkerPropagation
6398
+ );
6231
6399
  const healthBody = await safeJson(health);
6232
6400
  if (!health.ok || !isRecord8(healthBody) || healthBody.status !== "pass") {
6233
6401
  throw new DeploymentApplyError("HEALTH_CHECK_FAILED", "Worker liveness check failed");
@@ -6261,7 +6429,7 @@ var FetchManagedWorkerClient = class {
6261
6429
  if (typeof params.name === "string") {
6262
6430
  headers["mcp-name"] = params.name;
6263
6431
  }
6264
- const response = await this.fetchImpl(endpointUrl(endpoint, "/mcp"), {
6432
+ const response = await this.fetchWithRetry(endpointUrl(endpoint, "/mcp"), {
6265
6433
  method: "POST",
6266
6434
  headers,
6267
6435
  body: JSON.stringify({
@@ -6277,13 +6445,23 @@ var FetchManagedWorkerClient = class {
6277
6445
  }
6278
6446
  }
6279
6447
  })
6280
- });
6448
+ }, isRetryableWorkerPropagation);
6281
6449
  const body = await safeJson(response);
6282
6450
  if (!response.ok || !isRecord8(body) || body.jsonrpc !== "2.0" || body.id !== id || "error" in body || !("result" in body)) {
6283
6451
  throw new DeploymentApplyError("MCP_SMOKE_FAILED", `MCP ${method} check failed`);
6284
6452
  }
6285
6453
  return body.result;
6286
6454
  }
6455
+ async fetchWithRetry(input, init, retryable) {
6456
+ for (let attempt = 0; attempt < WORKER_PROPAGATION_ATTEMPTS; attempt += 1) {
6457
+ const response = await this.fetchImpl(input, init);
6458
+ if (!retryable(response.status) || attempt === WORKER_PROPAGATION_ATTEMPTS - 1) {
6459
+ return response;
6460
+ }
6461
+ await this.sleep(Math.min(500 * 2 ** attempt, WORKER_PROPAGATION_MAX_DELAY_MS));
6462
+ }
6463
+ throw new Error("Worker propagation retry loop exhausted unexpectedly");
6464
+ }
6287
6465
  };
6288
6466
  var PrivateDeploymentReceiptStore = class {
6289
6467
  constructor(baseDirectory = join7(homedir8(), ".config", "moodle-cli", "mcp", "deployments")) {
@@ -6366,6 +6544,12 @@ function ownershipId(accountId, workerName) {
6366
6544
  function endpointUrl(endpoint, path4) {
6367
6545
  return `${endpoint.replace(/\/$/u, "")}${path4}`;
6368
6546
  }
6547
+ function isRetryableSessionUpload(status) {
6548
+ return status === 401 || isRetryableWorkerPropagation(status);
6549
+ }
6550
+ function isRetryableWorkerPropagation(status) {
6551
+ return status === 404 || status === 429 || status >= 500;
6552
+ }
6369
6553
  async function safeJson(response) {
6370
6554
  try {
6371
6555
  return await response.json();
@@ -6469,18 +6653,6 @@ function firstWorkersDevUrl(value) {
6469
6653
  });
6470
6654
  return found;
6471
6655
  }
6472
- function productionEndpointFromPreview(preview2, workerName) {
6473
- const url = new URL(preview2);
6474
- const labels = url.hostname.split(".");
6475
- if (labels[0] !== workerName && labels[0]?.endsWith(`-${workerName}`)) {
6476
- labels[0] = workerName;
6477
- url.hostname = labels.join(".");
6478
- }
6479
- url.pathname = "";
6480
- url.search = "";
6481
- url.hash = "";
6482
- return url.origin;
6483
- }
6484
6656
  function visit(value, visitor, key = "") {
6485
6657
  visitor(key, value);
6486
6658
  if (Array.isArray(value)) {
@@ -7860,7 +8032,10 @@ Log: ${result.log_path}`, options);
7860
8032
  await outputMcpResult(runtime, result, options);
7861
8033
  });
7862
8034
  addOutputOptions(mcp.command("status").description("Show local and remote Moodle MCP readiness.")).option("--verbose", "Include sanitized deployment diagnostics.").option("--logs", "Include sanitized recent Worker logs.").action(async (options) => {
7863
- const result = await getMcpService().status({ verbose: Boolean(options.verbose), logs: Boolean(options.logs) });
8035
+ const result = await getMcpService().status({
8036
+ verbose: Boolean(options.verbose || program.opts().verbose),
8037
+ logs: Boolean(options.logs)
8038
+ });
7864
8039
  await outputMcpResult(runtime, result, options);
7865
8040
  });
7866
8041
  addOutputOptions(mutating(mcp.command("login").description("Acquire and upload a fresh Moodle session."))).action(
@@ -5996,7 +5996,7 @@ function problemResponse(status, code, title, detail, headers) {
5996
5996
  }
5997
5997
 
5998
5998
  // src/version.ts
5999
- var VERSION = "0.7.0-alpha.1";
5999
+ var VERSION = "0.7.0-alpha.3";
6000
6000
 
6001
6001
  // src/worker/http.ts
6002
6002
  var HEALTH_PATH = "/healthz";
@@ -22117,7 +22117,7 @@ var MoodleClientCore = class {
22117
22117
  constructor(baseUrl, options) {
22118
22118
  this.baseUrl = baseUrl.replace(/\/$/, "");
22119
22119
  const resolvedOptions = typeof options === "string" ? { cookie: { name: "MoodleSession", value: options } } : options;
22120
- this.fetchImpl = resolvedOptions.fetchImpl ?? fetch;
22120
+ this.fetchImpl = resolvedOptions.fetchImpl ?? ((input, init) => fetch(input, init));
22121
22121
  this.cookie = resolvedOptions.cookie;
22122
22122
  this.sesskey = resolvedOptions.pageContext?.sesskey ?? resolvedOptions.sesskey ?? null;
22123
22123
  this.userid = resolvedOptions.pageContext?.user_info.userid ?? resolvedOptions.userid ?? null;
@@ -23214,12 +23214,12 @@ var AJAX_PATH = "/lib/ajax/service.php";
23214
23214
  var SESSION_TOUCH = "core_session_touch";
23215
23215
  var SESSION_TIME_REMAINING = "core_session_time_remaining";
23216
23216
  var FetchMoodleSessionUpstream = class {
23217
- constructor(origin, fetchImpl = fetch) {
23218
- this.fetchImpl = fetchImpl;
23217
+ origin;
23218
+ fetchImpl;
23219
+ constructor(origin, fetchImpl) {
23219
23220
  this.origin = new URL(origin).origin;
23221
+ this.fetchImpl = fetchImpl ?? ((input, init) => fetch(input, init));
23220
23222
  }
23221
- fetchImpl;
23222
- origin;
23223
23223
  async validate(candidate) {
23224
23224
  if (new URL(candidate.moodleOrigin).origin !== this.origin) return { valid: false, code: "SESSION_INVALID" };
23225
23225
  const response = await this.fetchImpl(`${this.origin}${DASHBOARD_PATH2}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moodle-cli",
3
- "version": "0.7.0-alpha.1",
3
+ "version": "0.7.0-alpha.3",
4
4
  "description": "Terminal-first CLI for Moodle LMS",
5
5
  "license": "MIT",
6
6
  "type": "module",