neuro-canvas-lms 2.10.1 → 2.11.0

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/AGENTS.md CHANGED
@@ -30,6 +30,7 @@ Code map:
30
30
  - `owner.mjs`: owner binding, allowed OAuth clients. `i18n.mjs` + `pages.mjs`: web pages.
31
31
  - `update-check.mjs`: once a day reads the newest version number from npm; when it is newer the AI
32
32
  tells the user how to update. Also the `canvas_connector_version` tool.
33
- - `installer/`: the setup wizard (re-running it asks once, y/n, then updates with the saved
34
- settings; without a terminal it does not ask). `version.mjs`: shown on
35
- the home page and /healthz, compared with npm by update-check.mjs.
33
+ - `installer/`: the setup wizard. Re-running it asks once (y/n), then updates with the saved
34
+ settings (without a terminal it does not ask). `installer/verify.mjs`: the post-deploy online
35
+ check, also run alone by `npx -y neuro-canvas-lms check`.
36
+ - `version.mjs`: shown on the home page and /healthz, compared with npm by update-check.mjs.
package/installer/cli.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  // Neuro-CanvasLMS installer. Deploys THIS copy of the code to your own Cloudflare account.
2
2
  // npx -y neuro-canvas-lms install, or update an existing install
3
3
  // npx -y neuro-canvas-lms uninstall remove it
4
+ // npx -y neuro-canvas-lms check only check an installed connector (deploys nothing)
4
5
  // (from a clone: npm run setup / npm run setup -- uninstall)
5
6
  // Running it again updates the connector to the code you are running and keeps its storage, so
6
7
  // existing Claude/ChatGPT connections keep working.
7
8
  // Language: your system language if supported (en, zh, es, ja, hi), else English; --lang to choose.
8
9
  // Automation/tests: --name, --canvas-host, --school, --account-id, --yes; env NCLMS_HOME, NCLMS_LANG.
10
+ // Exit codes: 0 done, 1 failed, 2 deployed but not confirmed online yet (run `check` later).
9
11
  import { spawnSync } from 'node:child_process';
10
12
  import { createRequire } from 'node:module';
11
13
  import fs from 'node:fs';
@@ -30,11 +32,9 @@ import {
30
32
  isYes,
31
33
  needsSubdomain,
32
34
  subdomainLink,
33
- isWarmingUp,
34
- CHECK_WAIT,
35
- WARMUP_WAIT,
36
35
  } from './lib.mjs';
37
36
  import { msg } from './messages.mjs';
37
+ import { verifyDeployment } from './verify.mjs';
38
38
 
39
39
  const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
40
40
  // wrangler's package "exports" hide bin/; find the package root from its main entry instead.
@@ -230,41 +230,6 @@ async function runningVersion(url) {
230
230
  }
231
231
  }
232
232
 
