tsikol 0.1.0

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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CONTRIBUTING.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +579 -0
  6. data/Rakefile +12 -0
  7. data/docs/README.md +69 -0
  8. data/docs/api/middleware.md +721 -0
  9. data/docs/api/prompt.md +858 -0
  10. data/docs/api/resource.md +651 -0
  11. data/docs/api/server.md +509 -0
  12. data/docs/api/test-helpers.md +591 -0
  13. data/docs/api/tool.md +527 -0
  14. data/docs/cookbook/authentication.md +651 -0
  15. data/docs/cookbook/caching.md +877 -0
  16. data/docs/cookbook/dynamic-tools.md +970 -0
  17. data/docs/cookbook/error-handling.md +887 -0
  18. data/docs/cookbook/logging.md +1044 -0
  19. data/docs/cookbook/rate-limiting.md +717 -0
  20. data/docs/examples/code-assistant.md +922 -0
  21. data/docs/examples/complete-server.md +726 -0
  22. data/docs/examples/database-manager.md +1198 -0
  23. data/docs/examples/devops-tools.md +1382 -0
  24. data/docs/examples/echo-server.md +501 -0
  25. data/docs/examples/weather-service.md +822 -0
  26. data/docs/guides/completion.md +472 -0
  27. data/docs/guides/getting-started.md +462 -0
  28. data/docs/guides/middleware.md +823 -0
  29. data/docs/guides/project-structure.md +434 -0
  30. data/docs/guides/prompts.md +920 -0
  31. data/docs/guides/resources.md +720 -0
  32. data/docs/guides/sampling.md +804 -0
  33. data/docs/guides/testing.md +863 -0
  34. data/docs/guides/tools.md +627 -0
  35. data/examples/README.md +92 -0
  36. data/examples/advanced_features.rb +129 -0
  37. data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
  38. data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
  39. data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
  40. data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
  41. data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
  42. data/examples/basic-migrated/server.rb +25 -0
  43. data/examples/basic.rb +73 -0
  44. data/examples/full_featured.rb +175 -0
  45. data/examples/middleware_example.rb +112 -0
  46. data/examples/sampling_example.rb +104 -0
  47. data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
  48. data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
  49. data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
  50. data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
  51. data/examples/weather-service/server.rb +28 -0
  52. data/exe/tsikol +6 -0
  53. data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
  54. data/lib/tsikol/cli/templates/README.md.erb +38 -0
  55. data/lib/tsikol/cli/templates/gitignore.erb +49 -0
  56. data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
  57. data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
  58. data/lib/tsikol/cli/templates/server.rb.erb +24 -0
  59. data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
  60. data/lib/tsikol/cli.rb +203 -0
  61. data/lib/tsikol/error_handler.rb +141 -0
  62. data/lib/tsikol/health.rb +198 -0
  63. data/lib/tsikol/http_transport.rb +72 -0
  64. data/lib/tsikol/lifecycle.rb +149 -0
  65. data/lib/tsikol/middleware.rb +168 -0
  66. data/lib/tsikol/prompt.rb +101 -0
  67. data/lib/tsikol/resource.rb +53 -0
  68. data/lib/tsikol/router.rb +190 -0
  69. data/lib/tsikol/server.rb +660 -0
  70. data/lib/tsikol/stdio_transport.rb +108 -0
  71. data/lib/tsikol/test_helpers.rb +261 -0
  72. data/lib/tsikol/tool.rb +111 -0
  73. data/lib/tsikol/version.rb +5 -0
  74. data/lib/tsikol.rb +72 -0
  75. metadata +219 -0
