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.
@@ -0,0 +1,132 @@
1
+ # Local Page-Level Search Architecture
2
+
3
+ ## Problem
4
+
5
+ The Microsoft Graph OneNote API has a hard limit: when a OneDrive for Business document library contains more than 5,000 OneNote items, the API returns error 10008 and blocks all section/page listing endpoints. This affects accounts with many notebooks.
6
+
7
+ The Graph Search API (`/search/query`) only returns section-level (.one file) results — it cannot identify individual pages within a section.
8
+
9
+ ## Solution: Local Cache + Binary Text Extraction
10
+
11
+ ### How it works
12
+
13
+ 1. **Sync** (`onenote sync`): Downloads all `.one` section files from OneDrive to a local cache directory (`.onenote/cache/`)
14
+ 2. **Extract**: Parses the MS-ONESTORE binary format to extract readable text blocks, then segments them into pages based on binary gaps
15
+ 3. **Search** (`onenote search <query>`): Searches the local cache for matching text at the page level
16
+
17
+ ### Cache Structure
18
+
19
+ ```
20
+ .onenote/
21
+ cache/
22
+ {Notebook Name}/
23
+ {Section Name}.json # Extracted pages with text
24
+ ```
25
+
26
+ Each `.json` file contains:
27
+ ```json
28
+ {
29
+ "section": "Section Name",
30
+ "notebook": "Notebook Name",
31
+ "webUrl": "https://...Doc.aspx?sourcedoc={GUID}&...",
32
+ "pages": [
33
+ { "title": "Page Title", "body": "Full text content..." },
34
+ ...
35
+ ],
36
+ "cachedAt": "2025-01-01T00:00:00.000Z"
37
+ }
38
+ ```
39
+
40
+ ### Binary-Based Position Search
41
+
42
+ For accurate page attribution, the cache stores the original `.one` binary alongside extracted metadata. Search works by:
43
+
44
+ 1. Searching the binary directly for the query string (both UTF-8 and UTF-16LE encodings)
45
+ 2. For each match position, finding the nearest preceding page GUID anchor
46
+ 3. Returning that page as the result
47
+
48
+ This bypasses the imperfect text-block-to-page heuristics and gives accurate page attribution because the binary positions are ground truth — the matched text is physically located near the page anchor it belongs to.
49
+
50
+ ### .one Binary Text Extraction
51
+
52
+ The MS-ONESTORE format (`.one` files) stores text as UTF-8 and UTF-16LE encoded strings interspersed with binary data. The extraction algorithm:
53
+
54
+ 1. Read the file twice — once as UTF-8 (ASCII content) and once as UTF-16LE (CJK and other Unicode)
55
+ 2. Find contiguous runs of printable characters
56
+ 3. Filter runs that don't contain enough "common" characters (ratio threshold)
57
+ 4. Use the page GUID anchors (see below) to assign each text block to the correct page
58
+
59
+ ### Page GUID Extraction
60
+
61
+ Page-level URLs require knowing each page's UUID. We extract these from the binary using a pattern observed in MS-ONESTORE files:
62
+
63
+ ```
64
+ [UTF-16LE title text] 00 00 [10 00 00 00] [16-byte page GUID]
65
+ ```
66
+
67
+ The `10 00 00 00` is a uint32-LE size marker meaning "16 bytes follow", and the next 16 bytes are a UUIDv4 (version=4, variant=8-B). The text immediately preceding (UTF-16LE encoded) is the page title.
68
+
69
+ For each page GUID found, all text blocks at later offsets up to the next anchor are assigned to that page. This anchor-based grouping correctly maps content (including text in non-Latin scripts) to the right page even when the file structure is fragmented.
70
+
71
+ ### Page-Level URL Format
72
+
73
+ OneNote Online supports deep linking to specific pages via:
74
+
75
+ ```
76
+ {sectionUrl}&wd=target({pageTitle}|{pageGuid}/)
77
+ ```
78
+
79
+ Where:
80
+ - `{sectionUrl}` is the SharePoint Doc.aspx URL with the section's `sourcedoc` GUID
81
+ - `{pageTitle}` is the page title with `)` and `|` characters escaped as `\)` and `\|`
82
+ - `{pageGuid}` is the page's UUIDv4
83
+ - Trailing `/` is required
84
+
85
+ The full `wd=` parameter is then URL-encoded with strict encoding (parens encoded as `%28`/`%29`).
86
+
87
+ ### Search Output
88
+
89
+ Each result shows:
90
+ - **Page title** — extracted from the page anchor
91
+ - **Section and notebook** — which section/notebook contains the match
92
+ - **Context snippet** — text surrounding the match with keyword highlighted in `**bold**`
93
+ - **URL** — page-level OneNote Online deep link
94
+
95
+ ### Page-Level URL Resolution (Official URLs)
96
+
97
+ We use the OneNote Graph API endpoint `GET /me/onenote/sections/0-{guid}/pages` to fetch the official `links.oneNoteWebUrl.href` for each page. This endpoint works even when the 5,000-item document library limit blocks `/me/onenote/pages` and `/me/onenote/sections` listing — because the `0-{guid}` ID prefix targets the section directly via its sourcedoc GUID (extracted from the OneDrive driveItem webUrl).
98
+
99
+ Official OneNote URLs use the format:
100
+ ```
101
+ {driveRootPath}/{notebook}?wd=target({sectionFile}|{sectionGroupGuid}/{pageTitle}|{pageGuid}/)
102
+ ```
103
+
104
+ Unlike the simpler `Doc.aspx?sourcedoc=...&wd=target(...)` format, this URL **bypasses OneNote Online's session caching** and navigates directly to the specified page on first load. Verified working via browser automation testing.
105
+
106
+ The page navigation GUID (used in `wd=target`) is extracted from the `oneNoteWebUrl` URL itself (the last UUID in the URL), since the API's page `id` field uses a different identifier format (`1-{32hexchars}!{counter}-{sectionGuid}`) that doesn't match the navigation GUID.
107
+
108
+ ### Limitations
109
+
110
+ - **Page coverage**: Our binary parser detects ~50% of pages in some sections (those whose GUID-title binary pattern matches our heuristic). The other pages exist in the cache but without a precise GUID, so search results for them fall back to section-level URLs.
111
+ - **Cache freshness**: Cache is valid for 1 hour by default. Run `onenote sync` to refresh.
112
+ - **Binary parsing heuristic**: The UTF-8 text extraction may miss some content or include some binary noise. Page boundary detection is approximate.
113
+ - **Large sections**: Sections with thousands of pages (e.g., 5000+ extracted "pages") may have over-segmented results due to the binary gap heuristic.
114
+
115
+ ### Alternative Approaches Investigated
116
+
117
+ | Approach | Result |
118
+ |---|---|
119
+ | OneNote API `/me/onenote/pages` | Blocked by 5000 item limit (error 10008) |
120
+ | Graph Search API `driveItem` | Section-level only, no page granularity |
121
+ | Graph Search API `listItem` | Section-level only (pages not individually indexed) |
122
+ | OneDrive HTML conversion (`?format=html`) | Not supported for .one files (406) |
123
+ | SharePoint REST search | Requires separate OAuth scope, returns section-level |
124
+ | Site-specific OneNote API | Same 5000 limit applies |
125
+ | Beta Graph API endpoints | Same limitations |
126
+
127
+ ### Future Improvements
128
+
129
+ - Implement proper MS-ONESTORE parser for accurate page extraction with page GUIDs
130
+ - Use page GUIDs to construct page-level deep links: `Doc.aspx?sourcedoc={guid}&wd=target(section|/pageTitle)`
131
+ - Incremental sync (only download changed sections based on `lastModifiedDateTime`)
132
+ - SQLite or full-text search index for faster queries on large caches
@@ -0,0 +1,130 @@
1
+ # onen0te-cli UX Analysis
2
+
3
+ Analysis of [fatihdumanli/onen0te-cli](https://github.com/fatihdumanli/onen0te-cli) — a Go-based OneNote CLI tool. Key takeaways for informing our `onenote-cli` design.
4
+
5
+ ## Command Structure
6
+
7
+ ```
8
+ nnote new [-i "text" | -f file] [-a alias] [-t title] Create a note
9
+ nnote browse Interactive notebook/section/page navigation
10
+ nnote search <phrase> Search across all notebooks
11
+ nnote alias new [name] Create a section alias
12
+ nnote alias list List all aliases
13
+ nnote alias remove <name> Delete an alias
14
+ ```
15
+
16
+ ## Auth Flow
17
+
18
+ - Uses **OAuth 2.0 Authorization Code Flow** (not device code)
19
+ - Starts a local HTTP server on `localhost:5992` for the redirect callback
20
+ - Opens system browser for login
21
+ - Tokens stored in Bitcask DB at `/tmp/nnote`
22
+ - Auto-refreshes expired tokens silently before each API call
23
+ - On first run, prompts: "You haven't setup a Onenote account yet, would you like to setup one now?"
24
+
25
+ Comparison with our approach:
26
+ - We use **device code flow** which works better in SSH/headless environments
27
+ - They use browser redirect which is more seamless on desktop
28
+ - Both auto-refresh tokens
29
+
30
+ ## Key UX Patterns Worth Adopting
31
+
32
+ ### 1. Interactive Selection Prompts
33
+
34
+ When creating a note without an alias, the user is prompted to select a notebook, then a section via interactive dropdown (uses `survey` library). This is much better than requiring users to copy-paste IDs.
35
+
36
+ ### 2. Alias System
37
+
38
+ Maps a short name to a notebook+section pair. Avoids repeated interactive selection for frequently-used sections. Example:
39
+ ```
40
+ nnote new -a work -i "Meeting notes"
41
+ ```
42
+ After saving, if no alias exists for the section, suggests creating one.
43
+
44
+ ### 3. Browse Mode (Interactive Navigation Loop)
45
+
46
+ `nnote browse` creates an interactive loop:
47
+ 1. Select notebook
48
+ 2. Select section
49
+ 3. Select page
50
+ 4. View content (HTML rendered to text)
51
+ 5. Menu: back to sections / notebooks / open in browser / open in OneNote client / exit
52
+
53
+ Uses emoji indicators in menus for visual scanning.
54
+
55
+ ### 4. Multiple Note Input Methods
56
+
57
+ - `-i "text"` — Inline text
58
+ - `-f /path/to/file` — Import from file
59
+ - No flags — Opens `$EDITOR` for composing
60
+
61
+ ### 5. Spinner Animations
62
+
63
+ Shows spinners during API calls (GetNotebooks, GetSections, SaveNote, etc.) with success/fail status on completion.
64
+
65
+ ### 6. Styled Output
66
+
67
+ - Color-coded messages: green=success, red=error, yellow=warning
68
+ - Table rendering for structured data (aliases, notebooks)
69
+ - Breadcrumb display with metadata when viewing pages
70
+
71
+ ## Architecture Decisions
72
+
73
+ ### Storage
74
+
75
+ - Uses **Bitcask** (embedded key-value store) at `/tmp/nnote`
76
+ - Stores both OAuth tokens and aliases in the same DB
77
+ - No config files — minimal setup burden
78
+ - Concern: `/tmp` is volatile on some systems; better to use `~/.config/` or `~/.local/`
79
+
80
+ ### API Layer
81
+
82
+ - Custom REST client wrapper over `net/http`
83
+ - Endpoints used:
84
+ - `GET /me/onenote/notebooks` — list notebooks
85
+ - `GET /me/onenote/notebooks/{id}/sections` — list sections
86
+ - `GET /me/onenote/sections/{id}/pages` — list pages
87
+ - `GET /me/onenote/pages/{id}/content` — get page HTML
88
+ - `POST /me/onenote/sections/{id}/pages` — create page
89
+ - `GET /me/onenote/pages?search=...` — search (undocumented/deprecated?)
90
+ - Uses `html2text` library to convert page HTML to terminal-readable text
91
+
92
+ ### Error Handling
93
+
94
+ - All errors wrapped with context via `pkg/errors`
95
+ - Exit codes for different failure modes (0-7)
96
+ - Styled error messages (not raw stack traces)
97
+
98
+ ### Dependencies (Go)
99
+
100
+ | Library | Purpose |
101
+ |---------|---------|
102
+ | spf13/cobra | CLI framework |
103
+ | AlecAivazis/survey | Interactive prompts |
104
+ | pterm/pterm | Tables, spinners, styled output |
105
+ | k3a/html2text | HTML to text rendering |
106
+ | prologic/bitcask | Key-value storage |
107
+ | pkg/errors | Error wrapping |
108
+
109
+ ## Notable Gaps
110
+
111
+ - No pagination for large result sets
112
+ - No retry logic for HTTP 503/504 (marked as TODO)
113
+ - No handling of the 5000-item SharePoint limit (would fail silently)
114
+ - Search uses an older/undocumented Graph API pattern
115
+ - Hardcoded OAuth client ID — not configurable
116
+ - Storage path `/tmp/nnote` is volatile
117
+ - No `--json` output option for scripting
118
+
119
+ ## Ideas for onenote-cli
120
+
121
+ Based on this analysis, features worth considering for our CLI:
122
+
123
+ 1. **Interactive prompts** — Use `@inquirer/prompts` or similar for notebook/section selection instead of requiring raw IDs
124
+ 2. **Alias system** — Map short names to notebook+section pairs for quick note creation
125
+ 3. **Browse mode** — Interactive navigation loop through notebooks → sections → pages
126
+ 4. **HTML to text rendering** — Use `html-to-text` or `turndown` for terminal display of page content
127
+ 5. **Spinner/progress indicators** — Show progress during API calls
128
+ 6. **Open in browser/client** — Use the `links.oneNoteWebUrl` and `oneNoteClientUrl` from notebook metadata
129
+ 7. **$EDITOR integration** — Launch editor for composing notes
130
+ 8. **--json flag** — Output JSON for scripting/piping (improvement over onen0te-cli)
package/docs/setup.md ADDED
@@ -0,0 +1,141 @@
1
+ # onenote-cli Authentication Setup Guide
2
+
3
+ This guide walks you through setting up Azure AD authentication for `onenote-cli` to access Microsoft OneNote via the Graph API.
4
+
5
+ ## Prerequisites
6
+
7
+ - A Microsoft account (personal or organizational)
8
+ - An Azure account with an active subscription — [create one for free](https://azure.microsoft.com/pricing/purchase-options/azure-account)
9
+ - [Bun](https://bun.sh) runtime installed
10
+
11
+ ## Step 1: Register an Azure AD Application
12
+
13
+ 1. Sign in to the [Microsoft Entra admin center](https://entra.microsoft.com)
14
+ 2. Navigate to **Entra ID** > **App registrations** > **New registration**
15
+ 3. Fill in the registration form:
16
+ - **Name**: `onenote-cli`
17
+ - **Supported account types**: Select **"Accounts in any organizational directory and personal Microsoft accounts"**
18
+ (This allows both work/school and personal Microsoft accounts)
19
+ - **Redirect URI**: Leave blank for now
20
+ 4. Click **Register**
21
+ 5. On the Overview page, copy the **Application (client) ID** — you will need this
22
+
23
+ ## Step 2: Configure Platform for Device Code Flow
24
+
25
+ 1. In your app registration, go to **Authentication** > **Add a platform**
26
+ 2. Select **Mobile and desktop applications**
27
+ 3. Check the box for `https://login.microsoftonline.com/common/oauth2/nativeclient`
28
+ 4. Click **Configure**
29
+ 5. Scroll down and set **Allow public client flows** to **Yes**
30
+ 6. Click **Save**
31
+
32
+ > Device code flow requires "Allow public client flows" to be enabled. Without this, authentication will fail.
33
+
34
+ ## Step 3: Configure API Permissions
35
+
36
+ 1. In your app registration, go to **API permissions** > **Add a permission**
37
+ 2. Select **Microsoft Graph** > **Delegated permissions**
38
+ 3. Search and add the following permissions:
39
+ - `Notes.Read` — Read user OneNote notebooks
40
+ - `Notes.ReadWrite` — Read and write user OneNote notebooks
41
+ - `Notes.Read.All` — Read all notebooks the user can access
42
+ - `Notes.ReadWrite.All` — Read and write all notebooks the user can access
43
+ 4. Click **Add permissions**
44
+
45
+ > For organizational accounts, an admin may need to **Grant admin consent** for these permissions.
46
+
47
+ ## Step 4: Configure onenote-cli
48
+
49
+ You have two options to provide credentials:
50
+
51
+ ### Option A: Environment variables (recommended)
52
+
53
+ Copy `.env.example` to `.env.local` and fill in your client ID:
54
+
55
+ ```bash
56
+ cp .env.example .env.local
57
+ ```
58
+
59
+ Edit `.env.local`:
60
+
61
+ ```env
62
+ ONENOTE_CLIENT_ID=your-actual-client-id-here
63
+ ONENOTE_AUTHORITY=https://login.microsoftonline.com/common
64
+ ```
65
+
66
+ ### Option B: Config file
67
+
68
+ The CLI also reads from `~/.onenote-cli/config.json`. This file is auto-created on first run:
69
+
70
+ ```json
71
+ {
72
+ "clientId": "your-actual-client-id-here",
73
+ "authority": "https://login.microsoftonline.com/common"
74
+ }
75
+ ```
76
+
77
+ > Environment variables take priority over the config file.
78
+
79
+ ## Step 5: Login
80
+
81
+ ```bash
82
+ bun run src/index.ts login
83
+ ```
84
+
85
+ This initiates the **device code flow**:
86
+
87
+ 1. The CLI prints a URL and a code
88
+ 2. Open the URL in your browser
89
+ 3. Enter the code and sign in with your Microsoft account
90
+ 4. Grant the requested permissions
91
+ 5. Return to the terminal — you should see "Login successful!"
92
+
93
+ Tokens are cached at `~/.onenote-cli/msal-cache.json` so you don't need to login every time.
94
+
95
+ ## Step 6: Verify
96
+
97
+ ```bash
98
+ # List your notebooks
99
+ bun run src/index.ts notebooks list
100
+
101
+ # List all sections
102
+ bun run src/index.ts sections list
103
+
104
+ # List pages
105
+ bun run src/index.ts pages list
106
+ ```
107
+
108
+ ## Authority URL Reference
109
+
110
+ | Scenario | Authority URL |
111
+ |---|---|
112
+ | Multi-tenant + personal accounts | `https://login.microsoftonline.com/common` |
113
+ | Organizational accounts only (any tenant) | `https://login.microsoftonline.com/organizations` |
114
+ | Personal Microsoft accounts only | `https://login.microsoftonline.com/consumers` |
115
+ | Single tenant only | `https://login.microsoftonline.com/{tenant-id}` |
116
+
117
+ ## Logout
118
+
119
+ To clear cached tokens:
120
+
121
+ ```bash
122
+ bun run src/index.ts logout
123
+ ```
124
+
125
+ ## Troubleshooting
126
+
127
+ ### "AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'"
128
+
129
+ → Make sure **Allow public client flows** is set to **Yes** in your app's Authentication settings.
130
+
131
+ ### "AADSTS65001: The user or administrator has not consented to use the application"
132
+
133
+ → An admin needs to grant consent for the API permissions, or the user needs to consent during login.
134
+
135
+ ### "AADSTS700016: Application with identifier '...' was not found"
136
+
137
+ → Double-check that your `ONENOTE_CLIENT_ID` matches the Application (client) ID from Azure portal.
138
+
139
+ ### Token expired
140
+
141
+ Tokens are automatically refreshed using the cached refresh token. If refresh fails, run `onenote login` again.
@@ -0,0 +1,92 @@
1
+ # onenote-cli: 让你的 OneNote 笔记在 AI 时代存活下来
2
+
3
+ 你的 OneNote 笔记本里藏着多少年的记忆?工作笔记、学习资料、日记、灵感碎片……这些内容,AI 能帮你搜索吗?
4
+
5
+ 答案是:现在可以了。
6
+
7
+ onenote-cli 是一个用 Bun + TypeScript 构建的命令行工具,通过 Microsoft Graph API 直接操作你的 OneNote 笔记本。最关键的功能:**全文搜索,精确到页面级别,点击 URL 直接跳转到匹配的那一页。**
8
+
9
+ ## 为什么需要这个?
10
+
11
+ OneNote 的搜索功能只能在桌面端或网页端使用,无法被 AI 工具调用。当你想让 AI 帮你找一条几年前的笔记时,它做不到——因为 OneNote 没有暴露搜索 API 给第三方。
12
+
13
+ 更糟的是,如果你的 OneDrive 里有超过 5000 个 OneNote 项目(笔记本+分区+分区组),微软的 Graph API 会直接返回 403 错误,连列出分区都做不到。
14
+
15
+ onenote-cli 解决了这两个问题。
16
+
17
+ ## 它能做什么?
18
+
19
+ ### 全文搜索(精确到页面)
20
+
21
+ ```bash
22
+ $ onenote search "项目计划"
23
+
24
+ # (20240315) Q2 项目计划
25
+ Section: Work Notes | Notebook: My Notebook
26
+ ...下季度**项目计划**:1. 完成用户认证模块重构 2. 上线新的推荐算法...
27
+ https://onenote.com/...?wd=target(...)
28
+
29
+ 3 page-level results found.
30
+ ```
31
+
32
+ 搜索结果直接给出 OneNote Online 的页面级 URL,点击即可跳转到对应页面。不是打开整个分区,而是精确到那一页。
33
+
34
+ ### 笔记本管理
35
+
36
+ ```bash
37
+ $ onenote notebooks list
38
+ $ onenote sections list -n <notebook-id>
39
+ $ onenote pages create -s <section-id> -t "Meeting Notes" -b "<p>内容</p>"
40
+ ```
41
+
42
+ ### 5000 项目限制的解决方案
43
+
44
+ 当 Graph API 因为 SharePoint 文档库超过 5000 项而返回 403 时,onenote-cli 会:
45
+
46
+ 1. 通过 OneDrive API 直接下载 `.one` 二进制文件
47
+ 2. 从二进制中提取页面内容(支持 UTF-8 和 UTF-16LE,中日韩文字都能搜到)
48
+ 3. 提取页面 GUID,通过 OneNote API 获取官方页面 URL
49
+ 4. 构建本地缓存索引
50
+
51
+ ### AI 集成
52
+
53
+ 作为 Claude Code Skill 一键安装:
54
+
55
+ ```bash
56
+ $ npx skills add snomiao/onenote-cli
57
+ ```
58
+
59
+ 安装后,AI 可以直接搜索你的 OneNote 笔记,输出 Markdown 格式的结果:
60
+
61
+ ```markdown
62
+ [Q2 项目计划](https://onenote.com/...?wd=target(...))
63
+ Work Notes | My Notebook
64
+ ...下季度**项目计划**:1. 完成用户认证模块重构...
65
+ ```
66
+
67
+ ## 技术亮点
68
+
69
+ - **设备代码流认证**:支持 SSH / 无头环境
70
+ - **.one 二进制解析**:从 MS-ONESTORE 格式中提取页面文本和 GUID
71
+ - **官方页面 URL**:通过 `/me/onenote/sections/0-{guid}/pages` 端点获取,绕过 OneNote Online 的会话缓存
72
+ - **跨目录运行**:`.env.local` 和缓存从包目录自动加载
73
+
74
+ ## 开始使用
75
+
76
+ ```bash
77
+ git clone https://github.com/snomiao/onenote-cli.git
78
+ cd onenote-cli
79
+ bun install
80
+ cp .env.example .env.local # 填入你的 Azure AD Client ID
81
+ bun run src/index.ts auth login
82
+ bun run src/index.ts sync
83
+ bun run src/index.ts search "你想找的内容"
84
+ ```
85
+
86
+ 详细的 Azure AD 配置步骤见 GitHub 仓库的 [docs/setup.md](https://github.com/snomiao/onenote-cli/blob/main/docs/setup.md)。
87
+
88
+ ---
89
+
90
+ **GitHub**: https://github.com/snomiao/onenote-cli
91
+
92
+ MIT License | by snomiao
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "onenote-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to operate Microsoft OneNote via Graph API",
5
+ "type": "module",
6
+ "bin": {
7
+ "onenote-cli": "./src/index.ts",
8
+ "onenote": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "start": "bun run src/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "@azure/msal-node": "^2.16.0",
15
+ "yargs": "^17.7.2"
16
+ },
17
+ "devDependencies": {
18
+ "@types/yargs": "^17.0.33",
19
+ "typescript": "^5.7.0"
20
+ },
21
+ "license": "MIT"
22
+ }