moodle-cli 0.7.0-alpha.1 → 0.7.0-alpha.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
@@ -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.2` 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.2";
3704
3704
 
3705
3705
  // src/forum.ts
3706
3706
  function parseDiscussionReference(value) {
@@ -5073,7 +5073,7 @@ var ManagedMcpDeployment = class {
5073
5073
  `Worker ${intent.workerName} is not owned by Moodle MCP profile ${intent.profile}`
5074
5074
  );
5075
5075
  }
5076
- const receipt = replacingExisting ? null : matchingReceipt;
5076
+ const receipt = replacingExisting || remote === null ? null : matchingReceipt;
5077
5077
  const existing = remote && receipt ? { ...remote, productionEndpoint: receipt.productionEndpoint, releaseDigest: receipt.releaseDigest } : remote;
5078
5078
  const rotate = intent.rotateToken === true && credentials !== null;
5079
5079
  const releaseChanged = existing?.releaseDigest !== intent.releaseDigest;
@@ -5098,8 +5098,10 @@ var ManagedMcpDeployment = class {
5098
5098
  let credentials = null;
5099
5099
  let credentialsBefore = null;
5100
5100
  let secretsUploaded = false;
5101
+ let initializedWorker = null;
5101
5102
  let candidate = null;
5102
5103
  let appliedReceipt = null;
5104
+ let productionRestored = false;
5103
5105
  let moodleUser = null;
5104
5106
  try {
5105
5107
  yield started(activeStage);
@@ -5122,6 +5124,14 @@ var ManagedMcpDeployment = class {
5122
5124
  activeStage = "upload_private_credentials";
5123
5125
  yield started(activeStage);
5124
5126
  if (plan.uploadCandidate) {
5127
+ if (plan.operation === "create") {
5128
+ initializedWorker = await this.dependencies.wrangler.initializeWorker({
5129
+ accountId: plan.intent.accountId,
5130
+ workerName: plan.intent.workerName,
5131
+ configPath: prepared.wranglerConfigPath,
5132
+ releaseDigest: plan.intent.releaseDigest
5133
+ });
5134
+ }
5125
5135
  await this.dependencies.wrangler.uploadSecrets({
5126
5136
  accountId: plan.intent.accountId,
5127
5137
  workerName: plan.intent.workerName,
@@ -5134,17 +5144,30 @@ var ManagedMcpDeployment = class {
5134
5144
  activeStage = "deploy_candidate_version";
5135
5145
  yield started(activeStage);
5136
5146
  if (plan.uploadCandidate) {
5147
+ const productionEndpoint2 = initializedWorker?.productionEndpoint ?? plan.existing?.productionEndpoint;
5148
+ if (!productionEndpoint2) {
5149
+ throw new DeploymentApplyError("MISSING_ENDPOINT", "The Worker production endpoint is unavailable");
5150
+ }
5137
5151
  candidate = await this.dependencies.wrangler.uploadCandidate({
5138
5152
  accountId: plan.intent.accountId,
5139
5153
  workerName: plan.intent.workerName,
5140
5154
  configPath: prepared.wranglerConfigPath,
5141
- releaseDigest: plan.intent.releaseDigest
5155
+ releaseDigest: plan.intent.releaseDigest,
5156
+ productionEndpoint: productionEndpoint2
5142
5157
  });
5158
+ if (!candidate.previewEndpoint) {
5159
+ await this.dependencies.wrangler.promote({
5160
+ accountId: plan.intent.accountId,
5161
+ workerName: plan.intent.workerName,
5162
+ versionId: candidate.versionId
5163
+ });
5164
+ promoted = true;
5165
+ }
5143
5166
  }
5144
5167
  yield completed(activeStage);
5145
5168
  activeStage = "upload_moodle_session";
5146
5169
  yield started(activeStage);
5147
- const sessionEndpoint = candidate?.previewEndpoint ?? plan.existing?.productionEndpoint;
5170
+ const sessionEndpoint = candidate?.previewEndpoint ?? candidate?.productionEndpoint ?? plan.existing?.productionEndpoint;
5148
5171
  if (!sessionEndpoint) {
5149
5172
  throw new DeploymentApplyError("MISSING_ENDPOINT", "The Worker did not provide a session endpoint");
5150
5173
  }
@@ -5162,7 +5185,7 @@ var ManagedMcpDeployment = class {
5162
5185
  yield completed(activeStage);
5163
5186
  activeStage = "run_release_checks";
5164
5187
  yield started(activeStage);
5165
- if (candidate) {
5188
+ if (candidate?.previewEndpoint) {
5166
5189
  try {
5167
5190
  await this.dependencies.worker.runSmoke({
5168
5191
  endpoint: candidate.previewEndpoint,
@@ -5197,6 +5220,9 @@ var ManagedMcpDeployment = class {
5197
5220
  if (!promoted) {
5198
5221
  throw new DeploymentApplyError("PRODUCTION_VALIDATION_FAILED", "The existing Worker failed validation");
5199
5222
  }
5223
+ if (plan.operation === "create") {
5224
+ throw new DeploymentApplyError("PRODUCTION_VALIDATION_FAILED", "The new Worker failed production validation");
5225
+ }
5200
5226
  candidateRevision = await this.rollbackProduction(
5201
5227
  plan,
5202
5228
  credentials,
@@ -5204,6 +5230,7 @@ var ManagedMcpDeployment = class {
5204
5230
  productionEndpoint,
5205
5231
  candidateRevision
5206
5232
  );
5233
+ productionRestored = true;
5207
5234
  throw new DeploymentApplyError(
5208
5235
  "PRODUCTION_VALIDATION_FAILED_RESTORED",
5209
5236
  "The previous healthy release was restored with the current credentials and Moodle session"
@@ -5221,7 +5248,25 @@ var ManagedMcpDeployment = class {
5221
5248
  if (plan.intent.rotateToken && credentialsBefore && !secretsUploaded) {
5222
5249
  await this.dependencies.credentials.write(plan.intent.profile, credentialsBefore);
5223
5250
  }
5224
- if (!appliedReceipt && plan.receipt && candidateRevision !== null) {
5251
+ if (promoted && plan.existing && !productionRestored && !appliedReceipt) {
5252
+ await this.dependencies.wrangler.restoreProduction({
5253
+ accountId: plan.intent.accountId,
5254
+ workerName: plan.intent.workerName,
5255
+ previousVersionId: plan.existing.productionVersionId
5256
+ });
5257
+ productionRestored = true;
5258
+ if (plan.intent.rotateToken && credentialsBefore) {
5259
+ await this.dependencies.credentials.write(plan.intent.profile, credentialsBefore);
5260
+ }
5261
+ }
5262
+ if (initializedWorker && !appliedReceipt) {
5263
+ await this.dependencies.wrangler.removeWorker({
5264
+ accountId: plan.intent.accountId,
5265
+ workerName: plan.intent.workerName,
5266
+ deploymentId: deploymentId(plan.intent.accountId, plan.intent.workerName)
5267
+ });
5268
+ await this.removeLocalState(plan.intent.profile);
5269
+ } else if (!appliedReceipt && plan.receipt && candidateRevision !== null) {
5225
5270
  await this.dependencies.receipts.write({ ...plan.receipt, sessionRevision: candidateRevision });
5226
5271
  }
5227
5272
  const safe = asDeploymentError(error);
@@ -5458,6 +5503,9 @@ function validateIntent(intent) {
5458
5503
  throw new DeploymentPlanError("INVALID_INTENT", "Cloudflare account and release digest are required");
5459
5504
  }
5460
5505
  }
5506
+ function deploymentId(accountId, workerName) {
5507
+ return `moodle-cli:${accountId}:${workerName}`;
5508
+ }
5461
5509
  function isOwnedByProfile(worker, receipt, profile) {
5462
5510
  return receipt ? receipt.accountId === worker.accountId && receipt.workerName === worker.workerName && receipt.deploymentId === worker.ownershipTag : worker.ownershipTag === `moodle-cli:${profile}`;
5463
5511
  }
@@ -5930,6 +5978,8 @@ function isMissing3(error) {
5930
5978
 
5931
5979
  // src/mcp/deployment/node-adapters.ts
5932
5980
  var MODERN_MCP_VERSION = "2026-07-28";
5981
+ var WORKER_PROPAGATION_ATTEMPTS = 10;
5982
+ var WORKER_PROPAGATION_MAX_DELAY_MS = 4e3;
5933
5983
  var NodeDeploymentCommandRunner = class {
5934
5984
  async run(command, args, environment = {}) {
5935
5985
  return new Promise((resolve, reject) => {
@@ -6016,12 +6066,12 @@ ${error.stderr}`)) {
6016
6066
  return null;
6017
6067
  }
