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 +1 -1
- package/README.md +3 -3
- package/dist/moodle.js +100 -69
- package/dist/worker/worker.js +1 -1
- package/package.json +2 -2
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
|
[](https://www.npmjs.com/package/moodle-cli)
|
|
8
8
|
[](https://github.com/bunizao/moodle-cli/actions/workflows/ci.yml)
|
|
9
|
-
[](https://nodejs.org/)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
10
|
[](https://bun.sh/)
|
|
11
11
|
[](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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
5609
|
-
|
|
5610
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
6375
|
-
if (!
|
|
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:
|
|
6387
|
-
previousHealthyVersionId:
|
|
6388
|
-
releaseDigest:
|
|
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,
|
|
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
|
|
6875
|
-
const
|
|
6876
|
-
visit(value, (
|
|
6877
|
-
if (
|
|
6878
|
-
|
|
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
|
|
6917
|
+
return entries.sort((a, b) => b.createdOn - a.createdOn || b.index - a.index).map(({ versionId, message }) => ({ versionId, message }));
|
|
6882
6918
|
}
|
|
6883
|
-
function
|
|
6884
|
-
const
|
|
6885
|
-
|
|
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
|
|
6888
|
-
|
|
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
|
|
8066
|
-
|
|
8067
|
-
|
|
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) {
|
package/dist/worker/worker.js
CHANGED
|
@@ -20597,7 +20597,7 @@ function stringValue(value) {
|
|
|
20597
20597
|
}
|
|
20598
20598
|
|
|
20599
20599
|
// src/version.ts
|
|
20600
|
-
var VERSION = "0.7.0-alpha.
|
|
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.
|
|
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",
|