tina4ruby 3.11.17 → 3.11.19
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/tina4/background.rb +81 -0
- data/lib/tina4/dev_admin.rb +106 -0
- data/lib/tina4/docs.rb +636 -0
- data/lib/tina4/mcp.rb +15 -0
- data/lib/tina4/public/js/tina4-dev-admin.js +260 -111
- data/lib/tina4/public/js/tina4-dev-admin.min.js +260 -111
- data/lib/tina4/rack_app.rb +46 -1
- data/lib/tina4/response.rb +3 -0
- data/lib/tina4/shutdown.rb +10 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +12 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 913dfbf6820b99da3ff8f0b5a845fb23239736ccc1db30eb958d664fba6abe9f
|
|
4
|
+
data.tar.gz: c7c39ce01253b8dc60edc30550b004914ac9c4742ee2efabfdedab44dfe984f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c926a2b39b248a45112e3f323174049ccdb0955c07ce3491a9346c837776cca37707ce54ecca21320ad71599b8d537a78eedadf4e675cd8c2b71dcfa6415ca39
|
|
7
|
+
data.tar.gz: caf6b606d28154e7ed8ded6e2519cc4a45f10a9034644f15f82625eace4e2747dbc27d845dadf0f0881a1c180c38a465428976a7be6d7b1c52b4239f637fd793
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
# Periodic background task registry.
|
|
5
|
+
#
|
|
6
|
+
# Matches Python's `tina4_python.core.server.background(fn, interval)` and
|
|
7
|
+
# PHP's `$app->background($callback, $interval)` — a callback that runs
|
|
8
|
+
# periodically alongside the server lifecycle.
|
|
9
|
+
#
|
|
10
|
+
# Ruby has no asyncio event loop, so each task runs in its own thread.
|
|
11
|
+
# The GIL keeps it cooperative-enough for the periodic work this is meant
|
|
12
|
+
# for (queue draining, health checks, simulators). Errors in the callback
|
|
13
|
+
# are caught and logged so they don't kill the thread.
|
|
14
|
+
module Background
|
|
15
|
+
class << self
|
|
16
|
+
# Register a periodic callback.
|
|
17
|
+
#
|
|
18
|
+
# @param callback [#call, nil] Object responding to `call` with no args.
|
|
19
|
+
# @param interval [Float] Seconds between invocations (default 1.0).
|
|
20
|
+
# @param block [Proc] Optional block (used if callback is nil).
|
|
21
|
+
# @return [Hash] The registered task descriptor.
|
|
22
|
+
def register(callback = nil, interval: 1.0, &block)
|
|
23
|
+
cb = callback || block
|
|
24
|
+
raise ArgumentError, "background requires a callback or block" if cb.nil?
|
|
25
|
+
raise ArgumentError, "callback must respond to :call" unless cb.respond_to?(:call)
|
|
26
|
+
|
|
27
|
+
task = { callback: cb, interval: interval.to_f, thread: nil, running: false }
|
|
28
|
+
mutex.synchronize { tasks << task }
|
|
29
|
+
start_task(task)
|
|
30
|
+
task
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# All registered task descriptors. Tests use this for introspection.
|
|
34
|
+
def tasks
|
|
35
|
+
@tasks ||= []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Stop and join every running task. Called on graceful shutdown.
|
|
39
|
+
def stop_all(timeout: 2.0)
|
|
40
|
+
snapshot = mutex.synchronize { tasks.dup }
|
|
41
|
+
snapshot.each { |task| stop_task(task, timeout: timeout) }
|
|
42
|
+
mutex.synchronize { tasks.clear }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Stop a single task. Used by tests that register, fire, then stop.
|
|
46
|
+
def stop_task(task, timeout: 2.0)
|
|
47
|
+
task[:running] = false
|
|
48
|
+
thread = task[:thread]
|
|
49
|
+
return unless thread
|
|
50
|
+
|
|
51
|
+
thread.join(timeout) || thread.kill
|
|
52
|
+
task[:thread] = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def mutex
|
|
58
|
+
@mutex ||= Mutex.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def start_task(task)
|
|
62
|
+
task[:running] = true
|
|
63
|
+
task[:thread] = Thread.new do
|
|
64
|
+
while task[:running]
|
|
65
|
+
sleep task[:interval]
|
|
66
|
+
break unless task[:running]
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
task[:callback].call
|
|
70
|
+
rescue => e
|
|
71
|
+
# Never let a callback error kill the thread — next interval still fires.
|
|
72
|
+
if defined?(Tina4::Log) && Tina4::Log.respond_to?(:error)
|
|
73
|
+
Tina4::Log.error("background task error: #{e.class}: #{e.message}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -316,9 +316,55 @@ module Tina4
|
|
|
316
316
|
Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
|
|
317
317
|
end
|
|
318
318
|
|
|
319
|
+
# Write `.tina4/mcp.json` so MCP-aware tools (Claude Code, Cursor) can
|
|
320
|
+
# auto-discover this project's live docs server. Idempotent — no-op if
|
|
321
|
+
# the file already matches the desired contents. Also appends `.tina4/`
|
|
322
|
+
# to the project's `.gitignore` if a git repo is present.
|
|
323
|
+
def auto_discover_mcp!(project_root: Dir.pwd)
|
|
324
|
+
return if @_mcp_auto_discovered
|
|
325
|
+
@_mcp_auto_discovered = true
|
|
326
|
+
return unless enabled?
|
|
327
|
+
|
|
328
|
+
port = ENV["TINA4_PORT"] || ENV["PORT"] || "7147"
|
|
329
|
+
url = "http://localhost:#{port}/__dev/api/mcp"
|
|
330
|
+
target_dir = File.join(project_root, ".tina4")
|
|
331
|
+
target = File.join(target_dir, "mcp.json")
|
|
332
|
+
payload = {
|
|
333
|
+
"mcpServers" => {
|
|
334
|
+
"tina4-live-docs" => {
|
|
335
|
+
"url" => url,
|
|
336
|
+
"description" => "Live API docs for this Tina4 project (framework + user code)",
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
begin
|
|
341
|
+
FileUtils.mkdir_p(target_dir)
|
|
342
|
+
existing = if File.file?(target)
|
|
343
|
+
(JSON.parse(File.read(target)) rescue {})
|
|
344
|
+
else
|
|
345
|
+
{}
|
|
346
|
+
end
|
|
347
|
+
if existing != payload
|
|
348
|
+
File.write(target, JSON.pretty_generate(payload))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
gitignore = File.join(project_root, ".gitignore")
|
|
352
|
+
if File.directory?(File.join(project_root, ".git"))
|
|
353
|
+
current = File.file?(gitignore) ? File.read(gitignore) : ""
|
|
354
|
+
unless current.lines.map(&:strip).include?(".tina4/") || current.lines.map(&:strip).include?(".tina4")
|
|
355
|
+
prefix = (!current.empty? && !current.end_with?("\n")) ? "\n" : ""
|
|
356
|
+
File.write(gitignore, "#{current}#{prefix}.tina4/\n")
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
rescue StandardError => e
|
|
360
|
+
Tina4::Log.warning("auto_discover_mcp! failed: #{e.message}") if defined?(Tina4::Log)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
319
364
|
# Handle a /__dev request; returns [status, headers, body] or nil if not a dev path
|
|
320
365
|
def handle_request(env)
|
|
321
366
|
return nil unless enabled?
|
|
367
|
+
auto_discover_mcp!
|
|
322
368
|
|
|
323
369
|
path = env["PATH_INFO"] || "/"
|
|
324
370
|
method = env["REQUEST_METHOD"]
|
|
@@ -548,6 +594,16 @@ module Tina4
|
|
|
548
594
|
when ["POST", "/__dev/api/scaffold/run"]
|
|
549
595
|
body = read_json_body(env) || {}
|
|
550
596
|
json_response(scaffold_run(body))
|
|
597
|
+
when ["GET", "/__dev/api/docs/search"]
|
|
598
|
+
json_response(docs_search_payload(env))
|
|
599
|
+
when ["GET", "/__dev/api/docs/class"]
|
|
600
|
+
json_response(docs_class_payload(query_param(env, "name")))
|
|
601
|
+
when ["GET", "/__dev/api/docs/method"]
|
|
602
|
+
json_response(docs_method_payload(query_param(env, "class"), query_param(env, "name")))
|
|
603
|
+
when ["GET", "/__dev/api/docs/index"]
|
|
604
|
+
json_response(docs_index_payload(query_param(env, "source")))
|
|
605
|
+
when ["GET", "/__dev/api/docs/.well-known.json"]
|
|
606
|
+
json_response(docs_well_known_payload)
|
|
551
607
|
when ["GET", "/__dev/api/graphql/schema"]
|
|
552
608
|
begin
|
|
553
609
|
gql = Tina4::GraphQL.new
|
|
@@ -1286,6 +1342,56 @@ module Tina4
|
|
|
1286
1342
|
{ ok: false, error: "unknown kind: #{kind}" }
|
|
1287
1343
|
end
|
|
1288
1344
|
end
|
|
1345
|
+
|
|
1346
|
+
# ── Live Docs (Live API RAG) ─────────────────────────────────
|
|
1347
|
+
|
|
1348
|
+
def docs_search_payload(env)
|
|
1349
|
+
q = (query_param(env, "q") || "").to_s
|
|
1350
|
+
k = (query_param(env, "k") || "5").to_i
|
|
1351
|
+
source = (query_param(env, "source") || "all").to_s
|
|
1352
|
+
include_private = %w[1 true yes].include?((query_param(env, "include_private") || "").to_s.downcase)
|
|
1353
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1354
|
+
results = Tina4::Docs.cached(Dir.pwd).search(
|
|
1355
|
+
q, k: k, source: source, include_private: include_private
|
|
1356
|
+
)
|
|
1357
|
+
took_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0).round(2)
|
|
1358
|
+
{ ok: true, query: q, results: results, took_ms: took_ms }
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
def docs_class_payload(name)
|
|
1362
|
+
spec = Tina4::Docs.cached(Dir.pwd).class_spec(name.to_s)
|
|
1363
|
+
return { ok: false, error: "class not found: #{name}" } if spec.nil?
|
|
1364
|
+
{ ok: true, class: spec }
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
def docs_method_payload(class_fqn, name)
|
|
1368
|
+
spec = Tina4::Docs.cached(Dir.pwd).method_spec(class_fqn.to_s, name.to_s)
|
|
1369
|
+
return { ok: false, error: "method not found: #{class_fqn}##{name}" } if spec.nil?
|
|
1370
|
+
{ ok: true, method: spec }
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def docs_index_payload(source)
|
|
1374
|
+
idx = Tina4::Docs.cached(Dir.pwd).index
|
|
1375
|
+
idx = idx.select { |e| e[:source] == source } if source && %w[framework user vendor].include?(source.to_s)
|
|
1376
|
+
{ ok: true, count: idx.size, entries: idx }
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
def docs_well_known_payload
|
|
1380
|
+
{
|
|
1381
|
+
ok: true,
|
|
1382
|
+
service: "tina4-live-docs",
|
|
1383
|
+
version: Tina4::VERSION,
|
|
1384
|
+
framework: "tina4-ruby",
|
|
1385
|
+
endpoints: {
|
|
1386
|
+
search: "/__dev/api/docs/search?q=<query>&k=<int>&source=<framework|user|all>&include_private=<bool>",
|
|
1387
|
+
class: "/__dev/api/docs/class?name=<fqn>",
|
|
1388
|
+
method: "/__dev/api/docs/method?class=<fqn>&name=<method>",
|
|
1389
|
+
index: "/__dev/api/docs/index?source=<framework|user|all>",
|
|
1390
|
+
},
|
|
1391
|
+
mcp: { url: "/__dev/api/mcp", tools: %w[api_search api_class api_method] },
|
|
1392
|
+
spec: "https://tina4.com — Live API RAG plan/v3/22-LIVE-API-RAG.md",
|
|
1393
|
+
}
|
|
1394
|
+
end
|
|
1289
1395
|
end
|
|
1290
1396
|
end
|
|
1291
1397
|
end
|