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 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,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
5
+
6
+ require "zen_and_musashi"
7
+
8
+ ZenAndMusashi::CLI.start(ARGV)
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZenAndMusashi
4
+ VERSION = '0.3.0'
5
+ 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: []