data/docs/api/tool.md ADDED
@@ -0,0 +1,527 @@
1
+ # Tool API Reference
2
+
3
+ The `Tsikol::Tool` class provides the foundation for creating tools that AI assistants can call.
4
+
5
+ ## Class: Tsikol::Tool
6
+
7
+ ### Class Methods
8
+
9
+ #### `.description(text = nil)`
10
+
11
+ Set or get the tool description.
12
+
13
+ ```ruby
14
+ class MyTool < Tsikol::Tool
15
+ description "Performs useful operations"
16
+ end
17
+ ```
18
+
19
+ #### `.parameter(name, &block)`
20
+
21
+ Define a tool parameter with validation and completion.
22
+
23
+ ```ruby
24
+ class FileTool < Tsikol::Tool
25
+ parameter :path do
26
+ type :string
27
+ required
28
+ description "File path"
29
+
30
+ complete do |partial|
31
+ Dir.glob("#{partial}*")
32
+ end
33
+ end
34
+
35
+ parameter :options do
36
+ type :object
37
+ optional
38
+ default {}
39
+ description "Additional options"
40
+ end
41
+ end
42
+ ```
43
+
44
+ ### Parameter DSL
45
+
46
+ Parameters are defined using a DSL within the parameter block:
47
+
48
+ #### `type(symbol)`
49
+
50
+ Set the parameter type.
51
+
52
+ ```ruby
53
+ parameter :count do
54
+ type :integer # :string, :number, :integer, :boolean, :array, :object
55
+ end
56
+ ```
57
+
58
+ #### `required` / `optional`
59
+
60
+ Specify if parameter is required.
61
+
62
+ ```ruby
63
+ parameter :name do
64
+ type :string
65
+ required # or optional
66
+ end
67
+ ```
68
+
69
+ #### `default(value)`
70
+
71
+ Set default value for optional parameters.
72
+
73
+ ```ruby
74
+ parameter :format do
75
+ type :string
76
+ optional
77
+ default "json"
78
+ end
79
+ ```
80
+
81
+ #### `description(text)`
82
+
83
+ Describe the parameter's purpose.
84
+
85
+ ```ruby
86
+ parameter :query do
87
+ type :string
88
+ required
89
+ description "Search query string"
90
+ end
91
+ ```
92
+
93
+ #### `enum(values)`
94
+
95
+ Restrict parameter to specific values.
96
+
97
+ ```ruby
98
+ parameter :operation do
99
+ type :string
100
+ required
101
+ enum ["read", "write", "delete"]
102
+ description "File operation"
103
+ end
104
+ ```
105
+
106
+ #### `complete(&block)`
107
+
108
+ Define completion logic.
109
+
110
+ ```ruby
111
+ parameter :language do
112
+ type :string
113
+ required
114
+
115
+ complete do |partial|
116
+ languages = ["ruby", "python", "javascript", "go"]
117
+ languages.select { |lang| lang.start_with?(partial) }
118
+ end
119
+ end
120
+ ```
121
+
122
+ ### Instance Methods
123
+
124
+ #### `#execute(**params)`
125
+
126
+ Main method to implement tool logic. Override in subclasses.
127
+
128
+ ```ruby
129
+ class Calculator < Tsikol::Tool
130
+ parameter :a do
131
+ type :number
132
+ required
133
+ end
134
+
135
+ parameter :b do
136
+ type :number
137
+ required
138
+ end
139
+
140
+ parameter :operation do
141
+ type :string
142
+ required
143
+ enum ["add", "subtract", "multiply", "divide"]
144
+ end
145
+
146
+ def execute(a:, b:, operation:)
147
+ case operation
148
+ when "add" then a + b
149
+ when "subtract" then a - b
150
+ when "multiply" then a * b
151
+ when "divide"
152
+ raise Tsikol::ValidationError, "Division by zero" if b == 0
153
+ a.to_f / b
154
+ end
155
+ end
156
+ end
157
+ ```
158
+
159
+ #### `#set_server(server)`
160
+
161
+ Called when tool is registered with a server. Override to access server features.
162
+
163
+ ```ruby
164
+ def set_server(server)
165
+ @server = server
166
+
167
+ # Enable logging if server supports it
168
+ if @server.logging_enabled?
169
+ define_singleton_method(:log) do |level, message, data: nil|
170
+ @server.log(level, message, data: data)
171
+ end
172
+ end
173
+ end
174
+ ```
175
+
176
+ #### `#log(level, message, data: nil)`
177
+
178
+ Log messages (only available after server registration with logging enabled).
179
+
180
+ ```ruby
181
+ def execute(input:)
182
+ log :info, "Processing started", input_size: input.size
183
+
184
+ result = process(input)
185
+
186
+ log :info, "Processing complete", result_size: result.size
187
+
188
+ result
189
+ end
190
+ ```
191
+
192
+ ### Tool Registration
193
+
194
+ Tools can be registered in several ways:
195
+
196
+ #### Class Registration
197
+
198
+ ```ruby
199
+ server.register_tool_class(MyTool)
200
+ # or
201
+ server.tool MyTool
202
+ ```
203
+
204
+ #### Instance Registration
205
+
206
+ ```ruby
207
+ tool = MyTool.new
208
+ server.register_tool_instance(tool)
209
+ ```
210
+
211
+ #### Inline Registration
212
+
213
+ ```ruby
214
+ server.register_tool("simple_tool") do |param:|
215
+ "Result: #{param}"
216
+ end
217
+ # or
218
+ server.tool "simple_tool" do |param:|
219
+ "Result: #{param}"
220
+ end
221
+ ```
222
+
223
+ ### Parameter Validation
224
+
225
+ Parameters are automatically validated:
226
+
227
+ ```ruby
228
+ # Type validation
229
+ parameter :age do
230
+ type :integer
231
+ required
232
+ end
233
+ # Validates that age is an integer
234
+
235
+ # Enum validation
236
+ parameter :color do
237
+ type :string
238
+ enum ["red", "green", "blue"]
239
+ end
240
+ # Validates color is one of the allowed values
241
+
242
+ # Custom validation in execute
243
+ def execute(email:)
244
+ unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
245
+ raise Tsikol::ValidationError, "Invalid email format"
246
+ end
247
+
248
+ process_email(email)
249
+ end
250
+ ```
251
+
252
+ ### Error Handling
253
+
254
+ Tools should handle errors gracefully:
255
+
256
+ ```ruby
257
+ class SafeTool < Tsikol::Tool
258
+ def execute(input:)
259
+ validate_input!(input)
260
+ result = process(input)
261
+ format_result(result)
262
+
263
+ rescue ValidationError => e
264
+ # Return user-friendly error
265
+ "Validation failed: #{e.message}"
266
+
267
+ rescue NetworkError => e
268
+ # Log and return error
269
+ log :error, "Network error", error: e.message
270
+ "Network error occurred. Please try again."
271
+
272
+ rescue => e
273
+ # Unexpected errors
274
+ log :error, "Unexpected error", error: e.class.name, message: e.message
275
+ "An unexpected error occurred"
276
+ end
277
+ end
278
+ ```
279
+
280
+ ### Async Operations
281
+
282
+ Tools can perform async operations:
283
+
284
+ ```ruby
285
+ class AsyncTool < Tsikol::Tool
286
+ def execute(data:)
287
+ job_id = SecureRandom.uuid
288
+
289
+ # Start async processing
290
+ Thread.new do
291
+ begin
292
+ result = long_running_operation(data)
293
+ store_result(job_id, result)
294
+ rescue => e
295
+ store_error(job_id, e)
296
+ end
297
+ end
298
+
299
+ # Return immediately
300
+ {
301
+ job_id: job_id,
302
+ status: "processing",
303
+ check_back_in: 60
304
+ }.to_json
305
+ end
306
+ end
307
+ ```
308
+
309
+ ### Using Server Capabilities
310
+
311
+ Tools can use server features when available:
312
+
313
+ ```ruby
314
+ class AiTool < Tsikol::Tool
315
+ def execute(prompt:)
316
+ unless @server.sampling_enabled?
317
+ return "AI features not available"
318
+ end
319
+
320
+ response = @server.sample_text(
321
+ messages: [
322
+ { role: "user", content: { type: "text", text: prompt } }
323
+ ],
324
+ temperature: 0.7
325
+ )
326
+
327
+ if response[:error]
328
+ "AI error: #{response[:error]}"
329
+ else
330
+ response[:text]
331
+ end
332
+ end
333
+ end
334
+ ```
335
+
336
+ ## Complete Example
337
+
338
+ ```ruby
339
+ class FileManager < Tsikol::Tool
340
+ description "Manage files and directories"
341
+
342
+ parameter :operation do
343
+ type :string
344
+ required
345
+ enum ["read", "write", "list", "delete", "info"]
346
+ description "Operation to perform"
347
+ end
348
+
349
+ parameter :path do
350
+ type :string
351
+ required
352
+ description "File or directory path"
353
+
354
+ complete do |partial|
355
+ Dir.glob("#{partial}*").map do |path|
356
+ File.directory?(path) ? "#{path}/" : path
357
+ end
358
+ end
359
+ end
360
+
361
+ parameter :content do
362
+ type :string
363
+ optional
364
+ description "Content for write operations"
365
+ end
366
+
367
+ parameter :options do
368
+ type :object
369
+ optional
370
+ default {}
371
+ description "Additional options (encoding, etc.)"
372
+ end
373
+
374
+ def execute(operation:, path:, content: nil, options: {})
375
+ log :info, "File operation", operation: operation, path: path
376
+
377
+ case operation
378
+ when "read"
379
+ read_file(path, options)
380
+ when "write"
381
+ write_file(path, content, options)
382
+ when "list"
383
+ list_directory(path)
384
+ when "delete"
385
+ delete_file(path)
386
+ when "info"
387
+ file_info(path)
388
+ end
389
+
390
+ rescue Errno::ENOENT
391
+ raise Tsikol::ValidationError, "File not found: #{path}"
392
+ rescue Errno::EACCES
393
+ raise Tsikol::ValidationError, "Permission denied: #{path}"
394
+ rescue => e
395
+ log :error, "File operation failed", error: e.message
396
+ raise
397
+ end
398
+
399
+ private
400
+
401
+ def read_file(path, options)
402
+ encoding = options["encoding"] || "UTF-8"
403
+ File.read(path, encoding: encoding)
404
+ end
405
+
406
+ def write_file(path, content, options)
407
+ raise Tsikol::ValidationError, "Content required for write" unless content
408
+
409
+ mode = options["append"] ? "a" : "w"
410
+ File.open(path, mode) { |f| f.write(content) }
411
+
412
+ "Wrote #{content.bytesize} bytes to #{path}"
413
+ end
414
+
415
+ def list_directory(path)
416
+ unless File.directory?(path)
417
+ raise Tsikol::ValidationError, "Not a directory: #{path}"
418
+ end
419
+
420
+ entries = Dir.entries(path).reject { |e| e.start_with?(".") }
421
+ entries.map do |entry|
422
+ full_path = File.join(path, entry)
423
+ {
424
+ name: entry,
425
+ type: File.directory?(full_path) ? "directory" : "file",
426
+ size: File.size(full_path)
427
+ }
428
+ end.to_json
429
+ end
430
+
431
+ def delete_file(path)
432
+ if File.directory?(path)
433
+ Dir.rmdir(path)
434
+ "Deleted directory: #{path}"
435
+ else
436
+ File.delete(path)
437
+ "Deleted file: #{path}"
438
+ end
439
+ end
440
+
441
+ def file_info(path)
442
+ stat = File.stat(path)
443
+ {
444
+ path: path,
445
+ type: stat.directory? ? "directory" : "file",
446
+ size: stat.size,
447
+ modified: stat.mtime.iso8601,
448
+ permissions: stat.mode.to_s(8)
449
+ }.to_json
450
+ end
451
+
452
+ def set_server(server)
453
+ @server = server
454
+
455
+ if @server.logging_enabled?
456
+ define_singleton_method(:log) do |level, message, data: nil|
457
+ @server.log(level, message, data: data)
458
+ end
459
+ else
460
+ define_singleton_method(:log) do |*args|
461
+ # No-op if logging not enabled
462
+ end
463
+ end
464
+ end
465
+ end
466
+ ```
467
+
468
+ ## Testing Tools
469
+
470
+ ```ruby
471
+ require 'minitest/autorun'
472
+ require 'tsikol/test_helpers'
473
+
474
+ class FileManagerTest < Minitest::Test
475
+ include Tsikol::TestHelpers::Assertions
476
+
477
+ def setup
478
+ @server = Tsikol::Server.new(name: "test")
479
+ @server.logging true
480
+ @server.register_tool_instance(FileManager.new)
481
+ @client = Tsikol::TestHelpers::TestClient.new(@server)
482
+ @client.initialize_connection
483
+ end
484
+
485
+ def test_read_file
486
+ File.write("test.txt", "Hello, World!")
487
+
488
+ response = @client.call_tool("file_manager", {
489
+ "operation" => "read",
490
+ "path" => "test.txt"
491
+ })
492
+
493
+ assert_successful_response(response)
494
+ assert_equal "Hello, World!", response.dig(:result, :content, 0, :text)
495
+ ensure
496
+ File.delete("test.txt") if File.exist?("test.txt")
497
+ end
498
+
499
+ def test_parameter_validation
500
+ response = @client.call_tool("file_manager", {
501
+ "operation" => "invalid_op",
502
+ "path" => "test.txt"
503
+ })
504
+
505
+ assert_error_response(response, -32602)
506
+ assert_match /must be one of/, response[:error][:message]
507
+ end
508
+
509
+ def test_completion
510
+ response = @client.complete(
511
+ { type: "ref/tool", name: "file_manager" },
512
+ { name: "path", value: "te" }
513
+ )
514
+
515
+ assert_successful_response(response)
516
+ values = response.dig(:result, :completion, :values)
517
+ assert values.any? { |v| v.start_with?("te") }
518
+ end
519
+ end
520
+ ```
521
+
522
+ ## See Also
523
+
524
+ - [Tools Guide](../guides/tools.md)
525
+ - [Server API](server.md)
526
+ - [Testing Guide](../guides/testing.md)
527
+ - [Error Handling](../cookbook/error-handling.md)