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 +1 -1
- package/dist/moodle.js +226 -51
- package/dist/worker/worker.js +6 -6
- package/package.json +1 -1
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.
|
|
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.
|
|
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(
|
|
4627
|
-
|
|
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
|
-
"
|
|
4642
|
-
["
|
|
4643
|
-
|
|
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(
|
|
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
|
-
|
|
4660
|
-
|
|
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 (
|
|
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
|
|
6150
|
+
const deploymentId2 = ownershipId(accountId, workerName);
|
|
6020
6151
|
return {
|
|
6021
6152
|
accountId,
|
|
6022
6153
|
workerName,
|
|
6023
|
-
deploymentId,
|
|
6024
|
-
ownershipTag:
|
|
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
|
|
6062
|
-
throw new DeploymentApplyError("CANDIDATE_UPLOAD_INVALID", "Wrangler did not return a candidate version
|
|
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:
|
|
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 =
|
|
6189
|
-
this.
|
|
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.
|
|
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 (
|
|
6208
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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({
|
|
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(
|
package/dist/worker/worker.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
23218
|
-
|
|
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}`, {
|