rubyn-code 0.2.2 → 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 +4 -4
- data/README.md +91 -3
- data/lib/rubyn_code/agent/background_job_handler.rb +71 -0
- data/lib/rubyn_code/agent/conversation.rb +55 -56
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +99 -0
- data/lib/rubyn_code/agent/feedback_handler.rb +49 -0
- data/lib/rubyn_code/agent/llm_caller.rb +149 -0
- data/lib/rubyn_code/agent/loop.rb +175 -683
- data/lib/rubyn_code/agent/loop_detector.rb +50 -11
- data/lib/rubyn_code/agent/prompts.rb +109 -0
- data/lib/rubyn_code/agent/response_modes.rb +111 -0
- data/lib/rubyn_code/agent/response_parser.rb +111 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +205 -0
- data/lib/rubyn_code/agent/tool_processor.rb +158 -0
- data/lib/rubyn_code/agent/usage_tracker.rb +59 -0
- data/lib/rubyn_code/auth/oauth.rb +80 -64
- data/lib/rubyn_code/auth/server.rb +21 -24
- data/lib/rubyn_code/auth/token_store.rb +31 -44
- data/lib/rubyn_code/autonomous/daemon.rb +29 -18
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -4
- data/lib/rubyn_code/autonomous/task_claimer.rb +36 -40
- data/lib/rubyn_code/background/worker.rb +64 -76
- data/lib/rubyn_code/cli/app.rb +128 -114
- data/lib/rubyn_code/cli/commands/model.rb +75 -18
- data/lib/rubyn_code/cli/commands/new_session.rb +45 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +28 -11
- data/lib/rubyn_code/cli/renderer.rb +109 -60
- data/lib/rubyn_code/cli/repl.rb +42 -373
- data/lib/rubyn_code/cli/repl_commands.rb +176 -0
- data/lib/rubyn_code/cli/repl_lifecycle.rb +75 -0
- data/lib/rubyn_code/cli/repl_setup.rb +145 -0
- data/lib/rubyn_code/cli/setup.rb +6 -2
- data/lib/rubyn_code/cli/stream_formatter.rb +56 -49
- data/lib/rubyn_code/cli/version_check.rb +28 -11
- data/lib/rubyn_code/config/defaults.rb +10 -0
- data/lib/rubyn_code/config/project_profile.rb +185 -0
- data/lib/rubyn_code/config/settings.rb +100 -1
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/context_budget.rb +167 -0
- data/lib/rubyn_code/context/decision_compactor.rb +99 -0
- data/lib/rubyn_code/context/manager.rb +7 -5
- data/lib/rubyn_code/context/micro_compact.rb +29 -19
- data/lib/rubyn_code/context/schema_filter.rb +64 -0
- data/lib/rubyn_code/db/connection.rb +31 -26
- data/lib/rubyn_code/db/migrator.rb +44 -28
- data/lib/rubyn_code/hooks/built_in.rb +14 -10
- data/lib/rubyn_code/index/codebase_index.rb +245 -0
- data/lib/rubyn_code/learning/extractor.rb +65 -82
- data/lib/rubyn_code/learning/injector.rb +22 -23
- data/lib/rubyn_code/learning/instinct.rb +71 -42
- data/lib/rubyn_code/learning/shortcut.rb +95 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +270 -0
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +215 -0
- data/lib/rubyn_code/llm/adapters/base.rb +35 -0
- data/lib/rubyn_code/llm/adapters/json_parsing.rb +21 -0
- data/lib/rubyn_code/llm/adapters/openai.rb +246 -0
- data/lib/rubyn_code/llm/adapters/openai_compatible.rb +46 -0
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +90 -0
- data/lib/rubyn_code/llm/adapters/openai_streaming.rb +141 -0
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +60 -0
- data/lib/rubyn_code/llm/client.rb +55 -252
- data/lib/rubyn_code/llm/model_router.rb +237 -0
- data/lib/rubyn_code/llm/streaming.rb +4 -227
- data/lib/rubyn_code/mcp/client.rb +1 -1
- data/lib/rubyn_code/mcp/config.rb +9 -12
- data/lib/rubyn_code/mcp/sse_transport.rb +15 -13
- data/lib/rubyn_code/mcp/stdio_transport.rb +16 -18
- data/lib/rubyn_code/mcp/tool_bridge.rb +31 -62
- data/lib/rubyn_code/memory/session_persistence.rb +59 -58
- data/lib/rubyn_code/memory/store.rb +42 -55
- data/lib/rubyn_code/observability/budget_enforcer.rb +46 -32
- data/lib/rubyn_code/observability/cost_calculator.rb +32 -8
- data/lib/rubyn_code/observability/skill_analytics.rb +116 -0
- data/lib/rubyn_code/observability/token_analytics.rb +130 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +79 -61
- data/lib/rubyn_code/output/diff_renderer.rb +102 -77
- data/lib/rubyn_code/output/formatter.rb +11 -11
- data/lib/rubyn_code/permissions/policy.rb +11 -13
- data/lib/rubyn_code/permissions/prompter.rb +8 -9
- data/lib/rubyn_code/protocols/plan_approval.rb +25 -20
- data/lib/rubyn_code/skills/document.rb +33 -29
- data/lib/rubyn_code/skills/ttl_manager.rb +100 -0
- data/lib/rubyn_code/sub_agents/runner.rb +20 -25
- data/lib/rubyn_code/tasks/dag.rb +25 -24
- data/lib/rubyn_code/tools/ask_user.rb +44 -0
- data/lib/rubyn_code/tools/background_run.rb +2 -1
- data/lib/rubyn_code/tools/base.rb +26 -32
- data/lib/rubyn_code/tools/bash.rb +2 -1
- data/lib/rubyn_code/tools/edit_file.rb +74 -18
- data/lib/rubyn_code/tools/executor.rb +74 -24
- data/lib/rubyn_code/tools/file_cache.rb +95 -0
- data/lib/rubyn_code/tools/git_commit.rb +12 -10
- data/lib/rubyn_code/tools/git_log.rb +12 -10
- data/lib/rubyn_code/tools/glob.rb +23 -7
- data/lib/rubyn_code/tools/grep.rb +2 -1
- data/lib/rubyn_code/tools/load_skill.rb +13 -6
- data/lib/rubyn_code/tools/memory_search.rb +14 -13
- data/lib/rubyn_code/tools/memory_write.rb +2 -1
- data/lib/rubyn_code/tools/output_compressor.rb +185 -0
- data/lib/rubyn_code/tools/read_file.rb +11 -6
- data/lib/rubyn_code/tools/review_pr.rb +127 -80
- data/lib/rubyn_code/tools/run_specs.rb +26 -15
- data/lib/rubyn_code/tools/schema.rb +4 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +113 -82
- data/lib/rubyn_code/tools/spawn_teammate.rb +107 -64
- data/lib/rubyn_code/tools/spec_output_parser.rb +118 -0
- data/lib/rubyn_code/tools/task.rb +17 -17
- data/lib/rubyn_code/tools/web_fetch.rb +62 -47
- data/lib/rubyn_code/tools/web_search.rb +66 -48
- data/lib/rubyn_code/tools/write_file.rb +59 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +40 -1
- data/skills/rubyn_self_test.md +121 -0
- metadata +53 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
# Saves the current session and starts a fresh conversation.
|
|
7
|
+
# Like pressing Escape in Claude Code — clears context without quitting.
|
|
8
|
+
class NewSession < Base
|
|
9
|
+
def self.command_name = '/new'
|
|
10
|
+
def self.description = 'Save current session and start a fresh conversation'
|
|
11
|
+
def self.aliases = ['/reset'].freeze
|
|
12
|
+
|
|
13
|
+
def execute(_args, ctx)
|
|
14
|
+
save_current_session(ctx)
|
|
15
|
+
clear_conversation(ctx)
|
|
16
|
+
new_session_id = generate_session_id
|
|
17
|
+
|
|
18
|
+
ctx.renderer.info('Session saved. Starting fresh.')
|
|
19
|
+
ctx.renderer.info("New session: #{new_session_id[0..7]}")
|
|
20
|
+
|
|
21
|
+
{ action: :new_session, session_id: new_session_id }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def save_current_session(ctx)
|
|
27
|
+
ctx.session_persistence.save_session(
|
|
28
|
+
session_id: ctx.session_id,
|
|
29
|
+
project_path: ctx.project_root,
|
|
30
|
+
messages: ctx.conversation.messages,
|
|
31
|
+
model: Config::Defaults::DEFAULT_MODEL
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def clear_conversation(ctx)
|
|
36
|
+
ctx.conversation.clear!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate_session_id
|
|
40
|
+
SecureRandom.hex(16)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -18,14 +18,9 @@ module RubynCode
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def run
|
|
21
|
-
|
|
22
|
-
ensure_auth!
|
|
23
|
-
setup_database!
|
|
24
|
-
display_banner!
|
|
25
|
-
|
|
21
|
+
bootstrap!
|
|
26
22
|
daemon = build_daemon
|
|
27
23
|
daemon.start!
|
|
28
|
-
|
|
29
24
|
display_shutdown_summary(daemon)
|
|
30
25
|
rescue Interrupt
|
|
31
26
|
@renderer.info("\nShutting down daemon...")
|
|
@@ -38,6 +33,13 @@ module RubynCode
|
|
|
38
33
|
|
|
39
34
|
private
|
|
40
35
|
|
|
36
|
+
def bootstrap!
|
|
37
|
+
ensure_home_dir!
|
|
38
|
+
ensure_auth!
|
|
39
|
+
setup_database!
|
|
40
|
+
display_banner!
|
|
41
|
+
end
|
|
42
|
+
|
|
41
43
|
def build_daemon
|
|
42
44
|
Autonomous::Daemon.new(
|
|
43
45
|
agent_name: @daemon_opts[:agent_name],
|
|
@@ -79,13 +81,16 @@ module RubynCode
|
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def ensure_auth!
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
provider = @daemon_opts.fetch(:provider, Config::Defaults::DEFAULT_PROVIDER)
|
|
85
|
+
|
|
86
|
+
unless Auth::TokenStore.load_for_provider(provider)
|
|
87
|
+
@renderer.error("No valid authentication found for provider '#{provider}'.")
|
|
88
|
+
env_key = Config::Defaults::PROVIDER_ENV_KEYS.fetch(provider, "#{provider.upcase}_API_KEY")
|
|
89
|
+
@renderer.info("Set #{env_key} or run `rubyn-code --auth`.")
|
|
85
90
|
exit(1)
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
@llm_client = LLM::Client.new
|
|
93
|
+
@llm_client = LLM::Client.new(provider: provider)
|
|
89
94
|
end
|
|
90
95
|
|
|
91
96
|
def setup_database!
|
|
@@ -96,9 +101,18 @@ module RubynCode
|
|
|
96
101
|
end
|
|
97
102
|
|
|
98
103
|
def display_banner!
|
|
104
|
+
display_banner_header!
|
|
105
|
+
display_banner_details!
|
|
106
|
+
display_banner_footer!
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def display_banner_header!
|
|
99
110
|
@renderer.info('╔══════════════════════════════════════╗')
|
|
100
111
|
@renderer.info('║ GOLEM Daemon Starting ║')
|
|
101
112
|
@renderer.info('╚══════════════════════════════════════╝')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def display_banner_details!
|
|
102
116
|
@renderer.info(" Agent: #{@daemon_opts[:agent_name]}")
|
|
103
117
|
@renderer.info(" Role: #{@daemon_opts[:role]}")
|
|
104
118
|
@renderer.info(" Project: #{@project_root}")
|
|
@@ -106,6 +120,9 @@ module RubynCode
|
|
|
106
120
|
@renderer.info(" Max cost: $#{@daemon_opts[:max_cost]}")
|
|
107
121
|
@renderer.info(" Idle timeout: #{@daemon_opts[:idle_timeout]}s")
|
|
108
122
|
@renderer.info(" Poll interval: #{@daemon_opts[:poll_interval]}s")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def display_banner_footer!
|
|
109
126
|
@renderer.info('')
|
|
110
127
|
@renderer.info('Waiting for tasks... (Ctrl-C to stop)')
|
|
111
128
|
@renderer.info('Seed tasks via the REPL: /tasks or the task tool.')
|
|
@@ -122,7 +139,7 @@ module RubynCode
|
|
|
122
139
|
@renderer.info('╚══════════════════════════════════════╝')
|
|
123
140
|
@renderer.info(" Final state: #{status[:state]}")
|
|
124
141
|
@renderer.info(" Tasks completed: #{status[:runs_completed]}")
|
|
125
|
-
@renderer.info(
|
|
142
|
+
@renderer.info(format(' Total cost: $%.4f', status[:total_cost]))
|
|
126
143
|
end
|
|
127
144
|
end
|
|
128
145
|
end
|
|
@@ -33,14 +33,23 @@ module RubynCode
|
|
|
33
33
|
puts @pastel.cyan(" > #{name}: #{format_params(params)}")
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
DIFF_TOOLS = %w[edit_file write_file].freeze
|
|
37
|
+
DIFF_RESULT_LIMIT = 2000
|
|
38
|
+
DEFAULT_RESULT_LIMIT = 500
|
|
39
|
+
|
|
40
|
+
def tool_result(name, output)
|
|
41
|
+
raw = output.to_s
|
|
42
|
+
|
|
43
|
+
if DIFF_TOOLS.include?(name.to_s)
|
|
44
|
+
render_diff_result(raw[0...DIFF_RESULT_LIMIT].lines)
|
|
42
45
|
else
|
|
43
|
-
|
|
46
|
+
truncated = raw[0...DEFAULT_RESULT_LIMIT]
|
|
47
|
+
lines = truncated.lines
|
|
48
|
+
if lines.length > 6
|
|
49
|
+
render_truncated_result(lines)
|
|
50
|
+
else
|
|
51
|
+
puts @pastel.dim(" #{truncated.strip.gsub("\n", "\n ")}")
|
|
52
|
+
end
|
|
44
53
|
end
|
|
45
54
|
end
|
|
46
55
|
|
|
@@ -80,54 +89,93 @@ module RubynCode
|
|
|
80
89
|
|
|
81
90
|
def cost_summary(session_cost:, daily_cost:, tokens:)
|
|
82
91
|
puts @pastel.bold('Cost Summary:')
|
|
83
|
-
puts
|
|
84
|
-
puts
|
|
92
|
+
puts format(' Session: $%.4f', session_cost)
|
|
93
|
+
puts format(' Today: $%.4f', daily_cost)
|
|
85
94
|
puts " Tokens: #{tokens[:input]} in / #{tokens[:output]} out"
|
|
86
95
|
end
|
|
87
96
|
|
|
88
97
|
def prompt
|
|
89
|
-
if @yolo
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@pastel.bold.green('rubyn > ')
|
|
93
|
-
end
|
|
98
|
+
return @pastel.bold.green('rubyn ') + @pastel.bold.red('YOLO') + @pastel.bold.green(' > ') if @yolo
|
|
99
|
+
|
|
100
|
+
@pastel.bold.green('rubyn > ')
|
|
94
101
|
end
|
|
95
102
|
|
|
103
|
+
DIFF_COLORS = {
|
|
104
|
+
/\A \+ / => :green,
|
|
105
|
+
/\A - / => :red,
|
|
106
|
+
/\A @@ / => :cyan,
|
|
107
|
+
/\A(?:Created|Updated|Edited) / => :yellow
|
|
108
|
+
}.freeze
|
|
109
|
+
|
|
96
110
|
private
|
|
97
111
|
|
|
112
|
+
def render_diff_result(lines)
|
|
113
|
+
lines.each do |line|
|
|
114
|
+
stripped = line.rstrip
|
|
115
|
+
puts " #{colorize_diff_line(stripped)}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def colorize_diff_line(line)
|
|
120
|
+
DIFF_COLORS.each do |pattern, color|
|
|
121
|
+
return @pastel.decorate(line, color) if line.match?(pattern)
|
|
122
|
+
end
|
|
123
|
+
@pastel.dim(line)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def render_truncated_result(lines)
|
|
127
|
+
puts @pastel.dim(" #{lines[0..4].map(&:strip).join("\n ")}")
|
|
128
|
+
puts @pastel.dim(" ... (#{lines.length - 5} more lines)")
|
|
129
|
+
end
|
|
130
|
+
|
|
98
131
|
def render_markdown(text)
|
|
99
132
|
lines = text.lines
|
|
100
133
|
result = []
|
|
101
|
-
|
|
102
134
|
in_code_block = false
|
|
103
135
|
code_lang = nil
|
|
104
136
|
code_buffer = []
|
|
105
137
|
|
|
106
138
|
lines.each do |line|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
code_buffer = []
|
|
111
|
-
in_code_block = false
|
|
112
|
-
code_lang = nil
|
|
113
|
-
else
|
|
114
|
-
in_code_block = true
|
|
115
|
-
code_lang = line.strip.match(/\A```(\w*)/)[1]
|
|
116
|
-
code_lang = 'ruby' if code_lang.empty?
|
|
117
|
-
end
|
|
118
|
-
elsif in_code_block
|
|
119
|
-
code_buffer << line
|
|
120
|
-
else
|
|
121
|
-
result << render_line(line)
|
|
122
|
-
end
|
|
139
|
+
in_code_block, code_lang, code_buffer = process_markdown_line(
|
|
140
|
+
line, in_code_block, code_lang, code_buffer, result
|
|
141
|
+
)
|
|
123
142
|
end
|
|
124
143
|
|
|
125
144
|
# Flush any unclosed code block
|
|
126
|
-
|
|
145
|
+
flush_code_buffer(code_buffer, code_lang, result)
|
|
127
146
|
|
|
128
147
|
result.join
|
|
129
148
|
end
|
|
130
149
|
|
|
150
|
+
def process_markdown_line(line, in_code_block, code_lang, code_buffer, result)
|
|
151
|
+
if line.strip.match?(/\A```(\w*)/)
|
|
152
|
+
handle_code_fence(line, in_code_block, code_lang, code_buffer, result)
|
|
153
|
+
elsif in_code_block
|
|
154
|
+
code_buffer << line
|
|
155
|
+
[in_code_block, code_lang, code_buffer]
|
|
156
|
+
else
|
|
157
|
+
result << render_line(line)
|
|
158
|
+
[in_code_block, code_lang, code_buffer]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def handle_code_fence(line, in_code_block, code_lang, code_buffer, result)
|
|
163
|
+
if in_code_block
|
|
164
|
+
result << render_code_block(code_buffer.join, code_lang)
|
|
165
|
+
[false, nil, []]
|
|
166
|
+
else
|
|
167
|
+
lang = line.strip.match(/\A```(\w*)/)[1]
|
|
168
|
+
lang = 'ruby' if lang.empty?
|
|
169
|
+
[true, lang, []]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def flush_code_buffer(code_buffer, code_lang, result)
|
|
174
|
+
return if code_buffer.empty?
|
|
175
|
+
|
|
176
|
+
result << render_code_block(code_buffer.join, code_lang || 'ruby')
|
|
177
|
+
end
|
|
178
|
+
|
|
131
179
|
def render_code_block(code, lang)
|
|
132
180
|
lexer = Rouge::Lexer.find(lang) || Rouge::Lexers::PlainText.new
|
|
133
181
|
highlighted = @rouge_formatter.format(lexer.lex(code))
|
|
@@ -139,37 +187,38 @@ module RubynCode
|
|
|
139
187
|
end
|
|
140
188
|
|
|
141
189
|
def render_line(line)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
indent = line.match(/\A(\s*)/)[1]
|
|
159
|
-
num = line.match(/(\d+)\./)[1]
|
|
160
|
-
content = line.sub(/\A\s*\d+\.\s+/, '')
|
|
161
|
-
"#{indent} #{@pastel.cyan("#{num}.")} #{render_inline(content)}"
|
|
162
|
-
# Horizontal rules
|
|
163
|
-
elsif line.strip.match?(/\A-{3,}\z/)
|
|
164
|
-
"#{@pastel.dim('─' * [terminal_width - 4, 40].min)}\n"
|
|
165
|
-
# Table rows
|
|
166
|
-
elsif line.include?('|')
|
|
167
|
-
render_table_row(line)
|
|
168
|
-
else
|
|
169
|
-
render_inline(line)
|
|
190
|
+
case line
|
|
191
|
+
when /\A\s*\#{1,6}\s/ then render_header(line)
|
|
192
|
+
when /\A\s*[-*]\s/ then render_bullet(line)
|
|
193
|
+
when /\A\s*\d+\.\s/ then render_numbered_item(line)
|
|
194
|
+
when ->(l) { l.strip.match?(/\A-{3,}\z/) } then "#{@pastel.dim('─' * [terminal_width - 4, 40].min)}\n"
|
|
195
|
+
when /\|/ then render_table_row(line)
|
|
196
|
+
else render_inline(line)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def render_header(line)
|
|
201
|
+
level = line.match(/\A\s*(\#{1,6})\s/)[1].length
|
|
202
|
+
text = line.sub(/\A\s*\#{1,6}\s+/, '').rstrip
|
|
203
|
+
case level
|
|
204
|
+
when 1 then "#{@pastel.bold.underline(text)}\n"
|
|
205
|
+
else "#{@pastel.bold(text)}\n"
|
|
170
206
|
end
|
|
171
207
|
end
|
|
172
208
|
|
|
209
|
+
def render_bullet(line)
|
|
210
|
+
indent = line.match(/\A(\s*)/)[1]
|
|
211
|
+
content = line.sub(/\A\s*[-*]\s+/, '')
|
|
212
|
+
"#{indent} #{@pastel.cyan('•')} #{render_inline(content)}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def render_numbered_item(line)
|
|
216
|
+
indent = line.match(/\A(\s*)/)[1]
|
|
217
|
+
num = line.match(/(\d+)\./)[1]
|
|
218
|
+
content = line.sub(/\A\s*\d+\.\s+/, '')
|
|
219
|
+
"#{indent} #{@pastel.cyan("#{num}.")} #{render_inline(content)}"
|
|
220
|
+
end
|
|
221
|
+
|
|
173
222
|
def render_inline(text)
|
|
174
223
|
text
|
|
175
224
|
.gsub(/\*\*(.+?)\*\*/) { @pastel.bold(Regexp.last_match(1)) }
|