@fishdivinity/dsh-local-search 0.1.0 → 0.2.1

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,176 +1,177 @@
1
- <div align="center">
2
-
3
- <img src="https://raw.githubusercontent.com/fishdivinity/dsh-local-search/main/icon.svg" width="96" alt="Local Search icon">
4
-
5
- # Local Search for DeepSeek Harness
6
-
7
- **Read-only Everything filename search and Anytxt document-content search, behind one capability-based tool surface.**
8
-
9
- [![npm version](https://img.shields.io/npm/v/@fishdivinity/dsh-local-search)](https://www.npmjs.com/package/@fishdivinity/dsh-local-search)
10
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
- [![platform](https://img.shields.io/badge/platform-Windows-blue)](#requirements)
12
- [![dsh](https://img.shields.io/badge/dsh-%3E%3D0.2.1--0%20%3C0.3.0-blue)](#requirements)
13
- [![CI](https://img.shields.io/github/actions/workflow/status/fishdivinity/dsh-local-search/ci.yml?branch=main&label=CI)](https://github.com/fishdivinity/dsh-local-search/actions)
14
- [![GitHub stars](https://img.shields.io/github/stars/fishdivinity/dsh-local-search?style=flat)](https://github.com/fishdivinity/dsh-local-search)
15
-
16
- [English](README.md) · [简体中文](README-zh.md) · [Install](#install) · [Configuration](#configuration) · [Tools](#tools) · [Security](#security-and-limits)
17
-
18
- </div>
19
-
20
- ## Contents
21
-
22
- - [Tools](#tools)
23
- - [Requirements](#requirements)
24
- - [Install](#install)
25
- - [Configuration](#configuration)
26
- - [Scopes](#scopes)
27
- - [Engine selection](#engine-selection)
28
- - [Errors](#errors)
29
- - [Security and limits](#security-and-limits)
30
- - [Known limitations](#known-limitations)
31
- - [Related plugins](#related-plugins)
32
- - [Development](#development)
33
- - [License](#license)
34
-
35
- ## Tools
36
-
37
- | Tool | Engine | Use for |
38
- |---|---|---|
39
- | `local_search_files` | Everything via `es.exe` | File/folder names, paths, cross-drive discovery |
40
- | `local_search_content` | Anytxt JSON-RPC | Text inside PDF, DOCX, XLSX, PPTX, EPUB, RTF, TXT, MD |
41
- | `local_search_fragments` | Anytxt | Snippets around a match in one indexed file |
42
- | `local_search_read` | Anytxt | Extracted plain text of one indexed file, capped |
43
- | `local_search_status` | both | Check whether each engine is reachable |
44
-
45
- ## Requirements
46
-
47
- - Windows.
48
- - DeepSeek Harness `>=0.2.1-0 <0.3.0`.
49
- - [Everything](https://www.voidtools.com/) running, plus `es.exe` (a separate download from voidtools; the plugin auto-detects PATH/common install folders, or set `esPath`).
50
- - [Anytxt Searcher](https://anytxt.net/) for content search: the **GUI** (`ATGUI.exe`) must be running because the JSON-RPC service lives inside it. The indexing service alone is not enough.
51
-
52
- This plugin does **not** bundle Everything, `es.exe`, or Anytxt. Install and license them separately.
53
-
54
- ## Install
55
-
56
- ```powershell
57
- dsh plugin --profile desktop add @fishdivinity/dsh-local-search
58
- ```
59
-
60
- From GitHub before the first npm release:
61
-
62
- ```powershell
63
- dsh plugin --profile desktop add github:fishdivinity/dsh-local-search
64
- ```
65
-
66
- Restart the profile or open a new session. The five `local_search_*` tools appear in the model toolset.
67
-
68
- ## Configuration
69
-
70
- v0.1 has **no settings page**. Edit the plugin row in the profile's `cordis.patch.yml`, or use the profile patch override example below. A Plugins-page configuration form (sidebar **Plugins** → this plugin → **Configure**) is planned for v0.2.
71
-
72
- All keys are optional. The row config replaces the whole config object, never deep-merges.
73
-
74
- | Key | Default | Meaning |
75
- |---|---|---|
76
- | `esPath` | auto-detect | Absolute path to `es.exe`. Resolution order: `esPath`, `ES_PATH`, common install folders, `where.exe`. |
77
- | `anytxtUrl` | `http://127.0.0.1:9920` | Anytxt JSON-RPC endpoint. The default also tries `:9921`. |
78
- | `workspaceRoot` | session default directory | Root for `scope=workspace`. |
79
- | `workRoot` | empty | Root for `scope=work`; required before using that scope. |
80
- | `personalRoot` | empty | Root for `scope=personal`; required before using that scope. |
81
- | `allRoots` | `C:\` plus `D:\` when present | Semicolon-separated roots for Anytxt `scope=all`. Everything `scope=all` searches all volumes it indexes. |
82
- | `maxResults` | `20` | Default result cap. Hard cap is 100. |
83
- | `timeoutMs` | `30000` | Per-call timeout budget. |
84
- | `readMaxChars` | `8000` | Default `local_search_read` window; hard cap is 20,000 characters. |
85
- | `esEncoding` | `gbk` | Code page used to decode `es.exe` output. zh-CN Windows uses GBK. |
86
-
87
- Example row override:
88
-
89
- ```yaml
90
- - id: fishdivinity-local-search
91
- name: '@fishdivinity/dsh-local-search'
92
- config:
93
- workRoot: 'D:\Projects'
94
- personalRoot: 'D:\Documents'
95
- allRoots: 'C:\;D:\'
96
- ```
97
-
98
- ## Scopes
99
-
100
- - `workspace` (default): the session's working directory.
101
- - `work`: `workRoot`.
102
- - `personal`: `personalRoot`.
103
- - `all`: `allRoots` for Anytxt; all indexed volumes for Everything.
104
-
105
- Use `all` only when the user explicitly asks for a cross-drive/global search. The `path` argument overrides `scope`.
106
-
107
- ## Engine selection
108
-
109
- `engine` defaults to `auto`:
110
-
111
- - `local_search_files`: `auto` or `everything`.
112
- - `local_search_content` / `fragments` / `read`: `auto` or `anytxt`.
113
- - `local_search_status`: `auto`, `everything`, or `anytxt`.
114
-
115
- The tool names stay stable if more engines are added; `engine` values are additive. Content items carry both `engine` and a self-describing `ref` such as `anytxt:123`; pass the ref unchanged to `local_search_fragments`/`local_search_read`, and pass `engine` only as an assertion.
116
-
117
- ## Errors
118
-
119
- Failures are typed `HarnessError`s with stable codes:
120
-
121
- | Code | Meaning |
122
- |---|---|
123
- | `EVERYTHING_ES_NOT_FOUND` | `es.exe` is missing; install it or set `esPath`. |
124
- | `EVERYTHING_DOWN` | Everything is not running / cannot be reached. |
125
- | `EVERYTHING_TIMEOUT`, `EVERYTHING_TOO_LARGE` | Query too broad or slow; narrow it. |
126
- | `ANYTXT_DOWN` | Anytxt GUI is not running or the JSON-RPC endpoint is unreachable. |
127
- | `ANYTXT_TIMEOUT`, `ANYTXT_RPC`, `ANYTXT_HTTP` | Anytxt-side query/transport failure. |
128
- | `ROOT_NOT_FOUND`, `SCOPE_NOT_CONFIGURED`, `INVALID_SCOPE` | Root/scope configuration problem. |
129
- | `INVALID_ARGUMENT` | Empty query, bad limit, etc. |
130
-
131
- If a search reports `*_DOWN`, fall back to `grep`/`glob` and say that you fell back.
132
-
133
- ## Security and limits
134
-
135
- - **Read-only**: no write, delete, index-sync, OCR, export, or settings operation is registered.
136
- - `es.exe` is always spawned with a fixed argv; model text goes after `--`, so `-export-*`, `-save-settings`, `-clear-settings`, `-exit`, `-reindex`, `-save-db`, and run-count switches are unreachable.
137
- - Anytxt is only called through `Search`, `GetResult`, `GetFragmentAll`, and `GetRawTextByFID`.
138
- - No network access except the configured local Anytxt URL.
139
- - Results are capped; `local_search_read` caps the returned text.
140
- - The plugin runs with the user's privileges and can read files the user can read. Prefer `scope=workspace` unless a global search is requested.
141
-
142
- ## Known limitations
143
-
144
- - Windows only.
145
- - Everything 1.4 indexes metadata, not file contents; use Anytxt for contents.
146
- - Anytxt's JSON-RPC API is local and unofficial; GUI versions can change behavior. The plugin reports unreachable/wrong-shape responses instead of guessing.
147
- - Anytxt's index may be stale. Verify important hits with `read`/the original file.
148
- - No auto-start: starting `ATGUI.exe` writes the index and is left to the user.
149
- - No settings page in v0.1; configure through the profile patch. A Plugins-page form is planned.
150
- - Anytxt's terms limit personal/non-commercial use; commercial use needs its own license.
151
-
152
- ## Related plugins
153
-
154
- - [`dsh-tool-everything`](https://dshbase.com/zh/plugins/dsh-tool-everything/) covers Everything filename search through the SDK.
155
- - [`dsh-workspace-search`](https://github.com/tsonglew/dsh-workspace-search) covers workspace file names/contents in the sidebar.
156
- - This plugin's differentiator is Anytxt document content plus one routing surface for names and contents.
157
-
158
- ## Development
159
-
160
- No dependencies; tests use Node's built-in test runner.
161
-
162
- ```powershell
163
- npm test
164
- npm run check
165
- npm run pack:check
166
- ```
167
-
168
- The engine modules (`lib/everything.js`, `lib/anytxt.js`, `lib/roots.js`, `lib/format.js`) are independent of the DSH runtime so their tests run under plain Node. See [`docs/architecture.md`](docs/architecture.md), [`docs/extending-engines.md`](docs/extending-engines.md), and [`docs/publishing.md`](docs/publishing.md).
169
-
170
- ## Changelog
171
-
172
- See [CHANGELOG.md](CHANGELOG.md). It stays in the repository; the npm tarball ships only the runtime files, README, LICENSE, locale, and icon.
173
-
174
- ## License
175
-
176
- MIT. Everything and Anytxt are separate products with their own licenses and trademarks; this project is not affiliated with voidtools or CBEWIN.
1
+ <div align="center">
2
+
3
+ <img src="https://raw.githubusercontent.com/fishdivinity/dsh-local-search/main/icon.svg" width="96" alt="Local Search icon">
4
+
5
+ # Local Search for DeepSeek Harness
6
+
7
+ **Read-only Everything filename search and Anytxt document-content search, behind one capability-based tool surface.**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@fishdivinity/dsh-local-search)](https://www.npmjs.com/package/@fishdivinity/dsh-local-search)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
11
+ [![platform](https://img.shields.io/badge/platform-Windows-blue)](#requirements)
12
+ [![dsh](https://img.shields.io/badge/dsh-%3E%3D0.2.1--0%20%3C0.3.0-blue)](#requirements)
13
+ [![CI](https://img.shields.io/github/actions/workflow/status/fishdivinity/dsh-local-search/ci.yml?branch=main&label=CI)](https://github.com/fishdivinity/dsh-local-search/actions)
14
+ [![GitHub stars](https://img.shields.io/github/stars/fishdivinity/dsh-local-search?style=flat)](https://github.com/fishdivinity/dsh-local-search)
15
+
16
+ [English](README.md) · [简体中文](README-zh.md) · [Install](#install) · [Configuration](#configuration) · [Tools](#tools) · [Security](#security-and-limits)
17
+
18
+ </div>
19
+
20
+ ## Contents
21
+
22
+ - [Tools](#tools)
23
+ - [Requirements](#requirements)
24
+ - [Install](#install)
25
+ - [Configuration](#configuration)
26
+ - [Scopes](#scopes)
27
+ - [Engine selection](#engine-selection)
28
+ - [Errors](#errors)
29
+ - [Security and limits](#security-and-limits)
30
+ - [Known limitations](#known-limitations)
31
+ - [Related plugins](#related-plugins)
32
+ - [Development](#development)
33
+ - [License](#license)
34
+
35
+ ## Tools
36
+
37
+ | Tool | Engine | Use for |
38
+ |---|---|---|
39
+ | `local_search_files` | Everything via `es.exe` | File/folder names, paths, cross-drive discovery |
40
+ | `local_search_content` | Anytxt JSON-RPC | Text inside PDF, DOCX, XLSX, PPTX, EPUB, RTF, TXT, MD |
41
+ | `local_search_fragments` | Anytxt | Snippets around a match in one indexed file |
42
+ | `local_search_read` | Anytxt | Extracted plain text of one indexed file, capped |
43
+ | `local_search_status` | both | Check whether each engine is reachable |
44
+
45
+ ## Requirements
46
+
47
+ - Windows.
48
+ - DeepSeek Harness `>=0.2.1-0 <0.3.0`.
49
+ - [Everything](https://www.voidtools.com/) running, plus `es.exe` (a separate download from voidtools; the plugin auto-detects PATH/common install folders, or set `esPath`).
50
+ - [Anytxt Searcher](https://anytxt.net/) for content search: the **GUI** (`ATGUI.exe`) must be running because the JSON-RPC service lives inside it. The indexing service alone is not enough.
51
+
52
+ This plugin does **not** bundle Everything, `es.exe`, or Anytxt. Install and license them separately.
53
+
54
+ ## Install
55
+
56
+ ```powershell
57
+ dsh plugin --profile desktop add @fishdivinity/dsh-local-search
58
+ ```
59
+
60
+ From GitHub before the first npm release:
61
+
62
+ ```powershell
63
+ dsh plugin --profile desktop add github:fishdivinity/dsh-local-search
64
+ ```
65
+
66
+ Restart the profile or open a new session. The five `local_search_*` tools appear in the model toolset.
67
+
68
+ ## Configuration
69
+
70
+ Sidebar **Plugins** → the **Local Search** card in the **Official plugins** group → the plugin's own page. That is the same card shape the GitHub plugin contributes. Saves apply to the next tool call; leaving the page drops unsaved edits, and a field's reset returns it to the profile layer.
71
+
72
+ **Test connection**, directly under the Anytxt endpoint, probes that address as typed — nothing has to be saved first — together with Everything, and answers in one line. A failing engine gets a link to its official download page; this plugin never bundles, downloads, or installs either product.
73
+
74
+ Every key is optional, and the row config replaces the whole config object rather than merging. Only `timeoutMs` stays in the profile's `cordis.patch.yml`: each tool fixes its deadline when it registers, so the page leaves it out rather than showing a field that would not take effect.
75
+
76
+ | Key | Default | Meaning |
77
+ |---|---|---|
78
+ | `esPath` | auto-detect | Absolute path to `es.exe`. Resolution order: `esPath`, `ES_PATH`, common install folders, `where.exe`. |
79
+ | `anytxtUrl` | `http://127.0.0.1:9920` | Anytxt JSON-RPC endpoint on this machine (`127.0.0.1`, `localhost` or `[::1]` only). The default also tries `:9921`. |
80
+ | `workspaceRoot` | session default directory | Root for `scope=workspace`. |
81
+ | `workRoot` | empty | Root for `scope=work`; required before using that scope. |
82
+ | `personalRoot` | empty | Root for `scope=personal`; required before using that scope. |
83
+ | `allRoots` | `C:\` plus `D:\` when present | Semicolon-separated roots for Anytxt `scope=all`. Everything `scope=all` searches all volumes it indexes. |
84
+ | `maxResults` | `20` | Default result cap. Hard cap is 100. |
85
+ | `timeoutMs` | `30000` | Per-call timeout budget; profile YAML only. |
86
+ | `readMaxChars` | `8000` | Default `local_search_read` window; hard cap is 20,000 characters. |
87
+ | `esEncoding` | `gbk` | Code page used to decode `es.exe` output. The page offers `gbk` and `utf-8` as a two-choice control. |
88
+
89
+ Example row override:
90
+
91
+ ```yaml
92
+ - id: fishdivinity-local-search
93
+ name: '@fishdivinity/dsh-local-search'
94
+ config:
95
+ workRoot: 'D:\Projects'
96
+ personalRoot: 'D:\Documents'
97
+ allRoots: 'C:\;D:\'
98
+ ```
99
+
100
+ ## Scopes
101
+
102
+ - `workspace` (default): the session's working directory.
103
+ - `work`: `workRoot`.
104
+ - `personal`: `personalRoot`.
105
+ - `all`: `allRoots` for Anytxt; all indexed volumes for Everything.
106
+
107
+ Use `all` only when the user explicitly asks for a cross-drive/global search. The `path` argument overrides `scope`.
108
+
109
+ ## Engine selection
110
+
111
+ `engine` defaults to `auto`:
112
+
113
+ - `local_search_files`: `auto` or `everything`.
114
+ - `local_search_content` / `fragments` / `read`: `auto` or `anytxt`.
115
+ - `local_search_status`: `auto`, `everything`, or `anytxt`.
116
+
117
+ The tool names stay stable if more engines are added; `engine` values are additive. Content items carry both `engine` and a self-describing `ref` such as `anytxt:123`; pass the ref unchanged to `local_search_fragments`/`local_search_read`, and pass `engine` only as an assertion.
118
+
119
+ ## Errors
120
+
121
+ Failures are typed `HarnessError`s with stable codes:
122
+
123
+ | Code | Meaning |
124
+ |---|---|
125
+ | `EVERYTHING_ES_NOT_FOUND` | `es.exe` is missing; install it or set `esPath`. |
126
+ | `EVERYTHING_DOWN` | Everything is not running / cannot be reached. |
127
+ | `EVERYTHING_TIMEOUT`, `EVERYTHING_TOO_LARGE` | Query too broad or slow; narrow it. |
128
+ | `ANYTXT_DOWN` | Anytxt GUI is not running or the JSON-RPC endpoint is unreachable. |
129
+ | `ANYTXT_TIMEOUT`, `ANYTXT_RPC`, `ANYTXT_HTTP` | Anytxt-side query/transport failure. |
130
+ | `ROOT_NOT_FOUND`, `SCOPE_NOT_CONFIGURED`, `INVALID_SCOPE` | Root/scope configuration problem. |
131
+ | `INVALID_ARGUMENT` | Empty query, bad limit, etc. |
132
+
133
+ If a search reports `*_DOWN`, fall back to `grep`/`glob` and say that you fell back.
134
+
135
+ ## Security and limits
136
+
137
+ - **Read-only**: no write, delete, index-sync, OCR, export, or settings operation is registered.
138
+ - `es.exe` is always spawned with a fixed argv; model text goes after `--`, so `-export-*`, `-save-settings`, `-clear-settings`, `-exit`, `-reindex`, `-save-db`, and run-count switches are unreachable.
139
+ - Anytxt is only called through `Search`, `GetResult`, `GetFragmentAll`, and `GetRawTextByFID`.
140
+ - No network access except the configured local Anytxt URL.
141
+ - Results are capped; `local_search_read` caps the returned text.
142
+ - The plugin runs with the user's privileges and can read files the user can read. Prefer `scope=workspace` unless a global search is requested.
143
+
144
+ ## Known limitations
145
+
146
+ - Windows only.
147
+ - Everything 1.4 indexes metadata, not file contents; use Anytxt for contents.
148
+ - Anytxt's JSON-RPC API is local and unofficial; GUI versions can change behavior. The plugin reports unreachable/wrong-shape responses instead of guessing.
149
+ - Anytxt's index may be stale. Verify important hits with `read`/the original file.
150
+ - No auto-start: starting `ATGUI.exe` writes the index and is left to the user.
151
+ - Anytxt's terms limit personal/non-commercial use; commercial use needs its own license.
152
+
153
+ ## Related plugins
154
+
155
+ - [`dsh-tool-everything`](https://dshbase.com/zh/plugins/dsh-tool-everything/) covers Everything filename search through the SDK.
156
+ - [`dsh-workspace-search`](https://github.com/tsonglew/dsh-workspace-search) covers workspace file names/contents in the sidebar.
157
+ - This plugin's differentiator is Anytxt document content plus one routing surface for names and contents.
158
+
159
+ ## Development
160
+
161
+ No dependencies; tests use Node's built-in test runner.
162
+
163
+ ```powershell
164
+ npm test
165
+ npm run check
166
+ npm run pack:check
167
+ ```
168
+
169
+ The engine and config modules (`lib/everything.js`, `lib/anytxt.js`, `lib/roots.js`, `lib/format.js`, `lib/config.js`) are independent of the DSH runtime so their tests run under plain Node. `lib/client.js` ships as the browser half: it is written directly in the client module system's lazy-CJS factory format, so there is no build step. See [`docs/architecture.md`](docs/architecture.md), [`docs/extending-engines.md`](docs/extending-engines.md), and [`.github/RELEASING.md`](.github/RELEASING.md).
170
+
171
+ ## Changelog
172
+
173
+ See [CHANGELOG.md](CHANGELOG.md). It stays in the repository; the npm tarball ships only the runtime files, README, LICENSE, locale, and icon.
174
+
175
+ ## License
176
+
177
+ MIT. Everything and Anytxt are separate products with their own licenses and trademarks; this project is not affiliated with voidtools or CBEWIN.
package/cordis.patch.yml CHANGED
@@ -1,9 +1,9 @@
1
- # dsh-local-search bundle patch: installs the local-search plugin row.
2
- # Every key has a schema default; override any of them here by replacing this
3
- # row's config (the whole row is replaced, never deep-merged).
4
- # Keep this file neutral (config: {}) in the package: machine-specific paths
5
- # such as esPath / workRoot belong in the user's profile patch, not here.
6
- - insert:
7
- - id: fishdivinity-local-search
8
- name: '@fishdivinity/dsh-local-search'
9
- config: {}
1
+ # dsh-local-search bundle patch: installs the local-search plugin row.
2
+ # Every key has a schema default; override any of them here by replacing this
3
+ # row's config (the whole row is replaced, never deep-merged).
4
+ # Keep this file neutral (config: {}) in the package: machine-specific paths
5
+ # such as esPath / workRoot belong in the user's profile patch, not here.
6
+ - insert:
7
+ - id: fishdivinity-local-search
8
+ name: '@fishdivinity/dsh-local-search'
9
+ config: {}
package/icon.svg CHANGED
@@ -1,7 +1,35 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Local search">
2
- <rect width="64" height="64" rx="12" fill="#1f6feb"/>
3
- <circle cx="27" cy="27" r="13" fill="none" stroke="#ffffff" stroke-width="5"/>
4
- <line x1="36.5" y1="36.5" x2="50" y2="50" stroke="#ffffff" stroke-width="5" stroke-linecap="round"/>
5
- <rect x="21" y="22" width="12" height="3" rx="1.5" fill="#ffffff" opacity="0.9"/>
6
- <rect x="21" y="28" width="9" height="3" rx="1.5" fill="#ffffff" opacity="0.7"/>
7
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="Local search">
2
+ <defs>
3
+ <linearGradient id="tile" x1="8" y1="4" x2="56" y2="60" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0" stop-color="#8fb2ff"/>
5
+ <stop offset="0.55" stop-color="#7b83ff"/>
6
+ <stop offset="1" stop-color="#a466f2"/>
7
+ </linearGradient>
8
+ <linearGradient id="lens" x1="14" y1="14" x2="40" y2="42" gradientUnits="userSpaceOnUse">
9
+ <stop offset="0" stop-color="#ffffff"/>
10
+ <stop offset="1" stop-color="#dfe6ff"/>
11
+ </linearGradient>
12
+ <linearGradient id="page" x1="20" y1="18" x2="34" y2="38" gradientUnits="userSpaceOnUse">
13
+ <stop offset="0" stop-color="#ffd98a"/>
14
+ <stop offset="1" stop-color="#ffb457"/>
15
+ </linearGradient>
16
+ <linearGradient id="handle" x1="38" y1="38" x2="52" y2="52" gradientUnits="userSpaceOnUse">
17
+ <stop offset="0" stop-color="#ffffff"/>
18
+ <stop offset="1" stop-color="#c8d4ff"/>
19
+ </linearGradient>
20
+ <radialGradient id="glow" cx="0" cy="0" r="1" gradientTransform="translate(16 12) rotate(48) scale(44)" gradientUnits="userSpaceOnUse">
21
+ <stop offset="0" stop-color="#ffffff" stop-opacity="0.55"/>
22
+ <stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
23
+ </radialGradient>
24
+ </defs>
25
+
26
+ <rect width="64" height="64" rx="14" fill="url(#tile)"/>
27
+ <rect width="64" height="64" rx="14" fill="url(#glow)"/>
28
+
29
+ <circle cx="27" cy="27" r="14" fill="none" stroke="url(#lens)" stroke-width="5"/>
30
+ <rect x="21" y="19" width="12" height="15" rx="2.5" fill="url(#page)"/>
31
+ <rect x="23.5" y="23" width="7" height="2" rx="1" fill="#8a5a12" opacity="0.75"/>
32
+ <rect x="23.5" y="27" width="5" height="2" rx="1" fill="#8a5a12" opacity="0.55"/>
33
+ <line x1="37.6" y1="37.6" x2="50" y2="50" stroke="url(#handle)" stroke-width="6" stroke-linecap="round"/>
34
+ <circle cx="49" cy="17" r="3" fill="#7ef0d0" opacity="0.9"/>
35
+ </svg>