@grum-os/cli 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,52 +1,76 @@
1
- # grum
1
+ <img src="brand.svg" alt="Grum" width="228" height="64" />
2
2
 
3
- Keep a repository on the design contract your team reviewed in [Grum](https://app.grum.so).
4
-
5
- Grum reads the tokens the repository declares (CSS custom properties, a Tailwind v4 `@theme`, SCSS variables) and the button looks it defines. The team can add tokens in Grum before the code has them, and move others outside the contract. That reviewed set is what `grum` checks against. It does not invent a contract of its own, and it does not publish one.
3
+ # Grum
6
4
 
7
- On your machine it compares the working tree to that contract, lists only drift that is new on this branch, and can send a new item to the project's owners for review, with a picture of how it renders. The same check runs inside Cursor and, if you want, before a commit.
5
+ Keep a repository on the design contract your team reviewed in [Grum](https://app.grum.so).
8
6
 
9
- A check on its own prints a report and exits 0, including when you are signed out, offline, or the repository has never been scanned. It fails the process only when you ask it to.
7
+ Grum reads the tokens the repository declares (CSS custom properties, a Tailwind v4 `@theme`, SCSS variables) and the button looks it defines. The team can add tokens in Grum before the code has them, and move others outside the contract. That reviewed set is what `grum` checks against. It compares your working tree to the contract, lists only drift that is new on this branch, and can send a new item to the project's owners for review, with a picture of how it renders.
10
8
 
11
9
  Requires Node.js 22 or newer.
12
10
 
13
- ## Install
11
+ ## Quick start
12
+
13
+ Open a terminal in the repository, and run with no arguments:
14
14
 
15
15
  ```sh
16
- npm install -g @grum-os/cli
16
+ npx @grum-os/cli
17
17
  ```
18
18
 
19
- Or run it without installing:
19
+ Grum opens a wizard. The arrow keys move, Enter selects, and 1, 2, or 3 jump straight to a row. `q` quits.
20
20
 
21
- ```sh
22
- npx @grum-os/cli
21
+ ```text
22
+ 1 check Check this repository against its design contract
23
+ 2 setup Sign in, link this repository, set up Cursor
24
+ 3 login Sign in to Grum
23
25
  ```
24
26
 
25
- `grum` with no command opens a menu: check this repository, set it up, or sign in and out.
27
+ Choose **setup**. Grum walks through the rest:
26
28
 
27
- ## Set up a repository
29
+ 1. **Sign in.** It prints a code and a link. Approve the code in the browser. The token stays in your home directory (`~/.config/grum/credentials/`), never in the repository. Grum's Settings page lists every computer that is signed in.
30
+ 2. **Link this repository** to its project in Grum. If several workspaces use it, you pick one. If none does yet, Grum offers to create a workspace, asks for the live site, and waits while the first scan runs. A new workspace needs GitHub connected in Grum.
31
+ 3. **Set up Cursor.** It asks `Setup for cursor agents?` Press Enter to accept. That writes the hooks, the tools, and the rule described below. `n` skips it. It then asks `Add a pre-commit check?` Enter skips that. `y` adds `grum check --staged` to the hook this repository already uses.
28
32
 
29
- From the repository root:
33
+ Then choose **check**. New drift is listed with a one-line fix, and Grum offers to send each item for review. You do not need to type a command for any of that.
30
34
 
31
- ```sh
32
- grum setup
33
- ```
35
+ Once you are signed in, the third row is **logout**. Run `grum` again and choose **setup** to change the link or turn Cursor off. Enter keeps each current setting.
34
36
 
35
- Setup does three things.
37
+ ## Cursor
36
38
 
37
- 1. **Signs this computer in.** It prints a code and a link. Approve the code in the browser. The token is stored in your home directory (`~/.config/grum/credentials/`), never in the repository. `grum logout` removes it. Grum's Settings page lists every computer that is signed in, and can sign one out.
38
- 2. **Links the git remote to its Grum project.** If several workspaces use this repository, it asks which one. If none does yet, it offers to create a workspace, asks for the live site, and waits while Grum runs the first scan.
39
- 3. **Asks whether to set up Cursor.** That writes the hooks, the MCP server, the rule pointer, and a pre-commit check described below. Enter accepts. `n` skips it.
39
+ Accepting Cursor in the menu puts Grum inside the agent. The agent follows the same contract a person sees in Grum, and it can ask the owners to allow something the contract does not have.
40
40
 
41
- A new workspace needs GitHub connected in Grum, so the scan can read the repository.
41
+ **Before it writes UI**, it reads the contract: color, typography, spacing, and radius tokens, plus button components, each look, and the prop that selects it.
42
42
 
43
- ## Check for drift
43
+ **Before it finishes**, it checks what it changed. Drift already on the default branch stays off the list. New drift comes back with a fix.
44
44
 
45
- ```sh
46
- grum check
47
- ```
45
+ **When nothing in the contract fits**, it stops and asks you once:
46
+
47
+ 1. Use an existing token or button.
48
+ 2. Keep the new style and request review.
49
+
50
+ If you keep it, Grum captures how the element renders and sends the picture to the Review page. Owners approve or reject it there. A rejected style is not offered again: the agent changes the code to use the contract.
51
+
52
+ **While you edit**, Cursor hooks check the block that changed. They read the contract cached in `.grum/` and do not call the network, so an edit never waits on Grum. With [blocking](#fail-the-check) turned on, the agent is sent back while that drift stands (twice in one conversation), and `git commit` and `git push` from the agent are refused until it is resolved.
53
+
54
+ Reload the Cursor window once after setup, or turn **grum** on under Settings, Tools and MCP, so the agent sees the tools. Until then it reads `.grum/contract.json`.
48
55
 
49
- New drift is grouped by file. Each line names the value, the kind of drift, whether anyone has asked for review, and a one-line fix:
56
+ These are the files setup writes. They merge into what is already there, and a second run changes nothing.
57
+
58
+ | File | What it does |
59
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `.cursor/mcp.json` | Tools the agent calls: `grum_contract`, `grum_check`, `grum_request_review`, `grum_review_status`. |
61
+ | `.cursor/rules/grum.mdc` | Tells the agent to read the contract before writing UI and to check before it stops. The contract itself stays in Grum, so this file does not go stale. |
62
+ | `.cursor/hooks.json` | After an edit, checks that block. The stop hook sends the agent back on blocking drift. The shell hook can refuse `git commit` and `git push`. |
63
+ | pre-commit | Optional. When you say yes, adds `grum check --staged`. Husky's hook is committed with the repo. Git's own pre-commit hook stays on this computer. A lefthook repo gets the snippet to paste. |
64
+
65
+ `.grum/` is added to `.gitignore` the first time Grum writes there. Choose **setup** again and answer `n` to the Cursor question to remove what Grum added. Answer `n` to the pre-commit question to drop only that check.
66
+
67
+ `grum check --cursor` opens the findings in Cursor instead of walking through them in the terminal.
68
+
69
+ ## What a check shows
70
+
71
+ Choosing **check** in the menu prints the same report as `grum check`.
72
+
73
+ New drift is grouped by file. Each line names the value, the kind of drift, whether anyone has asked for review, and a fix:
50
74
 
51
75
  ```text
52
76
  src/pages/Hero.tsx
@@ -64,28 +88,9 @@ No new drift against Acme's contract (42 files checked). 3 known issues are on A
64
88
 
65
89
  Using a known value in more places than the default branch does is new drift.
66
90
 
67
- In a terminal, the check then offers each new item for review, one at a time. You can add a reason. Grum captures how the element renders and attaches the picture to the request. Owners approve or reject it on the Review page. Allowing it also dismisses that drift. A rejected item is not offered again: change the code to use the contract. If the code changed after the rejection, the next check offers it once more as a revised proposal, and the earlier note stays on the request.
68
-
69
- A request covers the branch it was asked on. It covers every branch once the change reaches the default branch and a scan counts the drift as existing. A full check also marks a rejected review **Addressed** when the drift is gone, and reopens it when the drift comes back.
70
-
71
- ### Flags
72
-
73
- ```sh
74
- grum check --staged # drift in the blocks staged for commit
75
- grum check --files src/Hero.tsx # only these paths
76
- grum check --json # project, cache time, known count, findings
77
- grum check --no-review # print the report and do not ask
78
- grum check --cursor # open the findings in Cursor
79
- grum check --offline # the cached contract, no network
80
- grum check --root apps/web # the app lives in a subdirectory
81
- grum check --report # also post this run to Grum
82
- grum check --ci # fail on open or rejected drift, and post the run
83
- grum check --strict # like --ci, and also fail while a review is pending
84
- ```
85
-
86
- `--json` is what you want when another program needs the drift key. CI, JSON, staged, and agent runs never prompt and never open Cursor.
91
+ In the terminal, the check then offers each new item for review. A request covers the branch it was asked on, until the change reaches the default branch and a scan counts the drift as existing. A full check also marks a rejected review **Addressed** when the drift is gone, and reopens it when the drift comes back.
87
92
 
88
- `--ci` and `--report` show up on the Drift page under Recent checks, one latest run per branch.
93
+ A check prints the report and exits 0, including when you are signed out, offline, or the repository has never been scanned. It fails the process only when you ask it to.
89
94
 
90
95
  ### What it looks for
91
96
 
@@ -97,9 +102,9 @@ grum check --strict # like --ci, and also fail while a review is
97
102
  | Radius | A radius off the scale |
98
103
  | Buttons | A look the contract does not have, a look whose state changed, a missing state, a raw `<button>` outside the button component, or a variant the component lacks |
99
104
 
100
- ## Ask for review of one item
105
+ ## Review one item yourself
101
106
 
102
- The key is the stable id of a finding (`color:raw:#3C3C3C` and the like). `grum check --json` prints it on each finding.
107
+ The menu's check already offers this. To send one finding directly, use its key (`grum check --json` prints it):
103
108
 
104
109
  ```sh
105
110
  grum request "color:raw:#3C3C3C" \
@@ -107,7 +112,7 @@ grum request "color:raw:#3C3C3C" \
107
112
  --url http://localhost:3000
108
113
  ```
109
114
 
110
- `--url` is the page where the element renders. It can also be an HTML file in the repo (`./index.html`) when there is no dev server. A page behind a login is captured through the Grum Chrome extension, in your own browser session. `--browser chrome` or `--browser safari` picks the browser. `--no-capture` sends the request with no picture.
115
+ `--url` is the page where the element renders, or an HTML file in the repo (`./index.html`) when there is no dev server. A page behind a login is captured through the Grum Chrome extension, in your own browser session. `--browser chrome` or `--browser safari` picks the browser. `--no-capture` sends the request with no picture.
111
116
 
112
117
  ```sh
113
118
  grum capture reset
@@ -121,32 +126,7 @@ Clears the capture-browser sign-in saved for this repository.
121
126
  grum contract
122
127
  ```
123
128
 
124
- Prints the packet an agent reads before writing styles: tokens by category (with a separate dark value when there is one), button components, each look and the prop that selects it, and the repository's written rules about those buttons. Review decisions are included, so a pending or rejected item is visible without another check.
125
-
126
- ## Cursor
127
-
128
- ```sh
129
- grum init cursor
130
- ```
131
-
132
- Merges into files that already exist. Entries Grum did not write are left as they are, and a second run changes nothing. It writes:
133
-
134
- | File | What it does |
135
- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
- | `.cursor/hooks.json` | After an edit, checks the block that changed. The stop hook sends the agent back while blocking drift stands, at most twice a conversation. The shell hook can refuse `git commit` and `git push`. Hooks read the cache and do not call the network. |
137
- | `.cursor/mcp.json` | Four tools: `grum_contract`, `grum_check`, `grum_request_review`, `grum_review_status`. |
138
- | `.cursor/rules/grum.mdc` | Tells the agent to read the contract before writing UI and to check before it stops. The contract itself stays in Grum, so this file does not go stale on every scan. |
139
- | pre-commit | Adds `grum check --staged`. Husky's hook is committed with the repo. Git's own pre-commit hook is local and is not shared. A lefthook repo gets the snippet to paste. |
140
-
141
- `.grum/` is added to `.gitignore` the first time Grum writes there. The cached contract (`.grum/contract.json`) stays on this computer.
142
-
143
- Reload the Cursor window after init (or turn **grum** on under Settings, Tools and MCP) so the agent sees the tools. Until then it follows `.grum/contract.json`.
144
-
145
- ```sh
146
- grum init cursor --remove # deletes exactly what init added
147
- grum init cursor --force # replaces a rule file Grum did not write
148
- grum init cursor --local-only # hooks and the rule, without a linked project
149
- ```
129
+ Prints the packet the Cursor agent reads before writing styles: tokens by category (with a separate dark value when there is one), button components, each look and the prop that selects it, and the repository's written rules about those buttons.
150
130
 
151
131
  ## Fail the check
152
132
 
@@ -182,7 +162,7 @@ Grum adds `.grum/` to `.gitignore` the first time it writes there, so `config.js
182
162
 
183
163
  ## Continuous integration
184
164
 
185
- Sign in once with `grum login`, then copy the `token` field from the credentials file for this Grum. For [app.grum.so](https://app.grum.so) that file is:
165
+ Sign in once from the menu, then copy the `token` field from the credentials file for this Grum. For [app.grum.so](https://app.grum.so) that file is:
186
166
 
187
167
  ```text
188
168
  ~/.config/grum/credentials/https_app_grum_so.json
@@ -202,16 +182,33 @@ Store it as a secret. Do not commit it. The token acts as you, in every workspac
202
182
 
203
183
  ## Commands
204
184
 
205
- | Command | What it does |
206
- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
207
- | `grum` | Menu: check, setup, or sign in and out. |
208
- | `grum login` / `grum logout` | Sign this computer in to Grum, or out. |
209
- | `grum setup [--root <dir>]` | Sign in, link this repository, and ask about Cursor. |
210
- | `grum check [flags]` | Check the working tree. See [Flags](#flags). |
211
- | `grum request <key> [--reason "..."] [--url <page>] [--browser chrome\|safari] [--no-capture]` | Ask the owners to allow one drift item. |
212
- | `grum capture reset` | Clear this repository's saved capture-browser sign-ins. |
213
- | `grum contract` | Print the contract packet. |
214
- | `grum init cursor [--remove] [--force] [--local-only]` | Install or remove the Cursor and pre-commit setup. |
185
+ The menu covers check, setup, and sign-in. These are the same actions as flags, for scripts and CI.
186
+
187
+ | Command | What it does |
188
+ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
189
+ | `grum` | The menu. Check, setup (including Cursor), or sign in. |
190
+ | `grum login` / `grum logout` | Sign this computer in to Grum, or out. |
191
+ | `grum setup [--root <dir>]` | Sign in, link this repository, and ask about Cursor and the pre-commit check. |
192
+ | `grum check [flags]` | Check the working tree. See the flags below. |
193
+ | `grum request <key> [--reason "..."] [--url <page>] [--browser chrome | safari] [--no-capture]` | Ask the owners to allow one drift item. |
194
+ | `grum capture reset` | Clear this repository's saved capture-browser sign-ins. |
195
+ | `grum contract` | Print the contract packet. |
196
+ | `grum init cursor [--remove] [--force] [--local-only] [--pre-commit]` | Write or remove the Cursor files without the menu. `--pre-commit` adds the commit check. `--no-pre-commit` removes it. |
197
+
198
+ ```sh
199
+ grum check --staged # drift in the blocks staged for commit
200
+ grum check --files src/Hero.tsx # only these paths
201
+ grum check --json # project, cache time, known count, findings
202
+ grum check --no-review # print the report and do not ask
203
+ grum check --cursor # open the findings in Cursor
204
+ grum check --offline # the cached contract, no network
205
+ grum check --root apps/web # the app lives in a subdirectory
206
+ grum check --report # also post this run to Grum
207
+ grum check --ci # fail on open or rejected drift, and post the run
208
+ grum check --strict # like --ci, and also fail while a review is pending
209
+ ```
210
+
211
+ `--ci` and `--report` show up on the Drift page under Recent checks, one latest run per branch. CI, JSON, staged, and agent runs never open the menu and never prompt.
215
212
 
216
213
  `grum hook` and `grum mcp` are the entry points Cursor runs. You do not call them yourself.
217
214
 
package/brand.svg ADDED
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="228" height="64" viewBox="0 0 114.0468 32" role="img" aria-label="Grum">
2
+ <style>
3
+ .word { fill: #1a1a1a; }
4
+ @media (prefers-color-scheme: dark) {
5
+ .word { fill: #f2f2f2; }
6
+ }
7
+ </style>
8
+ <path fill="#1c8ef1" d="M24.9594 0C27.4583 0 28.7083 0.000321239 29.6582 0.496867C30.4469 0.909293 31.0907 1.55298 31.5031 2.34169C31.9997 3.2915 32 4.54146 32 7.04014C32 9.53859 31.9997 10.788 31.5031 11.7378C31.0907 12.5265 30.4469 13.1702 29.6582 13.5826C28.7083 14.0792 27.4583 14.0795 24.9594 14.0795C24.4568 14.0795 24.0048 14.0793 23.5956 14.0751C22.5207 14.0639 21.5658 14.9246 21.5658 15.9996C21.5658 17.0746 22.5207 17.9355 23.5957 17.9247C24.0049 17.9206 24.4568 17.9205 24.9594 17.9205C27.4583 17.9205 28.7083 17.92 29.6582 18.4166C30.4471 18.829 31.0907 19.4733 31.5031 20.2622C31.9997 21.212 32 22.4612 32 24.9598C32 27.4584 31.9996 28.7077 31.5031 29.6575C31.0907 30.4464 30.4471 31.0907 29.6582 31.5031C28.7083 31.9997 27.4583 32 24.9594 32C22.4607 32 21.2112 31.9997 20.2614 31.5031C19.4724 31.0907 18.8281 30.4464 18.4156 29.6575C17.9192 28.7077 17.9195 27.4581 17.9195 24.9598C17.9195 24.4567 17.9195 24.0043 17.9237 23.5947C17.9345 22.5206 17.0742 21.5665 16 21.5665C14.9258 21.5665 14.0655 22.5206 14.0763 23.5946C14.0805 24.0043 14.0806 24.4567 14.0806 24.9598C14.0806 27.4584 14.0802 28.7077 13.5837 29.6575C13.1712 30.4464 12.5276 31.0907 11.7386 31.5031C10.7888 31.9997 9.53875 32 7.03988 32C4.54126 32 3.2917 31.9996 2.34187 31.5031C1.55293 31.0907 0.909362 30.4464 0.496905 29.6575C0.000398815 28.7077 0 27.4584 0 24.9598C2.87333e-10 22.4612 0.00033322 21.212 0.496905 20.2622C0.909395 19.4733 1.55291 18.829 2.34187 18.4166C3.2917 17.92 4.54126 17.9205 7.03988 17.9205C7.54303 17.9205 7.99554 17.9205 8.4052 17.9246C9.48015 17.9355 10.435 17.0745 10.435 15.9996C10.435 14.9246 9.48015 14.064 8.4052 14.0751C7.99555 14.0793 7.54303 14.0795 7.03988 14.0795C4.54126 14.0795 3.2917 14.0792 2.34187 13.5826C1.55308 13.1702 0.909354 12.5265 0.496905 11.7378C0.000347435 10.788 1.37868e-09 9.53859 0 7.04014C0 4.54146 0.000321269 3.2915 0.496905 2.34169C0.909354 1.55296 1.55308 0.909277 2.34187 0.496867C3.2917 0.000345826 4.54126 1.21426e-09 7.03988 0C9.53875 0 10.7888 0.000321239 11.7386 0.496867C12.5275 0.909285 13.1712 1.55297 13.5837 2.34169C14.0802 3.2915 14.0806 4.54146 14.0806 7.04014C14.0806 7.54427 14.0805 7.99753 14.0763 8.40772C14.0652 9.48211 14.9255 10.4366 16 10.4366C17.0745 10.4366 17.9349 9.48203 17.9238 8.40772C17.9196 7.99753 17.9195 7.54427 17.9195 7.04014C17.9195 4.54146 17.919 3.2915 18.4156 2.34169C18.828 1.5528 19.4724 0.909325 20.2614 0.496867C21.2112 0.00033775 22.4607 5.45318e-10 24.9594 0Z" />
9
+ <g class="word" transform="translate(46 6.145)">
10
+ <g transform="translate(-15.7148 0)">
11
+ <path d="M63.3766 8.1C65.3476 6.885 66.8866 5.94 68.1826 5.94C68.9926 5.94 69.1276 6.615 68.8306 7.695L67.3726 12.906H67.6426C69.6136 9.315 72.3136 5.94 74.7976 5.94C75.6886 5.94 76.6606 6.507 76.1476 8.073L74.6896 12.474H74.9326C76.9036 9.045 79.4686 5.94 81.8446 5.94C82.7356 5.94 83.6806 6.48 83.1946 8.073L80.6566 16.281C80.4406 16.956 80.6566 17.361 81.1426 17.361C81.8446 17.361 82.6546 16.983 83.7616 16.227V17.307C82.0336 18.711 79.9276 19.71 78.6046 19.71C77.7136 19.71 77.3626 19.332 77.9566 17.415L80.3326 9.72C80.6026 8.883 80.3866 8.64 80.1166 8.64C78.6586 8.64 74.2576 15.039 73.1236 19.17L70.5586 19.71L73.4476 9.477C73.6096 8.883 73.4206 8.64 73.1506 8.64C71.6926 8.64 67.1836 15.039 66.0496 19.17L63.5926 19.71L66.1846 9.126C66.3196 8.559 66.1846 8.1 63.3766 8.91V8.1Z" />
12
+ </g>
13
+ <path d="M33.7 19.71C32.809 19.71 31.756 19.143 32.35 17.577C33.376 14.85 34.294 12.096 35.023 9.369C35.212 8.694 35.023 8.289 34.537 8.289C33.835 8.289 33.025 8.667 31.918 9.423V8.343C33.646 6.939 35.752 5.94 37.075 5.94C37.966 5.94 38.209 6.372 37.723 8.235C36.994 10.962 36.103 13.689 35.077 16.416C34.942 16.767 35.158 17.01 35.428 17.01C36.886 17.01 41.611 10.611 42.745 6.48L45.202 5.94L42.745 16.281C42.583 16.956 42.772 17.361 43.258 17.361C43.96 17.361 44.77 16.983 45.877 16.227V17.307C44.149 18.711 42.043 19.71 40.72 19.71C39.829 19.71 39.532 19.332 40.072 17.415L41.638 11.961H41.503C39.397 15.822 36.346 19.71 33.7 19.71Z" />
14
+ <path d="M21.0836 8.1C23.0546 6.885 24.5936 5.94 25.8896 5.94C26.6996 5.94 26.9966 6.507 26.5376 7.695L25.3496 10.827H25.6196C27.5636 7.857 29.7776 5.94 31.2626 5.94C31.7216 5.94 31.9916 6.237 31.9916 6.696C31.9916 7.938 30.8846 8.91 30.3176 9.018L28.9136 9.261C26.8616 10.206 25.2416 13.014 23.9186 16.713L23.2166 19.17L20.7596 19.71L23.8106 9.126C23.9726 8.559 23.8106 8.1 21.0836 8.91V8.1Z" />
15
+ <path d="M8.775 19.71C2.97 19.71 0 16.686 0 12.501C0 6.561 5.697 0 13.122 0C15.255 0 17.199 0.351001 18.792 0.809999L18.225 5.94H17.55C16.956 2.646 15.093 1.485 11.88 1.485C6.75 1.485 3.159 6.642 3.159 11.799C3.159 15.552 5.265 18.225 8.91 18.225C11.691 18.225 13.527 17.28 13.851 15.768L14.931 10.611L10.341 10.746L10.935 9.261H20.007L19.467 10.476C18.063 10.476 17.469 10.827 17.172 12.231L16.416 15.714C16.389 15.849 16.389 15.957 16.389 16.065C16.389 16.632 16.767 16.821 17.874 16.821L17.82 17.469C14.094 18.792 11.475 19.71 8.775 19.71Z" />
16
+ </g>
17
+ </svg>
package/content.md5 CHANGED
@@ -1 +1 @@
1
- 915bb0fcab45a45db629397f8efd6a7e
1
+ 78a3046bc5a5bb380da405ed25060485