appydave-tools 0.84.0 → 0.85.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,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Appydave
6
+ module Tools
7
+ module AppContext
8
+ # Queries locations.json to find app files via context.globs.json patterns.
9
+ #
10
+ # Follows the same find/find_meta pattern as BrainQuery and OmiQuery.
11
+ class AppQuery
12
+ def initialize(options, jump_config: nil)
13
+ @options = options
14
+ @jump_config = jump_config
15
+ end
16
+
17
+ # Return absolute file paths matching the query
18
+ def find
19
+ return [] unless @options.query?
20
+
21
+ apps = resolve_apps
22
+ return [] if apps.empty?
23
+
24
+ apps.flat_map { |app| expand_app(app) }.uniq.sort
25
+ end
26
+
27
+ # Return structured metadata about the query results
28
+ def find_meta
29
+ return [] unless @options.query?
30
+
31
+ apps = resolve_apps
32
+ return [] if apps.empty?
33
+
34
+ apps.map { |app| build_meta(app) }
35
+ end
36
+
37
+ # List all available glob names for a specific app
38
+ def list_globs(app_name)
39
+ location = find_location(app_name)
40
+ return [] unless location
41
+
42
+ loader = build_loader(location)
43
+ return [] unless loader.available?
44
+
45
+ loader.available_names
46
+ end
47
+
48
+ # List all apps that have context.globs.json
49
+ def list_apps
50
+ jump_config.locations
51
+ .select { |loc| File.exist?(File.join(expand_path(loc.path), 'context.globs.json')) }
52
+ .map do |loc|
53
+ loader = build_loader(loc)
54
+ {
55
+ 'key' => loc.key,
56
+ 'path' => expand_path(loc.path),
57
+ 'pattern' => loader.pattern,
58
+ 'glob_count' => loader.globs.size
59
+ }
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def jump_config
66
+ @jump_config ||= Jump::Config.new
67
+ end
68
+
69
+ # Resolve app names to Location objects
70
+ def resolve_apps
71
+ if @options.pattern_filter
72
+ resolve_by_pattern(@options.pattern_filter)
73
+ else
74
+ @options.app_names.flat_map { |name| resolve_app(name) }.compact.uniq(&:key)
75
+ end
76
+ end
77
+
78
+ # 4-tier app resolution
79
+ def resolve_app(name)
80
+ name_down = name.downcase
81
+
82
+ # Tier 1: exact key match
83
+ loc = jump_config.find(name_down)
84
+ return [loc] if loc
85
+
86
+ # Tier 2: jump alias match
87
+ loc = jump_config.locations.find { |l| l.jump&.downcase == name_down }
88
+ return [loc] if loc
89
+
90
+ # Tier 3: substring match on key
91
+ matches = jump_config.locations.select { |l| l.key.downcase.include?(name_down) }
92
+ return matches unless matches.empty?
93
+
94
+ # Tier 4: substring match on description
95
+ jump_config.locations.select { |l| l.description&.downcase&.include?(name_down) }
96
+ end
97
+
98
+ def resolve_by_pattern(pattern)
99
+ jump_config.locations.select do |loc|
100
+ loader = build_loader(loc)
101
+ loader.available? && loader.pattern&.downcase == pattern.downcase
102
+ end
103
+ end
104
+
105
+ def expand_app(location)
106
+ loader = build_loader(location)
107
+ return [] unless loader.available?
108
+
109
+ glob_names = @options.glob_names
110
+ return [] if glob_names.empty?
111
+
112
+ loader.expand(glob_names)
113
+ end
114
+
115
+ def build_meta(location)
116
+ loader = build_loader(location)
117
+ glob_names = @options.glob_names
118
+ file_count = loader.available? && glob_names.any? ? loader.expand(glob_names).size : 0
119
+
120
+ resolved_from = glob_names.map { |n| describe_resolution(loader, n) }.join(', ')
121
+
122
+ {
123
+ 'app' => location.key,
124
+ 'path' => expand_path(location.path),
125
+ 'pattern' => loader.pattern,
126
+ 'matched_globs' => resolve_glob_names(loader, glob_names),
127
+ 'resolved_from' => resolved_from,
128
+ 'file_count' => file_count
129
+ }
130
+ end
131
+
132
+ def build_loader(location)
133
+ GlobsLoader.new(expand_path(location.path))
134
+ end
135
+
136
+ def expand_path(path)
137
+ File.expand_path(path)
138
+ end
139
+
140
+ def find_location(app_name)
141
+ results = resolve_app(app_name)
142
+ results&.first
143
+ end
144
+
145
+ # Describe how a glob name was resolved (for meta output)
146
+ def describe_resolution(loader, name)
147
+ name = name.strip.downcase
148
+ return "#{name} (glob)" if loader.globs.key?(name)
149
+ return "#{name} (alias)" if loader.aliases.key?(name)
150
+ return "#{name} (composite)" if loader.composites.key?(name)
151
+
152
+ "#{name} (fuzzy)"
153
+ end
154
+
155
+ # Resolve glob names to their constituent direct glob names
156
+ def resolve_glob_names(loader, names)
157
+ return [] unless loader.available?
158
+
159
+ names.flat_map do |name|
160
+ name = name.strip.downcase
161
+ if loader.globs.key?(name)
162
+ [name]
163
+ elsif loader.aliases.key?(name)
164
+ loader.aliases[name]
165
+ elsif loader.composites.key?(name)
166
+ members = loader.composites[name]
167
+ members == ['*'] ? loader.globs.keys : members
168
+ else # rubocop:disable Lint/DuplicateBranch
169
+ [name]
170
+ end
171
+ end.uniq
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Appydave
6
+ module Tools
7
+ module AppContext
8
+ # Loads and resolves named glob patterns from a context.globs.json file.
9
+ #
10
+ # Resolution is 3-tier:
11
+ # 1. Direct glob name — "services" → globs["services"]
12
+ # 2. Alias match — "backend" → aliases["backend"] → ["services", "routes"]
13
+ # 3. Composite match — "understand" → composites["understand"] → ["context", "docs", ...]
14
+ class GlobsLoader
15
+ attr_reader :project_path, :data
16
+
17
+ def initialize(project_path)
18
+ @project_path = project_path
19
+ @data = load_globs_file
20
+ end
21
+
22
+ def available?
23
+ !data.nil?
24
+ end
25
+
26
+ def globs
27
+ data&.fetch('globs', {}) || {}
28
+ end
29
+
30
+ def aliases
31
+ data&.fetch('aliases', {}) || {}
32
+ end
33
+
34
+ def composites
35
+ data&.fetch('composites', {}) || {}
36
+ end
37
+
38
+ def pattern
39
+ data&.fetch('pattern', nil)
40
+ end
41
+
42
+ def project_name
43
+ data&.fetch('project', nil)
44
+ end
45
+
46
+ # List all available glob names (direct + aliases + composites)
47
+ def available_names
48
+ names = globs.keys.map { |k| { name: k, type: 'glob' } }
49
+ names += aliases.keys.map { |k| { name: k, type: 'alias' } }
50
+ names += composites.keys.map { |k| { name: k, type: 'composite' } }
51
+ names
52
+ end
53
+
54
+ # Resolve a single name through the 3-tier hierarchy.
55
+ # Returns an array of raw glob patterns (strings).
56
+ def resolve(name)
57
+ name = name.strip.downcase
58
+
59
+ # Tier 1: direct glob name
60
+ return globs[name] if globs.key?(name)
61
+
62
+ # Tier 2: alias → list of glob names
63
+ return aliases[name].flat_map { |glob_name| globs[glob_name] || [] } if aliases.key?(name)
64
+
65
+ # Tier 3: composite → list of glob names (or "*" for all)
66
+ if composites.key?(name)
67
+ members = composites[name]
68
+ return globs.values.flatten if members == ['*']
69
+
70
+ return members.flat_map { |glob_name| resolve_single_glob(glob_name) }
71
+ end
72
+
73
+ # Tier 4: substring fallback
74
+ match = find_substring_match(name)
75
+ return resolve(match) if match
76
+
77
+ []
78
+ end
79
+
80
+ # Resolve multiple names, expand globs against the filesystem, return absolute paths.
81
+ def expand(names)
82
+ patterns = names.flat_map { |name| resolve(name) }.uniq
83
+
84
+ patterns.flat_map { |pat| Dir.glob(File.join(project_path, pat)) }
85
+ .select { |f| File.file?(f) }
86
+ .uniq
87
+ .sort
88
+ end
89
+
90
+ private
91
+
92
+ def globs_file_path
93
+ File.join(project_path, 'context.globs.json')
94
+ end
95
+
96
+ def load_globs_file
97
+ path = globs_file_path
98
+ return nil unless File.exist?(path)
99
+
100
+ JSON.parse(File.read(path))
101
+ rescue JSON::ParserError
102
+ nil
103
+ end
104
+
105
+ def resolve_single_glob(glob_name)
106
+ globs[glob_name] || []
107
+ end
108
+
109
+ def find_substring_match(name)
110
+ all_names = globs.keys + aliases.keys + composites.keys
111
+ all_names.find { |n| n.include?(name) }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module AppContext
6
+ # Options struct for app context query tool
7
+ class Options
8
+ attr_accessor :app_names, :glob_names, :pattern_filter,
9
+ :meta, :list, :list_apps,
10
+ :debug_level
11
+
12
+ def initialize
13
+ @app_names = []
14
+ @glob_names = []
15
+ @pattern_filter = nil
16
+ @meta = false
17
+ @list = false
18
+ @list_apps = false
19
+ @debug_level = 'none'
20
+ end
21
+
22
+ def query?
23
+ app_names.any? || !pattern_filter.nil?
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -34,11 +34,15 @@ module Appydave
34
34
  @tokens = false
