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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'webrick'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'routes/health'
|
|
6
|
+
require_relative 'routes/state'
|
|
7
|
+
require_relative 'routes/phases'
|
|
8
|
+
require_relative 'routes/roadmap'
|
|
9
|
+
require_relative '../ai/chat'
|
|
10
|
+
|
|
11
|
+
module Gsd
|
|
12
|
+
module API
|
|
13
|
+
# Custom HTTP servlet that supports PATCH and all REST methods
|
|
14
|
+
class APIServlet < WEBrick::HTTPServlet::AbstractServlet
|
|
15
|
+
def initialize(server, *options)
|
|
16
|
+
super(server)
|
|
17
|
+
@cwd = options.is_a?(Array) && options[0].is_a?(Hash) ? options[0][:cwd] : Dir.pwd
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def do_GET(req, res)
|
|
21
|
+
route(req, res)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def do_POST(req, res)
|
|
25
|
+
route(req, res)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def do_PUT(req, res)
|
|
29
|
+
route(req, res)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def do_PATCH(req, res)
|
|
33
|
+
route(req, res)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def do_OPTIONS(req, res)
|
|
37
|
+
set_cors_headers(res)
|
|
38
|
+
res.status = 204
|
|
39
|
+
res.body = ''
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def do_DELETE(req, res)
|
|
43
|
+
set_cors_headers(res)
|
|
44
|
+
res.status = 405
|
|
45
|
+
res.body = JSON.generate({ error: 'Method not allowed' })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def route(req, res)
|
|
51
|
+
set_cors_headers(res)
|
|
52
|
+
|
|
53
|
+
path = req.path_info
|
|
54
|
+
method = req.request_method
|
|
55
|
+
|
|
56
|
+
case path
|
|
57
|
+
when '/health', '/health/'
|
|
58
|
+
handle(req, res) { Routes::Health.check(cwd: @cwd) }
|
|
59
|
+
|
|
60
|
+
when '/state', '/state/'
|
|
61
|
+
route_state(method, req, res)
|
|
62
|
+
|
|
63
|
+
when '/phases', '/phases/'
|
|
64
|
+
route_phases(method, req, res)
|
|
65
|
+
|
|
66
|
+
when '/roadmap', '/roadmap/'
|
|
67
|
+
handle(req, res) do
|
|
68
|
+
if Gsd::Roadmap.exists?(cwd: @cwd)
|
|
69
|
+
Routes::Roadmap.analyze(@cwd)
|
|
70
|
+
else
|
|
71
|
+
JSON.pretty_generate({
|
|
72
|
+
success: true,
|
|
73
|
+
data: {
|
|
74
|
+
message: 'No ROADMAP.md found in this directory',
|
|
75
|
+
phases: [],
|
|
76
|
+
total_phases: 0
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
when '/ai/chat', '/ai/chat/'
|
|
83
|
+
route_ai_chat(method, req, res)
|
|
84
|
+
|
|
85
|
+
when '/ai/models', '/ai/models/'
|
|
86
|
+
route_ai_models(method, req, res)
|
|
87
|
+
|
|
88
|
+
when '/ai/cost', '/ai/cost/'
|
|
89
|
+
route_ai_cost(method, req, res)
|
|
90
|
+
|
|
91
|
+
else
|
|
92
|
+
if path.start_with?('/state/')
|
|
93
|
+
section = path.sub('/state/', '')
|
|
94
|
+
handle(req, res) { Routes::State.get_section(section, @cwd) }
|
|
95
|
+
elsif path.start_with?('/phases/')
|
|
96
|
+
num = path.sub('/phases/', '')
|
|
97
|
+
handle(req, res) { Routes::Phases.find(num, @cwd) }
|
|
98
|
+
else
|
|
99
|
+
res.status = 404
|
|
100
|
+
res.body = JSON.generate({
|
|
101
|
+
error: 'Not found',
|
|
102
|
+
path: req.path,
|
|
103
|
+
available_endpoints: [
|
|
104
|
+
'GET /api/health',
|
|
105
|
+
'GET /api/state',
|
|
106
|
+
'GET /api/state/:section',
|
|
107
|
+
'PATCH /api/state',
|
|
108
|
+
'PUT /api/state',
|
|
109
|
+
'GET /api/phases',
|
|
110
|
+
'GET /api/phases/:num',
|
|
111
|
+
'POST /api/phases',
|
|
112
|
+
'GET /api/roadmap',
|
|
113
|
+
'POST /api/ai/chat',
|
|
114
|
+
'GET /api/ai/models',
|
|
115
|
+
'GET /api/ai/cost'
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def route_state(method, req, res)
|
|
123
|
+
case method
|
|
124
|
+
when 'GET'
|
|
125
|
+
handle(req, res) { Routes::State.json(@cwd) }
|
|
126
|
+
when 'PATCH'
|
|
127
|
+
handle(req, res) do
|
|
128
|
+
body = parse_json_body(req)
|
|
129
|
+
Routes::State.patch(body['fields'], @cwd)
|
|
130
|
+
end
|
|
131
|
+
when 'PUT'
|
|
132
|
+
handle(req, res) do
|
|
133
|
+
body = parse_json_body(req)
|
|
134
|
+
Routes::State.update(body['field'], body['value'], @cwd)
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
res.status = 405
|
|
138
|
+
res.body = JSON.generate({ error: 'Method not allowed for /api/state' })
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def route_phases(method, req, res)
|
|
143
|
+
case method
|
|
144
|
+
when 'GET'
|
|
145
|
+
handle(req, res) { Routes::Phases.list(@cwd) }
|
|
146
|
+
when 'POST'
|
|
147
|
+
handle(req, res) do
|
|
148
|
+
body = parse_json_body(req)
|
|
149
|
+
Routes::Phases.add(body['description'], @cwd)
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
res.status = 405
|
|
153
|
+
res.body = JSON.generate({ error: 'Method not allowed for /api/phases' })
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def route_ai_chat(method, req, res)
|
|
158
|
+
if method == 'POST'
|
|
159
|
+
handle(req, res) do
|
|
160
|
+
body = parse_json_body(req)
|
|
161
|
+
message = body['message']
|
|
162
|
+
raise ArgumentError, "message is required" unless message && !message.to_s.strip.empty?
|
|
163
|
+
|
|
164
|
+
provider = (body['provider'] || 'anthropic').to_sym
|
|
165
|
+
model = body['model']
|
|
166
|
+
|
|
167
|
+
chat = Gsd::AI::Chat.new(provider: provider, model: model, cwd: @cwd)
|
|
168
|
+
response = chat.send(message, stream: false)
|
|
169
|
+
|
|
170
|
+
JSON.pretty_generate({
|
|
171
|
+
success: true,
|
|
172
|
+
data: { response: response }
|
|
173
|
+
})
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
res.status = 405
|
|
177
|
+
res.body = JSON.generate({ error: 'Method not allowed for /api/ai/chat' })
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def route_ai_models(method, req, res)
|
|
182
|
+
if method == 'GET'
|
|
183
|
+
handle(req, res) do
|
|
184
|
+
JSON.pretty_generate({
|
|
185
|
+
success: true,
|
|
186
|
+
data: {
|
|
187
|
+
providers: ['anthropic', 'openai', 'openrouter', 'ollama', 'lmstudio']
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
end
|
|
191
|
+
else
|
|
192
|
+
res.status = 405
|
|
193
|
+
res.body = JSON.generate({ error: 'Method not allowed for /api/ai/models' })
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def route_ai_cost(method, req, res)
|
|
198
|
+
if method == 'GET'
|
|
199
|
+
handle(req, res) do
|
|
200
|
+
persist_dir = File.join(@cwd, '.gsd', 'ai')
|
|
201
|
+
tracker = Gsd::AI::CostTracker.new(budget: 100.0, persist_dir: persist_dir)
|
|
202
|
+
JSON.pretty_generate({
|
|
203
|
+
success: true,
|
|
204
|
+
data: tracker.stats
|
|
205
|
+
})
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
res.status = 405
|
|
209
|
+
res.body = JSON.generate({ error: 'Method not allowed for /api/ai/cost' })
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def handle(req, res)
|
|
214
|
+
body = yield
|
|
215
|
+
res.status = 200
|
|
216
|
+
res.body = body
|
|
217
|
+
rescue Gsd::State::StateError, Gsd::Roadmap::RoadmapError, Gsd::Phase::PhaseError => e
|
|
218
|
+
res.status = 404
|
|
219
|
+
res.body = JSON.generate({ error: e.message })
|
|
220
|
+
rescue ArgumentError => e
|
|
221
|
+
res.status = 400
|
|
222
|
+
res.body = JSON.generate({ error: e.message })
|
|
223
|
+
rescue => e
|
|
224
|
+
res.status = 500
|
|
225
|
+
res.body = JSON.generate({ error: "Internal server error: #{e.message}" })
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def set_cors_headers(res)
|
|
229
|
+
res['Content-Type'] = 'application/json'
|
|
230
|
+
res['Access-Control-Allow-Origin'] = '*'
|
|
231
|
+
res['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
|
232
|
+
res['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-HTTP-Method-Override'
|
|
233
|
+
res['Access-Control-Max-Age'] = '86400'
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def parse_json_body(req)
|
|
237
|
+
return {} unless req.body && !req.body.empty?
|
|
238
|
+
|
|
239
|
+
JSON.parse(req.body)
|
|
240
|
+
rescue JSON::ParserError => e
|
|
241
|
+
raise ArgumentError, "Invalid JSON body: #{e.message}"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# API Server - WEBrick HTTP server exposing CLI functionality via REST API
|
|
246
|
+
class Server
|
|
247
|
+
def initialize(cwd: nil)
|
|
248
|
+
@cwd = cwd || Dir.pwd
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Start the server with given host and port
|
|
252
|
+
#
|
|
253
|
+
# @param host [String] Host to bind to
|
|
254
|
+
# @param port [Integer] Port to listen on
|
|
255
|
+
def start(host: '127.0.0.1', port: 3000)
|
|
256
|
+
puts "🚀 Fantasy API Server starting..."
|
|
257
|
+
puts "📍 Listening on http://#{host}:#{port}"
|
|
258
|
+
puts "📂 Working directory: #{@cwd}"
|
|
259
|
+
puts "🌐 Dashboard available at: http://#{host}:#{port}/"
|
|
260
|
+
puts "🔗 Available endpoints:"
|
|
261
|
+
puts " GET /api/health"
|
|
262
|
+
puts " GET /api/state"
|
|
263
|
+
puts " GET /api/state/:section"
|
|
264
|
+
puts " PATCH /api/state"
|
|
265
|
+
puts " PUT /api/state"
|
|
266
|
+
puts " GET /api/phases"
|
|
267
|
+
puts " GET /api/phases/:num"
|
|
268
|
+
puts " POST /api/phases"
|
|
269
|
+
puts " GET /api/roadmap"
|
|
270
|
+
puts " POST /api/ai/chat"
|
|
271
|
+
puts " GET /api/ai/models"
|
|
272
|
+
puts " GET /api/ai/cost"
|
|
273
|
+
puts ""
|
|
274
|
+
puts "Press Ctrl+C to stop"
|
|
275
|
+
|
|
276
|
+
server = WEBrick::HTTPServer.new(
|
|
277
|
+
Port: port,
|
|
278
|
+
BindAddress: host,
|
|
279
|
+
AccessLog: [],
|
|
280
|
+
Logger: WEBrick::Log.new($stdout, WEBrick::Log::WARN)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Mount public directory for static files (Web Dashboard)
|
|
284
|
+
public_dir = File.expand_path('public', __dir__)
|
|
285
|
+
server.mount('/', WEBrick::HTTPServlet::FileHandler, public_dir)
|
|
286
|
+
|
|
287
|
+
# Mount custom servlet to handle all API routes with full HTTP method support
|
|
288
|
+
server.mount('/api', APIServlet, cwd: @cwd)
|
|
289
|
+
|
|
290
|
+
# Handle graceful shutdown
|
|
291
|
+
trap('INT') do
|
|
292
|
+
puts "\n👋 Shutting down API server..."
|
|
293
|
+
server.shutdown
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
server.start
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
data/lib/gsd/api.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'api/server'
|
|
4
|
+
require_relative 'api/client'
|
|
5
|
+
|
|
6
|
+
module Gsd
|
|
7
|
+
# API module - Exposes CLI functionality via HTTP REST API
|
|
8
|
+
module API
|
|
9
|
+
class << self
|
|
10
|
+
# Returns an API client instance
|
|
11
|
+
#
|
|
12
|
+
# @param host [String] API server host (default: '127.0.0.1')
|
|
13
|
+
# @param port [Integer] API server port (default: 3000)
|
|
14
|
+
# @return [Gsd::API::Client]
|
|
15
|
+
def client(host: '127.0.0.1', port: 3000)
|
|
16
|
+
Gsd::API::Client.new(host: host, port: port)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Starts the API server
|
|
20
|
+
#
|
|
21
|
+
# @param host [String] Host to bind to (default: '127.0.0.1')
|
|
22
|
+
# @param port [Integer] Port to listen on (default: 3000)
|
|
23
|
+
# @param cwd [String] Working directory for API operations
|
|
24
|
+
# @return [void]
|
|
25
|
+
def serve(host: '127.0.0.1', port: 3000, cwd: nil)
|
|
26
|
+
cwd ||= Dir.pwd
|
|
27
|
+
server = Gsd::API::Server.new(cwd: cwd)
|
|
28
|
+
server.start(host: host, port: port)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/gsd/cli.rb
CHANGED
|
@@ -7,13 +7,29 @@ require_relative 'phase'
|
|
|
7
7
|
require_relative 'roadmap'
|
|
8
8
|
require_relative 'ai/cli'
|
|
9
9
|
require_relative 'tui/app'
|
|
10
|
+
require_relative 'api'
|
|
10
11
|
|
|
11
12
|
module Gsd
|
|
12
13
|
# CLI parser e dispatcher principal
|
|
13
14
|
class CLI
|
|
14
15
|
def initialize(args = [])
|
|
15
|
-
@args = args
|
|
16
|
-
@command = args.first
|
|
16
|
+
@args = normalize_args(args)
|
|
17
|
+
@command = @args.first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def normalize_args(args)
|
|
21
|
+
normalized = []
|
|
22
|
+
i = 0
|
|
23
|
+
while i < args.length
|
|
24
|
+
if %w[--port --host --body].include?(args[i]) && i + 1 < args.length && !args[i+1].start_with?('--')
|
|
25
|
+
normalized << "#{args[i]}=#{args[i+1]}"
|
|
26
|
+
i += 2
|
|
27
|
+
else
|
|
28
|
+
normalized << args[i]
|
|
29
|
+
i += 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
normalized
|
|
17
33
|
end
|
|
18
34
|
|
|
19
35
|
def run
|
|
@@ -36,6 +52,8 @@ module Gsd
|
|
|
36
52
|
run_phase(@args[1..])
|
|
37
53
|
when 'roadmap'
|
|
38
54
|
run_roadmap(@args[1..])
|
|
55
|
+
when 'api'
|
|
56
|
+
run_api(@args[1..]) || 0
|
|
39
57
|
else
|
|
40
58
|
warn "Unknown command: #{@command}"
|
|
41
59
|
print_help
|
|
@@ -321,6 +339,97 @@ module Gsd
|
|
|
321
339
|
1
|
|
322
340
|
end
|
|
323
341
|
|
|
342
|
+
# ========================================================================
|
|
343
|
+
# API Commands
|
|
344
|
+
# ========================================================================
|
|
345
|
+
|
|
346
|
+
def run_api(args)
|
|
347
|
+
subcommand = args.shift
|
|
348
|
+
|
|
349
|
+
case subcommand
|
|
350
|
+
when 'serve', nil
|
|
351
|
+
cmd_api_serve(args)
|
|
352
|
+
when 'status'
|
|
353
|
+
cmd_api_status(args)
|
|
354
|
+
when 'call'
|
|
355
|
+
cmd_api_call(args)
|
|
356
|
+
else
|
|
357
|
+
warn "Unknown api subcommand: #{subcommand}"
|
|
358
|
+
warn "Available: serve, status, call"
|
|
359
|
+
1
|
|
360
|
+
end
|
|
361
|
+
rescue => e
|
|
362
|
+
warn "Error: #{e.message}"
|
|
363
|
+
1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def cmd_api_serve(args)
|
|
367
|
+
host = '127.0.0.1'
|
|
368
|
+
port = 3000
|
|
369
|
+
|
|
370
|
+
args.each do |arg|
|
|
371
|
+
case arg
|
|
372
|
+
when /^--host=(.+)$/
|
|
373
|
+
host = Regexp.last_match(1)
|
|
374
|
+
when /^--port=(\d+)$/
|
|
375
|
+
port = Regexp.last_match(1).to_i
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
Gsd::API.serve(host: host, port: port, cwd: Dir.pwd)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def cmd_api_status(args)
|
|
383
|
+
host = parse_named_arg(args, 'host') || '127.0.0.1'
|
|
384
|
+
port = (parse_named_arg(args, 'port') || 3000).to_i
|
|
385
|
+
|
|
386
|
+
client = Gsd::API.client(host: host, port: port)
|
|
387
|
+
result = client.status
|
|
388
|
+
|
|
389
|
+
puts "✅ API Server is running on http://#{host}:#{port}"
|
|
390
|
+
if result.is_a?(Hash)
|
|
391
|
+
result.each do |k, v|
|
|
392
|
+
puts " #{k}: #{v}" unless v.is_a?(Hash) || v.is_a?(Array)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
0
|
|
396
|
+
rescue => e
|
|
397
|
+
puts "❌ API Server is not reachable: #{e.message}"
|
|
398
|
+
1
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def cmd_api_call(args)
|
|
402
|
+
method = args.shift
|
|
403
|
+
path = args.shift
|
|
404
|
+
|
|
405
|
+
unless method && path
|
|
406
|
+
output_error("method and path are required. Usage: gsd api call <METHOD> <PATH>")
|
|
407
|
+
return 1
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
host = parse_named_arg(args, 'host') || '127.0.0.1'
|
|
411
|
+
port = (parse_named_arg(args, 'port') || 3000).to_i
|
|
412
|
+
body_str = parse_named_arg(args, 'body')
|
|
413
|
+
|
|
414
|
+
body = nil
|
|
415
|
+
if body_str
|
|
416
|
+
begin
|
|
417
|
+
body = JSON.parse(body_str)
|
|
418
|
+
rescue JSON::ParserError
|
|
419
|
+
body = body_str
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
client = Gsd::API.client(host: host, port: port)
|
|
424
|
+
result = client.call(method, path, body: body)
|
|
425
|
+
|
|
426
|
+
output_json(result)
|
|
427
|
+
0
|
|
428
|
+
rescue => e
|
|
429
|
+
output_error("API call failed: #{e.message}")
|
|
430
|
+
1
|
|
431
|
+
end
|
|
432
|
+
|
|
324
433
|
# ========================================================================
|
|
325
434
|
# Helpers
|
|
326
435
|
# ========================================================================
|
|
@@ -338,7 +447,7 @@ module Gsd
|
|
|
338
447
|
def parse_named_args(args)
|
|
339
448
|
result = {}
|
|
340
449
|
return result if args.nil? || args.empty?
|
|
341
|
-
|
|
450
|
+
|
|
342
451
|
args.each do |arg|
|
|
343
452
|
next unless arg.is_a?(String) && arg.start_with?('--')
|
|
344
453
|
key, value = arg[2..].split('=', 2)
|
|
@@ -390,6 +499,7 @@ module Gsd
|
|
|
390
499
|
hello Print greeting from Go
|
|
391
500
|
ai AI Chat Interface (REPL + one-shot)
|
|
392
501
|
tui Terminal User Interface (TUI)
|
|
502
|
+
api API Server (HTTP REST interface)
|
|
393
503
|
version Print version information
|
|
394
504
|
state State operations
|
|
395
505
|
phase Phase operations
|
|
@@ -427,6 +537,13 @@ module Gsd
|
|
|
427
537
|
get-phase <phase> Extract phase section from ROADMAP.md
|
|
428
538
|
analyze Full roadmap analysis
|
|
429
539
|
|
|
540
|
+
API Subcommands:
|
|
541
|
+
serve Start API HTTP server
|
|
542
|
+
serve --port 3000 Custom port (default: 3000)
|
|
543
|
+
serve --host 0.0.0.0 Bind address (default: 127.0.0.1)
|
|
544
|
+
status Check API server status
|
|
545
|
+
call <METH> <PATH> Call API endpoint (e.g., call GET /api/state)
|
|
546
|
+
|
|
430
547
|
Options:
|
|
431
548
|
--cwd=<path> Working directory (default: current)
|
|
432
549
|
--field=<name> Field name (for state update)
|
|
@@ -442,6 +559,10 @@ module Gsd
|
|
|
442
559
|
gsd phase next-decimal 1
|
|
443
560
|
gsd roadmap get-phase 1
|
|
444
561
|
gsd roadmap analyze
|
|
562
|
+
gsd api serve
|
|
563
|
+
gsd api serve --port 8080
|
|
564
|
+
gsd api status
|
|
565
|
+
gsd api call GET /api/state
|
|
445
566
|
|
|
446
567
|
For more information, see README.md
|
|
447
568
|
HELP
|
data/lib/gsd/tui/app.rb
CHANGED
|
@@ -33,6 +33,9 @@ module Gsd
|
|
|
33
33
|
@output = []
|
|
34
34
|
@frame_count = 0
|
|
35
35
|
@render_count = 0
|
|
36
|
+
|
|
37
|
+
# Initialize status bar with current agent mode
|
|
38
|
+
update_status_mode
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
def run
|
|
@@ -349,6 +352,27 @@ module Gsd
|
|
|
349
352
|
end
|
|
350
353
|
else
|
|
351
354
|
@selected_agent = (@selected_agent + 1) % @agents.length
|
|
355
|
+
update_status_mode
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Map selected agent to mode and update status bar
|
|
360
|
+
def update_status_mode
|
|
361
|
+
mode = agent_to_mode(@agents[@selected_agent])
|
|
362
|
+
@status_bar.update_mode(mode)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Map agent name to mode
|
|
366
|
+
def agent_to_mode(agent)
|
|
367
|
+
case agent
|
|
368
|
+
when 'Code'
|
|
369
|
+
'CODE'
|
|
370
|
+
when 'Kilo Auto Free'
|
|
371
|
+
'PLAN'
|
|
372
|
+
when 'Kilo Gateway'
|
|
373
|
+
'DEBUG'
|
|
374
|
+
else
|
|
375
|
+
'NORMAL'
|
|
352
376
|
end
|
|
353
377
|
end
|
|
354
378
|
|
data/lib/gsd/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fantasy-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fantasy Team
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webrick
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.9'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.9'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: rake
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -129,6 +143,17 @@ files:
|
|
|
129
143
|
- lib/gsd/ai/repl.rb
|
|
130
144
|
- lib/gsd/ai/streaming.rb
|
|
131
145
|
- lib/gsd/ai/ui.rb
|
|
146
|
+
- lib/gsd/api.rb
|
|
147
|
+
- lib/gsd/api/client.rb
|
|
148
|
+
- lib/gsd/api/middleware/cors.rb
|
|
149
|
+
- lib/gsd/api/public/app.js
|
|
150
|
+
- lib/gsd/api/public/index.html
|
|
151
|
+
- lib/gsd/api/public/style.css
|
|
152
|
+
- lib/gsd/api/routes/health.rb
|
|
153
|
+
- lib/gsd/api/routes/phases.rb
|
|
154
|
+
- lib/gsd/api/routes/roadmap.rb
|
|
155
|
+
- lib/gsd/api/routes/state.rb
|
|
156
|
+
- lib/gsd/api/server.rb
|
|
132
157
|
- lib/gsd/buddy.rb
|
|
133
158
|
- lib/gsd/buddy/cli.rb
|
|
134
159
|
- lib/gsd/buddy/gacha.rb
|