tina4ruby 3.13.146 → 3.13.148
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/CHANGELOG.md +17 -0
- data/lib/tina4/dispatch_pipeline.rb +14 -2
- data/lib/tina4/docs.rb +47 -2
- data/lib/tina4/mcp.rb +63 -15
- data/lib/tina4/session.rb +12 -1
- data/lib/tina4/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f81aa317ab408c5f85fa182149d2bddf74f76a6dbe0d07eab1346335f2feb18
|
|
4
|
+
data.tar.gz: a877a9d71c42570d719c721427d164581919c15e235e17d366891a5337a7920c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4da03de83c10544ac690ce24d6b951b6ba688fab57fb01cb4cf5a630d1e1d41dd5fbab7397538a2910708409c3fb6fea74285ce84fd74559fb883d83303b66d6
|
|
7
|
+
data.tar.gz: cb21b614d729b774a1d9ec16ada8674cab6663e675a370ed7cb8b3f588b3c038036ba0d43bedcf764f1ab673b117df005e30063d50612ce8ace479300104d127
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ number means the same thing everywhere.
|
|
|
6
6
|
**The authoritative release notes for every shipped version live in the documentation:**
|
|
7
7
|
https://tina4.com/ruby/36-releases
|
|
8
8
|
|
|
9
|
+
## 3.13.148 — 2026-10-05
|
|
10
|
+
|
|
11
|
+
### Session
|
|
12
|
+
- An anonymous request stores no session — a request that never reads or writes session state no longer emits a `Set-Cookie` or persists an empty session record (cross-framework fix, at parity across Python, PHP, Ruby, Node.js).
|
|
13
|
+
|
|
14
|
+
### AI skills
|
|
15
|
+
- `tina4-developer-ruby` skill `updated_for_version` bumped to this release.
|
|
16
|
+
|
|
17
|
+
## 3.13.147 — 2026-10-05
|
|
18
|
+
|
|
19
|
+
### Dev MCP tools (fixes #271)
|
|
20
|
+
- `database_columns` reads schema metadata so an empty table returns its columns; a missing table returns a clear `table not found` error.
|
|
21
|
+
- Tool argument validation returns an actionable `missing/unknown argument` error instead of a raw Ruby error; `api_method` now populates `params` and `return`.
|
|
22
|
+
- `route_list` entries include each route's `middleware`.
|
|
23
|
+
- A new route file added at runtime surfaces a signal.
|
|
24
|
+
- Developer skill corrected: per-language hot-reload reality, and `api_method` shown with argument names.
|
|
25
|
+
|
|
9
26
|
## 3.13.146 — 2026-10-04
|
|
10
27
|
|
|
11
28
|
### AI skills
|
|
@@ -649,12 +649,11 @@ module Tina4
|
|
|
649
649
|
# renamed through TINA4_SESSION_NAME, so the auto-Set-Cookie would be
|
|
650
650
|
# needlessly re-emitted on every request that already carries the renamed
|
|
651
651
|
# cookie. Parity with Python's core/server._init_session cookie_prefix.
|
|
652
|
-
sid = sess.id
|
|
653
652
|
cookie_prefix = "#{Tina4::Session.cookie_name}="
|
|
654
653
|
cookie_val = (ctx.env["HTTP_COOKIE"] || "").split(";").map(&:strip)
|
|
655
654
|
.find { |part| part.start_with?(cookie_prefix) }
|
|
656
655
|
&.slice(cookie_prefix.length..)
|
|
657
|
-
if
|
|
656
|
+
if session_needs_cookie?(sess, cookie_val)
|
|
658
657
|
# Route through Session#cookie_header rather than hand-writing the
|
|
659
658
|
# header, so TINA4_SESSION_SECURE / _SAMESITE / _HTTPONLY / _NAME / _TTL
|
|
660
659
|
# are all honoured and Secure reflects the request scheme. The old
|
|
@@ -666,6 +665,19 @@ module Tina4
|
|
|
666
665
|
[status, headers, body_parts]
|
|
667
666
|
end
|
|
668
667
|
|
|
668
|
+
# Whether the response must carry a session cookie: the session has an id the
|
|
669
|
+
# request did not already send, and it is not a fresh one. A fresh session
|
|
670
|
+
# (minted for this request and never written: a route that only read it, or
|
|
671
|
+
# the replacement for a cookie the store does not know) was not stored, so a
|
|
672
|
+
# cookie for it would name nothing - the next request could not resume it and
|
|
673
|
+
# would be handed another one.
|
|
674
|
+
def session_needs_cookie?(sess, cookie_val)
|
|
675
|
+
sid = sess.id
|
|
676
|
+
return false unless sid && sid != cookie_val
|
|
677
|
+
|
|
678
|
+
!(sess.respond_to?(:fresh?) && sess.fresh?)
|
|
679
|
+
end
|
|
680
|
+
|
|
669
681
|
# Milliseconds since the request entered the pipeline.
|
|
670
682
|
def elapsed_ms(ctx)
|
|
671
683
|
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx.started_at) * 1000).round(3)
|
data/lib/tina4/docs.rb
CHANGED
|
@@ -432,6 +432,7 @@ module Tina4
|
|
|
432
432
|
private_flag = visibility != "public" || underscored
|
|
433
433
|
|
|
434
434
|
summary, doc = split_doc(pending_doc)
|
|
435
|
+
returns = pending_doc.join(" ")[/@return\s+\[([^\]]+)\]/, 1].to_s
|
|
435
436
|
method_fqn = if is_static
|
|
436
437
|
"#{current_class[:fqn]}.#{mname}"
|
|
437
438
|
else
|
|
@@ -446,6 +447,7 @@ module Tina4
|
|
|
446
447
|
class_fqn: current_class[:fqn],
|
|
447
448
|
class: current_class[:fqn],
|
|
448
449
|
signature: signature,
|
|
450
|
+
returns: returns,
|
|
449
451
|
summary: summary,
|
|
450
452
|
docstring: doc,
|
|
451
453
|
file: rel_file,
|
|
@@ -620,11 +622,54 @@ module Tina4
|
|
|
620
622
|
static: entry[:static] || false,
|
|
621
623
|
source: entry[:source],
|
|
622
624
|
version: entry[:version],
|
|
623
|
-
params: [],
|
|
624
|
-
return:
|
|
625
|
+
params: params_from_signature(entry[:signature]),
|
|
626
|
+
return: entry[:returns].to_s,
|
|
625
627
|
}
|
|
626
628
|
end
|
|
627
629
|
|
|
630
|
+
# Structured params parsed from a Ruby signature such as
|
|
631
|
+
# "fetch(sql, params = [], limit: 100, **opts)" -- same shape Python
|
|
632
|
+
# reports: [{name, type, required, default}]. Ruby has no inline type
|
|
633
|
+
# annotations, so type is "" unless the docblock says otherwise.
|
|
634
|
+
def params_from_signature(signature)
|
|
635
|
+
inner = signature.to_s[/\A[^(]*\((.*)\)\z/m, 1]
|
|
636
|
+
return [] if inner.nil? || inner.strip.empty?
|
|
637
|
+
|
|
638
|
+
split_top_level_commas(inner).map { |piece| param_entry(piece.strip) }
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def param_entry(piece)
|
|
642
|
+
prefix = piece[/\A(\*\*|\*|&)/, 1]
|
|
643
|
+
return { name: piece, type: "", required: false, default: nil } if prefix
|
|
644
|
+
|
|
645
|
+
keyword = piece.match(/\A([A-Za-z_]\w*):\s*(.*)\z/m)
|
|
646
|
+
if keyword
|
|
647
|
+
default = keyword[2].strip
|
|
648
|
+
return { name: keyword[1], type: "", required: default.empty?, default: default.empty? ? nil : default }
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
name, default = piece.split(/\s*=\s*/, 2)
|
|
652
|
+
{ name: name.strip, type: "", required: default.nil?, default: default&.strip }
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def split_top_level_commas(text)
|
|
656
|
+
pieces = []
|
|
657
|
+
depth = 0
|
|
658
|
+
current = +""
|
|
659
|
+
text.each_char do |ch|
|
|
660
|
+
depth += 1 if "([{".include?(ch)
|
|
661
|
+
depth -= 1 if ")]}".include?(ch)
|
|
662
|
+
if ch == "," && depth.zero?
|
|
663
|
+
pieces << current
|
|
664
|
+
current = +""
|
|
665
|
+
else
|
|
666
|
+
current << ch
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
pieces << current unless current.strip.empty?
|
|
670
|
+
pieces
|
|
671
|
+
end
|
|
672
|
+
|
|
628
673
|
# ── Sync writer ──────────────────────────────────────────────────
|
|
629
674
|
|
|
630
675
|
def render_generated_block
|
data/lib/tina4/mcp.rb
CHANGED
|
@@ -139,6 +139,17 @@ module Tina4
|
|
|
139
139
|
schema
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
# Readable name for one attached route middleware (class, instance, string
|
|
143
|
+
# spec or lambda) -- what route_list reports under "middleware".
|
|
144
|
+
def self.middleware_label(attached)
|
|
145
|
+
case attached
|
|
146
|
+
when ::String then attached
|
|
147
|
+
when ::Module then attached.name || attached.inspect
|
|
148
|
+
when ::Proc, ::Method then "proc"
|
|
149
|
+
else attached.class.name || attached.inspect
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
142
153
|
# Informational only — whether the CONFIGURED host looks local.
|
|
143
154
|
#
|
|
144
155
|
# NOT the security gate. This reads TINA4_HOST_NAME (the configured bind
|
|
@@ -510,22 +521,47 @@ module Tina4
|
|
|
510
521
|
raise ArgumentError, "Unknown tool: #{tool_name}" if tool.nil?
|
|
511
522
|
|
|
512
523
|
arguments = params.fetch("arguments", {})
|
|
524
|
+
arguments = {} if arguments.nil?
|
|
513
525
|
handler = tool["handler"]
|
|
514
526
|
|
|
527
|
+
# Validate against the tool's input schema BEFORE invoking, so a missing
|
|
528
|
+
# or unknown argument is an actionable message, never a raw Ruby
|
|
529
|
+
# ArgumentError. Python parity (tina4_python.mcp).
|
|
530
|
+
argument_error = validate_tool_arguments(tool_name, tool["inputSchema"], arguments)
|
|
531
|
+
return mcp_text_content(argument_error) if argument_error
|
|
532
|
+
|
|
515
533
|
# Call the handler -- support both keyword and positional args
|
|
516
534
|
result = _invoke_handler(handler, arguments)
|
|
517
535
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
536
|
+
mcp_text_content(result)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Format a tool result as MCP content.
|
|
540
|
+
def mcp_text_content(result)
|
|
541
|
+
text = case result
|
|
542
|
+
when String then result
|
|
543
|
+
when Hash, Array then JSON.pretty_generate(result)
|
|
544
|
+
else result.to_s
|
|
545
|
+
end
|
|
546
|
+
{ "content" => [{ "type" => "text", "text" => text }] }
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Returns nil when the arguments satisfy the schema, else an
|
|
550
|
+
# { "error" => "missing required argument 'p' (tool takes p1, p2)" } hash
|
|
551
|
+
# (or "unknown argument ..." with the same shape).
|
|
552
|
+
def validate_tool_arguments(tool_name, schema, arguments)
|
|
553
|
+
return { "error" => "arguments for #{tool_name} must be an object" } unless arguments.is_a?(Hash)
|
|
527
554
|
|
|
528
|
-
|
|
555
|
+
properties = (schema || {}).fetch("properties", {}).keys
|
|
556
|
+
takes = "(#{tool_name} takes #{properties.join(', ')})"
|
|
557
|
+
|
|
558
|
+
missing = (schema || {}).fetch("required", []).find { |name| !arguments.key?(name) }
|
|
559
|
+
return { "error" => "missing required argument '#{missing}' #{takes}" } if missing
|
|
560
|
+
|
|
561
|
+
unknown = arguments.keys.map(&:to_s).find { |name| !properties.include?(name) }
|
|
562
|
+
return { "error" => "unknown argument '#{unknown}' #{takes}" } if unknown
|
|
563
|
+
|
|
564
|
+
nil
|
|
529
565
|
end
|
|
530
566
|
|
|
531
567
|
def _handle_resources_list(_params)
|
|
@@ -888,7 +924,13 @@ module Tina4
|
|
|
888
924
|
unless table.to_s =~ /\A[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)?\z/
|
|
889
925
|
return { "error" => "Invalid table name" }
|
|
890
926
|
end
|
|
891
|
-
|
|
927
|
+
# Schema metadata (PRAGMA / information_schema), never the first row, so
|
|
928
|
+
# an EMPTY table still reports its columns. A table that does not exist
|
|
929
|
+
# has no columns: say so rather than answering [].
|
|
930
|
+
columns = db.columns(table)
|
|
931
|
+
next { "error" => "table not found: #{table}" } if columns.nil? || columns.empty?
|
|
932
|
+
|
|
933
|
+
columns
|
|
892
934
|
}, "Get column definitions for a table")
|
|
893
935
|
|
|
894
936
|
# ── Route Tools ───────────────────────────────────
|
|
@@ -899,7 +941,8 @@ module Tina4
|
|
|
899
941
|
{
|
|
900
942
|
"method" => route.method.to_s,
|
|
901
943
|
"path" => route.path.to_s,
|
|
902
|
-
"auth_required" => route.auth_required ? true : false
|
|
944
|
+
"auth_required" => route.auth_required ? true : false,
|
|
945
|
+
"middleware" => route.middleware.map { |attached| Tina4.middleware_label(attached) }
|
|
903
946
|
}
|
|
904
947
|
end
|
|
905
948
|
}, "List all registered routes")
|
|
@@ -1458,9 +1501,14 @@ module Tina4
|
|
|
1458
1501
|
Tina4::Docs.cached(project_root).class_spec(name.to_s)
|
|
1459
1502
|
}, "Full class reflection (methods, file, line) from the live API index")
|
|
1460
1503
|
|
|
1461
|
-
server.register_tool("api_method", lambda { |
|
|
1462
|
-
|
|
1463
|
-
|
|
1504
|
+
server.register_tool("api_method", lambda { |name:, **rest|
|
|
1505
|
+
class_name = rest[:class].to_s
|
|
1506
|
+
Tina4::Docs.cached(project_root).method_spec(class_name, name.to_s) ||
|
|
1507
|
+
{ "error" => "method not found: #{class_name}.#{name}" }
|
|
1508
|
+
}, "Single method spec (signature, params, return, file, line) from the live API index",
|
|
1509
|
+
{ "type" => "object",
|
|
1510
|
+
"properties" => { "class" => { "type" => "string" }, "name" => { "type" => "string" } },
|
|
1511
|
+
"required" => %w[class name] })
|
|
1464
1512
|
|
|
1465
1513
|
# ── Code/doc grounding (semantic FTS over this repo's source) ──
|
|
1466
1514
|
# code_search is the fuzzy DUAL of the structural api_* tools:
|
data/lib/tina4/session.rb
CHANGED
|
@@ -217,7 +217,7 @@ module Tina4
|
|
|
217
217
|
# false) therefore still no-ops, exactly as before; a modified session of
|
|
218
218
|
# any kind still writes.
|
|
219
219
|
def save
|
|
220
|
-
return true
|
|
220
|
+
return true if @id.nil? || fresh?
|
|
221
221
|
|
|
222
222
|
record = @data
|
|
223
223
|
if @stored
|
|
@@ -247,6 +247,17 @@ module Tina4
|
|
|
247
247
|
end
|
|
248
248
|
end
|
|
249
249
|
|
|
250
|
+
# Whether this session is only an id: nothing has stored it and it holds no
|
|
251
|
+
# data. Such a session has no record and needs no cookie, so #save writes
|
|
252
|
+
# nothing for it and the dispatcher sends no cookie for it. It stops being
|
|
253
|
+
# fresh when something is stored in it. Calls that mark a session changed
|
|
254
|
+
# without leaving anything in it (deleting a key it never had, #clear,
|
|
255
|
+
# #regenerate on an empty session) leave it fresh: a record with no data is
|
|
256
|
+
# not a session.
|
|
257
|
+
def fresh?
|
|
258
|
+
!@stored && @data.empty?
|
|
259
|
+
end
|
|
260
|
+
|
|
250
261
|
# Destroy the current session. ENDS the session: the stored record is
|
|
251
262
|
# removed and the id is CLEARED (@id = nil), so a later set()+save() with no
|
|
252
263
|
# new #start has no id to persist under and cannot RESURRECT the just-
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.148
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|