35
35
  @base_dir = Dir.pwd
36
36
 
37
- @omi_dir = File.expand_path('~/dev/raw-intake/omi')
37
+ configured_omi = read_setting('omi-directory-path')
38
+ @omi_dir = configured_omi || File.expand_path('~/dev/raw-intake/omi')
38
39
  end
39
40
 
40
41
  def brains_root
41
- @brains_root ||= File.expand_path('~/dev/ad/brains')
42
+ @brains_root ||= begin
43
+ configured = read_setting('brains-root-path')
44
+ configured || File.expand_path('~/dev/ad/brains')
45
+ end
42
46
  end
43
47
 
44
48
  def brains_index_path
@@ -52,6 +56,19 @@ module Appydave
52
56
  def omi_query?
53
57
  omi
54
58
  end
59
+
60
+ private
61
+
62
+ # Read a path value from settings config, expanding ~ if set.
63
+ # Returns nil if config is unavailable or key is absent/blank.
64
+ def read_setting(key)
65
+ value = Appydave::Tools::Configuration::Config.settings.get(key)
66
+ return nil if value.nil? || value.to_s.strip.empty?
67
+
68
+ File.expand_path(value)
69
+ rescue StandardError
70
+ nil
71
+ end
55
72
  end
56
73
  end
57
74
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Configuration
6
+ # Installs bundled example configuration files into the user's config directory.
7
+ #
8
+ # Example files live at config/examples/*.example.json inside the gem.
9
+ # Each file is installed without the `.example` segment in its name so that
10
+ # `settings.example.json` becomes `settings.json` in the target directory.
11
+ #
12
+ # Files are never overwritten — existing files are skipped and reported.
13
+ #
14
+ # @example Install all examples
15
+ # result = ExampleInstaller.new.install
16
+ # result[:installed] #=> ["settings.json", "locations.json"]
17
+ # result[:skipped] #=> []
18
+ class ExampleInstaller
19
+ EXAMPLES_PATH = File.expand_path('../../../../config/examples', __dir__)
20
+
21
+ # @param target_path [String, nil] Directory to install into.
22
+ # Defaults to the active Config.config_path (~/.config/appydave).
23
+ def initialize(target_path: nil)
24
+ @target_path = target_path || Config.config_path
25
+ end
26
+
27
+ # Install all bundled example files that do not yet exist.
28
+ #
29
+ # @return [Hash] with keys :installed (Array<String>) and :skipped (Array<String>)
30
+ def install
31
+ FileUtils.mkdir_p(@target_path)
32
+ results = { installed: [], skipped: [] }
33
+
34
+ example_files.each do |src|
35
+ dest = destination_for(src)
36
+ basename = File.basename(dest)
37
+
38
+ if File.exist?(dest)
39
+ results[:skipped] << basename
40
+ else
41
+ FileUtils.cp(src, dest)
42
+ results[:installed] << basename
43
+ end
44
+ end
45
+
46
+ results
47
+ end
48
+
49
+ # List the filenames that would be installed (target names, not source names).
50
+ #
51
+ # @return [Array<String>]
52
+ def available
53
+ example_files.map { |f| target_name(f) }
54
+ end
55
+
56
+ private
57
+
58
+ def example_files
59
+ Dir.glob(File.join(EXAMPLES_PATH, '*.example.*')).sort
60
+ end
61
+
62
+ def destination_for(src)
63
+ File.join(@target_path, target_name(src))
64
+ end
65
+
66
+ def target_name(src)
67
+ File.basename(src).sub('.example', '')
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -42,6 +42,18 @@ module Appydave
42
42
  get('current_user')
