appydave-tools 0.79.0 → 0.80.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec01667ec40e9b0b051f2e834800cd458c736f27107df033ddebeaf7de413823
4
- data.tar.gz: 6627f68aefaf25f40cd7d55ba87e8470bd8e5c5a99146927424149b84a31b3b6
3
+ metadata.gz: cb0b711e430f2082be55712eb0c3e630726bcc77628dc8ab144d72f27fc05bd7
4
+ data.tar.gz: 8fc9be5413601f6da5cccd649d324578df676efb842b138623998a1f6dd7b676
5
5
  SHA512:
6
- metadata.gz: 8e745ed73fc0f812a6275edb827df2901e5eb0b41c060b4ac68fdc27c9eff750b60b84d2244e21f2b6afb2cda98eacacc3895b6c82502673162202cb31075ddb
7
- data.tar.gz: 1814d3250ecf2aebf1ccb9cf50e117fc7b9f220334c263b5b1677336848db1cd7e37f9b684df2903aba96ecc69bbad96368c1acc87fbd6addfed4086eeffe00a
6
+ metadata.gz: d7527c25930ba5fb35206e6321fea074f12593a9188ed4b7d51f1f832f01f60a9929336b8670bf48bea0f68eeba0df150ecb542ea244be899334781ef432a7a1
7
+ data.tar.gz: 051f6c24d48fa6caa2ac342436a1105403d7524e871ee248996b772e0e16aafde612cd33386d4b6bba92b5f1f0c2e2f6cf61406e9f76db5a42e55d367031c642
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.79.0](https://github.com/appydave/appydave-tools/compare/v0.78.0...v0.79.0) (2026-04-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * add --stdin support to llm_context for piping file paths ([8890cfe](https://github.com/appydave/appydave-tools/commit/8890cfebb80cfda81b679da3e52a185bbb1c9141))
7
+
1
8
  # [0.78.0](https://github.com/appydave/appydave-tools/compare/v0.77.7...v0.78.0) (2026-04-03)
2
9
 
3
10
 
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'appydave/tools'
6
+
7
+ options = Appydave::Tools::BrainContextOptions.new
8
+
9
+ def setup_options(options)
10
+ OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
11
+ opts.banner = 'Usage: query_brain [options]'
12
+
13
+ opts.on('--brain NAME', 'Query brain by name, alias, or fuzzy match') do |name|
14
+ options.brain_names << name
15
+ end
16
+
17
+ opts.on('--tag TAG', 'Query brains by tag') do |tag|
18
+ options.tags << tag
19
+ end
20
+
21
+ opts.on('--category CAT', 'Query all brains in category') do |cat|
22
+ options.categories << cat
23
+ end
24
+
25
+ opts.on('--activity-min LEVEL', %w[high medium low none],
26
+ 'Filter brains by minimum activity level') do |level|
27
+ options.activity_levels << level
28
+ end
29
+
30
+ opts.on('--status STATUS', %w[active stable deprecated],
31
+ 'Filter brains by status (default: exclude deprecated)') do |status|
32
+ options.status = status
33
+ end
34
+
35
+ opts.on('--files-only', 'Exclude INDEX.md, only include content files') do
36
+ options.include_index = false
37
+ end
38
+
39
+ opts.on('-d', '--debug [MODE]', %w[none info params debug],
40
+ 'Debug output level') do |level|
41
+ options.debug_level = level || 'info'
42
+ end
43
+
44
+ opts.on('-v', '--version', 'Show version') do
45
+ puts "query_brain v#{Appydave::Tools::VERSION}"
46
+ exit 0
47
+ end
48
+
49
+ opts.on('-h', '--help', 'Show this message') do
50
+ puts opts
51
+ exit 0
52
+ end
53
+ end.parse!
54
+ end
55
+
56
+ setup_options(options)
57
+
58
+ # Query
59
+ finder = Appydave::Tools::BrainQuery.new(options)
60
+ paths = finder.find
61
+
62
+ # Output file paths, one per line
63
+ paths.each { |p| puts p }
64
+
65
+ exit 0
data/bin/query_omi.rb ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'appydave/tools'
6
+
7
+ options = Appydave::Tools::BrainContextOptions.new
8
+ options.omi = true
9
+
10
+ def setup_options(options)
11
+ OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
12
+ opts.banner = 'Usage: query_omi [options]'
13
+
14
+ opts.on('--signal SIGNAL', %w[work life ambient],
15
+ 'Filter by signal') do |signal|
16
+ options.omi_signals << signal
17
+ end
18
+
19
+ opts.on('--routing ROUTING',
20
+ 'Filter by routing (brain-update, todo-item, personal, til, archive; pipe-delimited)') do |routing|
21
+ options.omi_routings.concat(routing.split('|').map(&:strip))
22
+ end
23
+
24
+ opts.on('--activity ACTIVITY',
25
+ 'Filter by activity (planning, reviewing, learning, debugging, etc.; pipe-delimited)') do |activity|
26
+ options.omi_activities.concat(activity.split('|').map(&:strip))
27
+ end
28
+
29
+ opts.on('--date-from DATE', 'Include files from date (YYYY-MM-DD)') do |date|
30
+ options.date_from = date
31
+ end
32
+
33
+ opts.on('--date-to DATE', 'Include files up to date (YYYY-MM-DD)') do |date|
34
+ options.date_to = date
35
+ end
36
+
37
+ opts.on('--enriched-only', 'Skip raw (non-enriched) transcripts') do
38
+ options.enriched_only = true
39
+ end
40
+
41
+ opts.on('--brain NAME', 'Find OMI files mentioning this brain') do |name|
42
+ options.brain_names << name
43
+ end
44
+
45
+ opts.on('-d', '--debug [MODE]', %w[none info params debug],
46
+ 'Debug output level') do |level|
47
+ options.debug_level = level || 'info'
48
+ end
49
+
50
+ opts.on('-v', '--version', 'Show version') do
51
+ puts "query_omi v#{Appydave::Tools::VERSION}"
52
+ exit 0
53
+ end
54
+
55
+ opts.on('-h', '--help', 'Show this message') do
56
+ puts opts
57
+ exit 0
58
+ end
59
+ end.parse!
60
+ end
61
+
62
+ setup_options(options)
63
+
64
+ # Query
65
+ finder = Appydave::Tools::OmiQuery.new(options)
66
+ paths = finder.find
67
+
68
+ # Output file paths, one per line
69
+ paths.each { |p| puts p }
70
+
71
+ exit 0
data/exe/query_brain ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'appydave/tools'
6
+
7
+ load File.expand_path('../bin/query_brain.rb', __dir__)
data/exe/query_omi ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'appydave/tools'
6
+
7
+ load File.expand_path('../bin/query_omi.rb', __dir__)
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Appydave
6
+ module Tools
7
+ # Queries brains-index.json to find brain files
8
+ class BrainQuery
9
+ def initialize(options)
10
+ @options = options
11
+ @index = nil
12
+ @alias_map = nil
13
+ end
14
+
15
+ def find
16
+ return [] unless @options.brain_query?
17
+
18
+ load_index!
19
+ paths = []
20
+
21
+ # Handle brain name queries
22
+ @options.brain_names.each do |name|
23
+ paths.concat(find_by_name(name))
24
+ end
25
+
26
+ # Handle tag queries
27
+ @options.tags.each do |tag|
28
+ paths.concat(find_by_tag(tag))
29
+ end
30
+
31
+ # Handle category queries
32
+ @options.categories.each do |category|
33
+ paths.concat(find_by_category(category))
34
+ end
35
+
36
+ # Handle activity level filters
37
+ paths.select! { |p| matches_activity_level?(p) } if @options.activity_levels.any?
38
+
39
+ # Remove duplicates and return
40
+ paths.uniq.sort
41
+ end
42
+
43
+ private
44
+
45
+ def load_index!
46
+ return if @index
47
+
48
+ index_path = @options.brains_index_path
49
+ unless File.exist?(index_path)
50
+ raise "brains-index.json not found at #{index_path}. Run: python ~/dev/ad/brains/.claude/skills/brain-librarian/scripts/build_brain_index.py build --all ~/dev/ad/brains"
51
+ end
52
+
53
+ @index = JSON.parse(File.read(index_path))
54
+ build_alias_map!
55
+ end
56
+
57
+ def build_alias_map!
58
+ @alias_map = {}
59
+ @index['alias_index']&.each { |alias_name, brain_name| @alias_map[alias_name.downcase] = brain_name }
60
+ end
61
+
62
+ def find_by_name(name)
63
+ load_index!
64
+
65
+ # Step 1: Exact match on brain key
66
+ brain_entry = find_brain_in_index(name)
67
+ return brain_entries_to_paths([brain_entry]) if brain_entry
68
+
69
+ # Step 2: Alias match
70
+ if @alias_map && @alias_map[name.downcase]
71
+ brain_name = @alias_map[name.downcase]
72
+ brain_entry = find_brain_in_index(brain_name)
73
+ return brain_entries_to_paths([brain_entry]) if brain_entry
74
+ end
75
+
76
+ # Step 3: Substring match on brain key (case-insensitive)
77
+ matches = []
78
+ @index['categories'].each_value do |category_data|
79
+ category_data['brains'].each do |brain_name, brain_data|
80
+ matches << brain_data if brain_name.downcase.include?(name.downcase)
81
+ end
82
+ end
83
+ return brain_entries_to_paths(matches) if matches.length == 1
84
+ return brain_entries_to_paths(matches) if matches.any?
85
+
86
+ # Not found
87
+ []
88
+ end
89
+
90
+ def find_by_tag(tag)
91
+ load_index!
92
+ tag = tag.downcase.gsub('_', '-')
93
+
94
+ brain_names = @index['tag_index']&.[](tag) || []
95
+ brain_entries = []
96
+
97
+ brain_names.each do |brain_name|
98
+ entry = find_brain_in_index(brain_name)
99
+ brain_entries << entry if entry
100
+ end
101
+
102
+ brain_entries_to_paths(brain_entries)
103
+ end
104
+
105
+ def find_by_category(category)
106
+ load_index!
107
+ category_data = @index['categories'][category] || @index['categories'][category.downcase]
108
+
109
+ return [] unless category_data
110
+
111
+ brain_entries = category_data['brains'].values
112
+ brain_entries_to_paths(brain_entries)
113
+ end
114
+
115
+ def find_brain_in_index(brain_name)
116
+ @index['categories'].each_value do |category_data|
117
+ return category_data['brains'][brain_name] if category_data['brains'][brain_name]
118
+ end
119
+ nil
120
+ end
121
+
122
+ def brain_entries_to_paths(brain_entries)
123
+ paths = []
124
+
125
+ brain_entries.each do |entry|
126
+ next unless entry
127
+
128
+ # Add files from files[] array
129
+ entry['files']&.each do |file|
130
+ # File entry is relative to the brain directory
131
+ brain_name = extract_brain_name(entry)
132
+ full_path = File.join(@options.brains_root, brain_name, file)
133
+ paths << full_path if File.exist?(full_path)
134
+ end
135
+
136
+ # Add INDEX.md if requested
137
+ if @options.include_index
138
+ index_path = File.join(@options.brains_root, entry['index_path'])
139
+ paths << index_path if File.exist?(index_path)
140
+ end
141
+ end
142
+
143
+ paths
144
+ end
145
+
146
+ def extract_brain_name(entry)
147
+ # index_path is like "brain-name/INDEX.md"
148
+ entry['index_path'].split('/')[0]
149
+ end
150
+
151
+ def matches_activity_level?(file_path)
152
+ # Extract brain name from path
153
+ brain_name = file_path.match(%r{#{Regexp.escape(@options.brains_root)}/([^/]+)})&.[](1)
154
+ return false unless brain_name
155
+
156
+ # Find the brain entry
157
+ entry = find_brain_in_index(brain_name)
158
+ return false unless entry
159
+
160
+ # Check if activity level matches filter
161
+ @options.activity_levels.include?(entry['activity_level'])
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Appydave
6
+ module Tools
7
+ # Queries OMI transcript directory by enriched frontmatter
8
+ class OmiQuery
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+
13
+ def find
14
+ return [] unless @options.omi_query?
15
+
16
+ paths = []
17
+
18
+ Dir.glob(File.join(@options.omi_dir, '*.md')).each do |file_path|
19
+ next unless include_file?(file_path)
20
+
21
+ paths << file_path
22
+ end
23
+
24
+ paths.sort
25
+ end
26
+
27
+ private
28
+
29
+ def include_file?(file_path)
30
+ frontmatter = extract_frontmatter(file_path)
31
+ return false unless frontmatter
32
+
33
+ # If enriched-only requested, skip raw files
34
+ return false if @options.enriched_only && (!frontmatter['signal'] || !frontmatter['extraction_summary'])
35
+
36
+ # Apply filters
37
+ return false unless signal_matches?(frontmatter)
38
+ return false unless routing_matches?(frontmatter)
39
+ return false unless activity_matches?(frontmatter)
40
+ return false unless date_matches?(frontmatter)
41
+ return false unless brain_matches?(frontmatter)
42
+
43
+ true
44
+ end
45
+
46
+ def extract_frontmatter(file_path)
47
+ content = File.read(file_path, encoding: 'utf-8')
48
+ lines = content.split("\n")
49
+
50
+ return nil unless lines[0] == '---'
51
+
52
+ frontmatter = {}
53
+ i = 1
54
+ while i < lines.length
55
+ line = lines[i]
56
+ break if line == '---'
57
+
58
+ # Parse YAML-like: key: value
59
+ if line.match?(/^[a-z_]+:/)
60
+ key, value = parse_yaml_line(line)
61
+ frontmatter[key] = value
62
+ end
63
+
64
+ i += 1
65
+ end
66
+
67
+ frontmatter.empty? ? nil : frontmatter
68
+ end
69
+
70
+ def parse_yaml_line(line)
71
+ # Handle simple cases: key: value, key: [a, b, c]
72
+ match = line.match(/^([a-z_]+):\s*(.*)$/)
73
+ return [nil, nil] unless match
74
+
75
+ key = match[1]
76
+ value_str = match[2].strip
77
+
78
+ # Parse array [a, b, c]
79
+ if value_str.start_with?('[') && value_str.end_with?(']')
80
+ array_content = value_str[1..-2]
81
+ value = array_content.split(',').map { |v| v.strip.gsub(/^["']|["']$/, '') }
82
+ # Parse quoted string
83
+ elsif (value_str.start_with?('"') && value_str.end_with?('"')) ||
84
+ (value_str.start_with?("'") && value_str.end_with?("'"))
85
+ value = value_str[1..-2]
86
+ # Parse date (YYYY-MM-DD) or other values as-is
87
+ else
88
+ value = value_str
89
+ end
90
+
91
+ [key, value]
92
+ end
93
+
94
+ def signal_matches?(frontmatter)
95
+ return true if @options.omi_signals.empty?
96
+
97
+ signal = frontmatter['signal']
98
+ return false unless signal
99
+
100
+ @options.omi_signals.include?(signal)
101
+ end
102
+
103
+ def routing_matches?(frontmatter)
104
+ return true if @options.omi_routings.empty?
105
+
106
+ routing = frontmatter['routing']
107
+ return false unless routing
108
+
109
+ # routing can be pipe-delimited
110
+ routings = routing.split('|').map(&:strip)
111
+ routings.any? { |r| @options.omi_routings.include?(r) }
112
+ end
113
+
114
+ def activity_matches?(frontmatter)
115
+ return true if @options.omi_activities.empty?
116
+
117
+ activity = frontmatter['activity']
118
+ return false unless activity
119
+
120
+ # activity can be pipe-delimited
121
+ activities = activity.split('|').map(&:strip)
122
+ activities.any? { |a| @options.omi_activities.include?(a) }
123
+ end
124
+
125
+ def date_matches?(frontmatter)
126
+ extracted_at = frontmatter['extracted_at']
127
+ return true if extracted_at.nil? # No date field = include
128
+
129
+ begin
130
+ date = Date.parse(extracted_at)
131
+ rescue StandardError
132
+ return true # Can't parse = include
133
+ end
134
+
135
+ from_ok = @options.date_from.nil? || date >= Date.parse(@options.date_from)
136
+ to_ok = @options.date_to.nil? || date <= Date.parse(@options.date_to)
137
+
138
+ from_ok && to_ok
139
+ end
140
+
141
+ def brain_matches?(frontmatter)
142
+ return true if @options.brain_names.empty?
143
+
144
+ matched_brains = frontmatter['matched_brains']
145
+ return false unless matched_brains
146
+
147
+ # matched_brains is an array
148
+ matched_brains_list = matched_brains.is_a?(Array) ? matched_brains : [matched_brains]
149
+ matched_brains_list.any? { |brain| @options.brain_names.include?(brain) }
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ # Options struct for brain/OMI query tools
6
+ class BrainContextOptions
7
+ attr_accessor :brain_names, :tags, :categories, :activity_levels, :status,
8
+ :omi, :omi_signals, :omi_routings, :omi_activities,
9
+ :date_from, :date_to, :enriched_only,
10
+ :include_index, :output_targets, :formats, :line_limit,
11
+ :debug_level, :dry_run, :tokens, :base_dir, :omi_dir
12
+
13
+ def initialize
14
+ @brain_names = []
15
+ @tags = []
16
+ @categories = []
17
+ @activity_levels = []
18
+ @status = 'active' # default: exclude deprecated
19
+
20
+ @omi = false
21
+ @omi_signals = []
22
+ @omi_routings = []
23
+ @omi_activities = []
24
+ @date_from = nil
25
+ @date_to = nil
26
+ @enriched_only = false
27
+
28
+ @include_index = true
29
+ @output_targets = ['clipboard'] # default to clipboard
30
+ @formats = ['content']
31
+ @line_limit = nil
32
+ @debug_level = 'none'
33
+ @dry_run = false
34
+ @tokens = false
35
+ @base_dir = Dir.pwd
36
+
37
+ @omi_dir = File.expand_path('~/dev/raw-intake/omi')
38
+ end
39
+
40
+ def brains_root
41
+ @brains_root ||= File.expand_path('~/dev/ad/brains')
42
+ end
43
+
44
+ def brains_index_path
45
+ File.join(brains_root, 'audit', 'brains-index.json')
46
+ end
47
+
48
+ def brain_query?
49
+ brain_names.any? || tags.any? || categories.any? || activity_levels.any?
50
+ end
51
+
52
+ def omi_query?
53
+ omi
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.79.0'
5
+ VERSION = '0.80.0'
6
6
  end
7
7
  end
@@ -37,6 +37,10 @@ require 'appydave/tools/llm_context/options'
37
37
  require 'appydave/tools/llm_context/file_collector'
38
38
  require 'appydave/tools/llm_context/output_handler'
39
39
 
40
+ require 'appydave/tools/brain_context/options'
41
+ require 'appydave/tools/brain_context/brain_finder'
42
+ require 'appydave/tools/brain_context/omi_finder'
43
+
40
44
  require 'appydave/tools/configuration/openai'
41
45
  require 'appydave/tools/configuration/configurable'
42
46
  require 'appydave/tools/configuration/config'
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.79.0
4
+ version: 0.80.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
@@ -190,6 +190,8 @@ executables:
190
190
  - jump
191
191
  - llm_context
192
192
  - prompt_tools
193
+ - query_brain
194
+ - query_omi
193
195
  - subtitle_processor
194
196
  - youtube_automation
195
197
  - youtube_manager
@@ -229,6 +231,8 @@ files:
229
231
  - bin/llm_context.rb
230
232
  - bin/move_images.rb
231
233
  - bin/prompt_tools.rb
234
+ - bin/query_brain.rb
235
+ - bin/query_omi.rb
232
236
  - bin/setup
233
237
  - bin/subtitle_manager-old.rb
234
238
  - bin/subtitle_manager.rb
@@ -338,12 +342,17 @@ files:
338
342
  - exe/jump
339
343
  - exe/llm_context
340
344
  - exe/prompt_tools
345
+ - exe/query_brain
346
+ - exe/query_omi
341
347
  - exe/subtitle_processor
342
348
  - exe/youtube_automation
343
349
  - exe/youtube_manager
344
350
  - exe/zsh_history
345
351
  - images.log
346
352
  - lib/appydave/tools.rb
353
+ - lib/appydave/tools/brain_context/brain_finder.rb
354
+ - lib/appydave/tools/brain_context/omi_finder.rb
355
+ - lib/appydave/tools/brain_context/options.rb
347
356
  - lib/appydave/tools/cli_actions/_doc.md
348
357
  - lib/appydave/tools/cli_actions/base_action.rb
349
358
  - lib/appydave/tools/cli_actions/get_video_action.rb