onenote-cli 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/.env.example +10 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/SKILL.md +77 -0
- package/bun.lock +92 -0
- package/docs/azure-app-registration.md +80 -0
- package/docs/development-notes.md +84 -0
- package/docs/graph-api-endpoints.md +95 -0
- package/docs/local-search-architecture.md +132 -0
- package/docs/onen0te-cli-analysis.md +130 -0
- package/docs/setup.md +141 -0
- package/docs/zhihu-intro.md +92 -0
- package/package.json +22 -0
- package/src/auth.ts +179 -0
- package/src/cache.ts +763 -0
- package/src/graph.ts +264 -0
- package/src/index.ts +408 -0
- package/tsconfig.json +14 -0
package/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Azure AD App Registration
|
|
2
|
+
# Register at: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps
|
|
3
|
+
# 1. New registration -> Name: onenote-cli
|
|
4
|
+
# 2. Supported account types: Personal Microsoft accounts + Organizational
|
|
5
|
+
# 3. Platform: Mobile and desktop applications
|
|
6
|
+
# 4. Copy Application (client) ID here:
|
|
7
|
+
ONENOTE_CLIENT_ID=YOUR_CLIENT_ID
|
|
8
|
+
|
|
9
|
+
# Authority URL (default: common for multi-tenant + personal accounts)
|
|
10
|
+
ONENOTE_AUTHORITY=https://login.microsoftonline.com/common
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sno
|
|
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
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# onenote-cli
|
|
2
|
+
|
|
3
|
+
**Make your OneNote notebooks survive in the age of AI.**
|
|
4
|
+
|
|
5
|
+
A CLI that lets AI agents (and humans) search, read, and operate your OneNote — with full-text page-level search and deep links that open directly to the matching page.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Page-level search** — search inside all your OneNote pages, get results with URLs that open directly to the matching page in OneNote Online
|
|
10
|
+
- **Notebooks / Sections / Pages** — list, get, create, delete via Graph API
|
|
11
|
+
- **5,000-item workaround** — when Graph OneNote API is blocked by the SharePoint document library limit, automatically falls back to OneDrive file API + local binary parsing
|
|
12
|
+
- **Local cache** — downloads `.one` files, extracts text (UTF-8 + UTF-16LE), builds a searchable index
|
|
13
|
+
- **Official OneNote URLs** — resolves page GUIDs via `GET /me/onenote/sections/0-{guid}/pages` to get URLs that bypass OneNote Online's session caching
|
|
14
|
+
- **Device code flow auth** — works in SSH / headless / terminal environments
|
|
15
|
+
- **Cross-directory** — `.env.local` and cache are loaded from the package directory, so `onenote` works from any working directory
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
### As an AI Agent Skill
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Claude Code / OpenClaw / Codex / Cursor / any SKILL.md-compatible agent
|
|
23
|
+
npx skills add snomiao/onenote-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Manual
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/snomiao/onenote-cli.git
|
|
30
|
+
cd onenote-cli
|
|
31
|
+
bun install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
1. Register an Azure AD app at [entra.microsoft.com](https://entra.microsoft.com) (see [docs/setup.md](docs/setup.md) for full walkthrough)
|
|
37
|
+
2. Copy `.env.example` to `.env.local` and set your Client ID:
|
|
38
|
+
```bash
|
|
39
|
+
cp .env.example .env.local
|
|
40
|
+
# Edit .env.local with your Application (client) ID
|
|
41
|
+
```
|
|
42
|
+
3. Login:
|
|
43
|
+
```bash
|
|
44
|
+
bun run src/index.ts auth login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Auth
|
|
51
|
+
onenote auth login # Login via device code flow
|
|
52
|
+
onenote auth whoami # Show current user
|
|
53
|
+
onenote auth setup # Print setup instructions
|
|
54
|
+
onenote auth logout # Clear tokens
|
|
55
|
+
|
|
56
|
+
# Notebooks
|
|
57
|
+
onenote notebooks list
|
|
58
|
+
onenote notebooks get <id>
|
|
59
|
+
onenote notebooks create <name>
|
|
60
|
+
|
|
61
|
+
# Sections (falls back to OneDrive if Graph API is blocked)
|
|
62
|
+
onenote sections list -n <notebook-id>
|
|
63
|
+
onenote sections create -n <notebook-id> --name <name>
|
|
64
|
+
|
|
65
|
+
# Pages
|
|
66
|
+
onenote pages list -s <section-id>
|
|
67
|
+
onenote pages get <id>
|
|
68
|
+
onenote pages content <id>
|
|
69
|
+
onenote pages create -s <section-id> -t <title> -b "<html>"
|
|
70
|
+
onenote pages delete <id>
|
|
71
|
+
|
|
72
|
+
# Search
|
|
73
|
+
onenote sync # Download and cache all sections
|
|
74
|
+
onenote search <query> # Full-text page-level search (local)
|
|
75
|
+
onenote search <query> -o # Online section-level search (Graph API)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Search Example
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
$ onenote search 年金番号
|
|
82
|
+
|
|
83
|
+
# (20250303) nenkin bangou screenshot @mynaportal
|
|
84
|
+
Section: MyNumberCard-マイナンバカード | Notebook: sno@ja
|
|
85
|
+
基础**年金番号** ...
|
|
86
|
+
https://snomiao-my.sharepoint.com/.../Notebooks/sno@ja?wd=target(...)
|
|
87
|
+
|
|
88
|
+
2 page-level results found.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Clicking the URL opens OneNote Online directly on the matching page.
|
|
92
|
+
|
|
93
|
+
## How Search Works
|
|
94
|
+
|
|
95
|
+
1. `onenote sync` downloads all `.one` section files from OneDrive
|
|
96
|
+
2. Extracts page GUIDs from the MS-ONESTORE binary format
|
|
97
|
+
3. Fetches official page URLs via OneNote Graph API (`/me/onenote/sections/0-{guid}/pages`)
|
|
98
|
+
4. On search, scans the binary for matches (UTF-8 and UTF-16LE), attributes each match to the correct page via context-based anchor lookup
|
|
99
|
+
5. Returns results with official OneNote URLs that navigate directly to the page
|
|
100
|
+
|
|
101
|
+
See [docs/local-search-architecture.md](docs/local-search-architecture.md) for the full technical design.
|
|
102
|
+
|
|
103
|
+
## API Permissions Required
|
|
104
|
+
|
|
105
|
+
| Permission | Type | Purpose |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `Notes.Read` | Delegated | Read notebooks |
|
|
108
|
+
| `Notes.ReadWrite` | Delegated | Create/modify pages |
|
|
109
|
+
| `Notes.ReadWrite.All` | Delegated | Access all notebooks |
|
|
110
|
+
| `Files.Read` | Delegated | Download .one files from OneDrive |
|
|
111
|
+
| `Files.Read.All` | Delegated | Access all accessible files |
|
|
112
|
+
| `Sites.Read.All` | Delegated | Search via SharePoint listItem API |
|
|
113
|
+
|
|
114
|
+
## File Structure
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
src/
|
|
118
|
+
index.ts CLI entry point (yargs)
|
|
119
|
+
auth.ts MSAL device code flow + .env.local auto-loader
|
|
120
|
+
graph.ts Microsoft Graph API client (OneNote + OneDrive)
|
|
121
|
+
cache.ts Local cache, .one binary parser, page GUID extraction
|
|
122
|
+
docs/
|
|
123
|
+
setup.md Azure AD registration walkthrough
|
|
124
|
+
local-search-architecture.md Technical design of local search
|
|
125
|
+
graph-api-endpoints.md OneNote Graph API reference
|
|
126
|
+
development-notes.md Lessons learned
|
|
127
|
+
onen0te-cli-analysis.md Competitor UX analysis
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
| Source | Location | Priority |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `.env.local` | Package root (auto-loaded) | Highest |
|
|
135
|
+
| `~/.onenote-cli/config.json` | Home directory | Fallback |
|
|
136
|
+
| `ONENOTE_CLIENT_ID` env var | Shell environment | Overrides all |
|
|
137
|
+
|
|
138
|
+
Cache location: `<package>/.onenote/cache/` (override with `ONENOTE_CACHE_DIR`)
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onenote-cli
|
|
3
|
+
description: Make your OneNote notebooks survive in the age of AI. Search and operate OneNote from CLI via Microsoft Graph API. Use when the user wants to search OneNote notes, list notebooks, read pages, or manage OneNote content.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# onenote-cli
|
|
7
|
+
|
|
8
|
+
CLI for Microsoft OneNote via Microsoft Graph. Built with Bun + yargs + MSAL.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
onenote auth login # Device code flow login
|
|
14
|
+
onenote auth logout # Clear cached tokens
|
|
15
|
+
onenote auth whoami # Show current user
|
|
16
|
+
onenote auth setup # Show OAuth setup instructions
|
|
17
|
+
|
|
18
|
+
onenote notebooks list # List notebooks
|
|
19
|
+
onenote notebooks get <id> # Get notebook by ID
|
|
20
|
+
onenote notebooks create <name> # Create notebook
|
|
21
|
+
|
|
22
|
+
onenote sections list -n <nb-id> # List sections in a notebook
|
|
23
|
+
onenote sections create -n <nb> --name <name>
|
|
24
|
+
|
|
25
|
+
onenote pages list -s <sec-id> # List pages in a section
|
|
26
|
+
onenote pages content <id> # Get page HTML content
|
|
27
|
+
onenote pages create -s <sec> -t <title> -b <html>
|
|
28
|
+
onenote pages delete <id>
|
|
29
|
+
|
|
30
|
+
onenote sync # Build local cache (.one binary + page index)
|
|
31
|
+
onenote search <query> # Full-text search across cached pages
|
|
32
|
+
onenote search <query> --online # Online section-level search via Graph
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
See [docs/setup.md](docs/setup.md) for full Azure AD app registration walkthrough.
|
|
38
|
+
|
|
39
|
+
Quick setup:
|
|
40
|
+
1. Register app at https://entra.microsoft.com → App registrations → New
|
|
41
|
+
2. Supported account types: "Accounts in any organizational directory and personal Microsoft accounts"
|
|
42
|
+
3. Add platform: Mobile and desktop applications, redirect URI `https://login.microsoftonline.com/common/oauth2/nativeclient`
|
|
43
|
+
4. Settings → Allow public client flows: **Yes**
|
|
44
|
+
5. API permissions → Microsoft Graph → Delegated: `Notes.Read`, `Notes.ReadWrite`, `Notes.ReadWrite.All`, `Files.Read`, `Files.Read.All`, `Sites.Read.All`
|
|
45
|
+
6. Copy Application (client) ID into `.env.local`:
|
|
46
|
+
```env
|
|
47
|
+
ONENOTE_CLIENT_ID=your-client-id-here
|
|
48
|
+
ONENOTE_AUTHORITY=https://login.microsoftonline.com/common
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How Search Works
|
|
52
|
+
|
|
53
|
+
`onenote search <query>` returns page-level results with **official OneNote URLs** that navigate directly to the matching page (bypassing OneNote Online's session caching).
|
|
54
|
+
|
|
55
|
+
The search:
|
|
56
|
+
1. Downloads `.one` files via OneDrive (cached locally in `<package>/.onenote/cache/`)
|
|
57
|
+
2. Extracts page GUIDs from the binary
|
|
58
|
+
3. Resolves official `oneNoteWebUrl` via `/me/onenote/sections/0-{guid}/pages` (works around the 5,000-item document library limit)
|
|
59
|
+
4. Searches the binary for the query (UTF-8 + UTF-16LE) and attributes matches to the correct page using context-based lookup
|
|
60
|
+
|
|
61
|
+
See [docs/local-search-architecture.md](docs/local-search-architecture.md) for details.
|
|
62
|
+
|
|
63
|
+
## File Locations
|
|
64
|
+
|
|
65
|
+
- `.env.local` — at the package root (auto-loaded via `import.meta.dir`)
|
|
66
|
+
- Token cache — `~/.onenote-cli/msal-cache.json`
|
|
67
|
+
- Config fallback — `~/.onenote-cli/config.json`
|
|
68
|
+
- Page cache — `<package>/.onenote/cache/` (override with `ONENOTE_CACHE_DIR`)
|
|
69
|
+
|
|
70
|
+
## Common Issues
|
|
71
|
+
|
|
72
|
+
| Error | Fix |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `AADSTS7000218` | Enable "Allow public client flows" in app Authentication settings |
|
|
75
|
+
| `AADSTS65001` | Admin consent required for API permissions, or accept consent during login |
|
|
76
|
+
| `Graph API 403: error 10008` | Document library has > 5,000 OneNote items; use `onenote sync` and local search instead |
|
|
77
|
+
| Cache empty | Run `onenote sync` (auto-runs on first search) |
|
package/bun.lock
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "onenote-cli",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@azure/msal-node": "^2.16.0",
|
|
9
|
+
"yargs": "^17.7.2",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/yargs": "^17.0.33",
|
|
13
|
+
"typescript": "^5.7.0",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"packages": {
|
|
18
|
+
"@azure/msal-common": ["@azure/msal-common@14.16.1", "", {}, "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w=="],
|
|
19
|
+
|
|
20
|
+
"@azure/msal-node": ["@azure/msal-node@2.16.3", "", { "dependencies": { "@azure/msal-common": "14.16.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw=="],
|
|
21
|
+
|
|
22
|
+
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
|
|
23
|
+
|
|
24
|
+
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
|
|
25
|
+
|
|
26
|
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
|
27
|
+
|
|
28
|
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
|
29
|
+
|
|
30
|
+
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
|
31
|
+
|
|
32
|
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
|
33
|
+
|
|
34
|
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
|
35
|
+
|
|
36
|
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
|
37
|
+
|
|
38
|
+
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
|
39
|
+
|
|
40
|
+
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
|
41
|
+
|
|
42
|
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
|
43
|
+
|
|
44
|
+
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
|
45
|
+
|
|
46
|
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
|
47
|
+
|
|
48
|
+
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
|
49
|
+
|
|
50
|
+
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
|
51
|
+
|
|
52
|
+
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
|
53
|
+
|
|
54
|
+
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
|
55
|
+
|
|
56
|
+
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
|
57
|
+
|
|
58
|
+
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
|
59
|
+
|
|
60
|
+
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
|
61
|
+
|
|
62
|
+
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
|
63
|
+
|
|
64
|
+
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
|
65
|
+
|
|
66
|
+
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
|
67
|
+
|
|
68
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
69
|
+
|
|
70
|
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
|
71
|
+
|
|
72
|
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
73
|
+
|
|
74
|
+
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
|
75
|
+
|
|
76
|
+
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
77
|
+
|
|
78
|
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
79
|
+
|
|
80
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
81
|
+
|
|
82
|
+
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
|
83
|
+
|
|
84
|
+
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
|
85
|
+
|
|
86
|
+
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
|
87
|
+
|
|
88
|
+
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
|
89
|
+
|
|
90
|
+
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Azure AD App Registration for OneNote CLI
|
|
2
|
+
|
|
3
|
+
This document describes how to register an Azure AD application for use with `onenote-cli`, which uses the Microsoft Graph OneNote API with delegated (device code flow) authentication.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- A Microsoft account (personal or organizational)
|
|
8
|
+
- Access to [Microsoft Entra admin center](https://entra.microsoft.com)
|
|
9
|
+
|
|
10
|
+
## Step 1: Create the App Registration
|
|
11
|
+
|
|
12
|
+
1. Go to [Microsoft Entra admin center](https://entra.microsoft.com)
|
|
13
|
+
2. Navigate to **Entra ID** > **App registrations** > **New registration**
|
|
14
|
+
3. Fill in:
|
|
15
|
+
- **Name**: `onenote-cli`
|
|
16
|
+
- **Supported account types**: "Accounts in any organizational directory and personal Microsoft accounts"
|
|
17
|
+
- **Redirect URI**: Leave blank (configured in the next step)
|
|
18
|
+
4. Click **Register**
|
|
19
|
+
5. Copy the **Application (client) ID** from the Overview page
|
|
20
|
+
|
|
21
|
+
## Step 2: Configure Authentication Platform
|
|
22
|
+
|
|
23
|
+
1. Go to **Authentication** tab in your app registration
|
|
24
|
+
2. Click **Add a platform** > **Mobile and desktop applications**
|
|
25
|
+
3. Check these redirect URIs:
|
|
26
|
+
- `https://login.microsoftonline.com/common/oauth2/nativeclient`
|
|
27
|
+
- `msal{client-id}://auth` (MSAL-specific URI)
|
|
28
|
+
4. Click **Configure**
|
|
29
|
+
|
|
30
|
+
## Step 3: Enable Public Client Flows
|
|
31
|
+
|
|
32
|
+
1. In the **Authentication** tab, go to the **Settings** sub-tab
|
|
33
|
+
2. Find **Allow public client flows** and set it to **Enabled**
|
|
34
|
+
3. Click **Save**
|
|
35
|
+
|
|
36
|
+
This is required for device code flow authentication. Without it, you will get error `AADSTS7000218`.
|
|
37
|
+
|
|
38
|
+
## Step 4: Add API Permissions
|
|
39
|
+
|
|
40
|
+
1. Go to **API permissions** > **Add a permission**
|
|
41
|
+
2. Select **Microsoft Graph** > **Delegated permissions**
|
|
42
|
+
3. Search for "Notes" and expand the **Notes** group
|
|
43
|
+
4. Select:
|
|
44
|
+
- `Notes.Read` — Read user OneNote notebooks
|
|
45
|
+
- `Notes.ReadWrite` — Read and write user OneNote notebooks
|
|
46
|
+
- `Notes.ReadWrite.All` — Read and write all notebooks the user can access
|
|
47
|
+
5. Click **Add permissions**
|
|
48
|
+
|
|
49
|
+
The default `User.Read` permission is already included.
|
|
50
|
+
|
|
51
|
+
## Step 5: Configure the CLI
|
|
52
|
+
|
|
53
|
+
Set the client ID via environment variable or config file:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Option A: .env.local
|
|
57
|
+
ONENOTE_CLIENT_ID=your-client-id-here
|
|
58
|
+
ONENOTE_AUTHORITY=https://login.microsoftonline.com/common
|
|
59
|
+
|
|
60
|
+
# Option B: ~/.onenote-cli/config.json
|
|
61
|
+
{
|
|
62
|
+
"clientId": "your-client-id-here",
|
|
63
|
+
"authority": "https://login.microsoftonline.com/common"
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Environment variables take priority over the config file.
|
|
68
|
+
|
|
69
|
+
## Step 6: Login and Verify
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bun run src/index.ts login
|
|
73
|
+
bun run src/index.ts notebooks list
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- The Microsoft Graph OneNote API **does not support app-only authentication** as of March 2025. Only delegated (user) authentication is supported.
|
|
79
|
+
- The authority URL `https://login.microsoftonline.com/common` supports both organizational and personal Microsoft accounts.
|
|
80
|
+
- Tokens are cached at `~/.onenote-cli/msal-cache.json` and automatically refreshed.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Development Notes
|
|
2
|
+
|
|
3
|
+
Lessons learned and implementation notes from building `onenote-cli`.
|
|
4
|
+
|
|
5
|
+
## Architecture Decisions
|
|
6
|
+
|
|
7
|
+
### Bun + Yargs
|
|
8
|
+
|
|
9
|
+
- Bun is used as the runtime, allowing direct execution of TypeScript without a build step
|
|
10
|
+
- Yargs provides a natural command/subcommand CLI structure
|
|
11
|
+
- The `bin` field in package.json points directly to `src/index.ts` — no compilation needed
|
|
12
|
+
|
|
13
|
+
### MSAL Node for Authentication
|
|
14
|
+
|
|
15
|
+
- `@azure/msal-node` provides the `PublicClientApplication` class for device code flow
|
|
16
|
+
- Token caching is implemented via MSAL's `cachePlugin` interface, persisting to `~/.onenote-cli/msal-cache.json`
|
|
17
|
+
- Silent token acquisition is attempted first (using cached refresh tokens), falling back to device code flow
|
|
18
|
+
|
|
19
|
+
### Configuration Priority
|
|
20
|
+
|
|
21
|
+
Configuration is resolved in this order:
|
|
22
|
+
1. Environment variables (`ONENOTE_CLIENT_ID`, `ONENOTE_AUTHORITY`)
|
|
23
|
+
2. Config file (`~/.onenote-cli/config.json`)
|
|
24
|
+
3. Default values (which prompt the user to configure)
|
|
25
|
+
|
|
26
|
+
This allows both `.env.local` for development and the config file for installed usage.
|
|
27
|
+
|
|
28
|
+
## Implementation Details
|
|
29
|
+
|
|
30
|
+
### Device Code Flow
|
|
31
|
+
|
|
32
|
+
The device code flow is ideal for CLI applications because:
|
|
33
|
+
- No web server or redirect handler is needed
|
|
34
|
+
- Works in headless/SSH environments
|
|
35
|
+
- User authenticates in any browser, even on a different device
|
|
36
|
+
|
|
37
|
+
Flow:
|
|
38
|
+
1. CLI requests a device code from Azure AD
|
|
39
|
+
2. Azure AD returns a code and a URL
|
|
40
|
+
3. User opens the URL in a browser and enters the code
|
|
41
|
+
4. User authenticates and consents to permissions
|
|
42
|
+
5. CLI polls Azure AD and receives tokens once authentication completes
|
|
43
|
+
|
|
44
|
+
### Page Creation
|
|
45
|
+
|
|
46
|
+
OneNote pages are created by POSTing raw HTML (Content-Type: `text/html`), not JSON. The HTML must include a `<title>` in the `<head>` and content in the `<body>`. This is different from most Graph API endpoints which use JSON.
|
|
47
|
+
|
|
48
|
+
### App-Only Auth Not Supported
|
|
49
|
+
|
|
50
|
+
As of March 2025, Microsoft Graph OneNote API no longer supports app-only (client credentials) authentication. All access must use delegated permissions with a signed-in user. This means the CLI must always go through the device code flow for initial authentication.
|
|
51
|
+
|
|
52
|
+
## Azure Portal Navigation Tips
|
|
53
|
+
|
|
54
|
+
### Entra Admin Center vs Azure Portal
|
|
55
|
+
|
|
56
|
+
- App registrations are managed in the [Microsoft Entra admin center](https://entra.microsoft.com), not the classic Azure portal
|
|
57
|
+
- The path is: **Entra ID** > **App registrations**
|
|
58
|
+
- Direct URL: `https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade`
|
|
59
|
+
|
|
60
|
+
### Common Setup Mistakes
|
|
61
|
+
|
|
62
|
+
1. **Forgetting to enable public client flows** — Device code flow requires "Allow public client flows" to be set to Yes in Authentication > Settings. Without this, authentication fails with `AADSTS7000218`.
|
|
63
|
+
|
|
64
|
+
2. **Wrong account type selection** — For a CLI that should work with both personal and work accounts, select "Accounts in any organizational directory and personal Microsoft accounts".
|
|
65
|
+
|
|
66
|
+
3. **Missing redirect URI** — While device code flow doesn't strictly require a redirect URI, adding `https://login.microsoftonline.com/common/oauth2/nativeclient` ensures compatibility with the MSAL library.
|
|
67
|
+
|
|
68
|
+
4. **Not adding API permissions** — The default `User.Read` permission is not enough. You must explicitly add `Notes.Read` / `Notes.ReadWrite` delegated permissions under Microsoft Graph.
|
|
69
|
+
|
|
70
|
+
## Troubleshooting
|
|
71
|
+
|
|
72
|
+
### Token Refresh Failures
|
|
73
|
+
|
|
74
|
+
If silent token acquisition fails after a long period, the refresh token may have expired. Run `onenote login` again to re-authenticate.
|
|
75
|
+
|
|
76
|
+
### Graph API 403 Errors
|
|
77
|
+
|
|
78
|
+
Usually means the required permission was not consented. Check that:
|
|
79
|
+
- The permission is added in the app registration
|
|
80
|
+
- The user consented during login (or admin consent was granted)
|
|
81
|
+
|
|
82
|
+
### Graph API 404 Errors
|
|
83
|
+
|
|
84
|
+
OneNote resources use opaque IDs. If a notebook/section/page was deleted or moved, its ID becomes invalid.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Microsoft Graph OneNote API Endpoints
|
|
2
|
+
|
|
3
|
+
Reference for the OneNote REST API endpoints used by `onenote-cli`.
|
|
4
|
+
|
|
5
|
+
## Base URL
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
https://graph.microsoft.com/v1.0/me/onenote/
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Notebooks
|
|
12
|
+
|
|
13
|
+
| Method | Endpoint | Description |
|
|
14
|
+
|--------|----------|-------------|
|
|
15
|
+
| GET | `/me/onenote/notebooks` | List all notebooks |
|
|
16
|
+
| GET | `/me/onenote/notebooks/{id}` | Get a notebook by ID |
|
|
17
|
+
| POST | `/me/onenote/notebooks` | Create a notebook (`{ "displayName": "..." }`) |
|
|
18
|
+
|
|
19
|
+
## Sections
|
|
20
|
+
|
|
21
|
+
| Method | Endpoint | Description |
|
|
22
|
+
|--------|----------|-------------|
|
|
23
|
+
| GET | `/me/onenote/sections` | List all sections |
|
|
24
|
+
| GET | `/me/onenote/notebooks/{id}/sections` | List sections in a notebook |
|
|
25
|
+
| GET | `/me/onenote/sections/{id}` | Get a section by ID |
|
|
26
|
+
| POST | `/me/onenote/notebooks/{id}/sections` | Create a section (`{ "displayName": "..." }`) |
|
|
27
|
+
|
|
28
|
+
## Section Groups
|
|
29
|
+
|
|
30
|
+
| Method | Endpoint | Description |
|
|
31
|
+
|--------|----------|-------------|
|
|
32
|
+
| GET | `/me/onenote/sectionGroups` | List all section groups |
|
|
33
|
+
| GET | `/me/onenote/notebooks/{id}/sectionGroups` | List section groups in a notebook |
|
|
34
|
+
|
|
35
|
+
## Pages
|
|
36
|
+
|
|
37
|
+
| Method | Endpoint | Description |
|
|
38
|
+
|--------|----------|-------------|
|
|
39
|
+
| GET | `/me/onenote/pages` | List all pages |
|
|
40
|
+
| GET | `/me/onenote/sections/{id}/pages` | List pages in a section |
|
|
41
|
+
| GET | `/me/onenote/pages/{id}` | Get page metadata |
|
|
42
|
+
| GET | `/me/onenote/pages/{id}/content` | Get page HTML content |
|
|
43
|
+
| POST | `/me/onenote/sections/{id}/pages` | Create a page (Content-Type: text/html) |
|
|
44
|
+
| DELETE | `/me/onenote/pages/{id}` | Delete a page |
|
|
45
|
+
|
|
46
|
+
## Search
|
|
47
|
+
|
|
48
|
+
| Method | Endpoint | Description |
|
|
49
|
+
|--------|----------|-------------|
|
|
50
|
+
| GET | `/me/onenote/pages?$search={query}` | Search pages by content |
|
|
51
|
+
|
|
52
|
+
## Creating Pages
|
|
53
|
+
|
|
54
|
+
Pages are created by POSTing HTML to a section:
|
|
55
|
+
|
|
56
|
+
```http
|
|
57
|
+
POST /me/onenote/sections/{section-id}/pages
|
|
58
|
+
Content-Type: text/html
|
|
59
|
+
|
|
60
|
+
<!DOCTYPE html>
|
|
61
|
+
<html>
|
|
62
|
+
<head>
|
|
63
|
+
<title>Page Title</title>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<p>Page content here</p>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Required Permissions (Delegated)
|
|
72
|
+
|
|
73
|
+
| Permission | Description |
|
|
74
|
+
|------------|-------------|
|
|
75
|
+
| `Notes.Read` | Read notebooks |
|
|
76
|
+
| `Notes.ReadWrite` | Read and write notebooks |
|
|
77
|
+
| `Notes.Read.All` | Read all accessible notebooks |
|
|
78
|
+
| `Notes.ReadWrite.All` | Read and write all accessible notebooks |
|
|
79
|
+
|
|
80
|
+
## Additional Scopes
|
|
81
|
+
|
|
82
|
+
Notebooks can also be accessed via group or SharePoint site contexts:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
/groups/{id}/onenote/{notebooks|sections|pages}
|
|
86
|
+
/sites/{id}/onenote/{notebooks|sections|pages}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
These require corresponding group/site read permissions.
|
|
90
|
+
|
|
91
|
+
## References
|
|
92
|
+
|
|
93
|
+
- [OneNote API overview](https://learn.microsoft.com/en-us/graph/integrate-with-onenote)
|
|
94
|
+
- [OneNote REST API reference](https://learn.microsoft.com/en-us/graph/api/resources/onenote-api-overview)
|
|
95
|
+
- [Graph Explorer](https://developer.microsoft.com/graph/graph-explorer)
|