@fizzyflow/wdoublesync_cli 1.0.6 → 1.0.8

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
@@ -24,6 +24,7 @@ pnpm link --global
24
24
  wdoublesync push [vector-id] [path] [options] Sync folder to a vector (creates one if no id given)
25
25
  wdoublesync pull <vector-id> [path] [options] Restore vector contents to a folder
26
26
  wdoublesync info [vector-id] [path] [options] Show chain/wallet info, or full vector metadata
27
+ wdoublesync diff <vector-id> [path] [options] List files added/modified/deleted locally vs the on-chain version
27
28
  wdoublesync watch <vector-id> [path] [options] Watch folder: auto-push on changes, auto-pull on remote updates
28
29
  wdoublesync rebate <vector-id> [path] [options] Burn archive patches and push folder as a fresh snapshot
29
30
  ```
@@ -37,9 +38,10 @@ The `[path]` argument is optional — if omitted, the current working directory
37
38
  | `--chain <name>` | Chain: `mainnet`, `testnet`, `devnet`, `localnet` (default: `testnet`) |
38
39
  | `--key <suiprivkey>` | Sui private key (or set `WDOUBLESYNC_KEY` env var) |
39
40
  | `--phrase <mnemonic>` | Mnemonic phrase instead of a raw key |
40
- | `--version <n>` | Version to restore (`pull` only, default: latest) |
41
+ | `--version <n>` | Version to restore or compare against (`pull`, `diff`; default: latest) |
41
42
  | `--exclude <p1,p2>` | Extra exclude patterns (comma-separated) |
42
43
  | `--no-compress` | Disable gzip compression |
44
+ | `--no-seal` | Create a new vector public (no Seal encryption). Only applies when creating; ignored for existing vectors |
43
45
  | `--manifest` | Write/use `.wdoublesync` manifest for faster change detection |
44
46
  | `--force-snapshot` | Push a full snapshot regardless of prior history (repairs a corrupt vector) |
45
47
  | `--poll-interval <s>` | `watch`: seconds between remote version checks (default: `2`) |
@@ -56,7 +58,7 @@ Supply a signing key in one of three ways (checked in order):
56
58
  2. `--phrase "word1 word2 ..."`
57
59
  3. `WDOUBLESYNC_KEY` environment variable
58
60
 
59
- `pull` and `info` work without a key for public (unencrypted) vectors. A key is required for Seal-encrypted vectors and for any `push`.
61
+ `pull`, `info` and `diff` work without a key for public (unencrypted) vectors. A key is required for Seal-encrypted vectors and for any `push`.
60
62
 
61
63
  ## Examples
62
64
 
@@ -74,6 +76,12 @@ wdoublesync push ~/my-project --chain testnet --key suiprivkey1...
74
76
  # version 1 pushed (full snapshot, gzip compressed)
75
77
  ```
76
78
 
79
+ By default a new vector is Seal-encrypted. To create a public (unencrypted) vector that anyone can pull without a key:
80
+
81
+ ```bash
82
+ wdoublesync push --chain testnet --key suiprivkey1... --no-seal
83
+ ```
84
+
77
85
  ### Push an update
78
86
 
79
87
  ```bash
@@ -106,6 +114,29 @@ wdoublesync info 0xabc123... --chain testnet
106
114
  wdoublesync info 0xabc123... ~/my-project --chain testnet
107
115
  ```
108
116
 
117
+ ### See what changed locally (diff)
118
+
119
+ ```bash
120
+ # list files added/modified/deleted locally compared to the latest on-chain version
121
+ wdoublesync diff 0xabc123... --chain testnet
122
+ wdoublesync diff 0xabc123... ~/my-project --chain testnet
123
+
124
+ # compare against a specific version
125
+ wdoublesync diff 0xabc123... --chain testnet --version 2
126
+ ```
127
+
128
+ Output shows the changes from the local folder's perspective — what a `push` would apply:
129
+
130
+ ```
131
+ diff: ~/my-project vs 0xabc123... (version 3 of 3)
132
+ local changes vs chain (what push would apply):
133
+ added: docs/new-page.md
134
+ modified: src/index.js
135
+ deleted: old-config.json
136
+
137
+ 3 change(s): 1 added, 1 modified, 1 deleted
138
+ ```
139
+
109
140
  ### Fast incremental pushes with a manifest
