fantasy-cli 1.2.10 → 1.2.11
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 +4 -4
- data/lib/gsd/ai/commands/api.rb +42 -11
- data/lib/gsd/api/client.rb +90 -0
- data/lib/gsd/api/middleware/cors.rb +33 -0
- data/lib/gsd/api/public/app.js +133 -0
- data/lib/gsd/api/public/index.html +100 -0
- data/lib/gsd/api/public/style.css +416 -0
- data/lib/gsd/api/routes/health.rb +51 -0
- data/lib/gsd/api/routes/phases.rb +57 -0
- data/lib/gsd/api/routes/roadmap.rb +40 -0
- data/lib/gsd/api/routes/state.rb +68 -0
- data/lib/gsd/api/server.rb +300 -0
- data/lib/gsd/api.rb +32 -0
- data/lib/gsd/cli.rb +124 -3
- data/lib/gsd/tui/app.rb +24 -0
- data/lib/gsd/version.rb +1 -1
- metadata +26 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9d3e9147f450f7556df6f5bc25422413cb7d931602a891f6fa44760bf0c9199
|
|
4
|
+
data.tar.gz: 46a0e41b9cc69c9898b7041e11ad6c0c05370f8cb41330bfec0d21080f6e1cee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ff5b96ebe17b99f51c2c74a325fe1289be0ccaddd96a30ebd06565ee58b2186aede2d6bc3b12b1402b54aa15b517c37e54c450c2e76c281abc51af6f010875f
|
|
7
|
+
data.tar.gz: 815b60e5ef7e357f2b83e954af91261e392660df8d92e59c4276c0513ddaa1a849d2541a3f973e43c3f3aeef4a6d13b19c43795e98384a721f189dfb8de1dcd7
|
data/lib/gsd/ai/commands/api.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'stringio'
|
|
3
4
|
require 'gsd/ai/commands/base'
|
|
4
5
|
|
|
5
6
|
module Gsd
|
|
@@ -7,27 +8,57 @@ module Gsd
|
|
|
7
8
|
module Commands
|
|
8
9
|
# /api - Mostra informações da API do provider
|
|
9
10
|
class Api < Base
|
|
10
|
-
DESCRIPTION = 'Mostra informações
|
|
11
|
-
USAGE = '/api'
|
|
11
|
+
DESCRIPTION = 'Mostra informações ou configura API keys'
|
|
12
|
+
USAGE = '/api [set <provider> <key>]'
|
|
12
13
|
|
|
13
14
|
def execute
|
|
15
|
+
if @args.empty?
|
|
16
|
+
show_info
|
|
17
|
+
elsif @args[0] == 'set' && @args.length >= 3
|
|
18
|
+
set_key(@args[1], @args[2])
|
|
19
|
+
else
|
|
20
|
+
"❌ Uso incorreto. Exemplos:\n /api\n /api set openrouter sk-or-..."
|
|
21
|
+
end
|
|
22
|
+
rescue => e
|
|
23
|
+
"Erro na configuração da API: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def show_info
|
|
14
29
|
provider = @chat.provider
|
|
15
30
|
model = provider.model
|
|
16
31
|
|
|
17
32
|
<<~API
|
|
18
33
|
🤖 API Provider Info
|
|
19
34
|
|
|
20
|
-
Provider: #{provider.class::PROVIDER_NAME rescue provider.class.to_s.split('::').last}
|
|
21
|
-
Modelo: #{model}
|
|
35
|
+
Provider atual: #{provider.class::PROVIDER_NAME rescue provider.class.to_s.split('::').last}
|
|
36
|
+
Modelo atual: #{model}
|
|
37
|
+
|
|
38
|
+
Para configurar uma API key:
|
|
39
|
+
/api set <provider> <key>
|
|
40
|
+
Ex: /api set openrouter sk-or-...
|
|
22
41
|
|
|
23
|
-
Para trocar de provider:
|
|
24
|
-
/model
|
|
25
|
-
/model openai - GPT
|
|
26
|
-
/model ollama - Local
|
|
27
|
-
/model openrouter - Gratuito
|
|
42
|
+
Para trocar de provider/modelo:
|
|
43
|
+
/model <provider> <model>
|
|
28
44
|
API
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def set_key(provider_name, api_key)
|
|
48
|
+
require 'gsd/ai/config'
|
|
49
|
+
|
|
50
|
+
# Suprime o output do ConfigCLI para não duplicar no REPL e retorna a string capturada
|
|
51
|
+
original_stdout = $stdout
|
|
52
|
+
$stdout = StringIO.new
|
|
53
|
+
begin
|
|
54
|
+
config = Gsd::AI::ConfigCLI.new
|
|
55
|
+
config.set_key(provider_name, api_key)
|
|
56
|
+
output = $stdout.string
|
|
57
|
+
ensure
|
|
58
|
+
$stdout = original_stdout
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
output.strip
|
|
31
62
|
end
|
|
32
63
|
end
|
|
33
64
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Gsd
|
|
8
|
+
module API
|
|
9
|
+
# HTTP client to interact with the Gsd API server
|
|
10
|
+
class Client
|
|
11
|
+
attr_reader :host, :port
|
|
12
|
+
|
|
13
|
+
def initialize(host: '127.0.0.1', port: 3000)
|
|
14
|
+
@host = host
|
|
15
|
+
@port = port
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Checks the health status of the API server
|
|
19
|
+
#
|
|
20
|
+
# @return [Hash] Parsed JSON response from /api/health
|
|
21
|
+
def status
|
|
22
|
+
call('GET', '/api/health')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Makes an HTTP request to the API server
|
|
26
|
+
#
|
|
27
|
+
# @param method [String] HTTP method (GET, POST, PATCH, PUT, DELETE)
|
|
28
|
+
# @param path [String] Request path
|
|
29
|
+
# @param body [Hash, String, nil] Optional request body (will be converted to JSON if Hash)
|
|
30
|
+
# @return [Hash] Parsed JSON response
|
|
31
|
+
def call(method, path, body: nil)
|
|
32
|
+
# Ensure path starts with a slash
|
|
33
|
+
path = "/#{path}" unless path.start_with?('/')
|
|
34
|
+
|
|
35
|
+
uri = URI("http://#{@host}:#{@port}#{path}")
|
|
36
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
37
|
+
|
|
38
|
+
request_class = case method.to_s.upcase
|
|
39
|
+
when 'GET' then Net::HTTP::Get
|
|
40
|
+
when 'POST' then Net::HTTP::Post
|
|
41
|
+
when 'PUT' then Net::HTTP::Put
|
|
42
|
+
when 'PATCH' then Net::HTTP::Patch
|
|
43
|
+
when 'DELETE' then Net::HTTP::Delete
|
|
44
|
+
else
|
|
45
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
request = request_class.new(uri.request_uri)
|
|
49
|
+
request['Content-Type'] = 'application/json'
|
|
50
|
+
request['Accept'] = 'application/json'
|
|
51
|
+
|
|
52
|
+
if body
|
|
53
|
+
request.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
response = http.request(request)
|
|
58
|
+
parse_response(response)
|
|
59
|
+
rescue Errno::ECONNREFUSED
|
|
60
|
+
raise "Connection refused. Is the API server running on http://#{@host}:#{@port}?"
|
|
61
|
+
rescue => e
|
|
62
|
+
raise "API request failed: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def parse_response(response)
|
|
69
|
+
parsed_body = if response.body && !response.body.empty?
|
|
70
|
+
begin
|
|
71
|
+
JSON.parse(response.body)
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
{ 'raw_body' => response.body }
|
|
74
|
+
end
|
|
75
|
+
else
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# If it's not a successful response, ensure we include HTTP status info
|
|
80
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
81
|
+
parsed_body['http_status'] = response.code.to_i
|
|
82
|
+
parsed_body['http_message'] = response.message
|
|
83
|
+
parsed_body['error'] ||= "HTTP Error #{response.code}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
parsed_body
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsd
|
|
4
|
+
module API
|
|
5
|
+
module Middleware
|
|
6
|
+
# CORS Middleware - Adds Cross-Origin Resource Sharing headers to all responses
|
|
7
|
+
class CORS
|
|
8
|
+
def initialize(app)
|
|
9
|
+
@app = app
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
status, headers, body = @app.call(env)
|
|
14
|
+
|
|
15
|
+
# Add CORS headers to all responses
|
|
16
|
+
headers['Access-Control-Allow-Origin'] = '*'
|
|
17
|
+
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
|
18
|
+
headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
|
|
19
|
+
headers['Access-Control-Max-Age'] = '86400'
|
|
20
|
+
|
|
21
|
+
# Handle preflight OPTIONS requests
|
|
22
|
+
if env['REQUEST_METHOD'] == 'OPTIONS'
|
|
23
|
+
headers['Content-Length'] = '0'
|
|
24
|
+
status = 204
|
|
25
|
+
body = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
[status, headers, body]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
// --- 1. Dashboard Data Fetching ---
|
|
3
|
+
const stateContainer = document.getElementById('state-container');
|
|
4
|
+
const phasesContainer = document.getElementById('phases-container');
|
|
5
|
+
const roadmapContainer = document.getElementById('roadmap-container');
|
|
6
|
+
|
|
7
|
+
const fetchAndRender = async (url, container) => {
|
|
8
|
+
if (!container) return;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
// Show loading state for the container
|
|
12
|
+
container.innerHTML = '<span class="loading">Loading data...</span>';
|
|
13
|
+
|
|
14
|
+
const response = await fetch(url);
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
17
|
+
}
|
|
18
|
+
const data = await response.json();
|
|
19
|
+
|
|
20
|
+
// Format the JSON data for display in the UI
|
|
21
|
+
container.innerHTML = `<pre><code>${JSON.stringify(data, null, 2)}</code></pre>`;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(`Error fetching data from ${url}:`, error);
|
|
24
|
+
container.innerHTML = `<div class="error-message">Failed to load data: ${error.message}</div>`;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const fetchHealth = async () => {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch('/api/health');
|
|
31
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
console.log('API Health Check:', data);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Health Check Error:', error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Load initial data to populate the UI
|
|
40
|
+
const loadDashboard = () => {
|
|
41
|
+
fetchAndRender('/api/state', stateContainer);
|
|
42
|
+
fetchAndRender('/api/phases', phasesContainer);
|
|
43
|
+
fetchAndRender('/api/roadmap', roadmapContainer);
|
|
44
|
+
fetchHealth();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
loadDashboard();
|
|
48
|
+
|
|
49
|
+
// --- 2. AI Chat Logic ---
|
|
50
|
+
const chatForm = document.getElementById('chat-form');
|
|
51
|
+
// Fallback if there's no element explicitly with id="chat-input"
|
|
52
|
+
const chatInput = document.getElementById('chat-input') || (chatForm ? chatForm.querySelector('input[type="text"]') : null);
|
|
53
|
+
const chatLog = document.getElementById('chat-log');
|
|
54
|
+
|
|
55
|
+
// Utility to escape HTML and prevent XSS when rendering chat messages
|
|
56
|
+
const escapeHtml = (unsafe) => {
|
|
57
|
+
return (unsafe || '').toString()
|
|
58
|
+
.replace(/&/g, "&")
|
|
59
|
+
.replace(/</g, "<")
|
|
60
|
+
.replace(/>/g, ">")
|
|
61
|
+
.replace(/"/g, """)
|
|
62
|
+
.replace(/'/g, "'");
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const appendChatMessage = (role, text, isError = false) => {
|
|
66
|
+
if (!chatLog) return null;
|
|
67
|
+
|
|
68
|
+
const messageDiv = document.createElement('div');
|
|
69
|
+
messageDiv.className = `chat-message ${role}`;
|
|
70
|
+
if (isError) messageDiv.classList.add('error');
|
|
71
|
+
|
|
72
|
+
const roleLabel = role === 'user' ? 'You' : 'AI';
|
|
73
|
+
messageDiv.innerHTML = `<strong>${roleLabel}:</strong> <span class="content">${escapeHtml(text)}</span>`;
|
|
74
|
+
|
|
75
|
+
chatLog.appendChild(messageDiv);
|
|
76
|
+
// Auto-scroll to bottom of chat
|
|
77
|
+
chatLog.scrollTop = chatLog.scrollHeight;
|
|
78
|
+
|
|
79
|
+
return messageDiv;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (chatForm) {
|
|
83
|
+
chatForm.addEventListener('submit', async (e) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
|
|
86
|
+
const message = chatInput ? chatInput.value.trim() : '';
|
|
87
|
+
if (!message) return;
|
|
88
|
+
|
|
89
|
+
// Clear input field immediately
|
|
90
|
+
if (chatInput) chatInput.value = '';
|
|
91
|
+
|
|
92
|
+
// Display User Message
|
|
93
|
+
appendChatMessage('user', message);
|
|
94
|
+
|
|
95
|
+
// Display Loading State for AI
|
|
96
|
+
const loadingMsg = appendChatMessage('ai', 'Thinking...');
|
|
97
|
+
if (loadingMsg) loadingMsg.classList.add('loading-pulse');
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Send the POST request to the local API
|
|
101
|
+
const response = await fetch('/api/ai/chat', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json'
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
message: message,
|
|
108
|
+
provider: 'anthropic'
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new Error(`Server returned ${response.status}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
|
|
118
|
+
// Remove loading message once we get a response
|
|
119
|
+
if (loadingMsg) loadingMsg.remove();
|
|
120
|
+
|
|
121
|
+
// Display AI response
|
|
122
|
+
// Adapting to multiple possible standard JSON response formats
|
|
123
|
+
const aiReply = data.response || data.message || data.reply || data.text || JSON.stringify(data);
|
|
124
|
+
appendChatMessage('ai', aiReply);
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Chat API Error:', error);
|
|
128
|
+
if (loadingMsg) loadingMsg.remove();
|
|
129
|
+
appendChatMessage('ai', `Communication error: ${error.message}`, true);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Fantasy CLI - Nexus Control</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header>
|
|
12
|
+
<div class="logo">
|
|
13
|
+
<span class="glitch" data-text="FANTASY CLI">FANTASY CLI</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="subtitle">Nexus Control Dashboard</div>
|
|
16
|
+
<nav>
|
|
17
|
+
<a href="#project-state">State</a>
|
|
18
|
+
<a href="#phases-list">Phases</a>
|
|
19
|
+
<a href="#roadmap-summary">Roadmap</a>
|
|
20
|
+
<a href="#ai-chat">AI Link</a>
|
|
21
|
+
</nav>
|
|
22
|
+
<div id="health-indicator" class="status-dot"></div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<main class="grid-container">
|
|
26
|
+
<!-- Left Column: Data & State -->
|
|
27
|
+
<div class="column left">
|
|
28
|
+
<section id="project-state" class="panel">
|
|
29
|
+
<h2 class="panel-title">Project State</h2>
|
|
30
|
+
<div id="state-container" class="data-view">
|
|
31
|
+
<span class="loading">Initializing state uplink...</span>
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<section id="phases-list" class="panel">
|
|
36
|
+
<h2 class="panel-title">Phases Sequence</h2>
|
|
37
|
+
<div id="phases-container" class="data-view">
|
|
38
|
+
<span class="loading">Fetching sequence data...</span>
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Right Column: Roadmap & AI -->
|
|
44
|
+
<div class="column right">
|
|
45
|
+
<section id="roadmap-summary" class="panel">
|
|
46
|
+
<h2 class="panel-title">Roadmap</h2>
|
|
47
|
+
<div id="roadmap-container" class="data-view">
|
|
48
|
+
<span class="loading">Decrypting roadmap...</span>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section id="ai-chat" class="panel ai-panel">
|
|
53
|
+
<h2 class="panel-title glow-text">AI Neural Link</h2>
|
|
54
|
+
|
|
55
|
+
<div id="chat-log" class="chat-messages">
|
|
56
|
+
<div class="message ai">
|
|
57
|
+
<span class="sender">GSD AI:</span>
|
|
58
|
+
<div class="content">Nexus connection established. How can I assist you with this project?</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<form id="chat-form" class="chat-input-area">
|
|
63
|
+
<div class="config-row">
|
|
64
|
+
<div class="input-group">
|
|
65
|
+
<label for="provider">Provider</label>
|
|
66
|
+
<select id="provider" name="provider">
|
|
67
|
+
<option value="anthropic" selected>Anthropic</option>
|
|
68
|
+
<option value="openai">OpenAI</option>
|
|
69
|
+
<option value="openrouter">OpenRouter</option>
|
|
70
|
+
<option value="ollama">Ollama</option>
|
|
71
|
+
<option value="lmstudio">LM Studio</option>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="input-group">
|
|
76
|
+
<label for="model">Model</label>
|
|
77
|
+
<input type="text" id="model" name="model" placeholder="claude-3-haiku-20240307">
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="message-row">
|
|
82
|
+
<textarea id="message" name="message" placeholder="Type your command or question... (e.g. 'What is the current phase?')" required></textarea>
|
|
83
|
+
<button type="submit" class="neon-btn">SEND</button>
|
|
84
|
+
</div>
|
|
85
|
+
</form>
|
|
86
|
+
</section>
|
|
87
|
+
</div>
|
|
88
|
+
</main>
|
|
89
|
+
|
|
90
|
+
<footer>
|
|
91
|
+
<div class="stats">
|
|
92
|
+
<span id="cost-tracker">API Cost: Loading...</span> |
|
|
93
|
+
<span id="token-tracker">Tokens: Loading...</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="credits">GSD Core v1.2.10</div>
|
|
96
|
+
</footer>
|
|
97
|
+
|
|
98
|
+
<script src="app.js"></script>
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|