@lukeguo12210/canvas-cli 0.0.3 → 0.0.5

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
@@ -8,6 +8,10 @@ Connect Canvas courses to AI agents.
8
8
 
9
9
  [Install](#installation--quick-start) · [Agent Skills](#agent-skills) · [Auth](#authentication) · [Commands](#command-system) · [Security](#security--privacy) · [License](#license) · [Roadmap](#roadmap)
10
10
 
11
+ ## Star History
12
+
13
+ [![Star History Chart](https://api.star-history.com/svg?repos=lukeguo12210/canvas-cli&type=Date)](https://www.star-history.com/#lukeguo12210/canvas-cli&Date)
14
+
11
15
  ## Why @lukeguo12210/canvas-cli?
12
16
 
13
17
  - **Built for technical students** — bring Canvas into your terminal, scripts, and AI workflow.
@@ -74,6 +78,10 @@ npm install -g @lukeguo12210/canvas-cli
74
78
  # 1. Authenticate with your Canvas school
75
79
  canvas auth login
76
80
 
81
+ # Agent/non-interactive auth
82
+ canvas auth schools search "Columbia"
83
+ canvas auth login --school "Columbia" --token-env CANVAS_TOKEN
84
+
77
85
  # 2. List courses
78
86
  canvas courses list
79
87
 
@@ -86,6 +94,12 @@ canvas review pack --course-id <course-id> --out ./review/<course>
86
94
 
87
95
  ### Install Agent Skills
88
96
 
97
+ ```bash
98
+ canvas skills install
99
+ ```
100
+
101
+ Equivalent direct installer command:
102
+
89
103
  ```bash
90
104
  npx skills add lukeguo12210/canvas-cli -g --skill "*" -y
91
105
  ```
@@ -137,6 +151,9 @@ canvas review pack --course-id <course-id> --out ./review/<course> --include-all
137
151
 
138
152
  ```bash
139
153
  canvas auth login
154
+ canvas auth login --school "Columbia" --token-env CANVAS_TOKEN
155
+ canvas auth login --school-url https://courseworks2.columbia.edu --school-name "Columbia University (CourseWorks)" --token "paste-token-here"
156
+ canvas auth schools search "Columbia"
140
157
  canvas auth status
141
158
  canvas auth logout
142
159
  canvas config show
@@ -174,6 +191,10 @@ canvas review pack --course-id <course-id> --out ./review/<course> --include-all
174
191
 
175
192
  canvas api get /api/v1/courses
176
193
  canvas api get /api/v1/courses/<course-id>/modules --params '{"include":["items"]}'
194
+
195
+ canvas skills install
196
+ canvas skills command
197
+ canvas skills status
177
198
  ```
178
199
 
179
200
  MVP raw API access is GET-only.
@@ -519,16 +519,112 @@ async function runPostLoginBootstrap() {
519
519
  };
520
520
  }
521
521
 
522
+ // src/commands/shared.ts
523
+ async function activeCanvas() {
524
+ const profile = await new ConfigStore().activeProfile();
525
+ return {
526
+ profile,
527
+ client: new CanvasClient({
528
+ baseUrl: profile.baseUrl,
529
+ token: profile.token
530
+ })
531
+ };
532
+ }
533
+ function flagValue(argv, flag) {
534
+ const index = argv.indexOf(flag);
535
+ if (index === -1) {
536
+ return void 0;
537
+ }
538
+ return argv[index + 1];
539
+ }
540
+ function requiredFlag(argv, flag, usage) {
541
+ const value = flagValue(argv, flag);
542
+ if (!value) {
543
+ throw new Error(usage);
544
+ }
545
+ return value;
546
+ }
547
+ function hasFlag(argv, flag) {
548
+ return argv.includes(flag);
549
+ }
550
+ function csvFlag(argv, flag) {
551
+ const raw = flagValue(argv, flag);
552
+ if (!raw) {
553
+ return void 0;
554
+ }
555
+ const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
556
+ return values.length > 0 ? values : void 0;
557
+ }
558
+ function pageOptions(argv) {
559
+ const pageLimitRaw = flagValue(argv, "--page-limit");
560
+ return {
561
+ pageAll: hasFlag(argv, "--page-all"),
562
+ pageLimit: pageLimitRaw ? Number.parseInt(pageLimitRaw, 10) : void 0
563
+ };
564
+ }
565
+ function positionalArgs(argv) {
566
+ const valueFlags = /* @__PURE__ */ new Set([
567
+ "--course-id",
568
+ "--module-id",
569
+ "--item-id",
570
+ "--assignment-id",
571
+ "--quiz-id",
572
+ "--topic-id",
573
+ "--page",
574
+ "--path",
575
+ "--out",
576
+ "--format",
577
+ "--page-limit",
578
+ "--page-size",
579
+ "--page-delay",
580
+ "--enrollment-state",
581
+ "--state",
582
+ "--include",
583
+ "--params",
584
+ "--bucket",
585
+ "--search",
586
+ "--order-by",
587
+ "--sort",
588
+ "--file-id",
589
+ "--folder-id",
590
+ "--group-id",
591
+ "--content-type",
592
+ "--school",
593
+ "--school-query",
594
+ "--school-url",
595
+ "--url",
596
+ "--school-name",
597
+ "--name",
598
+ "--token",
599
+ "--token-env"
600
+ ]);
601
+ const values = [];
602
+ for (let index = 0; index < argv.length; index += 1) {
603
+ const arg = argv[index];
604
+ if (arg.startsWith("--")) {
605
+ if (valueFlags.has(arg)) {
606
+ index += 1;
607
+ }
608
+ continue;
609
+ }
610
+ values.push(arg);
611
+ }
612
+ return values;
613
+ }
614
+
522
615
  // src/commands/auth.ts
523
616
  var TOKEN_PURPOSE = "Hyperknow";
524
617
  async function handleAuthCommand(argv, options) {
525
618
  const [subcommand] = argv;
526
619
  if (subcommand === "login") {
527
- return authLogin(options);
620
+ return authLogin(argv.slice(1), options);
528
621
  }
529
622
  if (subcommand === "status") {
530
623
  return authStatus(options);
531
624
  }
625
+ if (subcommand === "schools") {
626
+ return authSchools(argv.slice(1), options);
627
+ }
532
628
  if (subcommand === "logout") {
533
629
  return authLogout(options);
534
630
  }
@@ -545,16 +641,21 @@ async function handleAuthCommand(argv, options) {
545
641
  );
546
642
  return 1;
547
643
  }
548
- async function authLogin(options) {
644
+ async function authLogin(argv, options) {
549
645
  const io = createPrompt();
550
646
  try {
551
- const school = await chooseSchool(io);
647
+ const nonInteractive = hasNonInteractiveLoginArgs(argv);
648
+ const school = nonInteractive ? resolveSchoolFromArgs(argv) : await chooseSchool(io);
552
649
  const settingsUrl = `${school.url}/profile/settings`;
553
- process.stdout.write(tokenInstructions(school, settingsUrl));
554
- await io.question("Press Enter to open Canvas settings in your browser...");
555
- await openBrowser(settingsUrl);
556
- process.stdout.write("\nWaiting for your Canvas personal access token.\n");
557
- const token = await promptHidden("Paste token: ");
650
+ const providedToken = await tokenFromArgs(argv);
651
+ let token = providedToken;
652
+ if (!token) {
653
+ process.stdout.write(tokenInstructions(school, settingsUrl));
654
+ await io.question("Press Enter to open Canvas settings in your browser...");
655
+ await openBrowser(settingsUrl);
656
+ process.stdout.write("\nWaiting for your Canvas personal access token.\n");
657
+ token = await promptHidden("Paste token: ");
658
+ }
558
659
  if (!token) {
559
660
  throw new CanvasCliError("EMPTY_TOKEN", "No token entered.");
560
661
  }
@@ -588,7 +689,7 @@ async function authLogin(options) {
588
689
  },
589
690
  user,
590
691
  contextBootstrap: bootstrap,
591
- next: "canvas context show"
692
+ next: "canvas courses list --active --page-all"
592
693
  },
593
694
  meta: {
594
695
  command: "auth login"
@@ -604,6 +705,25 @@ async function authLogin(options) {
604
705
  io.close();
605
706
  }
606
707
  }
708
+ async function authSchools(argv, options) {
709
+ const [subcommand] = argv;
710
+ const query = subcommand === "search" ? positionalArgs(argv.slice(1)).join(" ") : flagValue(argv, "--query") ?? positionalArgs(argv).join(" ");
711
+ await writeOutput(
712
+ {
713
+ ok: true,
714
+ data: searchSchools(query).map((school) => ({
715
+ name: school.name,
716
+ baseUrl: school.url
717
+ })),
718
+ meta: {
719
+ command: "auth schools",
720
+ query
721
+ }
722
+ },
723
+ options
724
+ );
725
+ return 0;
726
+ }
607
727
  async function authStatus(options) {
608
728
  const store = new ConfigStore();
609
729
  const config = await store.readRedacted();
@@ -694,11 +814,77 @@ async function chooseSchool(io, write = (message) => process.stdout.write(messag
694
814
  url: normalizeBaseUrl(school.url)
695
815
  };
696
816
  }
817
+ function resolveSchoolFromArgs(argv) {
818
+ const schoolUrl = flagValue(argv, "--school-url") ?? flagValue(argv, "--url");
819
+ if (schoolUrl) {
820
+ return makeCustomSchool(flagValue(argv, "--school-name") ?? flagValue(argv, "--name") ?? "Custom Canvas School", schoolUrl);
821
+ }
822
+ const schoolQuery = flagValue(argv, "--school") ?? flagValue(argv, "--school-query");
823
+ if (!schoolQuery) {
824
+ throw new CanvasCliError(
825
+ "MISSING_SCHOOL",
826
+ "Non-interactive auth requires --school <query> or --school-url <url>."
827
+ );
828
+ }
829
+ const matches = searchSchools(schoolQuery, 20);
830
+ if (matches.length === 0) {
831
+ throw new CanvasCliError(
832
+ "SCHOOL_NOT_FOUND",
833
+ `No Canvas school matched "${schoolQuery}". Use --school-url <url> for a custom Canvas URL.`
834
+ );
835
+ }
836
+ const exact = matches.find((school) => {
837
+ return school.name.toLowerCase() === schoolQuery.toLowerCase() || school.url.toLowerCase() === schoolQuery.toLowerCase();
838
+ });
839
+ if (exact) {
840
+ return {
841
+ name: exact.name,
842
+ url: normalizeBaseUrl(exact.url)
843
+ };
844
+ }
845
+ if (matches.length === 1) {
846
+ const school = matches[0];
847
+ return {
848
+ name: school.name,
849
+ url: normalizeBaseUrl(school.url)
850
+ };
851
+ }
852
+ throw new CanvasCliError(
853
+ "AMBIGUOUS_SCHOOL",
854
+ `Multiple schools matched "${schoolQuery}": ${matches.map((school) => `${school.name} (${school.url})`).join("; ")}. Use a more specific --school value or --school-url.`
855
+ );
856
+ }
857
+ async function tokenFromArgs(argv) {
858
+ const directToken = flagValue(argv, "--token");
859
+ if (directToken) {
860
+ return directToken.trim();
861
+ }
862
+ const envName = flagValue(argv, "--token-env");
863
+ if (envName) {
864
+ return process.env[envName]?.trim();
865
+ }
866
+ if (argv.includes("--token-stdin")) {
867
+ return readStdin().then((value) => value.trim());
868
+ }
869
+ return void 0;
870
+ }
697
871
  async function promptCustomSchool(io) {
698
872
  const name = await io.question("School display name: ");
699
873
  const url = await io.question("Canvas base URL: ");
700
874
  return makeCustomSchool(name, url);
701
875
  }
876
+ function hasNonInteractiveLoginArgs(argv) {
877
+ return Boolean(
878
+ flagValue(argv, "--school") || flagValue(argv, "--school-query") || flagValue(argv, "--school-url") || flagValue(argv, "--url")
879
+ );
880
+ }
881
+ async function readStdin() {
882
+ const chunks = [];
883
+ for await (const chunk of process.stdin) {
884
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
885
+ }
886
+ return Buffer.concat(chunks).toString("utf8");
887
+ }
702
888
  function tokenInstructions(school, settingsUrl) {
703
889
  return `
704
890
  Canvas token setup for ${school.name}
@@ -730,91 +916,6 @@ async function validateToken(client) {
730
916
  }
731
917
  }
732
918
 
733
- // src/commands/shared.ts
734
- async function activeCanvas() {
735
- const profile = await new ConfigStore().activeProfile();
736
- return {
737
- profile,
738
- client: new CanvasClient({
739
- baseUrl: profile.baseUrl,
740
- token: profile.token
741
- })
742
- };
743
- }
744
- function flagValue(argv, flag) {
745
- const index = argv.indexOf(flag);
746
- if (index === -1) {
747
- return void 0;
748
- }
749
- return argv[index + 1];
750
- }
751
- function requiredFlag(argv, flag, usage) {
752
- const value = flagValue(argv, flag);
753
- if (!value) {
754
- throw new Error(usage);
755
- }
756
- return value;
757
- }
758
- function hasFlag(argv, flag) {
759
- return argv.includes(flag);
760
- }
761
- function csvFlag(argv, flag) {
762
- const raw = flagValue(argv, flag);
763
- if (!raw) {
764
- return void 0;
765
- }
766
- const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
767
- return values.length > 0 ? values : void 0;
768
- }
769
- function pageOptions(argv) {
770
- const pageLimitRaw = flagValue(argv, "--page-limit");
771
- return {
772
- pageAll: hasFlag(argv, "--page-all"),
773
- pageLimit: pageLimitRaw ? Number.parseInt(pageLimitRaw, 10) : void 0
774
- };
775
- }
776
- function positionalArgs(argv) {
777
- const valueFlags = /* @__PURE__ */ new Set([
778
- "--course-id",
779
- "--module-id",
780
- "--item-id",
781
- "--assignment-id",
782
- "--quiz-id",
783
- "--topic-id",
784
- "--page",
785
- "--path",
786
- "--out",
787
- "--format",
788
- "--page-limit",
789
- "--page-size",
790
- "--page-delay",
791
- "--enrollment-state",
792
- "--state",
793
- "--include",
794
- "--params",
795
- "--bucket",
796
- "--search",
797
- "--order-by",
798
- "--sort",
799
- "--file-id",
800
- "--folder-id",
801
- "--group-id",
802
- "--content-type"
803
- ]);
804
- const values = [];
805
- for (let index = 0; index < argv.length; index += 1) {
806
- const arg = argv[index];
807
- if (arg.startsWith("--")) {
808
- if (valueFlags.has(arg)) {
809
- index += 1;
810
- }
811
- continue;
812
- }
813
- values.push(arg);
814
- }
815
- return values;
816
- }
817
-
818
919
  // src/commands/api.ts
819
920
  async function handleApiCommand(argv, options) {
820
921
  const [subcommand] = argv;
@@ -2153,8 +2254,137 @@ async function reviewPack(argv, options) {
2153
2254
  return 0;
2154
2255
  }
2155
2256
 
2257
+ // src/commands/skills.ts
2258
+ import { spawn as spawn2 } from "child_process";
2259
+ var SKILLS_INSTALL_COMMAND = [
2260
+ "npx",
2261
+ "skills",
2262
+ "add",
2263
+ "lukeguo12210/canvas-cli",
2264
+ "-g",
2265
+ "--skill",
2266
+ "*",
2267
+ "-y"
2268
+ ];
2269
+ var SKILLS_INSTALL_DISPLAY_COMMAND = 'npx skills add lukeguo12210/canvas-cli -g --skill "*" -y';
2270
+ async function handleSkillsCommand(argv, options) {
2271
+ const [subcommand] = argv;
2272
+ try {
2273
+ if (subcommand === "install") {
2274
+ return await installSkills(argv.slice(1), options);
2275
+ }
2276
+ if (subcommand === "command") {
2277
+ return await printSkillsCommand(options);
2278
+ }
2279
+ if (subcommand === "status") {
2280
+ return await skillsStatus(options);
2281
+ }
2282
+ await writeOutput(
2283
+ {
2284
+ ok: false,
2285
+ error: {
2286
+ code: "UNKNOWN_COMMAND",
2287
+ message: `Unknown skills command: ${argv.join(" ")}`,
2288
+ retryable: false
2289
+ }
2290
+ },
2291
+ options
2292
+ );
2293
+ return 1;
2294
+ } catch (error) {
2295
+ await writeOutput(toErrorEnvelope(error), options);
2296
+ return 1;
2297
+ }
2298
+ }
2299
+ async function installSkills(argv, options) {
2300
+ if (hasFlag(argv, "--dry-run") || hasFlag(argv, "--print")) {
2301
+ return printSkillsCommand(options);
2302
+ }
2303
+ const code = await run(SKILLS_INSTALL_COMMAND[0], SKILLS_INSTALL_COMMAND.slice(1));
2304
+ if (code !== 0) {
2305
+ await writeOutput(
2306
+ {
2307
+ ok: false,
2308
+ error: {
2309
+ code: "SKILLS_INSTALL_FAILED",
2310
+ message: `Skills installer exited with code ${code}. Try: ${SKILLS_INSTALL_DISPLAY_COMMAND}`,
2311
+ retryable: true
2312
+ }
2313
+ },
2314
+ options
2315
+ );
2316
+ return code;
2317
+ }
2318
+ await writeOutput(
2319
+ {
2320
+ ok: true,
2321
+ data: {
2322
+ installed: true,
2323
+ command: SKILLS_INSTALL_DISPLAY_COMMAND,
2324
+ next: "Restart or reload your agent so it can discover the updated Canvas skills."
2325
+ },
2326
+ meta: {
2327
+ command: "skills install"
2328
+ }
2329
+ },
2330
+ options
2331
+ );
2332
+ return 0;
2333
+ }
2334
+ async function printSkillsCommand(options) {
2335
+ await writeOutput(
2336
+ {
2337
+ ok: true,
2338
+ data: {
2339
+ command: SKILLS_INSTALL_DISPLAY_COMMAND,
2340
+ note: "Run this to install or update all Canvas agent skills from GitHub."
2341
+ },
2342
+ meta: {
2343
+ command: "skills command"
2344
+ }
2345
+ },
2346
+ options
2347
+ );
2348
+ return 0;
2349
+ }
2350
+ async function skillsStatus(options) {
2351
+ await writeOutput(
2352
+ {
2353
+ ok: true,
2354
+ data: {
2355
+ installCommand: SKILLS_INSTALL_DISPLAY_COMMAND,
2356
+ skills: [
2357
+ "canvas-shared",
2358
+ "canvas-courses",
2359
+ "canvas-modules",
2360
+ "canvas-pages",
2361
+ "canvas-files",
2362
+ "canvas-assignments",
2363
+ "canvas-review"
2364
+ ],
2365
+ note: "Use canvas skills install to install/update these skills."
2366
+ },
2367
+ meta: {
2368
+ command: "skills status"
2369
+ }
2370
+ },
2371
+ options
2372
+ );
2373
+ return 0;
2374
+ }
2375
+ function run(command, args) {
2376
+ return new Promise((resolve2) => {
2377
+ const child = spawn2(command, [...args], {
2378
+ stdio: "inherit",
2379
+ shell: process.platform === "win32"
2380
+ });
2381
+ child.on("close", (code) => resolve2(code ?? 1));
2382
+ child.on("error", () => resolve2(1));
2383
+ });
2384
+ }
2385
+
2156
2386
  // src/bin/canvas.ts
2157
- var VERSION = "0.0.3";
2387
+ var VERSION = "0.0.6";
2158
2388
  function helpText() {
2159
2389
  return `canvas \u2014 Canvas LMS CLI for students and agents.
2160
2390
 
@@ -2164,6 +2394,7 @@ USAGE:
2164
2394
  COMMANDS:
2165
2395
  auth login Interactive Canvas PAT setup
2166
2396
  auth status Show redacted auth status
2397
+ auth schools Search supported Canvas school URLs
2167
2398
  auth logout Remove local Canvas auth config
2168
2399
  config show Show redacted local config
2169
2400
  me Show current Canvas user profile
@@ -2178,6 +2409,7 @@ COMMANDS:
2178
2409
  folders list List course folders
2179
2410
  review pack Create a local course review pack
2180
2411
  api get Raw read-only Canvas API GET
2412
+ skills install Install/update bundled agent skills
2181
2413
  version Print CLI version
2182
2414
 
2183
2415
  FLAGS:
@@ -2243,6 +2475,9 @@ async function main(argv) {
2243
2475
  if (command === "api") {
2244
2476
  return handleApiCommand(parsed.argv.slice(1), { format: parsed.format });
2245
2477
  }
2478
+ if (command === "skills") {
2479
+ return handleSkillsCommand(parsed.argv.slice(1), { format: parsed.format });
2480
+ }
2246
2481
  await writeOutput(
2247
2482
  {
2248
2483
  ok: false,