110
141
 
111
142
  ```bash
@@ -164,8 +195,9 @@ wdoublesync push 0xabc123... --chain testnet --key suiprivkey1... --force-snapsh
164
195
  1. **push** — scans the current directory, computes a tree hash, and compares it against the last stored version. If changes are detected, a compressed diff (or full snapshot on the first push) is uploaded to Walrus and appended to the EndlessVector on-chain.
165
196
  2. **pull** — reads the requested version from the EndlessVector, decrypts it if Seal-encrypted, and writes only changed files to disk. Files absent from the stored version are deleted.
166
197
  3. **info** — reads EndlessVector metadata from the chain (version count, binary size, history) and checks whether the local folder matches any stored version.
167
- 4. **watch** — combines push and pull in a loop. A filesystem watcher triggers a debounced push on local changes. A poll interval checks the remote vector for new versions and pulls them if found. Push and pull never run concurrently.
168
- 5. **rebate** — burns all existing archive patches on-chain and pushes the current folder as a single fresh full snapshot, reducing Walrus storage costs after many incremental syncs.
198
+ 4. **diff** — restores the requested version in memory (nothing is written to disk), hashes every file on both sides, and lists files added, modified, or deleted locally compared to the on-chain version.
199
+ 5. **watch** — combines push and pull in a loop. A filesystem watcher triggers a debounced push on local changes. A poll interval checks the remote vector for new versions and pulls them if found. Push and pull never run concurrently.
200
+ 6. **rebate** — burns all existing archive patches on-chain and pushes the current folder as a single fresh full snapshot, reducing Walrus storage costs after many incremental syncs.
169
201
 
170
202
  ### Default excludes
171
203
 
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: wdoublesync
3
- version: 0.1.8
4
- description: Claude agent skill for decentralized file storage, versioning, and sync on Sui + Walrus + Seal. Enables agents to push local folders as on-chain vectors, pull versions, manage Walrus storage, and watch for changes.
3
+ version: 0.1.9
4
+ description: Agent skill for decentralized file storage, versioning, and sync on Sui + Walrus + Seal. Enables agents to push local folders as on-chain vectors, pull versions, manage Walrus storage, and watch for changes.
5
5
  keywords: [walrus, sui, storage, versioning, sync, seal-encryption, blockchain, defi]
6
6
  ---
7
7
 
@@ -66,7 +66,7 @@ wdoublesync push ~/my-folder
66
66
  # Output: created: 0x1234... / syncing ./ → 0x1234...
67
67
  ```
68
68
 
69
- This creates a new EndlessVector on-chain and pushes the folder as a snapshot encrypted by Seal. The output will show the vector ID (e.g., `0x1234...`) which you can use for future pulls or pushes. It's a good idea to save this ID to MemWal, with addition to the path info, so you can easily find it later when you want to pull or view the vector state. Ask a user if they want to set up MemWal if it's not available and you are going to push new vector.
69
+ This creates a new EndlessVector on-chain and pushes the folder as a snapshot encrypted by Seal. To create a **public** vector instead — unencrypted, pullable without a key and browsable from the dApp — add `--no-seal` (only effective on creation). The output will show the vector ID (e.g., `0x1234...`) which you can use for future pulls or pushes. It's a good idea to save this ID to MemWal, with addition to the path info, so you can easily find it later when you want to pull or view the vector state. Ask a user if they want to set up MemWal if it's not available and you are going to push new vector.
70
70
 
71
71
  ### Pull a vector to restore it
72
72
 
@@ -97,6 +97,7 @@ wdoublesync watch 0x1234... ~/my-data --poll-interval 5
97
97
  | **push** | `[vectorId]` `[path]` `[options]` | Sync folder to vector; creates new vector if no ID given |
98
98
  | **pull** | `vectorId` `[path]` `[options]` | Restore vector (default: latest version) into folder |
99
99
  | **info** | `[vectorId]` `[path]` | No ID: show chain + wallet. With ID: full vector details |
