relay.app 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +17 -0
- data/README.md +132 -0
- data/app/concerns/attachment.rb +12 -0
- data/app/concerns/context.rb +147 -0
- data/app/concerns/roda.rb +50 -0
- data/app/concerns/view.rb +90 -0
- data/app/forms/mcp/forgejo.rb +55 -0
- data/app/forms/mcp/github.rb +47 -0
- data/app/forms/mcp.rb +89 -0
- data/app/hooks/require_user.rb +10 -0
- data/app/init/database.rb +36 -0
- data/app/init/env.rb +21 -0
- data/app/init/router.rb +164 -0
- data/app/models/context.rb +82 -0
- data/app/models/mcp/preset.rb +60 -0
- data/app/models/mcp.rb +165 -0
- data/app/models/model_record.rb +70 -0
- data/app/models/song.rb +11 -0
- data/app/models/user.rb +31 -0
- data/app/pages/base.rb +25 -0
- data/app/pages/chat.rb +18 -0
- data/app/pages/mcp.rb +12 -0
- data/app/pages/sign_in.rb +14 -0
- data/app/prompts/system.md +129 -0
- data/app/resources/jukebox.yml +90 -0
- data/app/routes/base.rb +36 -0
- data/app/routes/clear_attachment.rb +13 -0
- data/app/routes/list_chat.rb +11 -0
- data/app/routes/list_contexts.rb +17 -0
- data/app/routes/list_controls.rb +11 -0
- data/app/routes/list_mcp.rb +16 -0
- data/app/routes/list_models.rb +14 -0
- data/app/routes/list_providers.rb +11 -0
- data/app/routes/list_tools.rb +13 -0
- data/app/routes/mcp/base.rb +16 -0
- data/app/routes/mcp/create.rb +19 -0
- data/app/routes/mcp/delete.rb +17 -0
- data/app/routes/mcp/form.rb +11 -0
- data/app/routes/mcp/new.rb +16 -0
- data/app/routes/mcp/show.rb +17 -0
- data/app/routes/mcp/toggle.rb +17 -0
- data/app/routes/mcp/update.rb +20 -0
- data/app/routes/settings/new_context.rb +23 -0
- data/app/routes/settings/set_context.rb +26 -0
- data/app/routes/settings/set_model.rb +23 -0
- data/app/routes/settings/set_provider.rb +38 -0
- data/app/routes/sign_in.rb +39 -0
- data/app/routes/upload_attachment.rb +35 -0
- data/app/routes/websocket/connection.rb +247 -0
- data/app/routes/websocket/interrupt.rb +25 -0
- data/app/routes/websocket/stream.rb +46 -0
- data/app/routes/websocket.rb +62 -0
- data/app/tools/add_song.rb +27 -0
- data/app/tools/juke_box.rb +41 -0
- data/app/tools/relay_knowledge.rb +59 -0
- data/app/tools/remove_song.rb +53 -0
- data/app/validators/mcp.rb +42 -0
- data/app/views/fragments/_append_message.erb +1 -0
- data/app/views/fragments/_chat.erb +15 -0
- data/app/views/fragments/_contexts.erb +7 -0
- data/app/views/fragments/_contexts_body.erb +35 -0
- data/app/views/fragments/_controls.erb +15 -0
- data/app/views/fragments/_iframe.erb +8 -0
- data/app/views/fragments/_input.erb +67 -0
- data/app/views/fragments/_mcp_settings.erb +52 -0
- data/app/views/fragments/_message.erb +31 -0
- data/app/views/fragments/_models.erb +25 -0
- data/app/views/fragments/_providers.erb +26 -0
- data/app/views/fragments/_remove_empty_state.erb +1 -0
- data/app/views/fragments/_replace_last_message.erb +1 -0
- data/app/views/fragments/_sidebar_menu.erb +11 -0
- data/app/views/fragments/_sidebar_status.erb +21 -0
- data/app/views/fragments/_status.erb +40 -0
- data/app/views/fragments/_stream.erb +26 -0
- data/app/views/fragments/_tools.erb +34 -0
- data/app/views/fragments/_tools_panel.erb +4 -0
- data/app/views/fragments/mcp/_editor.erb +54 -0
- data/app/views/fragments/mcp/_fields_forgejo.erb +16 -0
- data/app/views/fragments/mcp/_fields_github.erb +12 -0
- data/app/views/fragments/mcp/_list.erb +55 -0
- data/app/views/fragments/mcp/_workspace.erb +14 -0
- data/app/views/fragments/models/_loading.erb +4 -0
- data/app/views/fragments/settings/_chat.erb +1 -0
- data/app/views/fragments/settings/_input.erb +1 -0
- data/app/views/fragments/settings/_replace_contexts.erb +1 -0
- data/app/views/fragments/settings/_workspace.erb +4 -0
- data/app/views/layout.erb +19 -0
- data/app/views/pages/chat.erb +13 -0
- data/app/views/pages/mcps.erb +10 -0
- data/app/views/pages/sign_in.erb +45 -0
- data/app/views/partials/_sidebar.erb +24 -0
- data/bin/relay +38 -0
- data/config.ru +21 -0
- data/db/migrate/20260319131927_create_users.rb +12 -0
- data/db/migrate/20260327000000_create_contexts.rb +20 -0
- data/db/migrate/20260426130000_create_mcps.rb +19 -0
- data/db/migrate/20260426170000_create_model_infos.rb +20 -0
- data/db/migrate/20260503120000_create_songs.rb +17 -0
- data/db/migrate/20260503153000_drop_chat_from_model_infos.rb +8 -0
- data/db/migrate/20260503160000_rename_model_infos_to_model_records.rb +5 -0
- data/db/seeds.rb +13 -0
- data/lib/relay/attachment/session.rb +154 -0
- data/lib/relay/attachment.rb +55 -0
- data/lib/relay/cache/in_memory_cache.rb +60 -0
- data/lib/relay/cache.rb +5 -0
- data/lib/relay/jukebox.rb +96 -0
- data/lib/relay/markdown.rb +45 -0
- data/lib/relay/model.rb +12 -0
- data/lib/relay/reloader.rb +29 -0
- data/lib/relay/task.rb +66 -0
- data/lib/relay/task_monitor.rb +80 -0
- data/lib/relay/test.rb +11 -0
- data/lib/relay/theme.rb +5 -0
- data/lib/relay/tool.rb +12 -0
- data/lib/relay/version.rb +5 -0
- data/lib/relay.rb +183 -0
- data/libexec/relay/bootstrap +10 -0
- data/libexec/relay/configure +100 -0
- data/libexec/relay/migrate +7 -0
- data/libexec/relay/setup +10 -0
- data/libexec/relay/start +31 -0
- data/public/.gitkeep +0 -0
- data/public/images/relay.png +0 -0
- data/public/js/relay.js +68669 -0
- data/public/js/relay.js.map +1 -0
- data/public/stylesheets/application.css +2292 -0
- data/public/stylesheets/application.css.map +1 -0
- metadata +465 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relay
|
|
4
|
+
require "redcarpet"
|
|
5
|
+
|
|
6
|
+
##
|
|
7
|
+
# Renders markdown to HTML
|
|
8
|
+
# @param [String] text
|
|
9
|
+
# The markdown source
|
|
10
|
+
# @return [String]
|
|
11
|
+
def self.markdown(text)
|
|
12
|
+
renderer.render(text.to_s.gsub(/\r\n?/, "\n"))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @return [Redcarpet::Markdown]
|
|
17
|
+
# Returns the shared markdown renderer
|
|
18
|
+
def self.renderer
|
|
19
|
+
Redcarpet::Markdown.new(
|
|
20
|
+
Markdown.new(filter_html: false, safe_links_only: true),
|
|
21
|
+
autolink: true,
|
|
22
|
+
fenced_code_blocks: true,
|
|
23
|
+
lax_spacing: true,
|
|
24
|
+
no_intra_emphasis: true,
|
|
25
|
+
tables: true
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Markdown < Redcarpet::Render::HTML
|
|
30
|
+
require "erb"
|
|
31
|
+
include ERB::Util
|
|
32
|
+
##
|
|
33
|
+
# Renders fenced code blocks with a language class for highlight.js
|
|
34
|
+
# @param [String] code
|
|
35
|
+
# The code block contents
|
|
36
|
+
# @param [String, nil] language
|
|
37
|
+
# The fenced code language
|
|
38
|
+
# @return [String]
|
|
39
|
+
def block_code(code, language)
|
|
40
|
+
language = language.to_s.strip
|
|
41
|
+
language = "plaintext" if language.empty?
|
|
42
|
+
%(<pre><code class="language-#{h(language)}">#{h(code)}</code></pre>)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/relay/model.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relay
|
|
4
|
+
##
|
|
5
|
+
# Reloads Zeitwerk-managed application code on each request in
|
|
6
|
+
# development before passing control to the downstream Rack app.
|
|
7
|
+
class Reloader
|
|
8
|
+
##
|
|
9
|
+
# @param [#call] app
|
|
10
|
+
# @return [Relay::Reloader]
|
|
11
|
+
def initialize(app)
|
|
12
|
+
@app = app
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @param [Hash] env
|
|
17
|
+
# @return [Array(Integer, Hash, #each)]
|
|
18
|
+
def call(env)
|
|
19
|
+
reload!
|
|
20
|
+
@app.call(env)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def reload!
|
|
26
|
+
Relay.reload
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/relay/task.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# {Relay::Task} represents a Rake task that can be run
|
|
5
|
+
# in a separate process. The process becomes a group leader,
|
|
6
|
+
# so that if it spawns any child processes, they will be in
|
|
7
|
+
# the same process group.
|
|
8
|
+
#
|
|
9
|
+
# A task provides its status through a channel, which can be
|
|
10
|
+
# either "success" or "error" depending on whether the task
|
|
11
|
+
# completed successfully or not.
|
|
12
|
+
class Relay::Task
|
|
13
|
+
require "xchan"
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# @return [Chan::UNIXSocket]
|
|
17
|
+
attr_reader :ch
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @return [Integer]
|
|
21
|
+
attr_reader :pid
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @param [String] task
|
|
25
|
+
# The name of the Rake task to run
|
|
26
|
+
# @return [void]
|
|
27
|
+
def initialize(task)
|
|
28
|
+
@task = task
|
|
29
|
+
@ch = xchan(:pure)
|
|
30
|
+
@pid = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# Call the task in a separate process
|
|
35
|
+
# @return [Integer]
|
|
36
|
+
# The PID of the child process
|
|
37
|
+
def call
|
|
38
|
+
@pid = fork do
|
|
39
|
+
become_group_leader
|
|
40
|
+
invoke_task
|
|
41
|
+
record_success
|
|
42
|
+
rescue
|
|
43
|
+
record_error
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Read the status of a task (non-blocking)
|
|
49
|
+
# @raise [Chan::WaitReadable]
|
|
50
|
+
# When the channel is not ready to be read
|
|
51
|
+
# @return [String]
|
|
52
|
+
# Either "success" or "error"
|
|
53
|
+
def status
|
|
54
|
+
return @status if defined?(@status)
|
|
55
|
+
@status = ch.recv_nonblock
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def become_group_leader = Process.setpgrp
|
|
61
|
+
def invoke_task = Rake::Task[@task].invoke
|
|
62
|
+
|
|
63
|
+
def record(status) = @ch.send(status)
|
|
64
|
+
def record_success = record("success")
|
|
65
|
+
def record_error = record("error")
|
|
66
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# A task monitor that runs a list of tasks in parallel
|
|
5
|
+
# and monitors them afterwards. When a task fails, the
|
|
6
|
+
# monitor will exit and kill all other tasks.
|
|
7
|
+
#
|
|
8
|
+
# A {Relay::Task} becomes a process group leader, so killing
|
|
9
|
+
# the process group will kill all processes in the group which
|
|
10
|
+
# includes the task and all of its subprocesses.
|
|
11
|
+
class Relay::TaskMonitor
|
|
12
|
+
##
|
|
13
|
+
# @param [Array<String>] tasks
|
|
14
|
+
# A list of task names
|
|
15
|
+
# @return [Relay::TaskMonitor]
|
|
16
|
+
def initialize(tasks:)
|
|
17
|
+
@tasks = tasks.map { Relay::Task.new(_1) }
|
|
18
|
+
@pids = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Assign a block that is run before tasks are forked
|
|
23
|
+
# @return [void]
|
|
24
|
+
def prefork(&block)
|
|
25
|
+
@prefork = block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# Start the task monitor
|
|
30
|
+
# @return [void]
|
|
31
|
+
def monitor
|
|
32
|
+
@prefork&.call
|
|
33
|
+
run
|
|
34
|
+
wait
|
|
35
|
+
rescue Interrupt
|
|
36
|
+
@pids.each { Process.kill("TERM", -_1) }
|
|
37
|
+
wait
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Run all tasks in parallel and monitor their status
|
|
44
|
+
# @return [void]
|
|
45
|
+
def run
|
|
46
|
+
@pids = @tasks.map(&:call)
|
|
47
|
+
@tasks.each do |task|
|
|
48
|
+
if error?(task.status)
|
|
49
|
+
break(@tasks.size - 1)
|
|
50
|
+
end
|
|
51
|
+
rescue Chan::WaitReadable
|
|
52
|
+
sleep 0.05
|
|
53
|
+
retry
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Wait for all tasks to finish
|
|
59
|
+
# @return [void]
|
|
60
|
+
def wait
|
|
61
|
+
@pids.each { Process.wait(_1) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Read the status of a task
|
|
66
|
+
# @param [String] status
|
|
67
|
+
# Either "success" or "error"
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
def error?(status)
|
|
70
|
+
case status
|
|
71
|
+
when "error"
|
|
72
|
+
@tasks.each do
|
|
73
|
+
Process.kill("TERM", -_1.pid)
|
|
74
|
+
rescue Errno::ESRCH
|
|
75
|
+
end
|
|
76
|
+
true
|
|
77
|
+
when "success" then false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/relay/test.rb
ADDED
data/lib/relay/theme.rb
ADDED
data/lib/relay/tool.rb
ADDED
data/lib/relay.rb
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Relay
|
|
4
|
+
require_relative "relay/version"
|
|
5
|
+
require_relative "relay/cache"
|
|
6
|
+
require_relative "relay/attachment"
|
|
7
|
+
require_relative "relay/jukebox"
|
|
8
|
+
require_relative "relay/markdown"
|
|
9
|
+
require_relative "relay/theme"
|
|
10
|
+
require_relative "relay/task_monitor"
|
|
11
|
+
require_relative "relay/task"
|
|
12
|
+
require_relative "relay/tool"
|
|
13
|
+
require_relative "relay/model"
|
|
14
|
+
require_relative "relay/reloader"
|
|
15
|
+
|
|
16
|
+
PROVIDERS = {
|
|
17
|
+
"anthropic" => -> { LLM.anthropic(key: ENV["ANTHROPIC_SECRET"]) },
|
|
18
|
+
"deepseek" => -> { LLM.deepseek(key: ENV["DEEPSEEK_SECRET"]) },
|
|
19
|
+
"google" => -> { LLM.google(key: ENV["GOOGLE_SECRET"]) },
|
|
20
|
+
"openai" => -> { LLM.openai(key: ENV["OPENAI_SECRET"]) },
|
|
21
|
+
"xai" => -> { LLM.xai(key: ENV["XAI_SECRET"]) }
|
|
22
|
+
}.freeze
|
|
23
|
+
private_constant :PROVIDERS
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [String]
|
|
27
|
+
def self.banner
|
|
28
|
+
" ____ _ \n" \
|
|
29
|
+
"| _ \\ ___| | __ _ _ _ \n" \
|
|
30
|
+
"| |_) / _ \\ |/ _` | | | |\n" \
|
|
31
|
+
"| _ < __/ | (_| | |_| |\n" \
|
|
32
|
+
"|_| \\_\\___|_|\\__,_|\\__, |\n" \
|
|
33
|
+
" |___/ \n\n"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Returns all known providers
|
|
38
|
+
# @return [LLM::Object]
|
|
39
|
+
def self.providers
|
|
40
|
+
@providers ||= LLM::Object.from(PROVIDERS)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Returns the current Rack environment
|
|
45
|
+
# @return [String]
|
|
46
|
+
def self.environment
|
|
47
|
+
ENV["RACK_ENV"] || "development"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Returns true when running in development
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def self.development?
|
|
54
|
+
environment == "development"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Returns true when running in production
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def self.production?
|
|
61
|
+
environment == "production"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Returns an object that can be used to store application state
|
|
66
|
+
# that should persist between requests.
|
|
67
|
+
# @return [Relay::InMemoryCache]
|
|
68
|
+
def self.cache
|
|
69
|
+
@cache
|
|
70
|
+
end
|
|
71
|
+
@cache = Cache::InMemoryCache.new
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Returns the root path of the application
|
|
75
|
+
# @return [String]
|
|
76
|
+
def self.root
|
|
77
|
+
@root ||= File.realpath File.join(__dir__, "..")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Returns the writable Relay home directory
|
|
82
|
+
# @return [String]
|
|
83
|
+
def self.home
|
|
84
|
+
@home ||= ENV["RELAY_HOME"] || File.join(Dir.home, ".relay")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# Returns the path to the Relay env file
|
|
89
|
+
# @return [String]
|
|
90
|
+
def self.env_path
|
|
91
|
+
@env_path ||= File.join(home, "env")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# @return [Array<String>]
|
|
96
|
+
# Returns the tools directory
|
|
97
|
+
def self.tools_dir
|
|
98
|
+
@tools_dir ||= File.join(root, "app", "tools")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Returns the path to the public/ directory
|
|
103
|
+
# @return [String]
|
|
104
|
+
def self.public_dir
|
|
105
|
+
@public_dir ||= File.join(root, "public")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Returns the path to generated images
|
|
110
|
+
# @return [String]
|
|
111
|
+
def self.images_dir
|
|
112
|
+
@images_dir ||= File.join(home, "g")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Returns the path to the app/assets/ directory
|
|
117
|
+
# @return [String]
|
|
118
|
+
def self.assets_dir
|
|
119
|
+
@assets_dir ||= File.join(root, "app", "assets")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
##
|
|
123
|
+
# @return [String]
|
|
124
|
+
# Returns the path to the app/views/resources directory
|
|
125
|
+
def self.resources_dir
|
|
126
|
+
@resources_dir ||= File.join(root, "app", "resources")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Returns the path to the app/views/ directory
|
|
131
|
+
# @return [String]
|
|
132
|
+
def self.views_dir
|
|
133
|
+
@views_dir ||= File.join(root, "app", "views")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# Returns the path to the db/migrate directory
|
|
138
|
+
# @return [String]
|
|
139
|
+
def self.migrations_dir
|
|
140
|
+
@migrations_dir ||= File.join(root, "db", "migrate")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Returns the path to the app/views/fragments directory
|
|
145
|
+
# @return [String]
|
|
146
|
+
def self.fragments_dir
|
|
147
|
+
@fragments_dir ||= File.join(views_dir, "fragments")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# @return [String]
|
|
152
|
+
def self.logs_dir
|
|
153
|
+
@logs_dir ||= File.join(home, "tmp")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
##
|
|
157
|
+
# Renders an erb template
|
|
158
|
+
# @param [String] path
|
|
159
|
+
# @param [Hash] locals
|
|
160
|
+
# @return [String]
|
|
161
|
+
def self.erb(path, locals = {})
|
|
162
|
+
tmpl = File.read File.join(views_dir, path)
|
|
163
|
+
ERB.new(tmpl).result_with_hash(locals)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
##
|
|
167
|
+
# Reload Relay (useful in development enviroments)
|
|
168
|
+
# @param [Boolean] reload
|
|
169
|
+
# @return [Array<String>]
|
|
170
|
+
def self.reload
|
|
171
|
+
LLM::Tool.clear_registry!
|
|
172
|
+
Relay.loader.reload
|
|
173
|
+
paths = Dir[File.join(tools_dir, "*.rb")]
|
|
174
|
+
paths.concat Dir[File.join(home, "tools", "*.rb")]
|
|
175
|
+
paths.sort.each do
|
|
176
|
+
load(_1)
|
|
177
|
+
rescue => ex
|
|
178
|
+
warn "tool error\n"
|
|
179
|
+
"#{ex.class}: #{ex.message}\n"
|
|
180
|
+
"#{ex.backtrace.join("\n")}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "relay"
|
|
5
|
+
require File.join(Relay.root, "app", "init")
|
|
6
|
+
warn "relay: bootstrap the database"
|
|
7
|
+
Sequel.extension :migration
|
|
8
|
+
Sequel::Migrator.run(Relay::DB, Relay.migrations_dir)
|
|
9
|
+
load File.join(Relay.root, "db", "seeds.rb")
|
|
10
|
+
Relay::Models::ModelRecord.refresh_all
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "relay"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "io/console"
|
|
7
|
+
|
|
8
|
+
PROVIDER_KEYS = {
|
|
9
|
+
"OPENAI_SECRET" => "OpenAI API key",
|
|
10
|
+
"GOOGLE_SECRET" => "Google API key",
|
|
11
|
+
"ANTHROPIC_SECRET" => "Anthropic API key",
|
|
12
|
+
"DEEPSEEK_SECRET" => "DeepSeek API key",
|
|
13
|
+
"XAI_SECRET" => "xAI API key"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# utils
|
|
18
|
+
def load_env
|
|
19
|
+
return {} unless File.readable?(Relay.env_path)
|
|
20
|
+
File.readlines(Relay.env_path, chomp: true).each_with_object({}) do |line, env|
|
|
21
|
+
next if line.empty? || line.start_with?("#")
|
|
22
|
+
key, value = line.split("=", 2)
|
|
23
|
+
env[key] = value.to_s
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write_env(env)
|
|
28
|
+
FileUtils.mkdir_p Relay.home
|
|
29
|
+
lines = env.sort.filter_map do |key, value|
|
|
30
|
+
next if value.to_s.empty?
|
|
31
|
+
"#{key}=#{value}"
|
|
32
|
+
end
|
|
33
|
+
File.write(Relay.env_path, "#{lines.join("\n")}\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prompt(label, current: nil, secret: false)
|
|
37
|
+
suffix = current.to_s.empty? ? "" : " [configured]"
|
|
38
|
+
print "#{label}#{suffix}: "
|
|
39
|
+
value = secret ? STDIN.noecho(&:gets).to_s.chomp : STDIN.gets.to_s.chomp
|
|
40
|
+
puts if secret
|
|
41
|
+
value.empty? ? current.to_s : value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def prompt_required(label)
|
|
45
|
+
loop do
|
|
46
|
+
print "#{label}: "
|
|
47
|
+
value = STDIN.gets.to_s.strip
|
|
48
|
+
return value unless value.empty?
|
|
49
|
+
warn "#{label} is required."
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prompt_password
|
|
54
|
+
loop do
|
|
55
|
+
print "Password: "
|
|
56
|
+
password = STDIN.noecho(&:gets).to_s.chomp
|
|
57
|
+
puts
|
|
58
|
+
print "Confirm password: "
|
|
59
|
+
confirm = STDIN.noecho(&:gets).to_s.chomp
|
|
60
|
+
puts
|
|
61
|
+
return password unless password.empty? || password != confirm
|
|
62
|
+
warn "Passwords must match and cannot be empty."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# main
|
|
68
|
+
def main
|
|
69
|
+
unless STDIN.tty? && $stdout.tty?
|
|
70
|
+
warn "relay configure requires an interactive terminal"
|
|
71
|
+
throw(:exit, 1)
|
|
72
|
+
end
|
|
73
|
+
env = load_env
|
|
74
|
+
puts Relay.banner
|
|
75
|
+
puts "Relay configuration\n"
|
|
76
|
+
PROVIDER_KEYS.each do |key, label|
|
|
77
|
+
env[key] = prompt(label, current: env[key], secret: true)
|
|
78
|
+
end
|
|
79
|
+
name = prompt_required("Name")
|
|
80
|
+
email = prompt_required("Email")
|
|
81
|
+
password = prompt_password
|
|
82
|
+
write_env(env)
|
|
83
|
+
require File.join(Relay.root, "app", "init")
|
|
84
|
+
unless Relay::DB.table_exists?(:users)
|
|
85
|
+
warn "Relay has not been bootstrapped"
|
|
86
|
+
throw(:exit, 1)
|
|
87
|
+
end
|
|
88
|
+
user = Relay::Models::User.where(email:).first || Relay::Models::User.new(email:)
|
|
89
|
+
user.name = name
|
|
90
|
+
user.password = password
|
|
91
|
+
user.save
|
|
92
|
+
puts "\nConfigured Relay."
|
|
93
|
+
puts "Email: #{email}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
excode = catch(:exit) {
|
|
97
|
+
main
|
|
98
|
+
0
|
|
99
|
+
}
|
|
100
|
+
exit excode
|
data/libexec/relay/setup
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "relay"
|
|
5
|
+
|
|
6
|
+
bootstrap = File.join(Relay.root, "libexec", "relay", "bootstrap")
|
|
7
|
+
configure = File.join(Relay.root, "libexec", "relay", "configure")
|
|
8
|
+
|
|
9
|
+
system(bootstrap) || exit(1)
|
|
10
|
+
system(configure) || exit(1)
|
data/libexec/relay/start
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "relay"
|
|
5
|
+
require "optparse"
|
|
6
|
+
|
|
7
|
+
DEFAULT_BIND = "http://0.0.0.0:9292"
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# utils
|
|
11
|
+
def option_parser(bind)
|
|
12
|
+
OptionParser.new do |o|
|
|
13
|
+
o.banner = "Usage: relay start [OPTIONS] [URL]"
|
|
14
|
+
o.on("--bind URL", "Bind Falcon to a URL") { bind.replace(_1) }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# main
|
|
20
|
+
def main(argv)
|
|
21
|
+
bind = String.new(DEFAULT_BIND)
|
|
22
|
+
option_parser(bind).parse!(argv)
|
|
23
|
+
bind = argv.shift || bind
|
|
24
|
+
exec "falcon", "serve", "--bind", bind
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
excode = catch(:exit) {
|
|
28
|
+
main(ARGV)
|
|
29
|
+
0
|
|
30
|
+
}
|
|
31
|
+
exit excode
|
data/public/.gitkeep
ADDED
|
File without changes
|
|
Binary file
|