monkeyspaw 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8ce5d0090a65680d9d35a3db4165f9b1ecaa9a07da2077e109c69d00b0696773
4
+ data.tar.gz: 95657a5e46c0070e16ee55adb4ecbbffb0252f297760b695b83c7dc930001063
5
+ SHA512:
6
+ metadata.gz: 18e5f453eb95c34cc32873bba6a4cfc1ca4572bf47a1315fabe71badf5dd0b830a8d2e857118ca14892617b7388d1ab84113066a69bd8161d1dd1e4e91147079
7
+ data.tar.gz: 7bad4ea29d4dc33d18cbf15e723b3c87e07f9680f961865fd8325b0dfdaa625b6c0b9f9c7dbde9351e050d29d77f386b23d9f36182b0324f0ab5c77eeff71eb6
data/exe/monkeyspaw ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "monkeyspaw"
4
+ require "optparse"
@@ -0,0 +1,64 @@
1
+ require "pathname"
2
+
3
+ module MonkeysPaw
4
+ class Application
5
+ attr_reader :config
6
+ attr_accessor :root, :router
7
+
8
+ def initialize
9
+ @config = Config.new
10
+ @root = Pathname.new(Dir.pwd)
11
+ @server = nil
12
+ @router = nil
13
+ @prompt_manager = nil
14
+ end
15
+
16
+ def configure
17
+ yield config if block_given?
18
+ self
19
+ end
20
+
21
+ def use(provider, **options)
22
+ puts "Use called"
23
+ end
24
+
25
+ def pick_up!(**options)
26
+ setup_directories
27
+
28
+ @router ||= Router.new(self)
29
+
30
+ @prompt_manager ||= PromptManager.new(self)
31
+
32
+ @server ||= Server.new(self, **options)
33
+ @server.start
34
+ end
35
+
36
+ def pages_dir
37
+ root.join(config.pages_dir || 'pages')
38
+ end
39
+
40
+ def components_dir
41
+ root.join(config.components_dir || 'components')
42
+ end
43
+
44
+ def assets_dir
45
+ root.join(config.assets_dir || 'assets')
46
+ end
47
+
48
+ def get_page_prompt(path)
49
+ @prompt_manager.get_page_prompt(path)
50
+ end
51
+
52
+ def get_component_prompt(path)
53
+ @prompt_manager.get_component_prompt(path)
54
+ end
55
+
56
+ private
57
+
58
+ def setup_directories
59
+ [pages_dir, components_dir, assets_dir].each do |dir|
60
+ dir.mkpath unless dir.exist?
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,27 @@
1
+ module MonkeysPaw
2
+ class Config
3
+ attr_accessor :provider,
4
+ :model,
5
+ :pages_dir,
6
+ :components_dir,
7
+ :assets_dir,
8
+ :port,
9
+ :host
10
+
11
+ def initialize
12
+ @provider = nil
13
+ @model = nil
14
+ @pages_dir = 'pages'
15
+ @components_dir = 'components'
16
+ @assets_dir = 'assets'
17
+ @port = 1337
18
+ @host = "localhost"
19
+ end
20
+
21
+ def to_h
22
+ instance_variables.each_with_object({}) do |var, hash|
23
+ hash[var.to_s.delete('@').to_sym] = instance_variable_get(var)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ module MonkeysPaw
2
+ class HtmlFromDescriptionGenerator < Sublayer::Generators::Base
3
+ llm_output_adapter type: :single_string,
4
+ name: "html_code",
5
+ description: "The generated HTML code for the web page"
6
+
7
+ def initialize(description:)
8
+ @description = description
9
+ end
10
+
11
+ def generate
12
+ super
13
+ end
14
+
15
+ def prompt
16
+ <<-PROMPT
17
+ You are an expert HTML developer.
18
+
19
+ You are tasked with writing HTML code based on the following description:
20
+
21
+ #{@description}
22
+
23
+ Take a deep breath and think step by step before you start coding. Ensure the HTML is well-formed and valid.
24
+ PROMPT
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module MonkeysPaw
2
+ class PromptManager
3
+ attr_reader :app
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def get_page_prompt(path)
10
+ path = "/" if path.empty?
11
+
12
+ app.router.get_file(path)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module MonkeysPaw
2
+ class Router
3
+ attr_reader :app, :routes
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ @routes = {}
8
+ scan_pages
9
+ end
10
+
11
+ def scan_pages
12
+ @routes = {}
13
+ pages_dir = app.pages_dir
14
+
15
+ return unless pages_dir.exist?
16
+
17
+ page_files = Dir.glob(pages_dir.join('**', '*.md'))
18
+
19
+ page_files.each do |file|
20
+ rel_path = Pathname.new(file).relative_path_from(pages_dir)
21
+ route = '/' + rel_path.to_s.sub(/\.md$/, '')
22
+
23
+ route = route.sub(/\/index$/, '/')
24
+
25
+ @routes[route] = file
26
+ end
27
+
28
+ @routes = @routes.sort_by { |route, file| file }.to_h
29
+ end
30
+
31
+ def get_file(path)
32
+ @routes[path]
33
+ end
34
+
35
+ def next_page(current_path)
36
+ keys = @routes.keys
37
+ current_index = keys.find_index(current_path)
38
+ return nil unless current_index
39
+ keys[current_index + 1]
40
+ end
41
+
42
+ def prev_page(current_path)
43
+ keys = @routes.keys
44
+ current_index = keys.find_index(current_path)
45
+ return nil unless current_index || current_index == 0
46
+ keys[current_index - 1]
47
+ end
48
+
49
+ def get_route_for_file(file)
50
+ @routes.key(file)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,83 @@
1
+ require "rack"
2
+ require "rack/handler/puma"
3
+
4
+ module MonkeysPaw
5
+ class Server
6
+ attr_reader :app
7
+
8
+ def initialize(app, **options)
9
+ @app = app
10
+ @options = options
11
+ @rack_app = build_rack_app
12
+ end
13
+
14
+ def start
15
+ host = @options[:host] || app.config.host || 'localhost'
16
+ port = @options[:port] || app.config.port || 1337
17
+
18
+ puts "You pick up the monkey's paw. Be careful what you wish for on http://#{host}:#{port}"
19
+
20
+ setup_signal_handlers
21
+
22
+ Rack::Handler::Puma.run(@rack_app, Host: host, Port: port, Silent: true, Threads: "0:16", workers: 0)
23
+ end
24
+
25
+ def shutdown
26
+ puts "Enough! You throw the paw away. Hopefully there were no unintended consequences. Mua ha ha ha ha!"
27
+ exit
28
+ end
29
+
30
+ def generate_page(path, prompt_file)
31
+ HtmlFromDescriptionGenerator.new(description: File.read(prompt_file)).generate
32
+ end
33
+
34
+ private
35
+
36
+ def setup_signal_handlers
37
+ [:INT, :TERM].each do |signal|
38
+ trap(signal) { shutdown }
39
+ end
40
+ end
41
+
42
+ def build_rack_app
43
+ server = self
44
+
45
+ Rack::Builder.new do
46
+ use Rack::Static,
47
+ urls: ["/assets"],
48
+ root: server.app.root.to_s
49
+
50
+ run lambda { |env|
51
+ request = Rack::Request.new(env)
52
+
53
+ path = request.path
54
+ path = "/" if path.empty?
55
+
56
+ if request.params["preload"]
57
+ prompt_file = server.app.get_page_prompt(path)
58
+ Thread.new { server.generate_page(path, prompt_file) } if prompt_file
59
+
60
+ return [204, {}, []]
61
+ end
62
+
63
+ prompt_file = server.app.get_page_prompt(path)
64
+
65
+ if prompt_file
66
+ html = server.generate_page(path, prompt_file)
67
+
68
+ next_path = server.app.router.next_page(path)
69
+ if next_path
70
+ next_prompt = server.app.get_page_prompt(next_path)
71
+ Thread.new { server.generate_page(next_path, next_prompt) } if next_prompt
72
+ end
73
+
74
+ [200, { "Content-Type" => "text/html"}, [html]]
75
+ else
76
+ [404, {"Content-Type" => "text/html"}, [server.generate_error_page("Page not found: #{path}")]]
77
+ end
78
+ }
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ module MonkeysPaw
2
+ VERSION = "0.0.1"
3
+ end
data/lib/monkeyspaw.rb ADDED
@@ -0,0 +1,53 @@
1
+ require "sublayer"
2
+
3
+ require "monkeyspaw/version"
4
+ require "monkeyspaw/application"
5
+ require "monkeyspaw/config"
6
+ require "monkeyspaw/server"
7
+ require "monkeyspaw/router"
8
+ require "monkeyspaw/prompt_manager"
9
+
10
+ Dir[File.expand_path("../monkeyspaw/generators/*.rb", __FILE__)].each { |f| require f }
11
+
12
+ module MonkeysPaw
13
+ Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
14
+ Sublayer.configuration.ai_model = "gemini-2.0-flash"
15
+
16
+ class << self
17
+ def application
18
+ @application ||= Application.new
19
+ end
20
+
21
+ def configure(&block)
22
+ application.configure(&block)
23
+ end
24
+
25
+ def root
26
+ application.root
27
+ end
28
+
29
+ def pages_dir
30
+ application.pages_dir
31
+ end
32
+
33
+ def components_dir
34
+ application.components_dir
35
+ end
36
+
37
+ def assets_dir
38
+ application.assets_dir
39
+ end
40
+
41
+ def pick_up!(**options)
42
+ application.pick_up!(**options)
43
+ end
44
+
45
+ def root=(path)
46
+ application.root = path
47
+ end
48
+ end
49
+ end
50
+
51
+ if $0 == __FILE__
52
+ MonkeysPaw.pick_up!
53
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: monkeyspaw
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Scott Werner
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-04-04 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: puma
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '6.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '6.6'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sublayer
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.2.8
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.2.8
54
+ - !ruby/object:Gem::Dependency
55
+ name: bundler
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.6'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.6'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.2'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '13.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.13'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.13'
96
+ - !ruby/object:Gem::Dependency
97
+ name: pry
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.15'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.15'
110
+ description: MonkeysPaw is a micro web framework that grants your wishes through AI
111
+ prompts, but as with any wish-granting entity, the clarity of your request determines
112
+ what you receive. Craft your prompts with care, lest your website manifest in unexpected
113
+ ways.
114
+ email: scott@sublayer.com
115
+ executables:
116
+ - monkeyspaw
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - exe/monkeyspaw
121
+ - lib/monkeyspaw.rb
122
+ - lib/monkeyspaw/application.rb
123
+ - lib/monkeyspaw/config.rb
124
+ - lib/monkeyspaw/generators/html_from_description_generator.rb
125
+ - lib/monkeyspaw/prompt_manager.rb
126
+ - lib/monkeyspaw/router.rb
127
+ - lib/monkeyspaw/server.rb
128
+ - lib/monkeyspaw/version.rb
129
+ homepage: https://github.com/sublayerapp/monkeyspaw
130
+ licenses:
131
+ - MIT
132
+ metadata:
133
+ homepage_uri: https://github.com/sublayerapp/monkeyspaw
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '3.0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.6.6
149
+ specification_version: 4
150
+ summary: A prompt-driven web framework for Ruby - be careful what you wish for
151
+ test_files: []