43
43
  end
44
44
 
45
+ # Path to the root brains directory (second-brain knowledge base)
46
+ # Configure via settings.json key: brains-root-path
47
+ def brains_root_path
48
+ get('brains-root-path')
49
+ end
50
+
51
+ # Path to the OMI wearable transcripts directory
52
+ # Configure via settings.json key: omi-directory-path
53
+ def omi_directory_path
54
+ get('omi-directory-path')
55
+ end
56
+
45
57
  def print
46
58
  log.subheading 'Settings Configuration'
47
59
 
@@ -24,11 +24,12 @@ module Appydave
24
24
  #
25
25
  # @return [String] Formatted table
26
26
  def format
27
- return format_error unless success?
28
- return format_info if info_result?
29
- return format_summary if summary_result?
30
- return format_groups unless groups.empty?
31
- return format_empty if results.empty?
27
+ return format_error unless success?
28
+ return format_info if info_result?
29
+ return format_summary if summary_result?
30
+ return format_groups unless groups.empty?
31
+ return format_mutation if mutation_result?
32
+ return format_empty if results.empty?
32
33
  return format_definition_report if definition_report?
33
34
  return format_count_report if count_report?
34
35
  return format_category_report if category_report?
@@ -45,6 +46,16 @@ module Appydave
45
46
  lines.join("\n")
