zen-and-musashi 0.3.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 +7 -0
- data/README.md +307 -0
- data/bin/musashi +8 -0
- data/lib/zen_and_musashi/cli.rb +220 -0
- data/lib/zen_and_musashi/client.rb +62 -0
- data/lib/zen_and_musashi/local_wisdom.rb +25 -0
- data/lib/zen_and_musashi/seed.rb +95 -0
- data/lib/zen_and_musashi/store.rb +91 -0
- data/lib/zen_and_musashi/version.rb +5 -0
- data/lib/zen_and_musashi.rb +11 -0
- data/zen-and-musashi.gemspec +24 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 585ba7eb1c8b00337fcb7219221e6cf70697441247b6cf3c1225752f6d31f106
|
|
4
|
+
data.tar.gz: e61a6808e4cc4d0c95db36c4607921e2a145d855f2400c77f9996c6e0d8e78e6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fbd005d0ab968a2e371903e285b9234a4521bbfabb693ec1f2fcd0be0b29b344d8fb2f37c016ec972006a0824127514884591dbef82a0ebffc51495a17e92443
|
|
7
|
+
data.tar.gz: 2c53070c41607d2b6fad0b299f5231569230f564fe931b485e7ee9ccc9b9bb93a90518629b827a441f35837fbeeedbe985263af66e2925e0fb88b94a773f0719
|
data/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Zen & Musashi
|
|
2
|
+
|
|
3
|
+
> **Japanese wisdom for your terminal.**
|
|
4
|
+
>
|
|
5
|
+
> Local by default. Oracle optional.
|
|
6
|
+
|
|
7
|
+
`zen-and-musashi` is a Ruby command-line tool that delivers Zen and Musashi-style wisdom directly in your terminal. It requires **no API key**, runs entirely **offline**, and selects a fresh quote at random every time you invoke it.
|
|
8
|
+
|
|
9
|
+
If you want richer, AI-generated responses, an optional LLM shelf is available—but the sword is sharp before the oracle wakes.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Commands](#commands)
|
|
19
|
+
- [`zen` — Calm Reflection](#zen--calm-reflection)
|
|
20
|
+
- [`musashi` — Direct Critique](#musashi--direct-critique)
|
|
21
|
+
- [`add` — Add a Quote](#add--add-a-quote)
|
|
22
|
+
- [`list` — List Quotes](#list--list-quotes)
|
|
23
|
+
- [`remove` — Remove a Quote](#remove--remove-a-quote)
|
|
24
|
+
- [Optional LLM Mode](#optional-llm-mode)
|
|
25
|
+
- [Database](#database)
|
|
26
|
+
- [Configuration & Data Location](#configuration--data-location)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [Publishing](#publishing)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
| Feature | Description |
|
|
36
|
+
|---------|-------------|
|
|
37
|
+
| **100% Local** | No network calls, no API keys, no cloud dependency. |
|
|
38
|
+
| **Random Selection** | Every invocation returns a different quote from the database. |
|
|
39
|
+
| **Curated Seed Data** | Ships with 60 hand-picked quotes (30 Zen, 30 Musashi). |
|
|
40
|
+
| **Quote Management** | Add, list, and remove your own quotes via CLI. |
|
|
41
|
+
| **Zero Runtime Dependencies** | Uses only Ruby standard library (`pstore`, `optparse`, `net/http`). |
|
|
42
|
+
| **Optional LLM Mode** | Drop-in Anthropic Claude support when you want model-generated responses. |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
### From RubyGems (recommended)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
gem install zen-and-musashi
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### From Source
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/YumaKakuya/zenandmusashi.git
|
|
58
|
+
cd zenandmusashi
|
|
59
|
+
rake install
|
|
60
|
+
# or
|
|
61
|
+
gem build zen-and-musashi.gemspec
|
|
62
|
+
gem install zen-and-musashi-0.3.0.gem
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Requirements
|
|
66
|
+
|
|
67
|
+
- Ruby >= 3.0.0
|
|
68
|
+
|
|
69
|
+
No external gems are required for local mode.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Zen mode — calm, poetic reflection
|
|
77
|
+
musashi zen "My code has a bug"
|
|
78
|
+
|
|
79
|
+
# Musashi mode — direct, tactical critique
|
|
80
|
+
musashi musashi "My code has a bug"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Example output:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
$ musashi zen "My code has a bug"
|
|
87
|
+
The obstacle is the path.
|
|
88
|
+
|
|
89
|
+
$ musashi musashi "My code has a bug"
|
|
90
|
+
A dull blade is worse than no blade. Sharpen it or leave it.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Run the same command again and you will get a **different** quote.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Commands
|
|
98
|
+
|
|
99
|
+
### `zen` — Calm Reflection
|
|
100
|
+
|
|
101
|
+
Returns a randomly selected Zen quote.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
musashi zen [options] [input]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Options:**
|
|
108
|
+
- `--llm` — Use Anthropic Claude instead of the local database.
|
|
109
|
+
- `--api-key KEY` — Provide an API key for this invocation only.
|
|
110
|
+
|
|
111
|
+
**Examples:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
musashi zen "Should I refactor this?"
|
|
115
|
+
musashi zen --llm "Should I refactor this?"
|
|
116
|
+
musashi zen --llm --api-key sk-ant-xxxxx "Should I refactor this?"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `musashi` — Direct Critique
|
|
120
|
+
|
|
121
|
+
Returns a randomly selected Musashi quote.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
musashi musashi [options] [input]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Options:**
|
|
128
|
+
- `--llm` — Use Anthropic Claude instead of the local database.
|
|
129
|
+
- `--api-key KEY` — Provide an API key for this invocation only.
|
|
130
|
+
|
|
131
|
+
**Examples:**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
musashi musashi "CI failed again"
|
|
135
|
+
musashi musashi --llm "CI failed again"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `add` — Add a Quote
|
|
139
|
+
|
|
140
|
+
Add your own quote to the database.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
musashi add "Your quote text here" --mode <zen|musashi>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Constraints:**
|
|
147
|
+
- Maximum **200 characters**.
|
|
148
|
+
- Duplicate quotes are rejected.
|
|
149
|
+
- New quotes are automatically tagged as `composed`.
|
|
150
|
+
|
|
151
|
+
**Example:**
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
musashi add "The compiler is your first reviewer." --mode musashi
|
|
155
|
+
# => Added quote #31 to musashi mode.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `list` — List Quotes
|
|
159
|
+
|
|
160
|
+
Display all quotes for a given mode, including their ID, type, and text.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
musashi list --mode <zen|musashi>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Example:**
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
musashi list --mode zen
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Output:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
Quotes in zen mode (30 total):
|
|
176
|
+
----------------------------------------
|
|
177
|
+
#1 [authentic]: Before enlightenment, chop wood, carry water...
|
|
178
|
+
#2 [authentic]: The obstacle is the path.
|
|
179
|
+
...
|
|
180
|
+
#16 [composed]: Still water holds the sky.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `remove` — Remove a Quote
|
|
184
|
+
|
|
185
|
+
Delete a quote by its ID.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
musashi remove <id> --mode <zen|musashi>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Example:**
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
musashi remove 5 --mode zen
|
|
195
|
+
# => Removed quote #5 from zen mode.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Optional LLM Mode
|
|
201
|
+
|
|
202
|
+
If you want richer, context-aware responses, enable the LLM shelf:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
export ANTHROPIC_API_KEY=your_key_here
|
|
206
|
+
musashi zen --llm "My app has 47 dependencies"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
You can also pass the key per-command:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
musashi musashi --llm --api-key $ANTHROPIC_API_KEY "My code has a bug"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Note:** LLM mode incurs API usage costs. Musashi counted his arrows. You should count your tokens.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Database
|
|
220
|
+
|
|
221
|
+
Quotes are stored in a local PStore database. Each quote has the following structure:
|
|
222
|
+
|
|
223
|
+
| Field | Type | Description |
|
|
224
|
+
|-------|------|-------------|
|
|
225
|
+
| `id` | Integer | Auto-incrementing identifier |
|
|
226
|
+
| `text` | String | The quote itself (max 200 chars) |
|
|
227
|
+
| `type` | String | `authentic` (historical) or `composed` (original) |
|
|
228
|
+
|
|
229
|
+
### Seed Data
|
|
230
|
+
|
|
231
|
+
On first run, the database is automatically populated with:
|
|
232
|
+
|
|
233
|
+
- **Zen mode:** 15 authentic + 15 composed quotes
|
|
234
|
+
- **Musashi mode:** 15 authentic + 15 composed quotes
|
|
235
|
+
|
|
236
|
+
**Authentic quotes** are derived from historical Zen records and Miyamoto Musashi's *Go Rin no Sho* (The Book of Five Rings).
|
|
237
|
+
**Composed quotes** are original writing in the same spirit.
|
|
238
|
+
|
|
239
|
+
All seed data is in English and verified to be under 200 characters.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Configuration & Data Location
|
|
244
|
+
|
|
245
|
+
| Platform | Path |
|
|
246
|
+
|----------|------|
|
|
247
|
+
| Linux / macOS | `~/.config/zen-and-musashi/quotes.db` |
|
|
248
|
+
| Windows | `%USERPROFILE%\.config\zen-and-musashi\quotes.db` |
|
|
249
|
+
|
|
250
|
+
The directory is created automatically on first run.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
### Setup
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
git clone https://github.com/YumaKakuya/zenandmusashi.git
|
|
260
|
+
cd zenandmusashi
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Running Locally
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
ruby -Ilib bin/musashi zen "test input"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Running Tests
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
rake test
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Building the Gem
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
gem build zen-and-musashi.gemspec
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
This produces `zen-and-musashi-0.3.0.gem`.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Publishing
|
|
286
|
+
|
|
287
|
+
1. Update `lib/zen_and_musashi/version.rb` with the new version.
|
|
288
|
+
2. Run `gem build zen-and-musashi.gemspec`.
|
|
289
|
+
3. Push to RubyGems:
|
|
290
|
+
```bash
|
|
291
|
+
gem push zen-and-musashi-0.3.0.gem
|
|
292
|
+
```
|
|
293
|
+
4. Tag the release on GitHub:
|
|
294
|
+
```bash
|
|
295
|
+
git tag v0.3.0
|
|
296
|
+
git push origin v0.3.0
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
> *Before enlightenment, chop wood, carry water. After enlightenment, chop wood, carry water.*
|
data/bin/musashi
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require_relative 'version'
|
|
5
|
+
require_relative 'store'
|
|
6
|
+
require_relative 'seed'
|
|
7
|
+
require_relative 'local_wisdom'
|
|
8
|
+
require_relative 'client'
|
|
9
|
+
|
|
10
|
+
module ZenAndMusashi
|
|
11
|
+
class CLI
|
|
12
|
+
BANNER = "Japanese wisdom for your terminal.\nLocal by default. Oracle optional."
|
|
13
|
+
|
|
14
|
+
def self.start(argv)
|
|
15
|
+
new.run(argv)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(argv)
|
|
19
|
+
command = argv.shift
|
|
20
|
+
|
|
21
|
+
if command.nil? || %w[-h --help].include?(command)
|
|
22
|
+
puts main_help
|
|
23
|
+
exit 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
case command
|
|
27
|
+
when '-v', '--version'
|
|
28
|
+
puts "musashi version #{ZenAndMusashi::VERSION}"
|
|
29
|
+
exit 0
|
|
30
|
+
when 'zen', 'musashi'
|
|
31
|
+
handle_wisdom(command.to_sym, argv)
|
|
32
|
+
when 'add'
|
|
33
|
+
handle_add(argv)
|
|
34
|
+
when 'list'
|
|
35
|
+
handle_list(argv)
|
|
36
|
+
when 'remove'
|
|
37
|
+
handle_remove(argv)
|
|
38
|
+
else
|
|
39
|
+
warn "Error: Unknown command '#{command}'."
|
|
40
|
+
puts main_help
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
warn "Error: #{e.message}"
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def main_help
|
|
51
|
+
<<~HELP
|
|
52
|
+
#{BANNER}
|
|
53
|
+
|
|
54
|
+
Usage: musashi <command> [options] [args...]
|
|
55
|
+
|
|
56
|
+
Commands:
|
|
57
|
+
zen [options] [input] Calm, poetic reflection
|
|
58
|
+
musashi [options] [input] Direct, tactical critique
|
|
59
|
+
add [options] <quote> Add a new quote
|
|
60
|
+
list [options] List all quotes
|
|
61
|
+
remove [options] <id> Remove a quote by ID
|
|
62
|
+
|
|
63
|
+
Options for zen/musashi:
|
|
64
|
+
--llm Use LLM mode (requires ANTHROPIC_API_KEY)
|
|
65
|
+
--api-key KEY Anthropic API key for LLM mode
|
|
66
|
+
|
|
67
|
+
Options for add/list/remove:
|
|
68
|
+
--mode MODE Target mode: zen or musashi (required for add/list/remove)
|
|
69
|
+
|
|
70
|
+
Global options:
|
|
71
|
+
-v, --version Show version
|
|
72
|
+
-h, --help Show this help message
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
musashi zen "My code has a bug"
|
|
76
|
+
musashi musashi "My code has a bug"
|
|
77
|
+
musashi add "The mountain does not hurry." --mode zen
|
|
78
|
+
musashi list --mode zen
|
|
79
|
+
musashi remove 5 --mode zen
|
|
80
|
+
HELP
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def handle_wisdom(mode, argv)
|
|
84
|
+
options = { llm: false, api_key: nil }
|
|
85
|
+
|
|
86
|
+
parser = OptionParser.new do |p|
|
|
87
|
+
p.on '--llm', 'Use LLM mode (requires ANTHROPIC_API_KEY)' do
|
|
88
|
+
options[:llm] = true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
p.on '--api-key KEY', String, 'Anthropic API key for LLM mode' do |key|
|
|
92
|
+
options[:api_key] = key
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
parser.parse!(argv)
|
|
97
|
+
input = gather_input(argv)
|
|
98
|
+
|
|
99
|
+
if input.empty?
|
|
100
|
+
puts main_help
|
|
101
|
+
exit 0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
response =
|
|
105
|
+
if options[:llm]
|
|
106
|
+
api_key = options[:api_key] || ENV['ANTHROPIC_API_KEY']
|
|
107
|
+
unless api_key && !api_key.empty?
|
|
108
|
+
warn 'Error: ANTHROPIC_API_KEY is not set. Export it or pass --api-key.'
|
|
109
|
+
exit 1
|
|
110
|
+
end
|
|
111
|
+
Client.new(api_key: api_key).invoke(mode, input)
|
|
112
|
+
else
|
|
113
|
+
LocalWisdom.invoke(mode, input)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
puts response
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_add(argv)
|
|
120
|
+
options = { mode: nil }
|
|
121
|
+
|
|
122
|
+
parser = OptionParser.new do |p|
|
|
123
|
+
p.on '--mode MODE', String, 'Target mode: zen or musashi' do |mode|
|
|
124
|
+
options[:mode] = mode
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
parser.parse!(argv)
|
|
129
|
+
|
|
130
|
+
unless %w[zen musashi].include?(options[:mode])
|
|
131
|
+
warn "Error: --mode must be 'zen' or 'musashi'."
|
|
132
|
+
exit 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
text = argv.join(' ').strip
|
|
136
|
+
|
|
137
|
+
if text.empty?
|
|
138
|
+
warn 'Error: Quote text is required.'
|
|
139
|
+
puts 'Usage: musashi add "your quote" --mode <zen|musashi>'
|
|
140
|
+
exit 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
store = Store.new
|
|
144
|
+
id = store.add(options[:mode].to_sym, text)
|
|
145
|
+
puts "Added quote ##{id} to #{options[:mode]} mode."
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_list(argv)
|
|
149
|
+
options = { mode: nil }
|
|
150
|
+
|
|
151
|
+
parser = OptionParser.new do |p|
|
|
152
|
+
p.on '--mode MODE', String, 'Target mode: zen or musashi' do |mode|
|
|
153
|
+
options[:mode] = mode
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
parser.parse!(argv)
|
|
158
|
+
|
|
159
|
+
unless %w[zen musashi].include?(options[:mode])
|
|
160
|
+
warn "Error: --mode must be 'zen' or 'musashi'."
|
|
161
|
+
exit 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
store = Store.new
|
|
165
|
+
quotes = store.all(options[:mode].to_sym)
|
|
166
|
+
|
|
167
|
+
if quotes.empty?
|
|
168
|
+
puts "No quotes found in #{options[:mode]} mode."
|
|
169
|
+
else
|
|
170
|
+
puts "Quotes in #{options[:mode]} mode (#{quotes.length} total):"
|
|
171
|
+
puts '-' * 40
|
|
172
|
+
quotes.each do |q|
|
|
173
|
+
puts " ##{q[:id]} [#{q[:type]}]: #{q[:text]}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_remove(argv)
|
|
179
|
+
options = { mode: nil }
|
|
180
|
+
|
|
181
|
+
parser = OptionParser.new do |p|
|
|
182
|
+
p.on '--mode MODE', String, 'Target mode: zen or musashi' do |mode|
|
|
183
|
+
options[:mode] = mode
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
parser.parse!(argv)
|
|
188
|
+
|
|
189
|
+
unless %w[zen musashi].include?(options[:mode])
|
|
190
|
+
warn "Error: --mode must be 'zen' or 'musashi'."
|
|
191
|
+
exit 1
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
id_str = argv.shift
|
|
195
|
+
if id_str.nil? || id_str.strip.empty?
|
|
196
|
+
warn 'Error: Quote ID is required.'
|
|
197
|
+
puts 'Usage: musashi remove <id> --mode <zen|musashi>'
|
|
198
|
+
exit 1
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
id = Integer(id_str)
|
|
203
|
+
rescue ArgumentError
|
|
204
|
+
warn 'Error: ID must be a number.'
|
|
205
|
+
exit 1
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
store = Store.new
|
|
209
|
+
store.remove(options[:mode].to_sym, id)
|
|
210
|
+
puts "Removed quote ##{id} from #{options[:mode]} mode."
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def gather_input(args)
|
|
214
|
+
return args.join(' ') if args.any?
|
|
215
|
+
return $stdin.read unless $stdin.tty?
|
|
216
|
+
|
|
217
|
+
''
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module ZenAndMusashi
|
|
8
|
+
class Client
|
|
9
|
+
MODEL = "claude-haiku-4-5"
|
|
10
|
+
API_URL = "https://api.anthropic.com/v1/messages"
|
|
11
|
+
|
|
12
|
+
def initialize(api_key: nil)
|
|
13
|
+
@api_key = api_key || ENV["ANTHROPIC_API_KEY"]
|
|
14
|
+
validate_api_key!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def invoke(mode, input)
|
|
18
|
+
uri = URI(API_URL)
|
|
19
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
20
|
+
http.use_ssl = true
|
|
21
|
+
|
|
22
|
+
body = {
|
|
23
|
+
model: MODEL,
|
|
24
|
+
max_tokens: 300,
|
|
25
|
+
system: system_prompt(mode),
|
|
26
|
+
messages: [{ role: "user", content: input }]
|
|
27
|
+
}.to_json
|
|
28
|
+
|
|
29
|
+
request = Net::HTTP::Post.new(uri)
|
|
30
|
+
request["anthropic-version"] = "2023-06-01"
|
|
31
|
+
request["content-type"] = "application/json"
|
|
32
|
+
request["x-api-key"] = @api_key
|
|
33
|
+
request.body = body
|
|
34
|
+
|
|
35
|
+
response = http.request(request)
|
|
36
|
+
|
|
37
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
38
|
+
raise "HTTP Error: #{response.code}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
parsed = JSON.parse(response.body)
|
|
42
|
+
parsed["content"][0]["text"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate_api_key!
|
|
48
|
+
unless @api_key && !@api_key.empty?
|
|
49
|
+
raise "ANTHROPIC_API_KEY is not set. Export it or pass --api-key."
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def system_prompt(mode)
|
|
54
|
+
case mode
|
|
55
|
+
when :zen
|
|
56
|
+
"You are a calm, poetic Zen-inspired guide. Respond in 1-3 sentences. Be concise, poetic, and gently insightful. No bullet points. No emoji."
|
|
57
|
+
when :musashi
|
|
58
|
+
"You are a direct, tactical Musashi-inspired critic. Respond in 1-3 sentences. Be sharp, useful, and unflinching. No bullet points. No emoji."
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'store'
|
|
4
|
+
require_relative 'seed'
|
|
5
|
+
|
|
6
|
+
module ZenAndMusashi
|
|
7
|
+
module LocalWisdom
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def invoke(mode, _input)
|
|
11
|
+
store = Store.new
|
|
12
|
+
|
|
13
|
+
# 初回実行時にseed投入
|
|
14
|
+
Seed.run(store) if store.count(mode).zero?
|
|
15
|
+
|
|
16
|
+
quote = store.random(mode)
|
|
17
|
+
|
|
18
|
+
if quote.nil?
|
|
19
|
+
"No quotes found for #{mode} mode. Add one with: musashi add \"your quote\" --mode #{mode}"
|
|
20
|
+
else
|
|
21
|
+
quote[:text]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'store'
|
|
4
|
+
|
|
5
|
+
module ZenAndMusashi
|
|
6
|
+
module Seed
|
|
7
|
+
DEFAULT_QUOTES = {
|
|
8
|
+
'zen' => [
|
|
9
|
+
# Authentic — verified Zen sources
|
|
10
|
+
{ text: 'Before enlightenment, chop wood, carry water. After enlightenment, chop wood, carry water.',
|
|
11
|
+
type: 'authentic' },
|
|
12
|
+
{ text: 'The obstacle is the path.', type: 'authentic' },
|
|
13
|
+
{ text: 'The whole moon and the entire sky are reflected in one dewdrop on the grass.', type: 'authentic' },
|
|
14
|
+
{ text: 'Sitting quietly, doing nothing, spring comes and the grass grows by itself.', type: 'authentic' },
|
|
15
|
+
{ text: 'A flower falls, even though we love it; and a weed grows, even though we do not love it.',
|
|
16
|
+
type: 'authentic' },
|
|
17
|
+
{ text: 'If you understand, things are just as they are. If you do not understand, things are just as they are.',
|
|
18
|
+
type: 'authentic' },
|
|
19
|
+
{ text: 'No snowflake ever falls in the wrong place.', type: 'authentic' },
|
|
20
|
+
{ text: 'Not knowing is most intimate.', type: 'authentic' },
|
|
21
|
+
{ text: 'When walking, just walk. When sitting, just sit. Above all, do not wobble.', type: 'authentic' },
|
|
22
|
+
{
|
|
23
|
+
text: 'The ten thousand questions are one question. If you cut through the one question, then the ten thousand are answered.', type: 'authentic'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
text: 'In the landscape of spring there is neither better nor worse. The flowering branches grow naturally, some long, some short.', type: 'authentic'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
text: 'To study the self is to forget the self. To forget the self is to be enlightened by the ten thousand things.', type: 'authentic'
|
|
30
|
+
},
|
|
31
|
+
{ text: 'Do not seek to follow in the footsteps of the wise. Seek what they sought.', type: 'authentic' },
|
|
32
|
+
{ text: 'When you paint Spring, do not paint willows, plums, peaches, or apricots. Just paint Spring.',
|
|
33
|
+
type: 'authentic' },
|
|
34
|
+
{
|
|
35
|
+
text: 'Have good trust in yourself, not in the one that you think you should be, but in the one that you are.', type: 'authentic'
|
|
36
|
+
},
|
|
37
|
+
# Composed — original, Zen-voiced
|
|
38
|
+
{ text: 'Still water holds the sky.', type: 'composed' },
|
|
39
|
+
{ text: 'The stone does not ask why it sits.', type: 'composed' },
|
|
40
|
+
{ text: 'A single candle does not compete with the sun.', type: 'composed' },
|
|
41
|
+
{ text: 'The cup that is full cannot receive.', type: 'composed' },
|
|
42
|
+
{ text: 'Wind moves the grass. The root does not argue.', type: 'composed' },
|
|
43
|
+
{ text: 'Every step on the path is the path.', type: 'composed' },
|
|
44
|
+
{ text: 'A bird does not sing because it has an answer. It sings because it has a song.', type: 'composed' },
|
|
45
|
+
{ text: 'The shadow of a bird leaves no trace on the sky.', type: 'composed' },
|
|
46
|
+
{ text: 'The thread breaks where it is thinnest. The rope holds where it is braided.', type: 'composed' },
|
|
47
|
+
{ text: 'A tree that falls makes more noise than a forest that grows.', type: 'composed' },
|
|
48
|
+
{ text: 'The finger pointing at the moon is not the moon.', type: 'composed' },
|
|
49
|
+
{ text: 'Sand does not cling to the wave.', type: 'composed' },
|
|
50
|
+
{ text: 'The bell rings because it is empty.', type: 'composed' },
|
|
51
|
+
{ text: 'Ash does not return to wood.', type: 'composed' },
|
|
52
|
+
{ text: 'The river does not drink its own water.', type: 'composed' }
|
|
53
|
+
],
|
|
54
|
+
'musashi' => [
|
|
55
|
+
# Authentic — verified Go Rin no Sho / Dokkodo
|
|
56
|
+
{ text: 'Think lightly of yourself and deeply of the world.', type: 'authentic' },
|
|
57
|
+
{ text: 'Do nothing which is of no use.', type: 'authentic' },
|
|
58
|
+
{ text: 'Perceive that which cannot be seen with the eye.', type: 'authentic' },
|
|
59
|
+
{ text: 'In battle, if you make your opponent flinch, you have already won.', type: 'authentic' },
|
|
60
|
+
{ text: 'Do not let the enemy see your spirit.', type: 'authentic' },
|
|
61
|
+
{ text: 'The true science of martial arts means practicing them in such a way that they will be useful at any time.',
|
|
62
|
+
type: 'authentic' },
|
|
63
|
+
{ text: 'Today is victory over yourself of yesterday.', type: 'authentic' },
|
|
64
|
+
{ text: 'Step by step walk the thousand-mile road.', type: 'authentic' },
|
|
65
|
+
{ text: 'If you know the way broadly you will see it in everything.', type: 'authentic' },
|
|
66
|
+
{ text: 'You can only fight the way you practice.', type: 'authentic' },
|
|
67
|
+
{ text: "The important thing in strategy is to suppress the enemy's useful actions.", type: 'authentic' },
|
|
68
|
+
{ text: 'From one thing, know ten thousand things.', type: 'authentic' },
|
|
69
|
+
{ text: 'Do not sleep under a roof. Carry no money or food.', type: 'authentic' },
|
|
70
|
+
{ text: 'Respect Buddha and the gods without counting on their help.', type: 'authentic' },
|
|
71
|
+
{ text: 'Never let yourself be saddened by a separation.', type: 'authentic' },
|
|
72
|
+
# Composed — original, Musashi-voiced
|
|
73
|
+
{ text: 'A dull blade is worse than no blade. Sharpen it or leave it.', type: 'composed' },
|
|
74
|
+
{ text: 'Hesitation is a wound that bleeds before the battle.', type: 'composed' },
|
|
75
|
+
{ text: 'Study your enemy until you know him better than he knows himself.', type: 'composed' },
|
|
76
|
+
{ text: 'The ground you stand on is yours only while you defend it.', type: 'composed' },
|
|
77
|
+
{ text: 'The gap between knowing and doing is where battles are lost.', type: 'composed' },
|
|
78
|
+
{ text: 'Strike where the armor gaps. That is the only strike that matters.', type: 'composed' },
|
|
79
|
+
{ text: 'The sword has no opinion. The hand that holds it does.', type: 'composed' },
|
|
80
|
+
{ text: 'A stance with no opening gives the enemy nothing to read.', type: 'composed' },
|
|
81
|
+
{ text: 'Cut once. Step back. See what remains.', type: 'composed' },
|
|
82
|
+
{ text: 'The man who grips his sword too tightly cannot feel the blade.', type: 'composed' },
|
|
83
|
+
{ text: 'Your footwork tells the enemy more than your eyes do.', type: 'composed' },
|
|
84
|
+
{ text: 'A technique you have not tested in fatigue is a technique you do not own.', type: 'composed' },
|
|
85
|
+
{ text: 'The second strike exists because the first was not enough.', type: 'composed' },
|
|
86
|
+
{ text: 'Do not admire the sword. Admire the thousand hours behind it.', type: 'composed' },
|
|
87
|
+
{ text: 'An enemy who is calm is more dangerous than one who is angry.', type: 'composed' }
|
|
88
|
+
]
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
def self.run(store)
|
|
92
|
+
store.seed(DEFAULT_QUOTES)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pstore'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module ZenAndMusashi
|
|
7
|
+
class Store
|
|
8
|
+
DB_DIR = File.join(Dir.home, '.config', 'zen-and-musashi')
|
|
9
|
+
DB_PATH = File.join(DB_DIR, 'quotes.db')
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
FileUtils.mkdir_p(DB_DIR)
|
|
13
|
+
@pstore = PStore.new(DB_PATH)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def all(mode)
|
|
17
|
+
@pstore.transaction(true) do
|
|
18
|
+
@pstore[mode.to_s] || []
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def random(mode)
|
|
23
|
+
quotes = all(mode)
|
|
24
|
+
return nil if quotes.empty?
|
|
25
|
+
|
|
26
|
+
quotes.sample
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add(mode, text)
|
|
30
|
+
@pstore.transaction do
|
|
31
|
+
mode_key = mode.to_s
|
|
32
|
+
quotes = @pstore[mode_key] || []
|
|
33
|
+
|
|
34
|
+
# 重複チェック
|
|
35
|
+
raise "That quote already exists in #{mode} mode." if quotes.any? { |q| q[:text].casecmp(text).zero? }
|
|
36
|
+
|
|
37
|
+
# 文字数制限チェック(200文字)
|
|
38
|
+
raise 'Quote is too long. Maximum 200 characters.' if text.length > 200
|
|
39
|
+
|
|
40
|
+
new_id = (quotes.map { |q| q[:id] }.max || 0) + 1
|
|
41
|
+
quotes << { id: new_id, text: text, type: 'composed' }
|
|
42
|
+
@pstore[mode_key] = quotes
|
|
43
|
+
|
|
44
|
+
new_id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove(mode, id)
|
|
49
|
+
@pstore.transaction do
|
|
50
|
+
mode_key = mode.to_s
|
|
51
|
+
quotes = @pstore[mode_key] || []
|
|
52
|
+
|
|
53
|
+
idx = quotes.index { |q| q[:id] == id }
|
|
54
|
+
raise "Quote ID #{id} not found in #{mode} mode." unless idx
|
|
55
|
+
|
|
56
|
+
quotes.delete_at(idx)
|
|
57
|
+
@pstore[mode_key] = quotes
|
|
58
|
+
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def count(mode)
|
|
64
|
+
all(mode).length
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def seed(data)
|
|
68
|
+
@pstore.transaction do
|
|
69
|
+
data.each do |mode_key, quotes_array|
|
|
70
|
+
existing = @pstore[mode_key.to_s] || []
|
|
71
|
+
|
|
72
|
+
quotes_array.each do |quote_hash|
|
|
73
|
+
# 重複チェック
|
|
74
|
+
next if existing.any? { |q| q[:text].casecmp(quote_hash[:text]).zero? }
|
|
75
|
+
|
|
76
|
+
new_id = (existing.map { |q| q[:id] }.max || 0) + 1
|
|
77
|
+
existing << { id: new_id, text: quote_hash[:text], type: quote_hash[:type] || 'composed' }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@pstore[mode_key.to_s] = existing
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def clear(mode)
|
|
86
|
+
@pstore.transaction do
|
|
87
|
+
@pstore[mode.to_s] = []
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'zen_and_musashi/version'
|
|
4
|
+
require_relative 'zen_and_musashi/store'
|
|
5
|
+
require_relative 'zen_and_musashi/seed'
|
|
6
|
+
require_relative 'zen_and_musashi/local_wisdom'
|
|
7
|
+
require_relative 'zen_and_musashi/client'
|
|
8
|
+
require_relative 'zen_and_musashi/cli'
|
|
9
|
+
|
|
10
|
+
module ZenAndMusashi
|
|
11
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/zen_and_musashi/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'zen-and-musashi'
|
|
7
|
+
spec.version = ZenAndMusashi::VERSION
|
|
8
|
+
spec.authors = ['Sorted.']
|
|
9
|
+
spec.email = []
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Japanese wisdom for your terminal. Local Zen and Musashi-style critique, with optional LLM mode.'
|
|
12
|
+
spec.description = 'zen-and-musashi is a Ruby CLI that gives local, structured terminal wisdom in two modes: calm Zen-style reflection and fierce Musashi-style critique. It works without an API key by default, and can optionally use an LLM when explicitly enabled.'
|
|
13
|
+
spec.homepage = 'https://github.com/YumaKakuya/zenandmusashi'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
|
|
16
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
17
|
+
|
|
18
|
+
spec.files = Dir.glob('{bin,lib}/**/*') + %w[README.md zen-and-musashi.gemspec]
|
|
19
|
+
spec.bindir = 'bin'
|
|
20
|
+
spec.executables = ['musashi']
|
|
21
|
+
spec.require_paths = ['lib']
|
|
22
|
+
|
|
23
|
+
# No runtime dependencies — stdlib only
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zen-and-musashi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sorted.
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'zen-and-musashi is a Ruby CLI that gives local, structured terminal
|
|
14
|
+
wisdom in two modes: calm Zen-style reflection and fierce Musashi-style critique.
|
|
15
|
+
It works without an API key by default, and can optionally use an LLM when explicitly
|
|
16
|
+
enabled.'
|
|
17
|
+
email: []
|
|
18
|
+
executables:
|
|
19
|
+
- musashi
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- README.md
|
|
24
|
+
- bin/musashi
|
|
25
|
+
- lib/zen_and_musashi.rb
|
|
26
|
+
- lib/zen_and_musashi/cli.rb
|
|
27
|
+
- lib/zen_and_musashi/client.rb
|
|
28
|
+
- lib/zen_and_musashi/local_wisdom.rb
|
|
29
|
+
- lib/zen_and_musashi/seed.rb
|
|
30
|
+
- lib/zen_and_musashi/store.rb
|
|
31
|
+
- lib/zen_and_musashi/version.rb
|
|
32
|
+
- zen-and-musashi.gemspec
|
|
33
|
+
homepage: https://github.com/YumaKakuya/zenandmusashi
|
|
34
|
+
licenses:
|
|
35
|
+
- MIT
|
|
36
|
+
metadata: {}
|
|
37
|
+
post_install_message:
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 3.0.0
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 3.4.1
|
|
53
|
+
signing_key:
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Japanese wisdom for your terminal. Local Zen and Musashi-style critique,
|
|
56
|
+
with optional LLM mode.
|
|
57
|
+
test_files: []
|