@lukeguo12210/canvas-cli 0.0.2 → 0.0.4

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
@@ -2,36 +2,40 @@
2
2
 
3
3
  [![License: BUSL-1.1](https://img.shields.io/badge/License-BUSL--1.1-yellow.svg)](LICENSE)
4
4
 
5
- Canvas CLI for technical students and AI agents.
5
+ Connect Canvas courses to AI agents.
6
6
 
7
- `@lukeguo12210/canvas-cli` turns Canvas LMS into a local, scriptable, agent-readable workspace. It is built for students who already live in terminals, editors, notebooks, and AI coding tools. Use the `canvas` command directly, or install the bundled skills so Codex, Claude Code, and other local agents can authenticate, understand your courses, pull materials, inspect upcoming work, and build review packs.
7
+ `@lukeguo12210/canvas-cli` turns Canvas LMS into an agent-readable course workspace. Today, students dig through Canvas, download slides, find assignments, copy pages, and upload everything back into LLMs. Canvas CLI makes that one workflow: pull course materials, modules, files, assignments, pages, folders, and review packs into a structure AI agents can immediately understand.
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
- - **Built for technical students** — bring Canvas into your terminal, scripts, and local AI workflow.
17
+ - **Built for technical students** — bring Canvas into your terminal, scripts, and AI workflow.
14
18
  - **Agent-native design** — structured skills teach agents Canvas auth, pagination, course structure, and study workflows.
15
- - **Course context in minutes** — after login, the CLI gathers who you are, what courses you are taking, and where you are in the semester.
16
- - **Review-pack first** — export modules, pages, assignments, files, discussions, announcements, and calendar context into a structured local course folder.
19
+ - **No more manual Canvas digging** — pull slides, files, pages, assignments, folders, and module structure without download/upload loops.
20
+ - **Review-pack first** — export course structure into an agent-readable folder for study sessions.
17
21
  - **Canvas-native commands** — simple commands such as `canvas courses list`, `canvas modules list`, and `canvas review pack`.
18
- - **Local-first control** — personal access tokens stay local, and course exports are written to directories you choose.
22
+ - **Student-facing scope** — starts with the read-only course surface students need for review, planning, and coursework navigation.
19
23
 
20
24
  ## Features
21
25
 
22
26
  | Category | Capabilities |
23
27
  | --- | --- |
24
28
  | Auth | Interactive `canvas auth login`, school picker, Canvas settings walkthrough, local PAT config |
25
- | Context | Post-login context bootstrap: user profile, active courses, planner, calendar, upcoming assignments, modules |
26
29
  | Courses | List, search, inspect, and summarize active courses |
30
+ | Tabs | Inspect visible Canvas course tabs and external tool links |
27
31
  | Modules | Traverse modules and module items in Canvas order |
28
- | Pages | Fetch pages and export Canvas HTML to Markdown |
32
+ | Pages | List pages, inspect Canvas HTML, and export visible pages |
29
33
  | Files | List files/folders and download accessible course files safely |
30
- | Assignments | List assignments, due dates, descriptions, visible attachments, and assignment groups |
31
- | Calendar | Pull calendar, planner, and todo context for the semester |
32
- | Discussions | Inspect discussion prompts and announcements where visible |
33
- | Review Packs | Preserve Canvas course structure in an agent-readable local export |
34
- | Agent Skills | Lark-style skills for Codex, Claude Code, and other local agents |
34
+ | Folders | Browse Canvas folder trees and folder paths |
35
+ | Assignments | List assignments, due dates, descriptions, visible attachments, and exports |
36
+ | Raw API | Run read-only `GET` requests against Canvas API paths |
37
+ | Review Packs | Preserve Canvas course structure in an agent-readable export |
38
+ | Agent Skills | Skills that teach agents exact commands, output shapes, and Canvas workflows |
35
39
 
36
40
  ## Status
37
41
 
@@ -52,7 +56,7 @@ Planned next:
52
56
 
53
57
  - Post-login context bootstrap.
54
58
  - Broader review-pack indexing, citations, and linked-file resolution.
55
- - Grades, submissions, discussions, announcements, calendar, groups, and conversations.
59
+ - Grades, submissions, discussions, announcements, calendar, groups, conversations, and richer review indexing.
56
60
 
57
61
  ## Installation & Quick Start
58
62
 
@@ -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
 
@@ -133,24 +141,48 @@ canvas review pack --course-id <course-id> --out ./review/<course>
133
141
  canvas review pack --course-id <course-id> --out ./review/<course> --include-all-files
134
142
  ```
135
143
 
136
- ### Domain Commands
144
+ ### Full Command Surface
137
145
 
138
146
  ```bash
147
+ canvas auth login
148
+ canvas auth login --school "Columbia" --token-env CANVAS_TOKEN
149
+ canvas auth login --school-url https://courseworks2.columbia.edu --school-name "Columbia University (CourseWorks)" --token "paste-token-here"
150
+ canvas auth schools search "Columbia"
151
+ canvas auth status
152
+ canvas auth logout
153
+ canvas config show
154
+ canvas me
155
+
139
156
  canvas courses list
140
157
  canvas courses search "algorithms"
158
+ canvas courses show <course-id>
159
+ canvas courses overview <course-id>
160
+ canvas tabs list --course-id <course-id>
161
+
141
162
  canvas modules list --course-id <course-id>
142
163
  canvas modules items --course-id <course-id> --module-id <module-id>
164
+ canvas modules item --course-id <course-id> --module-id <module-id> --item-id <item-id>
165
+ canvas modules export --course-id <course-id> --module-id <module-id> --out ./module
166
+
143
167
  canvas assignments list --course-id <course-id>
168
+ canvas assignments show --course-id <course-id> --assignment-id <assignment-id>
169
+ canvas assignments export --course-id <course-id> --assignment-id <assignment-id> --out ./assignment
170
+
144
171
  canvas pages list --course-id <course-id>
145
172
  canvas pages show --course-id <course-id> --page <url-or-id>
173
+ canvas pages export --course-id <course-id> --page <url-or-id> --out ./page
174
+
146
175
  canvas files list --course-id <course-id>
176
+ canvas files show <file-id>
147
177
  canvas files download <file-id> --out ./files
178
+ canvas files download-linked --course-id <course-id> --out ./files
179
+
148
180
  canvas folders list --course-id <course-id>
149
- ```
181
+ canvas folders path --course-id <course-id> --path "course files/week 1"
150
182
 
151
- ### Raw Read-Only API
183
+ canvas review pack --course-id <course-id> --out ./review/<course>
184
+ canvas review pack --course-id <course-id> --out ./review/<course> --include-all-files
152
185
 
153
- ```bash
154
186
  canvas api get /api/v1/courses
155
187
  canvas api get /api/v1/courses/<course-id>/modules --params '{"include":["items"]}'
156
188
  ```
@@ -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;
@@ -2154,7 +2255,7 @@ async function reviewPack(argv, options) {
2154
2255
  }
2155
2256
 
2156
2257
  // src/bin/canvas.ts
2157
- var VERSION = "0.0.2";
2258
+ var VERSION = "0.0.4";
2158
2259
  function helpText() {
2159
2260
  return `canvas \u2014 Canvas LMS CLI for students and agents.
2160
2261
 
@@ -2164,6 +2265,7 @@ USAGE:
2164
2265
  COMMANDS:
2165
2266
  auth login Interactive Canvas PAT setup
2166
2267
  auth status Show redacted auth status
2268
+ auth schools Search supported Canvas school URLs
2167
2269
  auth logout Remove local Canvas auth config
2168
2270
  config show Show redacted local config
2169
2271
  me Show current Canvas user profile