100
+ | **diff** | `vectorId` `[path]` `[options]` | List files added/modified/deleted locally vs the on-chain version (read-only, writes nothing) |
100
101
  | **watch** | `vectorId` `[path]` `[options]` | Bi-directional sync: auto-push local changes, auto-pull remote updates |
101
102
  | **rebate** | `vectorId` `[path]` | Burn archive patches, push folder as fresh single snapshot |
102
103
 
@@ -111,6 +112,14 @@ wdoublesync push ~/my-data
111
112
  # syncing ./ → 0xabc123...
112
113
  ```
113
114
 
115
+ #### push (create public, unencrypted vector)
116
+
117
+ ```bash
118
+ wdoublesync push ~/my-data --no-seal
119
+ # Creates a public vector — anyone can pull it without a key, or browse it from the dApp.
120
+ # Only effective on creation; ignored when pushing to an existing vector.
121
+ ```
122
+
114
123
  #### push (to existing vector)
115
124
 
116
125
  ```bash
@@ -142,6 +151,19 @@ wdoublesync pull 0x1234... ~/restored
142
151
  wdoublesync pull 0x1234... ~/restored --version 10
143
152
  ```
144
153
 
154
+ #### diff (preview local changes before pushing)
155
+
156
+ ```bash
157
+ wdoublesync diff 0x1234... ~/my-data
158
+ # Output: local changes vs chain (what push would apply):
159
+ # added: docs/new-page.md
160
+ # modified: src/index.js
161
+ # deleted: old-config.json
162
+ # 3 change(s): 1 added, 1 modified, 1 deleted
163
+ # Compare against an older version with --version N
164
+ # Prints "no changes" when local matches the chain. Writes nothing to disk.
165
+ ```
166
+
145
167
  #### watch (bi-directional)
146
168
 
147
169
  ```bash
@@ -176,9 +198,10 @@ wdoublesync rebate 0x1234... ~/my-data
176
198
  | `--key` | Sui private key (`suiprivkey1...`) | — | Sign transactions (or use env vars) |
177
199
  | `--phrase` | Mnemonic phrase | — | Alternative: derive key from mnemonic |
178
200
  | `--no-compress` | — | — | Push: disable gzip compression |
201
+ | `--no-seal` | — | — | Push: create a new vector public (no Seal encryption). Only applies when creating a vector; ignored for existing ones |
179
202
  | `--manifest` | — | — | Push: write/use `.wdoublesync` file for faster change detection |
180
203
  | `--force-snapshot` | — | — | Push: full snapshot (repairs corrupt vectors) |
181
- | `--version` | Number | `latest` | Pull: restore specific version instead of latest |
204
+ | `--version` | Number | `latest` | Pull/diff: use a specific version instead of latest |
182
205
  | `--exclude` | Patterns (comma-separated) | — | Extra file patterns to exclude from sync |
183
206
  | `--poll-interval` | Seconds | `2` | Watch: check for remote updates every N seconds |
184
207
  | `--debounce` | Milliseconds | `1000` | Watch: quiet period before pushing after local change |
@@ -11,7 +11,7 @@ process.exit = (code) => {
11
11
  throw err;
12
12
  };
13
13
 
14
- import { push, pull, info, watch, rebate } from '../lib/commands/index.js';
14
+ import { push, pull, info, diff, watch, rebate } from '../lib/commands/index.js';
15
15
 
