architext 0.2.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.
@@ -0,0 +1,512 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/console'
4
+
5
+ require_relative 'terminal'
6
+ require_relative 'version'
7
+
8
+ module Architext
9
+ # rubocop:disable Metrics/ClassLength
10
+ class TUI
11
+ HELP = 'Up/k Down/j move space select a all / filter n new search v vault enter confirm q back'
12
+ KEY_BINDINGS = {
13
+ ' ' => :space,
14
+ 'k' => :up,
15
+ 'j' => :down,
16
+ 'a' => :all,
17
+ '/' => :filter,
18
+ 'n' => :new_query,
19
+ 'v' => :new_vault,
20
+ 'q' => :quit
21
+ }.freeze
22
+ LOGO = [
23
+ ' ___ __ _ __ __ ',
24
+ ' / | __________/ /_ (_) /____ _ __/ /_',
25
+ ' / /| | / ___/ ___/ __ \\/ / __/ _ \\| |/_/ __/',
26
+ ' / ___ |/ / / /__/ / / / / /_/ __/> </ /_ ',
27
+ '/_/ |_/_/ \\___/_/ /_/_/\\__/\\___/_/|_|\\__/ '
28
+ ].freeze
29
+
30
+ Selection = Data.define(:paths, :new_query, :new_vault, :reprompt_query)
31
+ QueryPrompt = Data.define(:query, :open_vault_config, :quit)
32
+ VaultConfigAction = Data.define(:session_vault, :set_default_vault, :clear_default, :back)
33
+
34
+ def initialize(stdin:, stdout:, stderr:, app_name:)
35
+ @stdin = stdin
36
+ @stdout = stdout
37
+ @stderr = stderr
38
+ @app_name = app_name
39
+ @color = Terminal.enabled?(@stdout)
40
+ @intro_rendered = false
41
+ end
42
+
43
+ def prompt_query(default:, context:)
44
+ draw_intro
45
+ draw_startup_vault_status(
46
+ context[:vault],
47
+ context[:vault_source],
48
+ context[:default_vault],
49
+ context[:default_vault_path],
50
+ context[:connection_report]
51
+ )
52
+ @stdout.print render("[bold][cyan]Search query[/] [dim](default: #{default})[/] [dim]| type 'v' for vault config, 'q' to quit:[/] ")
53
+ input = @stdin.gets&.strip
54
+ return QueryPrompt.new(query: nil, open_vault_config: false, quit: true) if input.nil?
55
+
56
+ normalized = input.strip
57
+ return QueryPrompt.new(query: nil, open_vault_config: true, quit: false) if %w[v /v vault /vault].include?(normalized.downcase)
58
+ return QueryPrompt.new(query: nil, open_vault_config: false, quit: true) if %w[q /q quit /quit].include?(normalized.downcase)
59
+
60
+ QueryPrompt.new(query: normalized.empty? ? default : normalized, open_vault_config: false, quit: false)
61
+ end
62
+
63
+ # rubocop:disable Metrics/AbcSize
64
+ def prompt_vault_config(active_vault:, active_vault_source:, default_vault:, default_vault_path:)
65
+ draw_intro
66
+ @stdout.puts render('[bold][cyan]Vault Configuration[/]')
67
+ @stdout.puts render("[dim]active:[/] #{format_vault_label(active_vault, active_vault_source)}")
68
+ @stdout.puts render("[dim]saved default:[/] #{format_saved_default(default_vault)}")
69
+ @stdout.puts render("[dim]default config path:[/] #{default_vault_path}")
70
+ @stdout.puts render('[dim]vault path resolution is handled by Obsidian CLI via vault=<name_or_id>.[/]')
71
+ @stdout.puts
72
+ @stdout.puts render('[dim]Commands:[/]')
73
+ @stdout.puts render(' [cyan]use <vault>[/] [dim]set active vault for this run[/]')
74
+ @stdout.puts render(' [cyan]save <vault>[/] [dim]save persistent default vault[/]')
75
+ @stdout.puts render(' [cyan]clear[/] [dim]clear persistent default vault[/]')
76
+ @stdout.puts render(' [cyan]none[/] [dim]clear active vault (use Obsidian CLI default)[/]')
77
+ @stdout.puts render(' [cyan]back[/] [dim]return to search prompt[/]')
78
+ @stdout.puts
79
+ @stdout.print render('[bold][cyan]vault-config[/]> ')
80
+ input = @stdin.gets&.strip
81
+ return VaultConfigAction.new(session_vault: nil, set_default_vault: nil, clear_default: false, back: true) if input.nil?
82
+
83
+ command = input.strip
84
+ return VaultConfigAction.new(session_vault: nil, set_default_vault: nil, clear_default: false, back: true) if command.empty?
85
+ return VaultConfigAction.new(session_vault: nil, set_default_vault: nil, clear_default: false, back: true) if command.casecmp('back').zero?
86
+ return VaultConfigAction.new(session_vault: '', set_default_vault: nil, clear_default: false, back: false) if command.casecmp('none').zero?
87
+ return VaultConfigAction.new(session_vault: nil, set_default_vault: nil, clear_default: true, back: false) if command.casecmp('clear').zero?
88
+
89
+ if (match = command.match(/\Asave\s+(.+)\z/i))
90
+ return VaultConfigAction.new(session_vault: nil, set_default_vault: match[1].strip, clear_default: false, back: false)
91
+ end
92
+
93
+ if (match = command.match(/\Ause\s+(.+)\z/i))
94
+ return VaultConfigAction.new(session_vault: match[1].strip, set_default_vault: nil, clear_default: false, back: false)
95
+ end
96
+
97
+ VaultConfigAction.new(session_vault: command, set_default_vault: nil, clear_default: false, back: false)
98
+ end
99
+ # rubocop:enable Metrics/AbcSize
100
+
101
+ # rubocop:disable Metrics/BlockLength, Metrics/MethodLength
102
+ def select(paths, query:, vault:, vault_source:)
103
+ state = {
104
+ query: query,
105
+ vault: vault,
106
+ vault_source: vault_source,
107
+ filter: '',
108
+ cursor: 0,
109
+ offset: 0,
110
+ selected: {}
111
+ }
112
+
113
+ with_screen do
114
+ loop do
115
+ visible = filtered_paths(paths, state[:filter])
116
+ clamp_cursor!(state, visible.length)
117
+ draw_selector(paths:, visible:, state:)
118
+
119
+ case read_key
120
+ when :up
121
+ state[:cursor] -= 1
122
+ when :down
123
+ state[:cursor] += 1
124
+ when :space
125
+ toggle_current(visible, state)
126
+ when :all
127
+ toggle_all(visible, state)
128
+ when :filter
129
+ state[:filter] = prompt_inline('Filter visible results', state[:filter])
130
+ when :new_query
131
+ return Selection.new(
132
+ paths: [],
133
+ new_query: prompt_inline('New Obsidian search', state[:query]),
134
+ new_vault: nil,
135
+ reprompt_query: false
136
+ )
137
+ when :new_vault
138
+ return Selection.new(
139
+ paths: [],
140
+ new_query: nil,
141
+ new_vault: prompt_inline('Vault name or id (blank clears)', state[:vault].to_s),
142
+ reprompt_query: false
143
+ )
144
+ when :enter
145
+ selected = selected_paths(paths, state)
146
+ return Selection.new(paths: selected, new_query: nil, new_vault: nil, reprompt_query: false)
147
+ when :quit
148
+ return Selection.new(paths: [], new_query: nil, new_vault: nil, reprompt_query: true)
149
+ when :ctrl_c
150
+ return Selection.new(paths: [], new_query: nil, new_vault: nil, reprompt_query: false)
151
+ end
152
+
153
+ clamp_cursor!(state, visible.length)
154
+ keep_cursor_visible!(state, visible.length)
155
+ end
156
+ end
157
+ end
158
+ # rubocop:enable Metrics/BlockLength, Metrics/MethodLength
159
+
160
+ def show_no_results(query, vault:, vault_source:, default_vault_path:, obsidian_executable:)
161
+ vault_label = format_vault_label(vault, vault_source)
162
+ @stderr.puts render("[red]No Obsidian notes matched[/] [amber]#{query.inspect}[/] #{vault_label}")
163
+ @stderr.puts render("[dim]default vault config:[/] #{default_vault_path}")
164
+ @stderr.puts render("[dim]obsidian cli:[/] #{obsidian_executable}")
165
+ @stderr.puts render('[amber]Tip:[/] at search prompt type [bold]v[/] for vault config, or pass [bold]--vault[/].')
166
+ end
167
+
168
+ def show_no_selection
169
+ @stderr.puts render('[amber]No files selected.[/]')
170
+ end
171
+
172
+ def show_copied(bytes)
173
+ @stdout.puts render("[green]Copied[/] [bold]#{bytes} bytes[/] [dim]to clipboard.[/]")
174
+ end
175
+
176
+ def show_error(message)
177
+ @stderr.puts render("[red]#{message}[/]")
178
+ end
179
+
180
+ def show_info(message)
181
+ @stdout.puts render("[green]#{message}[/]")
182
+ end
183
+
184
+ def show_dry_run(selected_paths, bytes)
185
+ draw_intro
186
+ @stdout.puts render("[bold][green]Dry run[/] [dim]#{selected_paths.length} selected file(s)[/]")
187
+ @stdout.puts
188
+ selected_paths.each { |path| @stdout.puts render(" [cyan]•[/] #{path}") }
189
+ @stdout.puts
190
+ @stdout.puts render("[dim]Estimated context size:[/] [bold]#{bytes} bytes[/]")
191
+ end
192
+
193
+ private
194
+
195
+ def draw_intro
196
+ return unless @stdout.tty?
197
+ return if @intro_rendered
198
+
199
+ width = terminal_size.last
200
+ play_intro_animation(width)
201
+ @intro_rendered = true
202
+ end
203
+
204
+ def play_intro_animation(width)
205
+ frames = @color ? [%i[faint dim], %i[blue dim], %i[cyan white]] : [[nil, nil]]
206
+
207
+ frames.each do |logo_style, version_style|
208
+ @stdout.write Terminal::HOME
209
+ @stdout.write Terminal::CLEAR
210
+ @stdout.puts
211
+ LOGO.each do |line|
212
+ styled = logo_style ? Terminal.paint(line, logo_style, enabled: @color) : line
213
+ @stdout.puts center(styled, width)
214
+ end
215
+ @stdout.puts center(render('[dim]Architect Obsidian context and stitch for agent work[/]'), width)
216
+ version = "v#{Architext::VERSION}"
217
+ styled_version = version_style ? Terminal.paint(version, version_style, enabled: @color) : version
218
+ @stdout.puts center(styled_version, width)
219
+ @stdout.puts
220
+ @stdout.flush
221
+ sleep(0.08) if frames.length > 1
222
+ end
223
+ end
224
+
225
+ def with_screen
226
+ use_alt_screen = Terminal.alt_screen_supported?(@stdout)
227
+ @selector_frame_started = false
228
+ @stdout.write Terminal::ALT_SCREEN if use_alt_screen
229
+ @stdout.write Terminal::HIDE_CURSOR
230
+ @stdout.write Terminal::HOME
231
+ @stdout.write Terminal::CLEAR
232
+ yield
233
+ ensure
234
+ @stdout.write Terminal::SHOW_CURSOR
235
+ @stdout.write Terminal::MAIN_SCREEN if use_alt_screen
236
+ @selector_frame_started = false
237
+ end
238
+
239
+ def draw_selector(paths:, visible:, state:)
240
+ height, terminal_width = terminal_size
241
+ width = usable_width(terminal_width)
242
+ list_height = [height - 8, 5].max
243
+ keep_cursor_visible!(state, visible.length, list_height:)
244
+ rows = visible.drop(state[:offset]).first(list_height)
245
+ lines = []
246
+
247
+ lines.concat(header_lines(width, state, paths.length, visible.length))
248
+ lines << section_title(width, 'Context Candidates')
249
+
250
+ rows.each_with_index do |path, index|
251
+ absolute_index = state[:offset] + index
252
+ active = absolute_index == state[:cursor]
253
+ checked = state[:selected][path]
254
+ marker = checked ? '[x]' : '[ ]'
255
+ pointer = active ? '>' : ' '
256
+ style = if active
257
+ :inverse
258
+ else
259
+ checked ? :green : :ink
260
+ end
261
+ line = "#{pointer} #{marker} #{path}"
262
+ truncated = Terminal.truncate(line, width)
263
+ lines << Terminal.paint(truncated, style, enabled: @color)
264
+ end
265
+
266
+ empty_rows = list_height - rows.length
267
+ empty_rows.times { lines << '' }
268
+
269
+ lines.concat(status_lines(width, state, visible.length))
270
+ frame = lines.map { |line| clear_line(line) }.join("\n")
271
+
272
+ @stdout.write Terminal::HOME
273
+ @stdout.write(@selector_frame_started ? Terminal::CLEAR_TO_END : Terminal::CLEAR)
274
+ @stdout.write frame
275
+ @stdout.write Terminal::CLEAR_TO_END
276
+ @stdout.flush
277
+ @selector_frame_started = true
278
+ end
279
+
280
+ def header_lines(width, state, total, visible_count)
281
+ filter_label = state[:filter].empty? ? 'none' : state[:filter]
282
+ [
283
+ Terminal.truncate(render("[bold][cyan]ARCHiTEXT[/] [dim]#{state[:vault] || 'obsidian default'}[/]"), width),
284
+ Terminal.truncate(render("[dim]query:[/] [amber]#{state[:query]}[/] [dim]filter:[/] [cyan]#{filter_label}[/]"), width),
285
+ Terminal.truncate(render("[dim]results:[/] #{visible_count}/#{total} [dim]selected:[/] #{state[:selected].length}"), width),
286
+ Terminal.paint('-' * width, :faint, enabled: @color)
287
+ ]
288
+ end
289
+
290
+ def section_title(width, title)
291
+ Terminal.paint(Terminal.truncate("-- #{title} ", width), :blue, enabled: @color)
292
+ end
293
+
294
+ def status_lines(width, state, visible_count)
295
+ detail = if visible_count.zero?
296
+ '[amber]No visible results. Press / to change filter or n for a new search.[/]'
297
+ elsif state[:selected].empty?
298
+ '[dim]Select one or more notes, then press enter.[/]'
299
+ else
300
+ "[green]Ready:[/] #{state[:selected].length} note(s) selected."
301
+ end
302
+ [
303
+ Terminal.paint('-' * width, :faint, enabled: @color),
304
+ Terminal.truncate(render("[dim]#{HELP}[/]"), width),
305
+ Terminal.truncate(render(detail), width)
306
+ ]
307
+ end
308
+
309
+ def clear_line(text)
310
+ "#{Terminal::CLEAR_LINE}\r#{text}"
311
+ end
312
+
313
+ def usable_width(width)
314
+ (width.to_i - 2).clamp(40, 160)
315
+ end
316
+
317
+ def prompt_inline(label, current)
318
+ @stdout.write Terminal::SHOW_CURSOR
319
+ @stdout.print render("\n[bold][cyan]#{label}[/] [dim](blank keeps current)[/]: ")
320
+ input = @stdin.gets&.strip
321
+ @stdout.write Terminal::HIDE_CURSOR
322
+ input.nil? || input.empty? ? current : input
323
+ end
324
+
325
+ def filtered_paths(paths, filter)
326
+ needle = filter.to_s.downcase
327
+ return paths if needle.empty?
328
+
329
+ paths.select { |path| path.downcase.include?(needle) }
330
+ end
331
+
332
+ def selected_paths(paths, state)
333
+ paths.select { |path| state[:selected][path] }
334
+ end
335
+
336
+ def toggle_current(visible, state)
337
+ path = visible[state[:cursor]]
338
+ return unless path
339
+
340
+ state[:selected][path] ? state[:selected].delete(path) : state[:selected][path] = true
341
+ end
342
+
343
+ def toggle_all(visible, state)
344
+ return if visible.empty?
345
+
346
+ all_selected = visible.all? { |path| state[:selected][path] }
347
+ visible.each do |path|
348
+ all_selected ? state[:selected].delete(path) : state[:selected][path] = true
349
+ end
350
+ end
351
+
352
+ def clamp_cursor!(state, count)
353
+ max = [count - 1, 0].max
354
+ state[:cursor] = state[:cursor].clamp(0, max)
355
+ end
356
+
357
+ def keep_cursor_visible!(state, count, list_height: nil)
358
+ list_height ||= [terminal_size.first - 8, 5].max
359
+ state[:offset] = state[:offset].clamp(0, [count - list_height, 0].max)
360
+ state[:offset] = state[:cursor] if state[:cursor] < state[:offset]
361
+ return unless state[:cursor] >= state[:offset] + list_height
362
+
363
+ state[:offset] = state[:cursor] - list_height + 1
364
+ end
365
+
366
+ def read_key
367
+ key = @stdin.getch
368
+ return :ctrl_c if key == "\u0003"
369
+
370
+ return :enter if ["\r", "\n"].include?(key)
371
+
372
+ mapped = KEY_BINDINGS[key]
373
+ return mapped if mapped
374
+
375
+ bytes = key.to_s.bytes
376
+ if bytes.first == 27 && bytes.length > 1
377
+ parse_escape_sequence(bytes[1..])
378
+ elsif windows_extended_combo?(key)
379
+ parse_windows_extended_code(bytes[1])
380
+ elsif windows_extended_key_prefix?(key)
381
+ parse_windows_extended_key
382
+ elsif key == "\e"
383
+ parse_escape_key
384
+ else
385
+ :unknown
386
+ end
387
+ end
388
+
389
+ def windows_extended_key_prefix?(key)
390
+ [0, 224].include?(key.to_s.bytes.first)
391
+ end
392
+
393
+ def windows_extended_combo?(key)
394
+ key.to_s.bytes.length > 1 && windows_extended_key_prefix?(key)
395
+ end
396
+
397
+ def parse_windows_extended_key
398
+ parse_windows_extended_code(@stdin.getch)
399
+ end
400
+
401
+ def parse_windows_extended_code(code)
402
+ case code_byte(code)
403
+ when 72
404
+ :up
405
+ when 80
406
+ :down
407
+ else
408
+ :unknown
409
+ end
410
+ end
411
+
412
+ def parse_escape_key
413
+ parse_escape_sequence(read_escape_sequence)
414
+ end
415
+
416
+ def parse_escape_sequence(sequence)
417
+ text = sequence.is_a?(Array) ? sequence.pack('C*') : sequence.to_s
418
+
419
+ case text
420
+ when /\A(?:\[A|OA)/
421
+ :up
422
+ when /\A(?:\[B|OB)/
423
+ :down
424
+ else
425
+ :unknown
426
+ end
427
+ end
428
+
429
+ def read_escape_sequence
430
+ sequence = []
431
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.08
432
+
433
+ loop do
434
+ sequence.concat(@stdin.read_nonblock(8).bytes)
435
+ break if escape_sequence_complete?(sequence)
436
+ rescue IO::WaitReadable, EOFError
437
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
438
+
439
+ sleep 0.005
440
+ rescue NotImplementedError
441
+ sequence.concat(read_escape_sequence_with_getch)
442
+ break
443
+ end
444
+
445
+ sequence
446
+ end
447
+
448
+ def escape_sequence_complete?(sequence)
449
+ sequence.pack('C*').match?(/\A(?:O[A-D]|\[[0-9;?]*[A-Za-z~])\z/)
450
+ end
451
+
452
+ def read_escape_sequence_with_getch
453
+ bytes = []
454
+ 2.times do
455
+ key = @stdin.getch
456
+ bytes.concat(key.to_s.bytes)
457
+ break if escape_sequence_complete?(bytes)
458
+ rescue EOFError
459
+ break
460
+ end
461
+ bytes
462
+ end
463
+
464
+ def code_byte(value)
465
+ return value if value.is_a?(Integer)
466
+
467
+ value.to_s.bytes.first
468
+ end
469
+
470
+ def terminal_size
471
+ @stdout.winsize
472
+ rescue StandardError
473
+ [28, 100]
474
+ end
475
+
476
+ def center(text, width)
477
+ visible = Terminal.visible_length(text)
478
+ "#{' ' * [(width - visible) / 2, 0].max}#{text}"
479
+ end
480
+
481
+ def render(markup)
482
+ Terminal.render(markup, enabled: @color)
483
+ end
484
+
485
+ def draw_startup_vault_status(vault, vault_source, default_vault, default_vault_path, connection_report)
486
+ @stdout.puts render("[dim]active vault:[/] #{format_vault_label(vault, vault_source)}")
487
+ @stdout.puts render("[dim]saved default:[/] #{format_saved_default(default_vault)}")
488
+ @stdout.puts render("[dim]default config path:[/] #{default_vault_path}")
489
+ @stdout.puts render("[dim]obsidian cli:[/] #{connection_report[:executable]}")
490
+ @stdout.puts render("[dim]obsidian version:[/] #{connection_report[:version] || 'unknown'}")
491
+ status_style = connection_report[:status] == 'ok' ? '[green]ok[/]' : '[red]error[/]'
492
+ @stdout.puts render("[dim]connection check:[/] #{status_style}")
493
+ @stdout.puts render("[dim]resolved vault:[/] #{connection_report[:resolved_vault_summary]}") if connection_report[:resolved_vault_summary]
494
+ @stdout.puts render("[amber]diagnostic:[/] #{connection_report[:warning]}") if connection_report[:warning]
495
+ @stdout.puts render('[dim]vault target semantics: CWD vault if inside one, otherwise active Obsidian vault unless overridden.[/]')
496
+ @stdout.puts
497
+ end
498
+
499
+ def format_vault_label(vault, source)
500
+ return "[amber]none selected[/] [dim](#{source})[/]" if vault.to_s.strip.empty?
501
+
502
+ "[cyan]#{vault}[/] [dim](#{source})[/]"
503
+ end
504
+
505
+ def format_saved_default(default_vault)
506
+ return '[dim]none[/]' if default_vault.to_s.strip.empty?
507
+
508
+ "[cyan]#{default_vault}[/]"
509
+ end
510
+ end
511
+ # rubocop:enable Metrics/ClassLength
512
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Architext
4
+ VERSION = '0.2.0'
5
+ end
data/lib/architext.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'architext/bundle'
4
+ require_relative 'architext/clipboard'
5
+ require_relative 'architext/cli'
6
+ require_relative 'architext/obsidian'
7
+ require_relative 'architext/picker'
8
+ require_relative 'architext/terminal'
9
+ require_relative 'architext/search_results'
10
+ require_relative 'architext/selection_parser'
11
+ require_relative 'architext/settings'
12
+ require_relative 'architext/tui'
13
+ require_relative 'architext/version'
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: architext
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Can
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: tty-prompt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.23'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.23'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.60'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.60'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop-minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.34'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.34'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop-rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.6'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.6'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '5.0'
82
+ description: A terminal interface to interactively select and bundle Obsidian notes
83
+ for AI agent context.
84
+ executables:
85
+ - architext
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - CHANGELOG.md
90
+ - LICENSE
91
+ - README.md
92
+ - bin/architext
93
+ - bin/setup
94
+ - lib/architext.rb
95
+ - lib/architext/bundle.rb
96
+ - lib/architext/cli.rb
97
+ - lib/architext/clipboard.rb
98
+ - lib/architext/obsidian.rb
99
+ - lib/architext/picker.rb
100
+ - lib/architext/search_results.rb
101
+ - lib/architext/selection_parser.rb
102
+ - lib/architext/settings.rb
103
+ - lib/architext/terminal.rb
104
+ - lib/architext/tui.rb
105
+ - lib/architext/version.rb
106
+ homepage: https://github.com/CanPixel/ARCHiTEXT#readme
107
+ licenses:
108
+ - MIT
109
+ metadata:
110
+ homepage_uri: https://github.com/CanPixel/ARCHiTEXT#readme
111
+ source_code_uri: https://github.com/CanPixel/ARCHiTEXT
112
+ bug_tracker_uri: https://github.com/CanPixel/ARCHiTEXT/issues
113
+ changelog_uri: https://github.com/CanPixel/ARCHiTEXT/blob/master/CHANGELOG.md
114
+ allowed_push_host: https://rubygems.org
115
+ rubygems_mfa_required: 'true'
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '3.2'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubygems_version: 3.6.9
131
+ specification_version: 4
132
+ summary: A visual Obsidian context stitching TUI for agent workflows.
133
+ test_files: []