6018
6068
  const productionEndpoint = firstWorkersDevUrl(document) ?? `https://${workerName}.workers.dev`;
6019
- const deploymentId = ownershipId(accountId, workerName);
6069
+ const deploymentId2 = ownershipId(accountId, workerName);
6020
6070
  return {
6021
6071
  accountId,
6022
6072
  workerName,
6023
- deploymentId,
6024
- ownershipTag: deploymentId,
6073
+ deploymentId: deploymentId2,
6074
+ ownershipTag: deploymentId2,
6025
6075
  productionEndpoint,
6026
6076
  productionVersionId: versionIds[0],
6027
6077
  previousHealthyVersionId: versionIds[1] ?? null,
@@ -6039,6 +6089,36 @@ ${error.stderr}`)) {
6039
6089
  input.configPath
6040
6090
  ], input.accountId);
6041
6091
  }
6092
+ async initializeWorker(input) {
6093
+ let result = null;
6094
+ try {
6095
+ result = await this.wrangler([
6096
+ "deploy",
6097
+ "--name",
6098
+ input.workerName,
6099
+ "--config",
6100
+ input.configPath,
6101
+ "--message",
6102
+ `moodle-cli-bootstrap:${input.releaseDigest}`
6103
+ ], input.accountId);
6104
+ const worker = await this.inspect(input.accountId, input.workerName);
6105
+ if (!worker) {
6106
+ throw new DeploymentApplyError("INITIAL_WORKER_INVALID", "Wrangler did not return the initialized Worker");
6107
+ }
6108
+ const productionEndpoint = firstWorkersDevUrl([result.stdout, result.stderr]);
6109
+ return productionEndpoint ? { ...worker, productionEndpoint } : worker;
6110
+ } catch (error) {
6111
+ const worker = await this.inspect(input.accountId, input.workerName).catch(() => null);
6112
+ if (worker || result) {
6113
+ await this.removeWorker({
6114
+ accountId: input.accountId,
6115
+ workerName: input.workerName,
6116
+ deploymentId: ownershipId(input.accountId, input.workerName)
6117
+ }).catch(() => void 0);
6118
+ }
6119
+ throw error;
6120
+ }
6121
+ }
6042
6122
  async uploadCandidate(input) {
6043
6123
  const outputFilePath = join7(dirname7(input.configPath), "wrangler-version-upload.jsonl");
6044
6124
  await rm6(outputFilePath, { force: true });
@@ -6058,13 +6138,13 @@ ${error.stderr}`)) {
6058
6138
  const document = await readWranglerVersionUpload(outputFilePath, input.workerName);
6059
6139
  const versionId = typeof document.version_id === "string" ? document.version_id : null;
6060
6140
  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");
6141
+ if (!versionId) {
6142
+ throw new DeploymentApplyError("CANDIDATE_UPLOAD_INVALID", "Wrangler did not return a candidate version");
6063
6143
  }
6064
6144
  return {
6065
6145
  versionId,
6066
- previewEndpoint,
6067
- productionEndpoint: productionEndpointFromPreview(previewEndpoint, input.workerName),
6146
+ previewEndpoint: previewEndpoint ?? null,
6147
+ productionEndpoint: input.productionEndpoint,
6068
6148
  deploymentId: ownershipId(input.accountId, input.workerName)
6069
6149
  };
6070
6150
  } finally {
@@ -6130,6 +6210,7 @@ var NodeReleaseMaterializer = class {
6130
6210
  account_id: plan.intent.accountId,
6131
6211
  main: `./${basename(workerFile)}`,
6132
6212
  compatibility_date: this.options.compatibilityDate,
6213
+ preview_urls: true,
6133
6214
  vars: { MOODLE_ORIGIN: plan.intent.moodleOrigin },
6134
6215
  durable_objects: {
6135
6216
  bindings: [{ name: "SESSION_BROKER", class_name: "SessionBroker" }]
@@ -6185,12 +6266,14 @@ function createBackgroundMoodleSessionSource(options = {}) {
6185
6266
  return new DefaultMoodleSessionSource({ ...options, interactive: false });
6186
6267
  }
6187
6268
  var FetchManagedWorkerClient = class {
6188
- constructor(fetchImpl = fetch) {
6189
- this.fetchImpl = fetchImpl;
6269
+ constructor(fetchImpl, sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))) {
6270
+ this.sleep = sleep;
6271
+ this.fetchImpl = fetchImpl ?? ((input, init) => fetch(input, init));
6190
6272
  }
6273
+ sleep;
6191
6274
  fetchImpl;
6192
6275
  async putSession(input) {
6193
- const response = await this.fetchImpl(endpointUrl(input.endpoint, "/session"), {
6276
+ const response = await this.fetchWithRetry(endpointUrl(input.endpoint, "/session"), {
6194
6277
  method: "PUT",
6195
6278
  headers: {
6196
6279
  authorization: `Bearer ${input.sessionSyncToken}`,
@@ -6202,18 +6285,18 @@ var FetchManagedWorkerClient = class {
6202
6285
  cookieValue: input.session.cookieValue,
6203
6286
  expectedRevision: input.expectedRevision
6204
6287
  })
6205
- });
6288
+ }, isRetryableSessionUpload);
6206
6289
  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");
6290
+ if (response.ok && isRecord8(body) && typeof body.revision === "number") {
6291
+ return { revision: body.revision };
6210
6292
  }
6211
- return { revision: body.revision };
6293
+ const code = isRecord8(body) && typeof body.code === "string" ? body.code : "SESSION_UPLOAD_FAILED";
6294
+ throw new DeploymentApplyError(code, "The Worker rejected the Moodle session update");
6212
6295
  }
6213
6296
  async getReadiness(input) {
6214
- const response = await this.fetchImpl(endpointUrl(input.endpoint, "/readyz"), {
6297
+ const response = await this.fetchWithRetry(endpointUrl(input.endpoint, "/readyz"), {
6215
6298
  headers: { authorization: `Bearer ${input.sessionSyncToken}` }
6216
- });
6299
+ }, isRetryableWorkerPropagation);
6217
6300
  const body = await safeJson(response);
6218
6301
  if (isRecord8(body) && (body.status === "pass" || body.status === "warn" || body.status === "fail")) {
6219
6302
  const session = firstHealthCheck(body, "moodle:session");
@@ -6227,7 +6310,11 @@ var FetchManagedWorkerClient = class {
6227
6310
  return { status: "fail", reasonCode: null, revision: null };
6228
6311
  }
6229
6312
  async runSmoke(input) {
6230
- const health = await this.fetchImpl(endpointUrl(input.endpoint, "/healthz"));
6313
+ const health = await this.fetchWithRetry(
6314
+ endpointUrl(input.endpoint, "/healthz"),
6315
+ void 0,
6316
+ isRetryableWorkerPropagation
6317
+ );
6231
6318
  const healthBody = await safeJson(health);
6232
6319
  if (!health.ok || !isRecord8(healthBody) || healthBody.status !== "pass") {
6233
6320
  throw new DeploymentApplyError("HEALTH_CHECK_FAILED", "Worker liveness check failed");
@@ -6261,7 +6348,7 @@ var FetchManagedWorkerClient = class {
6261
6348
  if (typeof params.name === "string") {
6262
6349
  headers["mcp-name"] = params.name;
6263
6350
  }
6264
- const response = await this.fetchImpl(endpointUrl(endpoint, "/mcp"), {
6351
+ const response = await this.fetchWithRetry(endpointUrl(endpoint, "/mcp"), {
6265
6352
  method: "POST",
6266
6353
  headers,
6267
6354
  body: JSON.stringify({
@@ -6277,13 +6364,23 @@ var FetchManagedWorkerClient = class {
6277
6364
  }
6278
6365
  }
6279
6366
  })
6280
- });
6367
+ }, isRetryableWorkerPropagation);
6281
6368
  const body = await safeJson(response);
6282
6369
  if (!response.ok || !isRecord8(body) || body.jsonrpc !== "2.0" || body.id !== id || "error" in body || !("result" in body)) {
6283
6370
  throw new DeploymentApplyError("MCP_SMOKE_FAILED", `MCP ${method} check failed`);
6284
6371
  }
6285
6372
  return body.result;
6286
6373
  }
6374
+ async fetchWithRetry(input, init, retryable) {
6375
+ for (let attempt = 0; attempt < WORKER_PROPAGATION_ATTEMPTS; attempt += 1) {
6376
+ const response = await this.fetchImpl(input, init);
6377
+ if (!retryable(response.status) || attempt === WORKER_PROPAGATION_ATTEMPTS - 1) {
6378
+ return response;
6379
+ }
6380
+ await this.sleep(Math.min(500 * 2 ** attempt, WORKER_PROPAGATION_MAX_DELAY_MS));
6381
+ }
6382
+ throw new Error("Worker propagation retry loop exhausted unexpectedly");
6383
+ }
6287
6384
  };
6288
6385
  var PrivateDeploymentReceiptStore = class {
6289
6386
  constructor(baseDirectory = join7(homedir8(), ".config", "moodle-cli", "mcp", "deployments")) {
@@ -6366,6 +6463,12 @@ function ownershipId(accountId, workerName) {
6366
6463
  function endpointUrl(endpoint, path4) {
6367
6464
  return `${endpoint.replace(/\/$/u, "")}${path4}`;
6368
6465
  }
6466
+ function isRetryableSessionUpload(status) {
6467
+ return status === 401 || isRetryableWorkerPropagation(status);
6468
+ }
6469
+ function isRetryableWorkerPropagation(status) {
6470
+ return status === 404 || status === 429 || status >= 500;
6471
+ }
6369
6472
  async function safeJson(response) {
6370
6473
  try {
6371
6474
  return await response.json();
@@ -6469,18 +6572,6 @@ function firstWorkersDevUrl(value) {
6469
6572
  });
6470
6573
  return found;
6471
6574
  }
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
6575
  function visit(value, visitor, key = "") {
6485
6576
  visitor(key, value);
6486
6577
  if (Array.isArray(value)) {
@@ -7860,7 +7951,10 @@ Log: ${result.log_path}`, options);
7860
7951
  await outputMcpResult(runtime, result, options);
7861
7952
  });
7862
7953
  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) });
7954
+ const result = await getMcpService().status({
7955
+ verbose: Boolean(options.verbose || program.opts().verbose),
7956
+ logs: Boolean(options.logs)
7957
+ });
7864
7958
  await outputMcpResult(runtime, result, options);
7865
7959
  });
7866
7960
  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.2";
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.2",
4
4
  "description": "Terminal-first CLI for Moodle LMS",
5
5
  "license": "MIT",
6
6
  "type": "module",