@ivanbard/dd2l 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ivanbard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # dd2l
2
+
3
+ i am not the biggest fan of D2L Brightspace, and sometimes wish i could get what im looking for without digging through 14 menus that differ from course to course. to solve that i made dd2l, a dynamic companion app to D2L Brightspace that does what you need, and adapts to your needs with time.
4
+
5
+ dd2l is a local student companion for Queen's onQ. Sync your courses, search
6
+ your materials, and ask questions with sources.
7
+
8
+ ## How it works
9
+
10
+ - sign in to onQ through Chrome, including your school's MFA
11
+ - collect course materials, assignments, grades, announcements and syllabus text
12
+ - search the local cache, browse sources, and customize your home screen widgets
13
+ - optionally connect ChatGPT for answers grounded in matching course excerpts
14
+
15
+ The connector is read-only. It does not submit work or change grades, posts,
16
+ quiz attempts or completion states. Coverage depends on what onQ makes accessible.
17
+
18
+ ## School not listed?
19
+ If your school/educational faculty uses D2L Brightspace and is not available during setup, please open an issue in the GitHub repo with some details as to your
20
+ provider's Brightspace link and faculty name. Feel free to also create a PR if you have added your own solution to your school not being listed.
21
+
22
+ ## Run
23
+
24
+ Requires Windows, Node.js 24 or newer, and Google Chrome. This repository includes
25
+ an npm CLI; the commands below work once the package is published to npm:
26
+
27
+ ```powershell
28
+ npx dd2l setup
29
+ ```
30
+
31
+ Setup explains local storage, asks before connecting onQ, opens Chrome for school
32
+ sign-in and MFA, then opens the app and starts collecting coursework. Keep that
33
+ one terminal open; Ctrl+C stops both the app and sync. The initial view may be
34
+ empty while collection runs. Collection advances through up to five courses per
35
+ pass every 15 minutes, so the first pass is not a complete download.
36
+
37
+ Next time, run `npx dd2l start`. If the school session expires, stop the app and
38
+ run `npx dd2l setup` again. Optional ChatGPT sign-in is in Settings.
39
+
40
+ Alternatively, install globally with `npm install -g dd2l`, then use `dd2l setup`
41
+ and `dd2l start`. Installation itself does not open browsers or connect accounts;
42
+ setup runs explicitly in an interactive terminal.
43
+
44
+ To run the current source checkout before publication:
45
+
46
+ ```powershell
47
+ npm ci
48
+ npm run setup
49
+ ```
50
+
51
+ On later launches use `npm start -- start`. The individual `login`, `audit`,
52
+ `sync`, and `npm run ui` commands remain available for manual operation.
53
+
54
+ Or try fictional data without a school login:
55
+
56
+ ```powershell
57
+ npm run ui -- --demo
58
+ ```
59
+
60
+ ## Your data
61
+
62
+ School sessions stay on your computer, encrypted with Windows DPAPI. The course
63
+ cache lives in `%LOCALAPPDATA%\dd2l` and is **not encrypted**. Local search does
64
+ not send your question to an AI provider.
65
+
66
+ For AI answers, connect ChatGPT in Settings and choose a model below the prompt.
67
+ Submitting sends your question and matching course excerpts to OpenAI and uses
68
+ your plan's limits. Check generated answers against the supplied sources.
69
+
70
+ Stop the app, then run `dd2l logout` (or `npm start -- logout` from source) to
71
+ remove the saved school session and inventory. Disconnect
72
+ ChatGPT separately in Settings; clear the site's browser data to remove saved
73
+ widget preferences and shortcut titles.
74
+
75
+ More docs are in [`docs/`](docs/README.md), including commands, ChatGPT setup,
76
+ collection limits and cache behavior.
77
+
78
+ Run `npm test` for tests and `npm run ui:check` for browser checks.
79
+ Third-party attribution is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
80
+
81
+ Original dd2l code is [MIT licensed](LICENSE); third-party assets retain their
82
+ own licenses. Maintainers: see [PUBLISHING.md](https://github.com/ivanbard/dd2l/blob/main/PUBLISHING.md)
83
+ for release setup.
@@ -0,0 +1,50 @@
1
+ # UI icons
2
+
3
+ The four sidebar action icons are from [Lucide](https://lucide.dev), version 0.468.0 (square-pen, search, book-open, settings), with their stroke color set to match the UI. The upstream ISC license notice is retained in [ui/assets/LUCIDE-LICENSE.txt](ui/assets/LUCIDE-LICENSE.txt). The SVGs are bundled locally; no icon runtime or external requests are required.
4
+
5
+ # Dot-matrix loading indicator
6
+
7
+ The app's 5×5 Origin Wave indicator is adapted from `DotmSquare12` (`@dotmatrix/dotm-square-12`), dot-matrix v1.5.0 by [zzzzshawn/matrix](https://github.com/zzzzshawn/matrix), as published at [Dot Matrix](https://dotmatrix.zzzzshawn.cloud/). `ui/loading.js` and the `dot-origin-wave` CSS preserve its origin, Manhattan ring delays, opacity curve and reduced-motion appearance in plain JavaScript/CSS. This adaptation is bundled for use inside dd2l, not offered as a standalone component or library. No React, Tailwind, registry CLI or external runtime is bundled.
8
+
9
+ The upstream license is retained below:
10
+
11
+ Custom Proprietary License
12
+
13
+ Copyright (c) 2026 dot-matrix contributors
14
+ All rights reserved.
15
+
16
+ Permission is granted to use this software, including in commercial and non-commercial products.
17
+
18
+ The following are strictly prohibited without prior written permission from the copyright holder:
19
+ 1. Publishing, distributing, or presenting any component from this project as a standalone reusable component, or claiming it as part of another component library or similar reusable component collection.
20
+ 2. Selling, sublicensing, renting, leasing, or otherwise commercializing any component from this project, whether modified or unmodified, as a standalone component offering.
21
+
22
+ Any use not expressly permitted by this license is prohibited.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ # Reference implementation
27
+
28
+ Authentication behavior and endpoint discovery were informed by [brightspace-mcp-server](https://github.com/RohanMuppa/brightspace-mcp-server), version 5.1.27, by Rohan Muppa. dd2l does not import or run that server. The applicable notice is retained here for attribution of the reference work:
29
+
30
+ MIT License
31
+
32
+ Copyright (c) 2026 Rohan Muppa
33
+
34
+ Permission is hereby granted, free of charge, to any person obtaining a copy
35
+ of this software and associated documentation files (the "Software"), to deal
36
+ in the Software without restriction, including without limitation the rights
37
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
38
+ copies of the Software, and to permit persons to whom the Software is
39
+ furnished to do so, subject to the following conditions:
40
+
41
+ The above copyright notice and this permission notice shall be included in all
42
+ copies or substantial portions of the Software.
43
+
44
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
50
+ SOFTWARE.
package/bin/dd2l.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ if (Number(process.versions.node.split('.')[0]) < 24) {
3
+ console.error('dd2l requires Node.js 24 or newer. Install it from https://nodejs.org, then retry.');
4
+ process.exitCode = 1;
5
+ } else {
6
+ await import('../src/cli.mjs');
7
+ }
package/docs/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # dd2l guide
2
+
3
+ A direct, read-only Brightspace connector for a customizable, promptable student companion. The first target is one Queen's onQ account on Windows.
4
+
5
+ The connector signs in using your installed Google Chrome browser, reuses a protected school session, inventories course data, and can refresh a selected course or rotate through all accessible courses. It runs directly against Brightspace; no MCP server or model provider is involved.
6
+
7
+ ## Run
8
+
9
+ The guided source-checkout path is `npm ci`, then `npm run setup`. Setup asks
10
+ before connecting onQ, completes school sign-in, opens the UI, and runs sync in
11
+ the same terminal. Use `npm start -- start` on later launches. Keep the terminal
12
+ open; Ctrl+C stops both. If sign-in expires, rerun setup. Collection remains
13
+ incremental, with the same batching and 15-minute cadence described below.
14
+
15
+ The npm package exposes `dd2l setup` and `dd2l start` (or `npx dd2l setup` and
16
+ `npx dd2l start` without a global install) once published. It has no interactive
17
+ install hook. The repository has not been published by this setup change.
18
+
19
+ For manual operation:
20
+
21
+ Requires Windows, Node.js 24 or newer, and Google Chrome. From this directory:
22
+
23
+ ```powershell
24
+ npm ci
25
+ npm run login
26
+ npm start -- courses
27
+ npm run audit
28
+ npm start -- syllabus
29
+ npm run status
30
+ ```
31
+
32
+ Complete school sign-in and MFA in Chrome when requested. No school password is collected or saved. The default audit prefers the most recently accessed active, accessible course, falling back to another accessible enrollment if necessary. To select a particular course, use its ID from `courses`:
33
+
34
+ ```powershell
35
+ npm run audit -- --course COURSE_ID --from 2026-09-01T00:00:00Z --to 2027-01-01T00:00:00Z
36
+ npm run sync -- --course COURSE_ID
37
+ npm start -- syllabus --course COURSE_ID
38
+ npm run audit -- --all
39
+ npm run sync -- --all
40
+ ```
41
+
42
+ Replace `COURSE_ID` with the numeric ID. `sync` keeps running in the terminal until Ctrl+C; shutdown waits for the current bounded audit to finish. Without `--course`, sync reuses the last selected course and pins that choice for subsequent polls. Keep the computer and terminal running. If the school session expires, the worker stops with `needs_login`; run `login`, then restart `sync`. Temporary failures back off, and server retry guidance is respected. Concurrent mutating commands are prevented by a process lock; `status` can inspect a running worker.
43
+
44
+ `--all` rotates through up to five accessible course offerings per pass, including historical courses still accessible to this account. It preserves the pinned single-course selection. Each course's assignment, document and discussion cursors are saved after its audit commits; another pass resumes the remaining objects. Sync waits 15 minutes between passes, so a full sweep can take multiple polling intervals. A rate-limit response stops further reads in that pass and honors the server's retry delay.
45
+
46
+ For a sustained foreground check, run `npm run soak -- --minutes 180`. This uses the real sync cadence, writes structural results to the ignored `.dd2l/soak-summary.json`, and stops after the observation window and any in-progress audit finish. It does not force session expiry or bypass MFA. A one-minute smoke run has passed; a multi-hour run remains to be performed.
47
+
48
+ Run `npm start -- --help` for commands and `npm test` for the focused checks.
49
+
50
+ ## UI prototype
51
+
52
+ ```powershell
53
+ npm run ui
54
+ ```
55
+
56
+ Open the printed [local URL](http://127.0.0.1:4173). The home screen implements the [Figma Home design](https://www.figma.com/design/ASr0dnwupPLbXmeT1AiPGg/dd2l?node-id=1-3): prompt, course selector, sidebar and profile. Search finds keyword matches in cached course materials and displays source excerpts with retrieval dates and stale-cache labels. Courses opens a material browser; Settings and Profile show connection status and refresh instructions. The source link opens the course in onQ using your normal browser sign-in.
57
+
58
+ Local search reads the existing inventory without changing it or loading school credentials. Run `npm run audit -- --all` or keep `npm run sync -- --all` running separately to collect data. Each search reads the current cache. Only courses confirmed accessible in cached enrollments are shown; unavailable material stays unavailable, and coverage is limited to what the connector has collected. The mode dropdown beside the course selector defaults to **Local search**, which matches keywords; choosing a connected ChatGPT model generates grounded answers when you submit.
59
+
60
+ The homescreen includes upcoming deadlines, recent class announcements, and shortcuts to classes or cached materials. **Customize** enables a four-column, eight-row grid: drag a widget to move it live, or drag any edge or corner to resize it. A cell preview marks the proposed placement; overlapping or out-of-bounds drops return to the original position. Escape cancels the gesture. Focus a widget and use arrow keys to move or Shift + arrows to resize without a pointer. Remove widgets or add them back with **Add widget**. Narrow screens stack widgets normally and show a horizontally scrollable grid while customizing. **Pin shortcut** opens a searchable class/material picker; selecting a shortcut browses its cached sources. New search returns to the homescreen.
61
+
62
+ Waiting states use the Dot Matrix Square 12 / Origin Wave indicator: search and AI requests, course/model loading, widget refreshes, panels and account actions. Its 5×5 ripple is adapted to plain JavaScript/CSS, bundled locally and static when reduced motion is preferred. Loading states clear after success, failure or cancellation; concurrent loads share an indicator until all finish. Attribution and the upstream usage license are in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md).
63
+
64
+ Layouts and up to 20 shortcut references save in browser local storage, separately for each school account and demo mode. They do not sync across browsers or ports. Shortcut references include course-material titles and remain after CLI logout; clear this site's browser data to remove saved preferences. Inaccessible material is not rendered even if its reference remains pinned. **Refresh cache view** rereads the local inventory, without running a school sync. Deadlines require explicit due dates, announcements use publication dates when available, and stale cache entries remain labeled; an empty widget does not establish that onQ has no updates.
65
+
66
+ ### Use your ChatGPT plan
67
+
68
+ In the live UI, open Settings or Profile and choose **Continue with ChatGPT**. Complete registration and plan-use consent on OpenAI's website, then return to dd2l. No API key is needed. The small mode dropdown below the prompt starts on **Local search**, even with a connected account. Choose a model to see the coursework-sharing notice, then press Enter or the send button to ask it. Changing the selection never submits a prompt. Select **Local search** to return to local retrieval; reloading or starting a new search also restores that default. Plan and usage information stays in Settings.
69
+
70
+ Each AI request sends the question and up to eight matching excerpts (at most 700 characters each), including titles, course names, retrieval dates and stale-cache labels, to OpenAI. The answer displays those sources with numbered references. The model is instructed to use only this evidence and report gaps; verify generated claims against the excerpts. Keyword retrieval can miss relevant material. No tool execution, Brightspace credentials, complete inventory upload, or persistent conversation history is provided. Each submission with an AI model selected grants permission for that answer; local mode never sends the question to an AI provider. Editing the question or changing modes or courses cancels an in-flight answer.
71
+
72
+ AI answers can compose interactive checklists, comparison tables, and grouped source cards in the same matte printed style as the homescreen widgets. The selected model chooses useful sections or plain text for a simple question. The Responses request uses [Structured Outputs](https://developers.openai.com/api/docs/guides/structured-outputs) with a bounded JSON schema; the app validates the result and citation numbers before rendering trusted components. No model-generated HTML, scripts, URLs or styling execute. Invalid layouts fall back to a bounded text summary and the available sources, with a notice; incomplete streams still fail rather than appearing successful. Provider refusals display as text. A model must support the structured-output format; errors do not trigger another model or a second billed request.
73
+
74
+ Checklist checks and progress belong to the current answer and reset on a new question. They do not update Brightspace or save to your homescreen. Every generated item or comparison row links to supplied source excerpts; stale cache labels remain visible. Valid structure and citations do not prove factual correctness or complete course coverage. Source retrieval still uses the existing keyword search and its limits.
75
+
76
+ This uses OpenAI's documented [local-app Sign in with ChatGPT flow](https://developers.openai.com/siwc/token-sharing-open-source), with PKCE, state/nonce and signed identity validation. A stable host ID and separate account registrations are saved under `%LOCALAPPDATA%\dd2l\llm\accounts.dpapi`, encrypted with Windows DPAPI. Refreshes are serialized across processes using a separate lock; the school sync can run alongside AI. Tokens stay on the server and never enter browser storage. `jose` handles JWT signature and claim verification. Remote Responses requests set `store: false` and `stream: true`; partial/failed streams do not become successful answers.
77
+
78
+ Settings lists saved account/workspace registrations and supports switching, reauthorization and sign-out of the active account. Sign-out revokes its renewable session when possible and removes its local tokens; a failed remote revocation is reported. The non-secret registration and host ID remain for later sign-in. School CLI logout affects only school data; disconnect ChatGPT separately in the UI. Manage plan/app limits in [ChatGPT Settings → Usage](https://chatgpt.com/settings/usage). Usage shares the account's plan limits and may use credits according to settings you authorize there; dd2l never falls back to API-key billing. Eligibility, available models and limits are controlled by OpenAI.
79
+
80
+ ChatGPT is the first supported subscription provider. There is no universal subscription login: other providers need separately verified authentication and plan-use support. Demo mode disables provider sign-in and AI requests and never loads AI credentials. Commercial or remotely hosted deployments need a separate access review against [OpenAI's integration requirements](https://developers.openai.com/siwc/request-client-id).
81
+
82
+ For an isolated walkthrough with fictional data and no school login:
83
+
84
+ ```powershell
85
+ npm run ui -- --demo
86
+ ```
87
+
88
+ Demo mode is visibly labeled and never reads the real inventory. Try `recursion`, `memory`, or `syllabus`, filter by course, or browse Courses. Use `--port 4174` to run another instance. The server binds only to `127.0.0.1`, rejects foreign browser origins, and serves private responses without caching. Stop it with Ctrl+C when finished.
89
+
90
+ `npm test` covers retrieval, cache access, widget layout validation, deadline/announcement projection and HTTP boundaries. `npm run ui:check` uses installed Chrome headlessly to check assets, core interactions, widget drag/move/resize/pin/removal and persistence, mobile overflow and error recovery; screenshots go in ignored `.dd2l/ui-*.png`. SF Pro uses the system font when available, with Segoe UI as the Windows fallback.
91
+
92
+ ## What is collected
93
+
94
+ The audit enumerates course offerings, then reads the chosen course's content tree, assignment folders, quiz list, own grade values, announcements, discussion forums, own groups/sections, and calendar events. It traverses assignments and accessible content files in resumable batches, including own submissions, released feedback, rubric tables and attachments. Discussion forum/topic/post reads and file attachments have independent parent-specific cursors. `traversal` reports the batch limits; an unfinished batch is not reported as complete.
95
+
96
+ Assignment traversal prioritizes graded and rubric-bearing folders, then continues through the others, up to 25 per pass. Feedback targets your verified user identity or your own resolved group; an unresolved group remains unknown. Full embedded rubric tables are retained, with a dedicated association lookup used when needed. Feedback denials are not interpreted as ungraded or unsubmitted work.
97
+
98
+ Quiz discovery combines list entries, explicit content references, gradebook candidates and permitted grade/tool associations. A grade item named “Quiz” is only a candidate until a real quiz identifier/association is verified. Quiz metadata and attempts are read separately; attempts are explicitly filtered to the verified current user. Denied or unread attempts remain unknown. For events marked recurring, the connector requests the server's occurrence list rather than guessing dates or timezone rules; this route still needs a real accessible recurring-event example on onQ.
99
+
100
+ Syllabus retrieval is part of normal audit/sync and can also run alone with `syllabus`. It reads the optional overview and its attachment, relevant module/topic descriptions, and file/HTML topics under syllabus or course-information modules. It preserves each source's location, parent context, retrieval time and current access/text status. An overview-specific 404 means `not_configured`; unrelated 404s retain their original meaning. An empty overview is not counted as a readable syllabus. Different documents remain separate instead of being silently merged into one authoritative text.
101
+
102
+ Original permitted JSON fields are retained, with authentication/password fields and credential-bearing URL parameters redacted. Modern DOCX/PPTX/XLSX, PDF, HTML and plain text support local extraction; Office macros and formulas are not executed. Images and scanned PDF pages can use Windows OCR with an installed user-profile OCR language. OCR text is marked for review. Legacy binary Office files, arbitrary archives and encrypted documents are not supported by these readers.
103
+
104
+ Documents have a 20 MiB read limit; raw bytes are retained only up to 2 MiB, while larger successfully parsed documents retain text and metadata. PDF workers have a 30-second deadline, 256 MiB JavaScript heap limit and 250-page limit. At most ten empty-text PDF pages per document are rendered for OCR, with a 16-million-pixel image limit. Text is capped at one million characters. Office expansion, HTML complexity and document/link counts are bounded; limits stay explicit as partial coverage. Syllabus retrieval processes up to 100 candidate sections and 20 document attempts per course. Ordinary traversal handles 20 content files per pass, five forums, five topics per selected forum, and two attachments per selected assignment/topic. API collections follow pagination with existing loop/page limits.
105
+
106
+ Linked school documents use same-school session cookies only on approved content paths, or use their verified API topic IDs. Public reference documents are read only from explicitly supported hosts without school credentials. Source links are deduplicated; referenced websites are not recursively crawled through their citations/navigation. Surveys, self-enrollment and other activity actions are never activated by the document reader. Public YouTube caption retrieval preserves timestamps when the provider returns a caption track; sign-in/bot checks or absent captions remain unavailable. Private SharePoint, Teams, publisher/LTI content and other providers need separate readers/authorization. No assignments, grades, posts, quiz attempts, or completion states are written. Browser navigation may update normal school access timestamps.
107
+
108
+ ## Local data and app integration
109
+
110
+ All runtime data lives outside this repository in `%LOCALAPPDATA%\dd2l`:
111
+
112
+ | File | Purpose |
113
+ | --- | --- |
114
+ | `session.dpapi` | Session cookies, XSRF material, API token and verified identity, encrypted with Windows DPAPI for the current Windows user. |
115
+ | `inventory.sqlite` | Private source payloads, extracted text/small files, traversal cursors, per-resource coverage, successful retrieval times and current outcomes. This database contains personal coursework and grades and is **not encrypted**. |
116
+ | `summary.json` | Latest structural audit summary: counts, field names and coverage without course names, source IDs, grades or document bodies. Review before sharing. |
117
+ | `syllabus-summary.json` | Latest syllabus-only command summary with readability, formats, coverage and unresolved counts. |
118
+ | `worker.lock` | Prevents competing login/audit/sync processes; a dead process's lock is recovered on the next run. |
119
+
120
+ `npm start -- logout` removes dd2l's saved session and local inventory. It does not sign out of other school applications. This is ordinary local deletion, not guaranteed forensic erasure or deletion of any copies you made.
121
+
122
+ The importable core is `BrightspaceClient` in `src/client.mjs`, `Session` in `src/session.mjs`, `Connector`/`sync` in `src/connector.mjs`, and `Store` in `src/store.mjs`. `Connector.syllabus({ course })` uses the same retrieval logic as `audit` without changing the selected sync course. The CLI uses that same core. SQLite's `resources` table separates the latest observation from the last successful `data`, `last_success`, and `last_source`. Failed or partial reads preserve that data with its original source/window; a confirmed denial invalidates the affected cached resource. A complete enrollment read also invalidates cached courses no longer accessible. Data from different accounts cannot mix; run logout before switching accounts.
123
+
124
+ Syllabus content lives in individual `syllabus_source` resource rows; the `syllabus` row is a catalog of source keys and current outcomes, not a duplicate of every document's text. A failed fetch or extraction retains the source's prior successful data as stale. Explicit access restrictions invalidate that source's cached data, and a documented missing overview clears the overview/attachment cache. Catalog readability means some source text was retrieved, while `textCoverageComplete` also accounts for unsupported formats, restrictions, read limits and unexpanded links. Neither field establishes that every possible syllabus location was discovered.
125
+
126
+ An audit can complete successfully as a command while reporting `complete: false`: resource-level denials, unavailable routes, or partial collections remain visible. A successful collection read establishes coverage of that endpoint, not of every activity the website or an external tool may contain.
127
+
128
+ Improved semantic retrieval, additional model providers, private course-content provider integrations and unattended Windows startup remain later milestones.
129
+
130
+ Do not commit credentials, session cookies, access tokens, downloaded coursework, grades, or classmates' personal information. Discovery evidence should contain access outcomes and field names rather than private payloads.
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
- {
2
- "name": "@ivanbard/dd2l",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@ivanbard/dd2l",
3
+ "version": "0.1.0",
4
+ "description": "A local, read-only student companion for Queen's onQ: sync coursework, search sources, and optionally connect ChatGPT.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ivanbard/dd2l.git"
9
+ },
10
+ "homepage": "https://github.com/ivanbard/dd2l#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/ivanbard/dd2l/issues"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public",
16
+ "registry": "https://registry.npmjs.org/"
17
+ },
18
+ "type": "module",
19
+ "bin": {
20
+ "dd2l": "bin/dd2l.mjs"
21
+ },
22
+ "files": [
23
+ "bin/",
24
+ "src/",
25
+ "ui/",
26
+ "docs/README.md",
27
+ "THIRD_PARTY_NOTICES.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=24"
31
+ },
32
+ "scripts": {
33
+ "start": "node src/cli.mjs",
34
+ "setup": "node src/cli.mjs setup",
35
+ "ui": "node src/ui-server.mjs",
36
+ "ui:check": "node scripts/check-ui.mjs",
37
+ "test": "node --test",
38
+ "package:check": "node scripts/check-package.mjs",
39
+ "release:check": "npm test && npm run ui:check && npm run package:check",
40
+ "prepublishOnly": "npm run release:check",
41
+ "login": "node src/cli.mjs login",
42
+ "audit": "node src/cli.mjs audit",
43
+ "sync": "node src/cli.mjs sync",
44
+ "status": "node src/cli.mjs status",
45
+ "soak": "node scripts/soak-sync.mjs"
46
+ },
47
+ "dependencies": {
48
+ "@napi-rs/canvas": "^1.0.13",
49
+ "fflate": "^0.8.3",
50
+ "htmlparser2": "12.0.0",
51
+ "jose": "^6.2.15",
52
+ "playwright-core": "1.58.2",
53
+ "unpdf": "2.0.0"
54
+ }
55
+ }
@@ -0,0 +1,35 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createUiServer } from './ui-server.mjs';
3
+ import { sync } from './connector.mjs';
4
+
5
+ function openBrowser(url) {
6
+ return new Promise((resolve, reject) => {
7
+ execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
8
+ `Start-Process -FilePath '${url}' -WindowStyle Hidden`], { windowsHide: true, timeout: 15_000 },
9
+ error => error ? reject(error) : resolve());
10
+ });
11
+ }
12
+
13
+ export async function runApp(connector, { onUpdate, server = createUiServer(),
14
+ open = openBrowser, signals = process, port = 4173, log = console.log } = {}) {
15
+ const controller = new AbortController();
16
+ const stop = () => controller.abort();
17
+ signals.on('SIGINT', stop);
18
+ signals.on('SIGTERM', stop);
19
+ try {
20
+ await new Promise((resolve, reject) => {
21
+ server.once('error', reject);
22
+ server.listen(port, '127.0.0.1', resolve);
23
+ });
24
+ const url = `http://127.0.0.1:${server.address().port}`;
25
+ log(`dd2l: ${url}\nKeep this terminal open. Ctrl+C stops the app and sync.`);
26
+ log('Collecting coursework now; the first view may be empty. Collection is incremental, up to five courses per pass, every 15 minutes.');
27
+ try { await open(url); }
28
+ catch { log(`Could not open your browser. Open ${url} manually.`); }
29
+ await sync(connector, { all: true }, { signal: controller.signal, onUpdate });
30
+ } finally {
31
+ signals.off('SIGINT', stop);
32
+ signals.off('SIGTERM', stop);
33
+ await new Promise(resolve => server.close(resolve));
34
+ }
35
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,120 @@
1
+ import { mkdir, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { BrightspaceClient } from './client.mjs';
6
+ import { Session, login, atomicWrite } from './session.mjs';
7
+ import { Store, lock } from './store.mjs';
8
+ import { Connector, sync } from './connector.mjs';
9
+ import { runApp } from './app-runner.mjs';
10
+
11
+ const help = `dd2l — direct, read-only Queen's onQ connector (Windows, Node 24+)
12
+
13
+ dd2l setup Guided school sign-in, then open the app and sync
14
+ dd2l start Open the app and sync with your saved school session
15
+ dd2l login Open Chrome for school sign-in and MFA
16
+ dd2l courses List your course IDs and names
17
+ dd2l audit [--course ID] Audit one course (defaults to an accessible active course)
18
+ dd2l audit --all Advance the all-course pass (five courses per run)
19
+ dd2l syllabus [--course ID] Retrieve syllabus/course-information sources and text
20
+ dd2l sync [--course ID] Repeat that audit every 15 minutes until Ctrl+C
21
+ dd2l sync --all Rotate through all accessible courses in bounded passes
22
+ dd2l status Show coverage and last successful reads
23
+ dd2l logout Delete dd2l's session and local inventory
24
+
25
+ Without a global install: npx dd2l <command>.
26
+ From source: npm start -- <command>, or npm run setup.
27
+
28
+ audit/sync: --from ISO --to ISO Calendar window (default: past 30 / next 120 days)
29
+ syllabus: --course ID Choose the course without changing the sync selection
30
+
31
+ Data: %LOCALAPPDATA%\\dd2l (session protected for this Windows user; inventory is private local data).
32
+ ChatGPT is optional in the UI; connector commands make no external model calls.`;
33
+
34
+ async function main() {
35
+ const { positionals, values } = parseArgs({ allowPositionals: true, options: {
36
+ course: { type: 'string' }, from: { type: 'string' }, to: { type: 'string' }, all: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
37
+ } });
38
+ const command = positionals[0];
39
+ if (values.help || !command) { console.log(help); return; }
40
+ if (positionals.length > 1 || !['setup', 'start', 'login', 'courses', 'audit', 'syllabus', 'sync', 'status', 'logout'].includes(command)) throw new Error('Unknown command. Run dd2l --help.');
41
+ if (!['audit', 'sync', 'syllabus'].includes(command) && Object.keys(values).length) throw new Error('Date/course options apply only to audit, syllabus and sync.');
42
+ if (command === 'syllabus' && (values.from || values.to)) throw new Error('Calendar date options apply only to audit and sync.');
43
+ if (values.all && (!['audit', 'sync'].includes(command) || values.course)) throw new Error('--all applies to audit/sync and cannot be combined with --course.');
44
+ if (process.platform !== 'win32' || !process.env.LOCALAPPDATA) throw new Error('This first connector requires Windows with LOCALAPPDATA.');
45
+ const directory = join(process.env.LOCALAPPDATA, 'dd2l');
46
+ if (command === 'setup') {
47
+ console.log(`Welcome to dd2l. Requires Google Chrome and a Queen's onQ account.
48
+ School sign-in and MFA happen in a separate Chrome window; no school password is saved.
49
+ Session credentials are encrypted for your Windows account. Coursework and grades
50
+ are cached unencrypted in ${directory}. The connector is read-only.
51
+ ChatGPT is optional: connect it later in Settings. Local search stays on this computer.`);
52
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error('Run dd2l setup in an interactive terminal.');
53
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
54
+ try {
55
+ const answer = await prompt.question('Connect onQ and start collecting coursework? [y/N] ');
56
+ if (!/^y(?:es)?$/i.test(answer.trim())) { console.log('Setup cancelled. No school connection was made.'); return; }
57
+ } finally { prompt.close(); }
58
+ }
59
+ await mkdir(directory, { recursive: true });
60
+ // Status is read-only and can inspect a running worker without taking its lock.
61
+ const release = command === 'status' ? async () => {} : await lock(directory);
62
+ let store;
63
+ try {
64
+ store = new Store(directory);
65
+ const session = new Session(directory);
66
+ const client = new BrightspaceClient(session);
67
+ const connector = new Connector(client, store);
68
+ if (command === 'login' || command === 'setup') {
69
+ await login(session, client, { expectedAccount: store.getMeta('account') });
70
+ store.meta('state', { state: 'ready', updatedAt: new Date().toISOString() });
71
+ }
72
+ if (command === 'start' || command === 'setup') {
73
+ await runApp(connector, { onUpdate: async summary => {
74
+ await atomicWrite(join(directory, 'summary.json'), JSON.stringify(summary, null, 2));
75
+ console.log(summary.state === 'retry_wait' ? 'Collection delayed; will retry automatically.' : 'Coursework pass saved. Reopen Courses or search to see updates.');
76
+ } });
77
+ } else if (command === 'courses') {
78
+ const { enrollments } = await connector.courses();
79
+ console.log(JSON.stringify({ complete: enrollments.complete, courses: enrollments.data.map(item => ({
80
+ id: item.OrgUnit?.Id, name: item.OrgUnit?.Name, active: item.Access?.IsActive, canAccess: item.Access?.CanAccess,
81
+ })) }, null, 2));
82
+ } else if (command === 'syllabus') {
83
+ const summary = await connector.syllabus(values);
84
+ await atomicWrite(join(directory, 'syllabus-summary.json'), JSON.stringify(summary, null, 2));
85
+ console.log(JSON.stringify(summary, null, 2));
86
+ } else if (command === 'audit' || command === 'sync') {
87
+ const report = async summary => {
88
+ await atomicWrite(join(directory, 'summary.json'), JSON.stringify(summary, null, 2));
89
+ console.log(JSON.stringify(summary, null, 2));
90
+ };
91
+ if (command === 'audit') await report(await connector.audit(values));
92
+ else {
93
+ const controller = new AbortController();
94
+ const stop = () => controller.abort();
95
+ process.on('SIGINT', stop);
96
+ process.on('SIGTERM', stop);
97
+ try { await sync(connector, values, { signal: controller.signal, onUpdate: report }); }
98
+ finally { process.off('SIGINT', stop); process.off('SIGTERM', stop); }
99
+ }
100
+ } else if (command === 'status') console.log(JSON.stringify(store.status(), null, 2));
101
+ else if (command === 'logout') {
102
+ await rm(session.path, { force: true });
103
+ await rm(join(directory, 'summary.json'), { force: true });
104
+ await rm(join(directory, 'syllabus-summary.json'), { force: true });
105
+ store.clear();
106
+ store.close();
107
+ store = null;
108
+ for (const name of ['inventory.sqlite', 'inventory.sqlite-wal', 'inventory.sqlite-shm']) {
109
+ await rm(join(directory, name), { force: true });
110
+ }
111
+ console.log('dd2l session and inventory removed. School sign-in on other apps is unchanged.');
112
+ }
113
+ } catch (error) {
114
+ if (store && error.code === 'needs_login') store.meta('state', { state: 'needs_login', updatedAt: new Date().toISOString() });
115
+ if (error.code === 'needs_login' && ['start', 'setup'].includes(command)) error.message += ' Run dd2l setup to sign in and restart.';
116
+ throw error;
117
+ } finally { store?.close(); await release(); }
118
+ }
119
+
120
+ main().catch(error => { console.error(`${error.code || 'error'}: ${error.message}`); process.exitCode = 1; });