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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97769253f39b85a122d12008484a7605a706c5a496b439bae2bacf3e775a82c6
4
- data.tar.gz: f24db3335249ce2c18187778aedfbfce6634d6c862704efcad6145189e85578f
3
+ metadata.gz: e9d3e9147f450f7556df6f5bc25422413cb7d931602a891f6fa44760bf0c9199
4
+ data.tar.gz: 46a0e41b9cc69c9898b7041e11ad6c0c05370f8cb41330bfec0d21080f6e1cee
5
5
  SHA512:
6
- metadata.gz: c82bb104a336a2134e14ec6e80f008738ce87eb94cde02cb3195cd91bf6f46ac81ca9dacbc79341caf36bab14071124e995df3179e1e55d1507e08882dce4e11
7
- data.tar.gz: 26556e231d6d5b3465ed3dc245b06835c139fbcff7525dd99c49d0cac13dbf4ef989b0911c330fbae33cc9b8ba7627327635f17cb775e516a7f857922663a830
6
+ metadata.gz: 6ff5b96ebe17b99f51c2c74a325fe1289be0ccaddd96a30ebd06565ee58b2186aede2d6bc3b12b1402b54aa15b517c37e54c450c2e76c281abc51af6f010875f
7
+ data.tar.gz: 815b60e5ef7e357f2b83e954af91261e392660df8d92e59c4276c0513ddaa1a849d2541a3f973e43c3f3aeef4a6d13b19c43795e98384a721f189dfb8de1dcd7
@@ -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 da API do provider atual'
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 anthropic - Claude (padrão)
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
- rescue => e
30
- "Erro ao obter info da API: #{e.message}"
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, "&amp;")
59
+ .replace(/</g, "&lt;")
60
+ .replace(/>/g, "&gt;")
61
+ .replace(/"/g, "&quot;")
62
+ .replace(/'/g, "&#039;");
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>