46
47
  end
47
48
 
49
+ def mutation_result?
50
+ data.key?(:message) && (data.key?(:location) || data[:message].to_s.match?(/removed|updated|added/i))
51
+ end
52
+
53
+ def format_mutation
54
+ lines = [colorize(data[:message], :green)]
55
+ lines << colorize(data[:warning], :yellow) if data[:warning]
56
+ lines.join("\n")
57
+ end
58
+
48
59
  def format_empty
49
60
  message = case data[:report]
50
61
  when 'brands'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Appydave
4
4
  module Tools
5
- VERSION = '0.84.0'
5
+ VERSION = '0.85.0'
6
6
  end
7
7
  end
@@ -108,8 +108,8 @@ module Appydave
108
108
  ^Finished in \\d
109
109
  ^\\d+ examples, \\d+ failures
110
110
 
111
- # Process listing output
112
- ^davidcruwys\\s+\\d+
111
+ # Process listing output (current user)
112
+ ^#{ENV.fetch('USER', ENV.fetch('USERNAME', ''))}\\s+\\d+
113
113
  PATTERNS
114
114
 
115
115
  # Create crash-recovery profile
@@ -34,8 +34,7 @@ module Appydave
34
34
  '^head ',
35
35
  '^tail ',
36
36
  '^echo \\$',
37
- '^\\[\\d+\\]', # Output like [1234]
38
- '^davidcruwys\\s+\\d+', # Process listing output
37
+ '^\\[\\d+\\]', # Output like [1234]
39
38
  '^zsh: command not found',
40
39
  '^X Process completed',
41
40
  '^Coverage report',
@@ -138,9 +137,16 @@ module Appydave
138
137
  end
139
138
 
140
139
  def load_exclude_patterns
141
- # Try loading from config, fall back to defaults
140
+ # Try loading from config, fall back to defaults (with dynamic user pattern appended)
142
141
  config_patterns = config.exclude_patterns
143
- config_patterns || DEFAULT_EXCLUDE_PATTERNS
142
+ config_patterns || default_exclude_patterns_with_user
143
+ end
144
+
145
+ def default_exclude_patterns_with_user
146
+ username = ENV.fetch('USER', ENV.fetch('USERNAME', ''))
147
+ return DEFAULT_EXCLUDE_PATTERNS if username.empty?
148
+
149
+ DEFAULT_EXCLUDE_PATTERNS + ["^#{username}\\s+\\d+"]
144
150
  end
145
151
 
146
152
  def load_include_patterns
@@ -41,6 +41,10 @@ require 'appydave/tools/brain_context/options'
41
41
  require 'appydave/tools/brain_context/brain_finder'
42
42
  require 'appydave/tools/brain_context/omi_finder'
43
43
 
44
+ require 'appydave/tools/app_context/options'
45
+ require 'appydave/tools/app_context/globs_loader'
46
+ require 'appydave/tools/app_context/app_finder'
47
+
44
48
  require 'appydave/tools/random_context/query_entry'
45
49
  require 'appydave/tools/random_context/randomizer'
46
50
 
@@ -52,6 +56,7 @@ require 'appydave/tools/configuration/models/settings_config'
52
56
  require 'appydave/tools/configuration/models/brands_config'
53
57
  require 'appydave/tools/configuration/models/channels_config'
