appydave-tools 0.70.0 → 0.71.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.
- checksums.yaml +4 -4
- data/.claude/commands/brainstorming-agent.md +227 -0
- data/.claude/commands/cli-test.md +251 -0
- data/.claude/commands/dev.md +234 -0
- data/.claude/commands/po.md +227 -0
- data/.claude/commands/progress.md +51 -0
- data/.claude/commands/uat.md +321 -0
- data/.rubocop.yml +9 -0
- data/AGENTS.md +43 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +26 -3
- data/README.md +15 -0
- data/bin/dam +21 -1
- data/bin/jump.rb +29 -0
- data/bin/subtitle_processor.rb +54 -1
- data/bin/zsh_history.rb +846 -0
- data/docs/README.md +162 -69
- data/docs/architecture/cli/exe-bin-convention.md +434 -0
- data/docs/architecture/cli-patterns.md +631 -0
- data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
- data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
- data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
- data/docs/architecture/testing/testing-patterns.md +762 -0
- data/docs/backlog.md +120 -0
- data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
- data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
- data/docs/specs/fr-003-jump-location-tool.md +779 -0
- data/docs/specs/zsh-history-tool.md +820 -0
- data/docs/uat/FR-3-jump-location-tool.md +741 -0
- data/exe/jump +11 -0
- data/exe/{subtitle_manager → subtitle_processor} +1 -1
- data/exe/zsh_history +11 -0
- data/lib/appydave/tools/configuration/openai.rb +1 -1
- data/lib/appydave/tools/dam/file_helper.rb +28 -0
- data/lib/appydave/tools/dam/project_listing.rb +4 -30
- data/lib/appydave/tools/dam/s3_operations.rb +2 -1
- data/lib/appydave/tools/dam/ssd_status.rb +226 -0
- data/lib/appydave/tools/dam/status.rb +3 -51
- data/lib/appydave/tools/jump/cli.rb +561 -0
- data/lib/appydave/tools/jump/commands/add.rb +52 -0
- data/lib/appydave/tools/jump/commands/base.rb +43 -0
- data/lib/appydave/tools/jump/commands/generate.rb +153 -0
- data/lib/appydave/tools/jump/commands/remove.rb +58 -0
- data/lib/appydave/tools/jump/commands/report.rb +214 -0
- data/lib/appydave/tools/jump/commands/update.rb +42 -0
- data/lib/appydave/tools/jump/commands/validate.rb +54 -0
- data/lib/appydave/tools/jump/config.rb +233 -0
- data/lib/appydave/tools/jump/formatters/base.rb +48 -0
- data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
- data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
- data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
- data/lib/appydave/tools/jump/location.rb +134 -0
- data/lib/appydave/tools/jump/path_validator.rb +47 -0
- data/lib/appydave/tools/jump/search.rb +230 -0
- data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools/zsh_history/command.rb +37 -0
- data/lib/appydave/tools/zsh_history/config.rb +235 -0
- data/lib/appydave/tools/zsh_history/filter.rb +184 -0
- data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
- data/lib/appydave/tools/zsh_history/parser.rb +101 -0
- data/lib/appydave/tools.rb +25 -0
- data/package.json +1 -1
- metadata +51 -4
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# exe/ vs bin/ Directory Convention
|
|
2
|
+
|
|
3
|
+
This document explains the convention for organizing CLI executables in appydave-tools, following standard RubyGems practices.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Directory Purposes](#directory-purposes)
|
|
9
|
+
- [How It Works](#how-it-works)
|
|
10
|
+
- [File Structure Examples](#file-structure-examples)
|
|
11
|
+
- [Creating New CLI Tools](#creating-new-cli-tools)
|
|
12
|
+
- [Gemspec Configuration](#gemspec-configuration)
|
|
13
|
+
- [Development vs Installation](#development-vs-installation)
|
|
14
|
+
- [Why This Pattern?](#why-this-pattern)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
The project uses two directories for executables:
|
|
21
|
+
|
|
22
|
+
| Directory | Purpose | File Extension | When Used |
|
|
23
|
+
|-----------|---------|----------------|-----------|
|
|
24
|
+
| `bin/` | Full CLI implementation | `.rb` | Development |
|
|
25
|
+
| `exe/` | Thin wrapper for gem installation | None | Gem users |
|
|
26
|
+
|
|
27
|
+
This follows the standard RubyGems convention where:
|
|
28
|
+
- **`bin/`** contains development scripts and the full CLI implementation
|
|
29
|
+
- **`exe/`** contains the executables that get installed when users run `gem install`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Directory Purposes
|
|
34
|
+
|
|
35
|
+
### bin/ Directory
|
|
36
|
+
|
|
37
|
+
**Purpose:** Contains the actual CLI implementation code.
|
|
38
|
+
|
|
39
|
+
**Contents:**
|
|
40
|
+
- Full Ruby scripts with CLI classes
|
|
41
|
+
- OptionParser configuration
|
|
42
|
+
- Command routing logic
|
|
43
|
+
- All CLI-specific code
|
|
44
|
+
|
|
45
|
+
**File naming:** `tool_name.rb` (with `.rb` extension)
|
|
46
|
+
|
|
47
|
+
**Example:**
|
|
48
|
+
```
|
|
49
|
+
bin/
|
|
50
|
+
├── subtitle_processor.rb # Full 200-line CLI implementation
|
|
51
|
+
├── gpt_context.rb # Full CLI implementation
|
|
52
|
+
├── youtube_manager.rb # Full CLI implementation
|
|
53
|
+
└── console # IRB console for development
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### exe/ Directory
|
|
57
|
+
|
|
58
|
+
**Purpose:** Contains thin wrapper scripts that get installed as system commands.
|
|
59
|
+
|
|
60
|
+
**Contents:**
|
|
61
|
+
- Minimal Ruby scripts (typically 3-7 lines)
|
|
62
|
+
- Just loads the corresponding `bin/` file
|
|
63
|
+
- No business logic
|
|
64
|
+
|
|
65
|
+
**File naming:** `tool_name` (NO `.rb` extension)
|
|
66
|
+
|
|
67
|
+
**Example:**
|
|
68
|
+
```
|
|
69
|
+
exe/
|
|
70
|
+
├── subtitle_processor # Wrapper → loads bin/subtitle_processor.rb
|
|
71
|
+
├── gpt_context # Wrapper → loads bin/gpt_context.rb
|
|
72
|
+
├── youtube_manager # Wrapper → loads bin/youtube_manager.rb
|
|
73
|
+
└── ad_config # Wrapper → loads bin/configuration.rb
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## How It Works
|
|
79
|
+
|
|
80
|
+
### The Wrapper Pattern
|
|
81
|
+
|
|
82
|
+
Each `exe/` file is a thin wrapper that loads its corresponding `bin/` implementation:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
#!/usr/bin/env ruby
|
|
86
|
+
# frozen_string_literal: true
|
|
87
|
+
|
|
88
|
+
require 'appydave/tools'
|
|
89
|
+
|
|
90
|
+
load File.expand_path('../bin/subtitle_processor.rb', __dir__)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Flow Diagram
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
User runs: subtitle_processor clean -f input.srt -o output.srt
|
|
97
|
+
│
|
|
98
|
+
▼
|
|
99
|
+
┌─────────────────────────────────────────────────────────┐
|
|
100
|
+
│ exe/subtitle_processor (installed in PATH) │
|
|
101
|
+
│ ├── require 'appydave/tools' │
|
|
102
|
+
│ └── load '../bin/subtitle_processor.rb' │
|
|
103
|
+
└─────────────────────────────────────────────────────────┘
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
┌─────────────────────────────────────────────────────────┐
|
|
107
|
+
│ bin/subtitle_processor.rb (full implementation) │
|
|
108
|
+
│ ├── class SubtitleProcessorCLI │
|
|
109
|
+
│ ├── def clean_subtitles(args) │
|
|
110
|
+
│ ├── def join_subtitles(args) │
|
|
111
|
+
│ └── SubtitleProcessorCLI.new.run │
|
|
112
|
+
└─────────────────────────────────────────────────────────┘
|
|
113
|
+
│
|
|
114
|
+
▼
|
|
115
|
+
┌─────────────────────────────────────────────────────────┐
|
|
116
|
+
│ lib/appydave/tools/subtitle_processor/clean.rb │
|
|
117
|
+
│ (business logic) │
|
|
118
|
+
└─────────────────────────────────────────────────────────┘
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## File Structure Examples
|
|
124
|
+
|
|
125
|
+
### Complete Example: subtitle_processor
|
|
126
|
+
|
|
127
|
+
**exe/subtitle_processor** (7 lines - wrapper):
|
|
128
|
+
```ruby
|
|
129
|
+
#!/usr/bin/env ruby
|
|
130
|
+
# frozen_string_literal: true
|
|
131
|
+
|
|
132
|
+
require 'appydave/tools'
|
|
133
|
+
|
|
134
|
+
load File.expand_path('../bin/subtitle_processor.rb', __dir__)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**bin/subtitle_processor.rb** (200+ lines - full implementation):
|
|
138
|
+
```ruby
|
|
139
|
+
#!/usr/bin/env ruby
|
|
140
|
+
# frozen_string_literal: true
|
|
141
|
+
|
|
142
|
+
require 'optparse'
|
|
143
|
+
|
|
144
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
145
|
+
|
|
146
|
+
require 'appydave/tools'
|
|
147
|
+
|
|
148
|
+
class SubtitleProcessorCLI
|
|
149
|
+
def initialize
|
|
150
|
+
@commands = {
|
|
151
|
+
'clean' => method(:clean_subtitles),
|
|
152
|
+
'join' => method(:join_subtitles),
|
|
153
|
+
'transcript' => method(:transcript_subtitles)
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def run
|
|
158
|
+
command, *args = ARGV
|
|
159
|
+
# ... command routing
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def clean_subtitles(args)
|
|
165
|
+
# ... 50 lines of option parsing and execution
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def join_subtitles(args)
|
|
169
|
+
# ... 50 lines of option parsing and execution
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# ... more methods
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
SubtitleProcessorCLI.new.run
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Example: gpt_context
|
|
179
|
+
|
|
180
|
+
**exe/gpt_context**:
|
|
181
|
+
```ruby
|
|
182
|
+
#!/usr/bin/env ruby
|
|
183
|
+
# frozen_string_literal: true
|
|
184
|
+
|
|
185
|
+
require 'appydave/tools'
|
|
186
|
+
|
|
187
|
+
load File.expand_path('../bin/gpt_context.rb', __dir__)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Example: ad_config (different naming)
|
|
191
|
+
|
|
192
|
+
Sometimes the exe/ name differs from bin/ name:
|
|
193
|
+
|
|
194
|
+
**exe/ad_config** → **bin/configuration.rb**:
|
|
195
|
+
```ruby
|
|
196
|
+
#!/usr/bin/env ruby
|
|
197
|
+
# frozen_string_literal: true
|
|
198
|
+
|
|
199
|
+
require 'appydave/tools'
|
|
200
|
+
|
|
201
|
+
load File.expand_path('../bin/configuration.rb', __dir__)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Creating New CLI Tools
|
|
207
|
+
|
|
208
|
+
### Step 1: Create the bin/ Implementation
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
touch bin/my_tool.rb
|
|
212
|
+
chmod +x bin/my_tool.rb
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**bin/my_tool.rb:**
|
|
216
|
+
```ruby
|
|
217
|
+
#!/usr/bin/env ruby
|
|
218
|
+
# frozen_string_literal: true
|
|
219
|
+
|
|
220
|
+
require 'optparse'
|
|
221
|
+
|
|
222
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
223
|
+
|
|
224
|
+
require 'appydave/tools'
|
|
225
|
+
|
|
226
|
+
class MyToolCLI
|
|
227
|
+
def run
|
|
228
|
+
# Full CLI implementation here
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
MyToolCLI.new.run
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Step 2: Create the exe/ Wrapper
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
touch exe/my_tool
|
|
239
|
+
chmod +x exe/my_tool
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**exe/my_tool:**
|
|
243
|
+
```ruby
|
|
244
|
+
#!/usr/bin/env ruby
|
|
245
|
+
# frozen_string_literal: true
|
|
246
|
+
|
|
247
|
+
require 'appydave/tools'
|
|
248
|
+
|
|
249
|
+
load File.expand_path('../bin/my_tool.rb', __dir__)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Step 3: Verify in Gemspec
|
|
253
|
+
|
|
254
|
+
The gemspec automatically includes all `exe/` files:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
# appydave-tools.gemspec
|
|
258
|
+
spec.bindir = 'exe'
|
|
259
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Step 4: Test Both Ways
|
|
263
|
+
|
|
264
|
+
**Development (from project directory):**
|
|
265
|
+
```bash
|
|
266
|
+
bin/my_tool.rb --help
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**After gem install:**
|
|
270
|
+
```bash
|
|
271
|
+
my_tool --help
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Gemspec Configuration
|
|
277
|
+
|
|
278
|
+
The gemspec defines how executables are installed:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# appydave-tools.gemspec
|
|
282
|
+
|
|
283
|
+
Gem::Specification.new do |spec|
|
|
284
|
+
# ...
|
|
285
|
+
|
|
286
|
+
# The directory containing executables
|
|
287
|
+
spec.bindir = 'exe'
|
|
288
|
+
|
|
289
|
+
# Automatically find all files in exe/ directory
|
|
290
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
291
|
+
|
|
292
|
+
# ...
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### What This Means
|
|
297
|
+
|
|
298
|
+
1. **`spec.bindir = 'exe'`** - Tells RubyGems to look in `exe/` for executables
|
|
299
|
+
2. **`spec.executables = ...`** - Lists all files in `exe/` as installable commands
|
|
300
|
+
3. **No `.rb` extension** - Files in `exe/` don't have extensions, so `exe/gpt_context` becomes the `gpt_context` command
|
|
301
|
+
|
|
302
|
+
### Verification
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# List what will be installed as executables
|
|
306
|
+
ruby -e "puts Gem::Specification.load('appydave-tools.gemspec').executables"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Expected output:
|
|
310
|
+
```
|
|
311
|
+
gpt_context
|
|
312
|
+
youtube_manager
|
|
313
|
+
prompt_tools
|
|
314
|
+
youtube_automation
|
|
315
|
+
ad_config
|
|
316
|
+
dam
|
|
317
|
+
subtitle_processor
|
|
318
|
+
zsh_history
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Development vs Installation
|
|
324
|
+
|
|
325
|
+
### During Development
|
|
326
|
+
|
|
327
|
+
Run tools directly from `bin/` with the `.rb` extension:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# From project root
|
|
331
|
+
bin/subtitle_processor.rb clean -f input.srt -o output.srt
|
|
332
|
+
|
|
333
|
+
# Or make it executable and run directly
|
|
334
|
+
chmod +x bin/subtitle_processor.rb
|
|
335
|
+
./bin/subtitle_processor.rb clean -f input.srt -o output.srt
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Why use bin/ during development:**
|
|
339
|
+
- No gem installation needed
|
|
340
|
+
- Changes take effect immediately
|
|
341
|
+
- `$LOAD_PATH` is set to use local `lib/`
|
|
342
|
+
|
|
343
|
+
### After Gem Installation
|
|
344
|
+
|
|
345
|
+
Users run the command without extension:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# After: gem install appydave-tools
|
|
349
|
+
subtitle_processor clean -f input.srt -o output.srt
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**What happens:**
|
|
353
|
+
1. Shell finds `subtitle_processor` in PATH (installed by gem)
|
|
354
|
+
2. That's actually `exe/subtitle_processor`
|
|
355
|
+
3. Which loads `bin/subtitle_processor.rb` from the installed gem
|
|
356
|
+
4. Which uses `lib/appydave/tools/` from the installed gem
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Why This Pattern?
|
|
361
|
+
|
|
362
|
+
### 1. Separation of Concerns
|
|
363
|
+
|
|
364
|
+
| Directory | Responsibility |
|
|
365
|
+
|-----------|----------------|
|
|
366
|
+
| `exe/` | Entry point (what gets installed) |
|
|
367
|
+
| `bin/` | CLI implementation |
|
|
368
|
+
| `lib/` | Business logic |
|
|
369
|
+
|
|
370
|
+
### 2. Standard RubyGems Convention
|
|
371
|
+
|
|
372
|
+
This follows how most Ruby gems organize executables:
|
|
373
|
+
- Rails uses `exe/rails` → internal implementation
|
|
374
|
+
- Bundler uses `exe/bundler` → internal implementation
|
|
375
|
+
- RuboCop uses `exe/rubocop` → internal implementation
|
|
376
|
+
|
|
377
|
+
### 3. Clean Installation
|
|
378
|
+
|
|
379
|
+
Users get clean command names without `.rb` extension:
|
|
380
|
+
```bash
|
|
381
|
+
# Clean
|
|
382
|
+
subtitle_processor clean -f input.srt
|
|
383
|
+
|
|
384
|
+
# Not
|
|
385
|
+
subtitle_processor.rb clean -f input.srt
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 4. Development Flexibility
|
|
389
|
+
|
|
390
|
+
Developers can:
|
|
391
|
+
- Run `bin/*.rb` directly during development
|
|
392
|
+
- Test changes without reinstalling gem
|
|
393
|
+
- Keep development scripts (like `bin/console`) separate from installed commands
|
|
394
|
+
|
|
395
|
+
### 5. Single Source of Truth
|
|
396
|
+
|
|
397
|
+
The `exe/` wrappers just load `bin/` files, so:
|
|
398
|
+
- Only one place to edit CLI code (`bin/`)
|
|
399
|
+
- No duplication between development and installed versions
|
|
400
|
+
- Easy to maintain
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Current exe/ Files
|
|
405
|
+
|
|
406
|
+
| exe/ file | Loads | Command |
|
|
407
|
+
|-----------|-------|---------|
|
|
408
|
+
| `exe/gpt_context` | `bin/gpt_context.rb` | `gpt_context` |
|
|
409
|
+
| `exe/youtube_manager` | `bin/youtube_manager.rb` | `youtube_manager` |
|
|
410
|
+
| `exe/prompt_tools` | `bin/prompt_tools.rb` | `prompt_tools` |
|
|
411
|
+
| `exe/youtube_automation` | `bin/youtube_automation.rb` | `youtube_automation` |
|
|
412
|
+
| `exe/ad_config` | `bin/configuration.rb` | `ad_config` |
|
|
413
|
+
| `exe/dam` | `bin/dam` | `dam` |
|
|
414
|
+
| `exe/subtitle_processor` | `bin/subtitle_processor.rb` | `subtitle_processor` |
|
|
415
|
+
| `exe/zsh_history` | `bin/zsh_history.rb` | `zsh_history` |
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Summary
|
|
420
|
+
|
|
421
|
+
| Aspect | bin/ | exe/ |
|
|
422
|
+
|--------|------|------|
|
|
423
|
+
| **Purpose** | Full CLI implementation | Thin wrapper for installation |
|
|
424
|
+
| **Extension** | `.rb` | None |
|
|
425
|
+
| **Size** | 50-500+ lines | 3-7 lines |
|
|
426
|
+
| **Contains** | OptionParser, routing, CLI classes | Just `require` and `load` |
|
|
427
|
+
| **Used during** | Development | After gem install |
|
|
428
|
+
| **Edited** | Frequently | Rarely (only when adding new tools) |
|
|
429
|
+
|
|
430
|
+
**Key takeaway:** Edit `bin/`, don't touch `exe/` unless adding a new tool.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
**Last updated:** 2025-12-13
|