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 +7 -0
- data/exe/monkeyspaw +4 -0
- data/lib/monkeyspaw/application.rb +64 -0
- data/lib/monkeyspaw/config.rb +27 -0
- data/lib/monkeyspaw/generators/html_from_description_generator.rb +27 -0
- data/lib/monkeyspaw/prompt_manager.rb +15 -0
- data/lib/monkeyspaw/router.rb +53 -0
- data/lib/monkeyspaw/server.rb +83 -0
- data/lib/monkeyspaw/version.rb +3 -0
- data/lib/monkeyspaw.rb +53 -0
- metadata +151 -0
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,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,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
|
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: []
|