233
- async function healthCheck(url) {
234
- let detail = 'no answer';
235
- let told = false;
236
- const start = Date.now();
237
- for (let i = 0; Date.now() - start < (isWarmingUp(detail) ? WARMUP_WAIT : CHECK_WAIT); i++) {
238
- if (i) await new Promise(r => setTimeout(r, 3000));
239
- try {
240
- const h = await fetch(`${url}/healthz`, {
241
- headers: { 'Cache-Control': 'no-cache' },
242
- signal: AbortSignal.timeout(10000),
243
- });
244
- const j = h.ok ? await h.json() : null;
245
- if (!j?.ok) detail = `/healthz HTTP ${h.status}`;
246
- else if (j.version !== VERSION) detail = `still ${j.version}`;
247
- else {
248
- const mcp = await fetch(`${url}/mcp`, {
249
- method: 'POST',
250
- body: '{}',
251
- signal: AbortSignal.timeout(10000),
252
- });
253
- if (mcp.status === 401) return { ok: true, connected: j.connected === true };
254
- detail = `/mcp HTTP ${mcp.status}`;
255
- }
256
- } catch (e) {
257
- // fetch says only "fetch failed"; the cause names the real problem (TLS, DNS, …)
258
- detail = e.cause?.code ?? e.cause?.message ?? e.message;
259
- if (isWarmingUp(detail) && !told) {
260
- say('cert_wait');
261
- told = true;
262
- }
263
- }
264
- }
265
- return { ok: false, detail };
266
- }
267
-
268
233
  async function install() {
269
234
  console.log(`\n ${msg(lang, 'title')}\n`);
270
235
  const [major] = process.versions.node.split('.').map(Number);
@@ -319,8 +284,8 @@ async function install() {
319
284
  save({ url, version: VERSION });
320
285
 
321
286
  say('check');
322
- const check = await healthCheck(url);
323
- if (!check.ok) fail(msg(lang, 'check_bad', { detail: check.detail }));
287
+ const check = await verifyDeployment(url, VERSION, { onWarmup: () => say('cert_wait') });
288
+ if (!check.ok) return notConfirmed(name, url, check.detail);
324
289
  ok('check_ok', { version: VERSION });
325
290
 
326
291
  // Already connected: an update needs nothing from the user, so don't send them to make a token,
@@ -333,6 +298,22 @@ async function install() {
333
298
  console.log('');
334
299
  return;
335
300
  }
301
+ nextSteps(url, host);
302
+ }
303
+
304
+ // The deploy worked but the address did not answer correctly in time (usually a brand-new
305
+ // workers.dev address still getting its certificate). Not a failure: say where it is and how to
306
+ // check again without deploying. Exit code 2 tells scripts and agents exactly that.
307
+ function notConfirmed(name, url, detail) {
308
+ const cmd = 'npx -y neuro-canvas-lms check' + (name === DEFAULT_NAME ? '' : ` --name ${name}`);
309
+ console.log('');
310
+ say('check_pending', { url, detail, cmd });
311
+ console.log('');
312
+ process.exitCode = 2;
313
+ }
314
+
315
+ // What the user does next, once the connector is online and not yet connected to an AI app.
316
+ function nextSteps(url, host) {
336
317
  copy(`${url}/mcp`);
337
318
  console.log(`\n ${msg(lang, 'done')}`);
338
319
  say('done_1', { canvas: `https://${host}` });
@@ -347,6 +328,28 @@ async function install() {
347
328
  if (!args.yes) openUrl(`https://${host}/profile/settings`);
348
329
  }
349
330
 
331
+ // Only check an installed connector (after "not confirmed online yet"): nothing is deployed.
332
+ async function check() {
333
+ console.log(`\n ${msg(lang, 'title')}\n`);
334
+ const name = args.name ?? DEFAULT_NAME;
335
+ if (!validWorkerName(name)) fail('invalid --name');
336
+ const statePath = path.join(HOME, name, 'state.json');
337
+ const state = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, 'utf8')) : {};
338
+ if (!state.url) {
339
+ console.error(' ✗ ' + msg(lang, 'check_none', { name }));
340
+ process.exitCode = 1;
341
+ return;
342
+ }
343
+ say('check_only', { url: state.url });
344
+ const result = await verifyDeployment(state.url, state.version, {
345
+ onWarmup: () => say('cert_wait'),
346
+ });
347
+ if (!result.ok) return notConfirmed(name, state.url, result.detail);
348
+ ok('check_ok', { version: result.version });
349
+ if (result.connected) return ok('check_connected');
350
+ nextSteps(state.url, state.host);
351
+ }
352
+
350
353
  async function uninstall() {
351
354
  const name = args.name ?? DEFAULT_NAME;
352
355
  const dir = path.join(HOME, name);
@@ -386,11 +389,11 @@ const help = async () =>
386
389
  fs
387
390
  .readFileSync(fileURLToPath(import.meta.url), 'utf8')
388
391
  .split('\n')
389
- .slice(0, 8)
392
+ .slice(0, 10)
390
393
  .join('\n'),
391
394
  );
