appydave-tools 0.70.0 → 0.71.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/brainstorming-agent.md +227 -0
  3. data/.claude/commands/cli-test.md +251 -0
  4. data/.claude/commands/dev.md +234 -0
  5. data/.claude/commands/po.md +227 -0
  6. data/.claude/commands/progress.md +51 -0
  7. data/.claude/commands/uat.md +321 -0
  8. data/.rubocop.yml +9 -0
  9. data/AGENTS.md +43 -0
  10. data/CHANGELOG.md +12 -0
  11. data/CLAUDE.md +26 -3
  12. data/README.md +15 -0
  13. data/bin/dam +21 -1
  14. data/bin/jump.rb +29 -0
  15. data/bin/subtitle_processor.rb +54 -1
  16. data/bin/zsh_history.rb +846 -0
  17. data/docs/README.md +162 -69
  18. data/docs/architecture/cli/exe-bin-convention.md +434 -0
  19. data/docs/architecture/cli-patterns.md +631 -0
  20. data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
  21. data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
  22. data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
  23. data/docs/architecture/testing/testing-patterns.md +762 -0
  24. data/docs/backlog.md +120 -0
  25. data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
  26. data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
  27. data/docs/specs/fr-003-jump-location-tool.md +779 -0
  28. data/docs/specs/zsh-history-tool.md +820 -0
  29. data/docs/uat/FR-3-jump-location-tool.md +741 -0
  30. data/exe/jump +11 -0
  31. data/exe/{subtitle_manager → subtitle_processor} +1 -1
  32. data/exe/zsh_history +11 -0
  33. data/lib/appydave/tools/configuration/openai.rb +1 -1
  34. data/lib/appydave/tools/dam/file_helper.rb +28 -0
  35. data/lib/appydave/tools/dam/project_listing.rb +4 -30
  36. data/lib/appydave/tools/dam/s3_operations.rb +2 -1
  37. data/lib/appydave/tools/dam/ssd_status.rb +226 -0
  38. data/lib/appydave/tools/dam/status.rb +3 -51
  39. data/lib/appydave/tools/jump/cli.rb +561 -0
  40. data/lib/appydave/tools/jump/commands/add.rb +52 -0
  41. data/lib/appydave/tools/jump/commands/base.rb +43 -0
  42. data/lib/appydave/tools/jump/commands/generate.rb +153 -0
  43. data/lib/appydave/tools/jump/commands/remove.rb +58 -0
  44. data/lib/appydave/tools/jump/commands/report.rb +214 -0
  45. data/lib/appydave/tools/jump/commands/update.rb +42 -0
  46. data/lib/appydave/tools/jump/commands/validate.rb +54 -0
  47. data/lib/appydave/tools/jump/config.rb +233 -0
  48. data/lib/appydave/tools/jump/formatters/base.rb +48 -0
  49. data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
  50. data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
  51. data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
  52. data/lib/appydave/tools/jump/location.rb +134 -0
  53. data/lib/appydave/tools/jump/path_validator.rb +47 -0
  54. data/lib/appydave/tools/jump/search.rb +230 -0
  55. data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
  56. data/lib/appydave/tools/version.rb +1 -1
  57. data/lib/appydave/tools/zsh_history/command.rb +37 -0
  58. data/lib/appydave/tools/zsh_history/config.rb +235 -0
  59. data/lib/appydave/tools/zsh_history/filter.rb +184 -0
  60. data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
  61. data/lib/appydave/tools/zsh_history/parser.rb +101 -0
  62. data/lib/appydave/tools.rb +25 -0
  63. data/package.json +1 -1
  64. metadata +51 -4
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Generate command creates shell aliases and help content
8
+ class Generate < Base
9
+ VALID_TARGETS = %w[aliases help all].freeze
10
+
11
+ attr_reader :target, :output_path, :output_dir
12
+
13
+ # rubocop:disable Metrics/ParameterLists
14
+ def initialize(config, target, output_path: nil, output_dir: nil, path_validator: PathValidator.new, **options)
15
+ super(config, path_validator: path_validator, **options)
16
+ @target = target
17
+ @output_path = output_path
18
+ @output_dir = output_dir
19
+ end
20
+ # rubocop:enable Metrics/ParameterLists
21
+
22
+ def run
23
+ unless VALID_TARGETS.include?(target)
24
+ return error_result(
25
+ "Unknown generate target: #{target}",
26
+ code: 'INVALID_INPUT',
27
+ suggestion: "Valid targets: #{VALID_TARGETS.join(', ')}"
28
+ )
29
+ end
30
+
31
+ send("generate_#{target}")
32
+ end
33
+
34
+ private
35
+
36
+ def generate_aliases
37
+ content = build_aliases_content
38
+ write_or_return(content, output_path, 'aliases-jump.zsh')
39
+ end
40
+
41
+ def generate_help
42
+ content = build_help_content
43
+ write_or_return(content, output_path, 'jump-help.txt')
44
+ end
45
+
46
+ def generate_all
47
+ aliases_content = build_aliases_content
48
+ help_content = build_help_content
49
+
50
+ if output_dir
51
+ aliases_path = File.join(output_dir, 'aliases-jump.zsh')
52
+ help_path = File.join(output_dir, 'data', 'jump-help.txt')
53
+
54
+ FileUtils.mkdir_p(File.dirname(aliases_path))
55
+ FileUtils.mkdir_p(File.dirname(help_path))
56
+
57
+ File.write(aliases_path, aliases_content)
58
+ File.write(help_path, help_content)
59
+
60
+ success_result(
61
+ message: 'Generated all files',
62
+ files: [aliases_path, help_path]
63
+ )
64
+ else
65
+ success_result(
66
+ aliases: aliases_content,
67
+ help: help_content
68
+ )
69
+ end
70
+ end
71
+
72
+ def build_aliases_content
73
+ lines = []
74
+ lines << '# Jump Location Aliases'
75
+ lines << "# Generated by: jump generate aliases (#{Time.now.strftime('%Y-%m-%d %H:%M')})"
76
+ lines << '# Source: ~/.config/appydave/locations.json'
77
+ lines << ''
78
+ lines << '# Usage: source this file in your .zshrc'
79
+ lines << '# source ~/.oh-my-zsh/custom/aliases-jump.zsh'
80
+ lines << ''
81
+
82
+ # Group by brand/client
83
+ grouped = group_locations_for_aliases
84
+
85
+ grouped.each do |group_name, locations|
86
+ lines << "# #{group_name}"
87
+ locations.each do |loc|
88
+ path = path_validator.expand(loc.path)
89
+ desc = loc.description ? " # #{loc.description}" : ''
90
+ lines << "alias #{loc.jump}=\"cd '#{path}'\"#{desc}"
91
+ end
92
+ lines << ''
93
+ end
94
+
95
+ lines.join("\n")
96
+ end
97
+
98
+ def build_help_content
99
+ lines = []
100
+ lines << '# Jump Location Help'
101
+ lines << "# Generated: #{Time.now.strftime('%Y-%m-%d %H:%M')}"
102
+ lines << ''
103
+
104
+ # Format for fzf/ah integration
105
+ config.locations.each do |loc|
106
+ path = path_validator.expand(loc.path)
107
+ brand_client = loc.brand || loc.client || ''
108
+ type = loc.type || ''
109
+ desc = loc.description || ''
110
+
111
+ # Format: jump_alias | path | brand/client | type | description
112
+ lines << "#{loc.jump}\t#{path}\t#{brand_client}\t#{type}\t#{desc}"
113
+ end
114
+
115
+ lines.join("\n")
116
+ end
117
+
118
+ def group_locations_for_aliases
119
+ grouped = {}
120
+
121
+ config.locations.each do |loc|
122
+ group = loc.brand || loc.client || 'Other'
123
+ grouped[group] ||= []
124
+ grouped[group] << loc
125
+ end
126
+
127
+ # Sort groups and locations within groups
128
+ grouped.sort.to_h.transform_values do |locs|
129
+ locs.sort_by(&:key)
130
+ end
131
+ end
132
+
133
+ def write_or_return(content, path, default_filename)
134
+ if path
135
+ FileUtils.mkdir_p(File.dirname(path))
136
+ File.write(path, content)
137
+ success_result(
138
+ message: "Generated #{default_filename}",
139
+ path: path,
140
+ lines: content.lines.count
141
+ )
142
+ else
143
+ success_result(
144
+ content: content,
145
+ lines: content.lines.count
146
+ )
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Remove command deletes a location entry
8
+ class Remove < Base
9
+ attr_reader :key, :force
10
+
11
+ def initialize(config, key, force: false, path_validator: PathValidator.new, **options)
12
+ super(config, path_validator: path_validator, **options)
13
+ @key = key
14
+ @force = force
15
+ end
16
+
17
+ def run
18
+ # Validate key exists
19
+ location = config.find(key)
20
+ unless location
21
+ suggestions = find_suggestions(key)
22
+ suggestion = suggestions.empty? ? nil : "Did you mean: #{suggestions.join(', ')}?"
23
+ return error_result("Location '#{key}' not found", code: 'NOT_FOUND', suggestion: suggestion)
24
+ end
25
+
26
+ # Require --force
27
+ unless force
28
+ return error_result(
29
+ "Use --force to confirm removal of '#{key}'",
30
+ code: 'CONFIRMATION_REQUIRED'
31
+ )
32
+ end
33
+
34
+ # Remove
35
+ config.remove(key)
36
+ config.save
37
+
38
+ success_result(
39
+ message: "Location '#{key}' removed successfully",
40
+ removed: location.to_h
41
+ )
42
+ rescue ArgumentError => e
43
+ error_result(e.message, code: 'NOT_FOUND')
44
+ end
45
+
46
+ private
47
+
48
+ def find_suggestions(query)
49
+ config.locations
50
+ .map(&:key)
51
+ .select { |k| k.include?(query) || query.include?(k) || k.start_with?(query[0..1]) }
52
+ .first(3)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Report command generates various reports about locations
8
+ class Report < Base
9
+ VALID_REPORTS = %w[
10
+ categories brands clients types tags
11
+ by-brand by-client by-type by-tag
12
+ summary
13
+ ].freeze
14
+
15
+ attr_reader :report_type, :filter
16
+
17
+ def initialize(config, report_type, filter: nil, path_validator: PathValidator.new, **options)
18
+ super(config, path_validator: path_validator, **options)
19
+ @report_type = report_type
20
+ @filter = filter
21
+ end
22
+
23
+ def run
24
+ unless VALID_REPORTS.include?(report_type)
25
+ return error_result(
26
+ "Unknown report type: #{report_type}",
27
+ code: 'INVALID_INPUT',
28
+ suggestion: "Valid types: #{VALID_REPORTS.join(', ')}"
29
+ )
30
+ end
31
+
32
+ send("report_#{report_type.gsub('-', '_')}")
33
+ end
34
+
35
+ private
36
+
37
+ def report_categories
38
+ categories = config.categories.map do |name, info|
39
+ {
40
+ name: name,
41
+ description: info['description'] || info[:description],
42
+ values: info['values'] || info[:values] || []
43
+ }
44
+ end
45
+
46
+ success_result(
47
+ report: 'categories',
48
+ count: categories.size,
49
+ results: categories
50
+ )
51
+ end
52
+
53
+ def report_brands
54
+ brands = config.brands.map do |key, info|
55
+ location_count = config.locations.count { |loc| loc.brand == key }
56
+ {
57
+ key: key,
58
+ description: info['description'] || info[:description],
59
+ aliases: info['aliases'] || info[:aliases] || [],
60
+ location_count: location_count
61
+ }
62
+ end
63
+
64
+ success_result(
65
+ report: 'brands',
66
+ count: brands.size,
67
+ results: brands.sort_by { |b| -b[:location_count] }
68
+ )
69
+ end
70
+
71
+ def report_clients
72
+ clients = config.clients.map do |key, info|
73
+ location_count = config.locations.count { |loc| loc.client == key }
74
+ {
75
+ key: key,
76
+ description: info['description'] || info[:description],
77
+ aliases: info['aliases'] || info[:aliases] || [],
78
+ location_count: location_count
79
+ }
80
+ end
81
+
82
+ success_result(
83
+ report: 'clients',
84
+ count: clients.size,
85
+ results: clients.sort_by { |c| -c[:location_count] }
86
+ )
87
+ end
88
+
89
+ def report_types
90
+ type_counts = Hash.new(0)
91
+ config.locations.each do |loc|
92
+ type_counts[loc.type || 'untyped'] += 1
93
+ end
94
+
95
+ types = type_counts.map do |type, count|
96
+ { type: type, location_count: count }
97
+ end
98
+
99
+ success_result(
100
+ report: 'types',
101
+ count: types.size,
102
+ results: types.sort_by { |t| -t[:location_count] }
103
+ )
104
+ end
105
+
106
+ def report_tags
107
+ tag_counts = Hash.new(0)
108
+ config.locations.each do |loc|
109
+ loc.tags.each { |tag| tag_counts[tag] += 1 }
110
+ end
111
+
112
+ tags = tag_counts.map do |tag, count|
113
+ { tag: tag, location_count: count }
114
+ end
115
+
116
+ success_result(
117
+ report: 'tags',
118
+ count: tags.size,
119
+ results: tags.sort_by { |t| -t[:location_count] }
120
+ )
121
+ end
122
+
123
+ def report_by_brand
124
+ grouped = group_by_field(:brand, filter)
125
+ success_result(
126
+ report: 'by-brand',
127
+ filter: filter,
128
+ groups: grouped
129
+ )
130
+ end
131
+
132
+ def report_by_client
133
+ grouped = group_by_field(:client, filter)
134
+ success_result(
135
+ report: 'by-client',
136
+ filter: filter,
137
+ groups: grouped
138
+ )
139
+ end
140
+
141
+ def report_by_type
142
+ grouped = group_by_field(:type, filter)
143
+ success_result(
144
+ report: 'by-type',
145
+ filter: filter,
146
+ groups: grouped
147
+ )
148
+ end
149
+
150
+ def report_by_tag
151
+ grouped = {}
152
+ config.locations.each do |loc|
153
+ loc.tags.each do |tag|
154
+ next if filter && tag != filter
155
+
156
+ grouped[tag] ||= []
157
+ grouped[tag] << location_summary(loc)
158
+ end
159
+ end
160
+
161
+ success_result(
162
+ report: 'by-tag',
163
+ filter: filter,
164
+ groups: grouped.sort_by { |_, locs| -locs.size }.to_h
165
+ )
166
+ end
167
+
168
+ def report_summary
169
+ success_result(
170
+ report: 'summary',
171
+ total_locations: config.locations.size,
172
+ total_brands: config.brands.size,
173
+ total_clients: config.clients.size,
174
+ by_type: count_by_field(:type),
175
+ by_brand: count_by_field(:brand),
176
+ by_client: count_by_field(:client),
177
+ config_info: config.info
178
+ )
179
+ end
180
+
181
+ def group_by_field(field, filter_value)
182
+ grouped = {}
183
+ config.locations.each do |loc|
184
+ value = loc.send(field) || 'unassigned'
185
+ next if filter_value && value != filter_value
186
+
187
+ grouped[value] ||= []
188
+ grouped[value] << location_summary(loc)
189
+ end
190
+ grouped.sort_by { |_, locs| -locs.size }.to_h
191
+ end
192
+
193
+ def count_by_field(field)
194
+ counts = Hash.new(0)
195
+ config.locations.each do |loc|
196
+ value = loc.send(field) || 'unassigned'
197
+ counts[value] += 1
198
+ end
199
+ counts.sort_by { |_, count| -count }.to_h
200
+ end
201
+
202
+ def location_summary(location)
203
+ {
204
+ key: location.key,
205
+ path: location.path,
206
+ jump: location.jump,
207
+ description: location.description
208
+ }.compact
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Update command modifies an existing location entry
8
+ class Update < Base
9
+ attr_reader :key, :attrs
10
+
11
+ def initialize(config, key, attrs, path_validator: PathValidator.new, **options)
12
+ super(config, path_validator: path_validator, **options)
13
+ @key = key
14
+ @attrs = attrs
15
+ end
16
+
17
+ def run
18
+ # Validate key exists
19
+ return error_result("Location '#{key}' not found", code: 'NOT_FOUND') unless config.key_exists?(key)
20
+
21
+ # Validate path if provided
22
+ @path_warning = "Warning: Path '#{attrs[:path]}' does not exist" if attrs[:path] && !path_validator.exists?(attrs[:path])
23
+
24
+ # Update
25
+ config.update(key, attrs)
26
+ config.save
27
+
28
+ updated = config.find(key)
29
+ result = success_result(
30
+ message: "Location '#{key}' updated successfully",
31
+ location: updated.to_h
32
+ )
33
+ result[:warning] = @path_warning if @path_warning
34
+ result
35
+ rescue ArgumentError => e
36
+ error_result(e.message, code: 'INVALID_INPUT')
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Validate command checks if location paths exist
8
+ class Validate < Base
9
+ attr_reader :key
10
+
11
+ def initialize(config, key: nil, path_validator: PathValidator.new, **options)
12
+ super(config, path_validator: path_validator, **options)
13
+ @key = key
14
+ end
15
+
16
+ def run
17
+ locations_to_validate = if key
18
+ location = config.find(key)
19
+ return error_result("Location '#{key}' not found", code: 'NOT_FOUND') unless location
20
+
21
+ [location]
22
+ else
23
+ config.locations
24
+ end
25
+
26
+ results = locations_to_validate.map do |loc|
27
+ {
28
+ key: loc.key,
29
+ path: loc.path,
30
+ expanded_path: path_validator.expand(loc.path),
31
+ valid: path_validator.exists?(loc.path),
32
+ jump: loc.jump
33
+ }
34
+ end
35
+
36
+ valid_count = results.count { |r| r[:valid] }
37
+ invalid_count = results.count { |r| !r[:valid] }
38
+
39
+ # Update validation timestamp
40
+ config.touch_validated
41
+ config.save
42
+
43
+ success_result(
44
+ count: results.size,
45
+ valid_count: valid_count,
46
+ invalid_count: invalid_count,
47
+ results: results
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end