16
16
  const USAGE = `wdoublesync — sync local folders to EndlessVector on Sui+Walrus
17
17
 
@@ -19,6 +19,7 @@ Usage:
19
19
  wdoublesync push [vector-id] [options] Sync current folder to vector (creates if no id)
20
20
  wdoublesync pull <vector-id> [options] Restore vector contents to current folder
21
21
  wdoublesync info <vector-id> [options] Show vector metadata
22
+ wdoublesync diff <vector-id> [options] List local files added/modified/deleted vs the on-chain version
22
23
  wdoublesync watch <vector-id> [options] Watch folder and auto-push/pull
23
24
  wdoublesync rebate <vector-id> [options] Archive history, burn archives, push fresh snapshot
24
25
 
@@ -26,9 +27,10 @@ Options:
26
27
  --chain <name> Chain: mainnet, testnet, devnet, localnet (default: testnet)
27
28
  --key <suiprivkey> Sui private key (or set WDOUBLESYNC_KEY env var)
28
29
  --phrase <mnemonic> Mnemonic phrase
29
- --version <n> Version to restore (pull only, default: latest)
30
+ --version <n> Version to restore/compare (pull, diff; default: latest)
30
31
  --exclude <p1,p2> Extra exclude patterns (comma-separated)
31
32
  --no-compress Disable gzip compression
33
+ --no-seal Create a new vector public (no Seal encryption); only applies on creation
32
34
  --manifest Write/use .wdoublesync manifest for faster change detection
33
35
  --force-snapshot Push a full snapshot regardless of prior history (repairs corrupt vectors)
34
36
  --poll-interval <s> Watch: seconds between remote checks (default: 2)
@@ -49,6 +51,7 @@ function parseArgs(argv) {
49
51
  version: null,
50
52
  exclude: null,
51
53
  compress: 'gzip',
54
+ seal: true,
52
55
  manifest: false,
53
56
  forceSnapshot: false,
54
57
  pollInterval: 2,
@@ -100,6 +103,8 @@ function parseArgs(argv) {
100
103
  args.exclude = raw[++i];
101
104
  } else if (flag === '--no-compress') {
102
105
  args.compress = false;
106
+ } else if (flag === '--no-seal') {
107
+ args.seal = false;
103
108
  } else if (flag === '--manifest') {
104
109
  args.manifest = true;
105
110
  } else if (flag === '--force-snapshot') {
@@ -134,6 +139,8 @@ try {
134
139
  await pull(args);
135
140
  } else if (args.command === 'info') {
136
141
  await info(args);
142
+ } else if (args.command === 'diff') {
143
+ await diff(args);
137
144
  } else if (args.command === 'watch') {
138
145
  await watch(args);
139
146
  } else if (args.command === 'rebate') {
@@ -4,6 +4,10 @@ import { SuiGrpcClient, GrpcWebFetchTransport } from '@mysten/sui/grpc';
4
4
  import { Transaction } from '@mysten/sui/transactions';
5
5
  import { fromHex } from '@mysten/sui/utils';
6
6
 
7
+ // Mysten's mainnet Seal committee key server requires an API key.
8
+ const MAINNET_SEAL_API_KEY_NAME = 'x-api-key';
9
+ const MAINNET_SEAL_API_KEY = 'c2f9b915-a4f3-41a2-bf91-99a630894db3';
10
+
7
11
  class WalrusSealClient {
8
12
  /** @type {SuiGrpcClient} */
9
13
  suiClient;
@@ -156,10 +160,12 @@ class WalrusSealClient {
156
160
  { objectId: '0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8', weight: 1 },
157
161
  ],
158
162
  mainnet: [
159
- {
160
- objectId: '0x686098f1439237fff9f36b99c7329683c22979d2005c2465cb891acb012a7595',
163
+ {
164
+ objectId: '0x686098f1439237fff9f36b99c7329683c22979d2005c2465cb891acb012a7595',
161
165
  weight: 1,
162
- aggregatorUrl: "https://seal-aggregator-mainnet.mystenlabs.com",
166
+ aggregatorUrl: 'https://seal-aggregator-mainnet.mystenlabs.com',
167
+ apiKeyName: MAINNET_SEAL_API_KEY_NAME,
168
+ apiKey: MAINNET_SEAL_API_KEY,
163
169
  },
164
170
  ],
165
171
  };
@@ -0,0 +1,114 @@
1
+ import EndlessVector from '@fizzyflow/endless-vector';
2
+ import { WDoubleSync } from '@fizzyflow/wdoublesync';
3
+ import { DoubleSyncFSFolder } from '../DoubleSyncFSFolder.js';
4
+ import {
5
+ makeClientForRead,
6
+ resolvePath,
7
+ makeExcludes,
8
+ formatPath,
9
+ hashLocalTree,
10
+ } from './shared.js';
11
+
12
+ export function compareFileMaps(localFiles, remoteFiles) {
13
+ const added = [];
14
+ const modified = [];
15
+ const deleted = [];
16
+
17
+ for (const path of Object.keys(localFiles)) {
18
+ if (!(path in remoteFiles)) {
19
+ added.push(path);
20
+ } else if (localFiles[path] !== remoteFiles[path]) {
21
+ modified.push(path);
22
+ }
23
+ }
24
+ for (const path of Object.keys(remoteFiles)) {
25
+ if (!(path in localFiles)) {
26
+ deleted.push(path);
27
+ }
28
+ }
29
+
30
+ added.sort();
31
+ modified.sort();
32
+ deleted.sort();
33
+
34
+ return { added, modified, deleted };
35
+ }
36
+
37
+ export async function diff(args) {
38
+ if (!args.vectorId) {
39
+ throw new Error('vector-id is required for diff');
40
+ }
41
+
42
+ const wsc = await makeClientForRead(args);
43
+
44
+ const evParams = {
45
+ suiClient: wsc.suiClient,
46
+ id: args.vectorId,
47
+ packageId: args.chain,
48
+ walrusClient: wsc.walrusClient,
49
+ aggregatorUrl: wsc.aggregatorUrl,
50
+ };
51
+ if (wsc.sealClient && wsc.suiMaster) {
52
+ evParams.sealClient = wsc.sealClient;
53
+ evParams.signAndExecuteTransaction = (tx) => wsc.signAndExecuteTransaction(tx);
54
+ evParams.senderAddress = wsc.suiMaster.address;
55
+ evParams.signer = wsc.suiMaster._keypair;
56
+ }
57
+
58
+ const ev = new EndlessVector(evParams);
59
+ const wdsync = new WDoubleSync({ endlessVector: ev });
60
+
61
+ await ev.initialize();
62
+ const totalVersions = ev.length;
63
+
64
+ if (totalVersions === 0) {
65
+ console.log('vector', args.vectorId, 'has no versions yet — nothing to diff');
66
+ return;
67
+ }
68
+
69
+ const version = args.version != null ? Number(args.version) : totalVersions;
70
+ if (!Number.isInteger(version) || version < 1 || version > totalVersions) {
71
+ throw new Error('version must be between 1 and ' + totalVersions);
72
+ }
73
+
74
+ const destPath = resolvePath(args.path);
75
+ console.log('diff:', formatPath(destPath), 'vs', args.vectorId, '(version ' + version + ' of ' + totalVersions + ')');
76
+ console.log(' restoring chain version...');
77
+
78
+ let remoteFolder;
79
+ try {
80
+ remoteFolder = await wdsync.restore(version === totalVersions ? undefined : version);
81
+ } catch (err) {
82
+ if (err.message?.includes('sealClient not configured') || err.message?.includes('signer or sessionKey is required')) {
83
+ throw new Error('This vector is Seal-encrypted. Provide --key or --phrase to decrypt.');
84
+ }
85
+ throw err;
86
+ }
87
+
88
+ const remoteFiles = await hashLocalTree(remoteFolder);
89
+ const fsFolder = new DoubleSyncFSFolder(destPath, makeExcludes(args));
90
+ let localFiles = {};
91
+ try {
92
+ localFiles = await hashLocalTree(fsFolder);
93
+ } catch {
94
+ // local folder missing or unreadable — treat as empty
95
+ }
96
+
97
+ const { added, modified, deleted } = compareFileMaps(localFiles, remoteFiles);
98
+ const total = added.length + modified.length + deleted.length;
99
+
100
+ if (total === 0) {
101
+ console.log(' no changes — local folder matches version', version);
102
+ return;
103
+ }
104
+
105
+ console.log('\nlocal changes vs chain (what push would apply):');
106
+ for (const path of added) console.log(' added: ', path);
107
+ for (const path of modified) console.log(' modified: ', path);
108
+ for (const path of deleted) console.log(' deleted: ', path);
109
+
110
+ console.log('\n ' + total + ' change(s): ' +
111
+ added.length + ' added, ' +
112
+ modified.length + ' modified, ' +
113
+ deleted.length + ' deleted');
114
+ }
@@ -1,5 +1,6 @@
1
1
  export { push } from './push.js';
2
2
  export { pull } from './pull.js';
3
3
  export { info } from './info.js';
4
+ export { diff } from './diff.js';
4
5
  export { watch } from './watch.js';
5
6
  export { rebate } from './rebate.js';
@@ -48,10 +48,15 @@ export async function push(args) {
48
48
  };
49
49
 
50
50
  if (!vectorId) {
51
- console.log('creating new EndlessVector on', chain, '...');
52
- const ev = await EndlessVector.create(evParams);
51
+ // Seal encryption is decided at creation time, solely by whether a sealClient
52
+ // is passed to create(). Omit it for a public (unencrypted) vector.
53
+ const createParams = args.seal === false ? { ...evParams, sealClient: undefined } : evParams;
54
+ console.log('creating new EndlessVector on', chain, args.seal === false ? '(public, no Seal)' : '(Seal-encrypted)', '...');
55
+ const ev = await EndlessVector.create(createParams);
53
56
  vectorId = ev.id;
54
57
  console.log(' created:', vectorId);
58
+ } else if (args.seal === false) {
59
+ console.log(' note: --no-seal only applies when creating a new vector; pushing to an existing vector keeps its current encryption');
55
60
  }
56
61
 
57
62
  console.log('syncing ./ →', vectorId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fizzyflow/wdoublesync_cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "type": "module",
5
5
  "description": "CLI tool built on the wdoublesync library to sync local folders to Walrus decentralised storage on the Sui network. Stores versioned gzip-compressed snapshots and diffs inside an EndlessVector on-chain object. Supports Seal encryption, manifest-based fast change detection, and full version history with point-in-time restore.",
6
6
  "bin": {
@@ -11,11 +11,13 @@
11
11
  },
12
12
  "scripts": {
13
13
  "test": "echo \"Error: no test specified\" && exit 1",
14
- "pull:test": "mkdir -p test_pull && cd test_pull && node ../bin/wdoublesync.js pull 0x69d7d77c55d66c4c6d0a87d120caf0535fdbb3c8c414658f81f66a4d50c80120 --chain testnet",
15
- "push:test": "cd test_pull && node ../bin/wdoublesync.js push 0x69d7d77c55d66c4c6d0a87d120caf0535fdbb3c8c414658f81f66a4d50c80120 --chain testnet",
16
- "pushfull:test": "cd test_pull && node ../bin/wdoublesync.js push 0x69d7d77c55d66c4c6d0a87d120caf0535fdbb3c8c414658f81f66a4d50c80120 --chain testnet --force-snapshot",
17
- "info:test": "cd test_pull && node ../bin/wdoublesync.js info 0x69d7d77c55d66c4c6d0a87d120caf0535fdbb3c8c414658f81f66a4d50c80120 --chain testnet",
18
- "watch:test": "cd test_pull && node ../bin/wdoublesync.js watch 0x69d7d77c55d66c4c6d0a87d120caf0535fdbb3c8c414658f81f66a4d50c80120 --chain testnet"
14
+ "pushnew:test": "cd test_pull && node ../bin/wdoublesync.js push --chain mainnet",
15
+ "pull:test": "mkdir -p test_pull && cd test_pull && node ../bin/wdoublesync.js pull 0x77f779ed40404203406f1854bb7dd183fbc4743d28beb056057823c6b2b565c0 --chain mainnet",
16
+ "push:test": "cd test_pull && node ../bin/wdoublesync.js push 0x77f779ed40404203406f1854bb7dd183fbc4743d28beb056057823c6b2b565c0 --chain mainnet",
17
+ "pushfull:test": "cd test_pull && node ../bin/wdoublesync.js push 0x77f779ed40404203406f1854bb7dd183fbc4743d28beb056057823c6b2b565c0 --chain mainnet --force-snapshot",
18
+ "info:test": "cd test_pull && node ../bin/wdoublesync.js info 0x77f779ed40404203406f1854bb7dd183fbc4743d28beb056057823c6b2b565c0 --chain mainnet",
19
+ "diff:test": "cd test_pull && node ../bin/wdoublesync.js diff 0x77f779ed40404203406f1854bb7dd183fbc4743d28beb056057823c6b2b565c0 --chain mainnet",
20
+ "watch:test": "cd test_pull && node ../bin/wdoublesync.js watch 0x77f779ed40404203406f1854bb7dd183fbc4743d28beb056057823c6b2b565c0 --chain mainnet"
19
21
  },
20
22
  "keywords": [],
21
23
  "author": "suidouble (https://github.com/suidouble)",