54
58
  require 'appydave/tools/configuration/models/youtube_automation_config'
59
+ require 'appydave/tools/configuration/example_installer'
55
60
  require 'appydave/tools/name_manager/project_name'
56
61
 
57
62
  require 'appydave/tools/prompt_tools/prompt_completion'
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appydave-tools",
3
- "version": "0.84.0",
3
+ "version": "0.85.0",
4
4
  "description": "AppyDave YouTube Automation Tools",
5
5
  "scripts": {
6
6
  "release": "semantic-release"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appydave-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.84.0
4
+ version: 0.85.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cruwys
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-05 00:00:00.000000000 Z
11
+ date: 2026-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -190,6 +190,7 @@ executables:
190
190
  - jump
191
191
  - llm_context
192
192
  - prompt_tools
193
+ - query_apps
193
194
  - query_brain
194
195
  - query_omi
195
196
  - subtitle_processor
@@ -231,6 +232,7 @@ files:
231
232
  - bin/llm_context.rb
232
233
  - bin/move_images.rb
233
234
  - bin/prompt_tools.rb
235
+ - bin/query_apps.rb
234
236
  - bin/query_brain.rb
235
237
  - bin/query_omi.rb
236
238
  - bin/random_context.rb
@@ -242,7 +244,10 @@ files:
242
244
  - bin/youtube_automation.rb
243
245
  - bin/youtube_manager.rb
244
246
  - bin/zsh_history.rb
247
+ - config/examples/locations.example.json
248
+ - config/examples/settings.example.json
245
249
  - config/random-queries.yml
250
+ - context.globs.json
246
251
  - docs/README.md
247
252
  - docs/ai-instructions/behavioral-regression-audit.md
248
253
  - docs/ai-instructions/code-quality-retrospective.md
@@ -329,8 +334,11 @@ files:
329
334
  - docs/planning/micro-cleanup/AGENTS.md
330
335
  - docs/planning/micro-cleanup/IMPLEMENTATION_PLAN.md
331
336
  - docs/planning/micro-cleanup/assessment.md
337
+ - docs/planning/multi-user-support.md
338
+ - docs/planning/query-apps-design.md
332
339
  - docs/planning/query-location-feature/IMPLEMENTATION_PLAN.md
333
340
  - docs/planning/query-location-feature/system-context-gap-analysis.md
341
+ - docs/planning/query-skills-plan.md
334
342
  - docs/planning/s3-operations-split/AGENTS.md
335
343
  - docs/planning/s3-operations-split/IMPLEMENTATION_PLAN.md
336
344
  - docs/planning/test-coverage-gaps/AGENTS.md
@@ -338,6 +346,7 @@ files:
338
346
  - docs/planning/test-coverage-gaps/assessment.md
339
347
  - docs/specs/fr-002-gpt-context-help-system.md
340
348
  - docs/specs/fr-003-jump-location-tool.md
349
+ - docs/specs/jump-add-display-fix.md
341
350
  - docs/specs/zsh-history-tool.md
342
351
  - docs/templates/.env.example
343
352
  - docs/templates/channels.example.json
@@ -348,6 +357,7 @@ files:
348
357
  - exe/jump
349
358
  - exe/llm_context
350
359
  - exe/prompt_tools
360
+ - exe/query_apps
351
361
  - exe/query_brain
352
362
  - exe/query_omi
353
363
  - exe/subtitle_processor
@@ -356,6 +366,9 @@ files:
356
366
  - exe/zsh_history
357
367
  - images.log
358
368
  - lib/appydave/tools.rb
369
+ - lib/appydave/tools/app_context/app_finder.rb
370
+ - lib/appydave/tools/app_context/globs_loader.rb
371
+ - lib/appydave/tools/app_context/options.rb
359
372
  - lib/appydave/tools/brain_context/brain_finder.rb
360
373
  - lib/appydave/tools/brain_context/omi_finder.rb
361
374
  - lib/appydave/tools/brain_context/options.rb
@@ -367,6 +380,7 @@ files:
367
380
  - lib/appydave/tools/configuration/_doc.md
368
381
  - lib/appydave/tools/configuration/config.rb
369
382
  - lib/appydave/tools/configuration/configurable.rb
383
+ - lib/appydave/tools/configuration/example_installer.rb
370
384
  - lib/appydave/tools/configuration/models/brands_config.rb
371
385
  - lib/appydave/tools/configuration/models/channels_config.rb
372
386
  - lib/appydave/tools/configuration/models/config_base.rb