392
- ({ install, uninstall, help })[args.command]().then(
393
- () => process.exit(0),
395
+ ({ install, uninstall, help, check })[args.command]().then(
396
+ () => process.exit(process.exitCode ?? 0),
394
397
  e => {
395
398
  console.error('\n ✗ ' + msg(lang, 'failed', { detail: e?.message ?? e }) + '\n');
396
399
  process.exit(1);
package/installer/lib.mjs CHANGED
@@ -68,7 +68,7 @@ export function parseArgs(argv) {
68
68
  const out = { command: 'install' };
69
69
  for (let i = 0; i < argv.length; i++) {
70
70
  const a = argv[i];
71
- if (a === 'install' || a === 'uninstall' || a === 'help') out.command = a;
71
+ if (['install', 'uninstall', 'help', 'check'].includes(a)) out.command = a;
72
72
  else if (a === '--yes' || a === '-y') out.yes = true;
73
73
  else if (a.startsWith('--')) {
74
74
  const [k, v] = a.includes('=') ? a.slice(2).split(/=(.*)/s) : [a.slice(2), argv[++i]];
@@ -23,7 +23,6 @@ const M = {
23
23
  deploy_ok: 'Connector is live: {url}',
24
24
  check: 'Step 4/4 · Checking the connector…',
25
25
  check_ok: 'Everything works (version {version}).',
26
- check_bad: 'The connector did not pass its check: {detail}',
27
26
  done: 'Done! Last step, in your AI:',
28
27
  done_1:
29
28
  '1. Create a Canvas access token: {canvas}/profile/settings → "+ New Access Token" (copy it; Canvas shows it once).',
@@ -47,6 +46,12 @@ const M = {
47
46
  un_done:
48
47
  'Removed. Also delete the Canvas token (Canvas → Account → Settings) and the connector in your AI app.',
49
48
  cancelled: 'Cancelled.',
49
+ check_pending:
50
+ 'Deployed to {url}, but it could not be confirmed online yet ({detail}). A new address can take a few more minutes the first time. Nothing needs to be deployed again: in a few minutes, check it with\n {cmd}',
51
+ check_none:
52
+ 'No install named "{name}" was found (installed under another name? add --name <name>). Install it first with npx -y neuro-canvas-lms.',
53
+ check_only: 'Checking the connector at {url}…',
54
+ check_connected: 'It is connected to your AI apps and ready to use.',
50
55
  cert_wait:
51
56
  'The new workers.dev address is still getting its HTTPS certificate. The first time this can take a few minutes; waiting…',
52
57
  pick_account_flag:
@@ -89,7 +94,6 @@ const M = {
89
94
  deploy_ok: '连接器已上线:{url}',
90
95
  check: '第 4/4 步 · 检查连接器…',
91
96
  check_ok: '一切正常(版本 {version})。',
92
- check_bad: '连接器没通过检查:{detail}',
93
97
  done: '装好了!最后一步,在你的 AI 里:',
94
98
  done_1:
95
99
  '1. 生成 Canvas 访问令牌:{canvas}/profile/settings →「+ 新建访问许可证」(立刻复制,Canvas 只显示一次)。',
@@ -107,6 +111,12 @@ const M = {
107
111
  un_failed: '没能删除{what}:{detail}\n 本地安装记录已保留,再运行一次同一条命令即可重试。',
108
112
  un_done: '已删除。另外请删掉 Canvas 令牌(Canvas → 账户 → 设置)和 AI 应用里的连接器。',
109
113
  cancelled: '已取消。',
114
+ check_pending:
115
+ '已部署到 {url},但还没能确认它已上线({detail})。新地址第一次可能还要几分钟。不需要重新部署:过几分钟用这条命令只做检查:\n {cmd}',
116
+ check_none:
117
+ '没找到名为「{name}」的安装(装的时候用了别的名字?加上 --name <名字>)。请先用 npx -y neuro-canvas-lms 安装。',
118
+ check_only: '检查连接器 {url}…',
119
+ check_connected: '已经连接到你的 AI 应用,可以直接用。',
110
120
  cert_wait: '新的 workers.dev 地址还在申请 HTTPS 证书,第一次可能要几分钟,正在等待…',
111
121
  pick_account_flag:
112
122
  '这个 Cloudflare 登录下有多个账号。请在同一条命令后加上其中一个,再运行一次:\n{list}',
@@ -150,7 +160,6 @@ const M = {
150
160
  deploy_ok: 'El conector está activo: {url}',
151
161
  check: 'Paso 4/4 · Comprobando el conector…',
152
162
  check_ok: 'Todo funciona (versión {version}).',
153
- check_bad: 'El conector no pasó la comprobación: {detail}',
154
163
  done: '¡Listo! Último paso, en tu IA:',
155
164
  done_1:
156
165
  '1. Crea un token de acceso de Canvas: {canvas}/profile/settings → "+ Nuevo token de acceso" (cópialo; Canvas lo muestra una sola vez).',
@@ -175,6 +184,12 @@ const M = {
175
184
  un_done:
176
185
  'Eliminado. Borra también el token de Canvas (Canvas → Cuenta → Configuración) y el conector en tu app de IA.',
177
186
  cancelled: 'Cancelado.',
187
+ check_pending:
188
+ 'Desplegado en {url}, pero aún no se pudo confirmar que está en línea ({detail}). Una dirección nueva puede tardar unos minutos más la primera vez. No hace falta volver a desplegar: dentro de unos minutos, compruébalo con\n {cmd}',
189
+ check_none:
190
+ 'No se encontró ninguna instalación llamada "{name}" (¿la instalaste con otro nombre? añade --name <nombre>). Instálala primero con npx -y neuro-canvas-lms.',
191
+ check_only: 'Comprobando el conector en {url}…',
192
+ check_connected: 'Está conectado a tus apps de IA y listo para usar.',
178
193
  cert_wait:
179
194
  'La nueva dirección workers.dev todavía está obteniendo su certificado HTTPS. La primera vez puede tardar unos minutos; esperando…',
180
195
  pick_account_flag:
@@ -220,7 +235,6 @@ const M = {
220
235
  deploy_ok: 'コネクタが公開されました:{url}',
221
236
  check: 'ステップ 4/4 · コネクタを確認しています…',
222
237
  check_ok: 'すべて正常です(バージョン {version})。',
223
- check_bad: 'コネクタが確認に失敗しました:{detail}',
224
238
  done: '完了!最後のステップ(AI 側):',
225
239
  done_1:
226
240
  '1. Canvas アクセストークンを作成:{canvas}/profile/settings →「+ 新しいアクセストークン」(すぐコピー。表示は一度きりです)。',
@@ -244,6 +258,12 @@ const M = {
244
258
  un_done:
245
259
  '削除しました。Canvas トークン(Canvas → アカウント → 設定)と AI アプリのコネクタも削除してください。',
246
260
  cancelled: 'キャンセルしました。',
261
+ check_pending:
262
+ '{url} へのデプロイは完了しましたが、まだオンラインであることを確認できていません({detail})。新しいアドレスは初回にさらに数分かかることがあります。再デプロイは不要です。数分後に次のコマンドで確認だけ行ってください:\n {cmd}',
263
+ check_none:
264
+ '「{name}」という名前のインストールが見つかりません(別の名前でインストールした場合は --name <名前> を付けてください)。先に npx -y neuro-canvas-lms でインストールしてください。',
265
+ check_only: '{url} のコネクタを確認しています…',
266
+ check_connected: 'AI アプリに接続済みで、すぐに使えます。',
247
267
  cert_wait:
248
268
  '新しい workers.dev アドレスの HTTPS 証明書を準備しています。初回は数分かかることがあります。待機しています…',
249
269
  pick_account_flag:
@@ -289,7 +309,6 @@ const M = {
289
309
  deploy_ok: 'कनेक्टर चालू है: {url}',
290
310
  check: 'चरण 4/4 · कनेक्टर की जाँच हो रही है…',
291
311
  check_ok: 'सब कुछ ठीक है (संस्करण {version})।',
292
- check_bad: 'कनेक्टर जाँच में विफल रहा: {detail}',
293
312
  done: 'हो गया! आख़िरी कदम, अपने AI में:',
294
313
  done_1:
295
314
  '1. Canvas एक्सेस टोकन बनाएँ: {canvas}/profile/settings → "+ New Access Token" (तुरंत कॉपी करें; Canvas इसे केवल एक बार दिखाता है)।',
@@ -313,6 +332,12 @@ const M = {
313
332
  un_done:
314
333
  'हटा दिया गया। Canvas टोकन (Canvas → Account → Settings) और अपने AI ऐप में कनेक्टर भी हटाएँ।',
315
334
  cancelled: 'रद्द किया गया।',
335
+ check_pending:
336
+ '{url} पर डिप्लॉय हो गया, लेकिन अभी पुष्टि नहीं हो पाई कि यह ऑनलाइन है ({detail})। नए पते को पहली बार कुछ और मिनट लग सकते हैं। दोबारा डिप्लॉय करने की ज़रूरत नहीं: कुछ मिनट बाद इस कमांड से सिर्फ़ जाँच करें:\n {cmd}',
337
+ check_none:
338
+ '"{name}" नाम का कोई इंस्टॉल नहीं मिला (किसी और नाम से इंस्टॉल किया था? --name <नाम> जोड़ें)। पहले npx -y neuro-canvas-lms से इंस्टॉल करें।',
339
+ check_only: '{url} पर कनेक्टर की जाँच हो रही है…',
340
+ check_connected: 'यह आपके AI ऐप से जुड़ा है और इस्तेमाल के लिए तैयार है।',
316
341
  cert_wait:
317
342
  'नए workers.dev पते का HTTPS प्रमाणपत्र अभी तैयार हो रहा है। पहली बार इसमें कुछ मिनट लग सकते हैं; प्रतीक्षा हो रही है…',
318
343
  pick_account_flag:
@@ -0,0 +1,61 @@
1
+ // Is a freshly deployed connector online and running the expected version?
2
+ // Ready means: /healthz answers ok with that version AND /mcp refuses an unauthenticated request
3
+ // with 401 (the OAuth layer is in front). Each request has its own timeout and the whole check a
4
+ // deadline, so it always ends. A new workers.dev address can take minutes for its DNS and HTTPS
5
+ // certificate: for those "not ready yet" errors the deadline is longer. Everything that touches
6
+ // the network or the clock can be replaced, so the behaviour is tested (verify.test.mjs).
7
+ import { isWarmingUp, CHECK_WAIT, WARMUP_WAIT } from './lib.mjs';
8
+
9
+ // fetch reports only "fetch failed"; the cause names the real problem (a TLS alert, DNS, …).
10
+ export function errorDetail(e) {
11
+ if (e?.name === 'TimeoutError' || e?.cause?.name === 'TimeoutError') return 'no answer (timeout)';
12
+ return String(e?.cause?.code ?? e?.cause?.message ?? e?.message ?? e);
13
+ }
14
+
15
+ export async function verifyDeployment(
16
+ url,
17
+ version,
18
+ {
19
+ fetcher = fetch,
20
+ sleep = ms => new Promise(r => setTimeout(r, ms)),
21
+ now = Date.now,
22
+ requestTimeout = 10 * 1000,
23
+ interval = 3 * 1000,
24
+ normalWait = CHECK_WAIT,
25
+ warmupWait = WARMUP_WAIT,
26
+ onWarmup = () => {},
27
+ } = {},
28
+ ) {
29
+ let detail = 'no answer';
30
+ let told = false;
31
+ const start = now();
32
+ for (let i = 0; now() - start < (isWarmingUp(detail) ? warmupWait : normalWait); i++) {
33
+ if (i) await sleep(interval);
34
+ try {
35
+ const h = await fetcher(`${url}/healthz`, {
36
+ headers: { 'Cache-Control': 'no-cache' },
37
+ signal: AbortSignal.timeout(requestTimeout),
38
+ });
39
+ const j = h.ok ? await h.json() : null;
40
+ if (!j?.ok) detail = `/healthz HTTP ${h.status}`;
41
+ else if (version && j.version !== version) detail = `still version ${j.version}`;
42
+ else {
43
+ const mcp = await fetcher(`${url}/mcp`, {
44
+ method: 'POST',
45
+ body: '{}',
46
+ signal: AbortSignal.timeout(requestTimeout),
47
+ });
48
+ if (mcp.status === 401)
49
+ return { ok: true, version: j.version, connected: j.connected === true };
50
+ detail = `/mcp HTTP ${mcp.status} (expected 401)`;
51
+ }
52
+ } catch (e) {
53
+ detail = errorDetail(e);
54
+ if (isWarmingUp(detail) && !told) {
55
+ told = true;
56
+ onWarmup(detail);
57
+ }
58
+ }
59
+ }
60
+ return { ok: false, detail };
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neuro-canvas-lms",
3
- "version": "2.10.1",
3
+ "version": "2.11.0",
4
4
  "description": "Neuro-CanvasLMS: read-only connector that lets Claude or ChatGPT read any Canvas LMS (remote MCP server on your own Cloudflare account)",
5
5
  "keywords": [
6
6
  "canvas",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/BloodyReginaa/Neuro-CanvasLMS.git"
23
+ "url": "git+https://github.com/BloodyReginaa/Neuro-CanvasLMS.git"
24
24
  },
25
25
  "license": "MIT",
26
26
  "author": "BloodyReginaa",
package/version.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  // The connector's version, shown on its home page and at /healthz (the installer checks that the
2
2
  // deployed version answers). Bump it together with package.json when something changes.
3
- export const VERSION = '2.10.1';
3
+ export const VERSION = '2.11.0';