trak_flow 0.1.3

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/CHANGELOG.md +69 -0
  4. data/COMMITS.md +196 -0
  5. data/Gemfile +8 -0
  6. data/Gemfile.lock +281 -0
  7. data/README.md +479 -0
  8. data/Rakefile +16 -0
  9. data/bin/tf +6 -0
  10. data/bin/tf_mcp +81 -0
  11. data/docs/.keep +0 -0
  12. data/docs/api/database.md +434 -0
  13. data/docs/api/ruby-library.md +349 -0
  14. data/docs/api/task-model.md +341 -0
  15. data/docs/assets/stylesheets/extra.css +53 -0
  16. data/docs/assets/trak_flow.jpg +0 -0
  17. data/docs/cli/admin-commands.md +369 -0
  18. data/docs/cli/dependency-commands.md +321 -0
  19. data/docs/cli/label-commands.md +222 -0
  20. data/docs/cli/overview.md +163 -0
  21. data/docs/cli/plan-commands.md +344 -0
  22. data/docs/cli/task-commands.md +333 -0
  23. data/docs/core-concepts/dependencies.md +232 -0
  24. data/docs/core-concepts/labels.md +217 -0
  25. data/docs/core-concepts/overview.md +178 -0
  26. data/docs/core-concepts/plans-workflows.md +264 -0
  27. data/docs/core-concepts/tasks.md +205 -0
  28. data/docs/getting-started/configuration.md +120 -0
  29. data/docs/getting-started/installation.md +79 -0
  30. data/docs/getting-started/quick-start.md +245 -0
  31. data/docs/index.md +169 -0
  32. data/docs/mcp/integration.md +302 -0
  33. data/docs/mcp/overview.md +206 -0
  34. data/docs/mcp/resources.md +284 -0
  35. data/docs/mcp/tools.md +457 -0
  36. data/examples/basic_usage.rb +365 -0
  37. data/examples/cli_demo.sh +314 -0
  38. data/examples/mcp/Gemfile +9 -0
  39. data/examples/mcp/Gemfile.lock +226 -0
  40. data/examples/mcp/http_demo.rb +232 -0
  41. data/examples/mcp/stdio_demo.rb +146 -0
  42. data/lib/trak_flow/cli/admin_commands.rb +136 -0
  43. data/lib/trak_flow/cli/config_commands.rb +260 -0
  44. data/lib/trak_flow/cli/dep_commands.rb +71 -0
  45. data/lib/trak_flow/cli/label_commands.rb +76 -0
  46. data/lib/trak_flow/cli/main_commands.rb +386 -0
  47. data/lib/trak_flow/cli/plan_commands.rb +185 -0
  48. data/lib/trak_flow/cli/workflow_commands.rb +133 -0
  49. data/lib/trak_flow/cli.rb +110 -0
  50. data/lib/trak_flow/config/defaults.yml +114 -0
  51. data/lib/trak_flow/config/section.rb +74 -0
  52. data/lib/trak_flow/config.rb +276 -0
  53. data/lib/trak_flow/graph/dependency_graph.rb +288 -0
  54. data/lib/trak_flow/id_generator.rb +52 -0
  55. data/lib/trak_flow/mcp/resources/base_resource.rb +25 -0
  56. data/lib/trak_flow/mcp/resources/dependency_graph.rb +31 -0
  57. data/lib/trak_flow/mcp/resources/label_list.rb +21 -0
  58. data/lib/trak_flow/mcp/resources/plan_by_id.rb +27 -0
  59. data/lib/trak_flow/mcp/resources/plan_list.rb +21 -0
  60. data/lib/trak_flow/mcp/resources/task_by_id.rb +31 -0
  61. data/lib/trak_flow/mcp/resources/task_list.rb +21 -0
  62. data/lib/trak_flow/mcp/resources/task_next.rb +30 -0
  63. data/lib/trak_flow/mcp/resources/workflow_by_id.rb +27 -0
  64. data/lib/trak_flow/mcp/resources/workflow_list.rb +21 -0
  65. data/lib/trak_flow/mcp/server.rb +140 -0
  66. data/lib/trak_flow/mcp/tools/base_tool.rb +29 -0
  67. data/lib/trak_flow/mcp/tools/comment_add.rb +33 -0
  68. data/lib/trak_flow/mcp/tools/dep_add.rb +34 -0
  69. data/lib/trak_flow/mcp/tools/dep_remove.rb +25 -0
  70. data/lib/trak_flow/mcp/tools/label_add.rb +28 -0
  71. data/lib/trak_flow/mcp/tools/label_remove.rb +25 -0
  72. data/lib/trak_flow/mcp/tools/plan_add_step.rb +35 -0
  73. data/lib/trak_flow/mcp/tools/plan_create.rb +33 -0
  74. data/lib/trak_flow/mcp/tools/plan_run.rb +58 -0
  75. data/lib/trak_flow/mcp/tools/plan_start.rb +58 -0
  76. data/lib/trak_flow/mcp/tools/task_block.rb +27 -0
  77. data/lib/trak_flow/mcp/tools/task_close.rb +26 -0
  78. data/lib/trak_flow/mcp/tools/task_create.rb +51 -0
  79. data/lib/trak_flow/mcp/tools/task_defer.rb +27 -0
  80. data/lib/trak_flow/mcp/tools/task_start.rb +25 -0
  81. data/lib/trak_flow/mcp/tools/task_update.rb +36 -0
  82. data/lib/trak_flow/mcp/tools/workflow_discard.rb +28 -0
  83. data/lib/trak_flow/mcp/tools/workflow_summarize.rb +34 -0
  84. data/lib/trak_flow/mcp.rb +38 -0
  85. data/lib/trak_flow/models/comment.rb +71 -0
  86. data/lib/trak_flow/models/dependency.rb +96 -0
  87. data/lib/trak_flow/models/label.rb +90 -0
  88. data/lib/trak_flow/models/task.rb +188 -0
  89. data/lib/trak_flow/storage/database.rb +638 -0
  90. data/lib/trak_flow/storage/jsonl.rb +259 -0
  91. data/lib/trak_flow/time_parser.rb +15 -0
  92. data/lib/trak_flow/version.rb +5 -0
  93. data/lib/trak_flow.rb +100 -0
  94. data/mkdocs.yml +143 -0
  95. metadata +392 -0
@@ -0,0 +1,288 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Graph
5
+ # Dependency graph operations for visualizing and analyzing task relationships
6
+ class DependencyGraph
7
+ # Graph visualization colors (dark theme compatible)
8
+ COLORS = {
9
+ status: {
10
+ closed: "#4a5568",
11
+ tombstone: "#4a5568",
12
+ in_progress: "#3182ce",
13
+ blocked: "#e53e3e",
14
+ deferred: "#d69e2e",
15
+ pinned: "#805ad5"
16
+ },
17
+ priority: {
18
+ critical: "#e53e3e",
19
+ high: "#ed8936",
20
+ medium: "#48bb78",
21
+ low: "#4299e1",
22
+ backlog: "#a0aec0"
23
+ },
24
+ edge: {
25
+ blocks: "#e53e3e",
26
+ parent_child: "#3182ce",
27
+ related: "#a0aec0",
28
+ discovered_from: "#805ad5"
29
+ }
30
+ }.freeze
31
+
32
+ def initialize(db)
33
+ @db = db
34
+ end
35
+
36
+ # Build a tree representation of dependencies for a task
37
+ def dependency_tree(task_id, direction: :blocking, max_depth: 10)
38
+ task = @db.find_task!(task_id)
39
+ build_tree_node(task, direction, max_depth, Set.new)
40
+ end
41
+
42
+ # Find all tasks that block the given task (directly or transitively)
43
+ def all_blockers(task_id)
44
+ collect_related_tasks(task_id, :incoming, Models::Dependency::BLOCKING_TYPES)
45
+ end
46
+
47
+ # Find all tasks blocked by the given task (directly or transitively)
48
+ def all_blocked(task_id)
49
+ collect_related_tasks(task_id, :outgoing, Models::Dependency::BLOCKING_TYPES)
50
+ end
51
+
52
+ # Find the critical path - longest chain of blocking dependencies
53
+ def critical_path(root_task_id)
54
+ visited = {}
55
+ find_longest_path(root_task_id, visited)
56
+ end
57
+
58
+ # Get all leaf tasks (tasks with no children/blocked tasks)
59
+ def leaf_tasks
60
+ all_targets = Set.new
61
+
62
+ @db.all_task_ids.each do |id|
63
+ @db.find_dependencies(id, direction: :outgoing).each do |dep|
64
+ all_targets << dep.target_id if dep.blocking?
65
+ end
66
+ end
67
+
68
+ @db.list_tasks.reject { |task| all_targets.include?(task.id) }
69
+ end
70
+
71
+ # Get all root tasks (tasks with no parents/blockers)
72
+ def root_tasks
73
+ all_sources = Set.new
74
+
75
+ @db.all_task_ids.each do |id|
76
+ @db.find_dependencies(id, direction: :incoming).each do |dep|
77
+ all_sources << dep.source_id if dep.blocking?
78
+ end
79
+ end
80
+
81
+ @db.list_tasks.reject { |task| all_sources.include?(task.id) }
82
+ end
83
+
84
+ # Generate a DOT representation for Graphviz
85
+ def to_dot(task_ids: nil, include_closed: false)
86
+ task_ids ||= @db.all_task_ids
87
+ tasks = task_ids.map { |id| @db.find_task(id) }.compact
88
+ tasks = tasks.reject(&:closed?) unless include_closed
89
+
90
+ lines = ["digraph dependencies {"]
91
+ lines << ' rankdir=TB;'
92
+ lines << ' node [shape=box, style=filled];'
93
+ lines << ""
94
+
95
+ tasks.each do |task|
96
+ color = node_color(task)
97
+ label = "#{task.id}\\n#{truncate(task.title, 30)}"
98
+ lines << " \"#{task.id}\" [label=\"#{label}\", fillcolor=\"#{color}\"];"
99
+ end
100
+
101
+ lines << ""
102
+
103
+ task_set = Set.new(tasks.map(&:id))
104
+
105
+ tasks.each do |task|
106
+ @db.find_dependencies(task.id, direction: :outgoing).each do |dep|
107
+ next unless task_set.include?(dep.target_id)
108
+
109
+ style = edge_style(dep)
110
+ lines << " \"#{dep.source_id}\" -> \"#{dep.target_id}\" [#{style}];"
111
+ end
112
+ end
113
+
114
+ lines << "}"
115
+ lines.join("\n")
116
+ end
117
+
118
+ # Generate SVG using Graphviz (if available)
119
+ def to_svg(task_ids: nil, include_closed: false)
120
+ dot = to_dot(task_ids: task_ids, include_closed: include_closed)
121
+
122
+ require "open3"
123
+ stdout, stderr, status = Open3.capture3("dot", "-Tsvg", stdin_data: dot)
124
+
125
+ unless status.success?
126
+ raise Error, "Graphviz error: #{stderr}"
127
+ end
128
+
129
+ # Make background transparent for dark theme compatibility
130
+ stdout.gsub(/fill="white"/, 'fill="none"')
131
+ end
132
+
133
+ # Analyze the graph for potential problems
134
+ def analyze
135
+ {
136
+ total_tasks: @db.all_task_ids.size,
137
+ open_tasks: @db.list_tasks(status: "open").size,
138
+ ready_tasks: @db.ready_tasks.size,
139
+ blocked_tasks: @db.blocked_tasks.size,
140
+ orphan_tasks: find_orphans.size,
141
+ potential_cycles: find_potential_bottlenecks
142
+ }
143
+ end
144
+
145
+ private
146
+
147
+ def build_tree_node(task, direction, remaining_depth, visited)
148
+ return nil if remaining_depth <= 0 || visited.include?(task.id)
149
+
150
+ visited << task.id
151
+
152
+ node = {
153
+ id: task.id,
154
+ title: task.title,
155
+ status: task.status,
156
+ priority: task.priority,
157
+ children: []
158
+ }
159
+
160
+ dep_direction = direction == :blocking ? :incoming : :outgoing
161
+ deps = @db.find_dependencies(task.id, direction: dep_direction)
162
+ deps = deps.select(&:blocking?) if direction == :blocking
163
+
164
+ deps.each do |dep|
165
+ related_id = direction == :blocking ? dep.source_id : dep.target_id
166
+ related_task = @db.find_task(related_id)
167
+ next unless related_task
168
+
169
+ child_node = build_tree_node(related_task, direction, remaining_depth - 1, visited.dup)
170
+ node[:children] << child_node if child_node
171
+ end
172
+
173
+ node
174
+ end
175
+
176
+ def collect_related_tasks(start_id, direction, types)
177
+ visited = Set.new
178
+ queue = [start_id]
179
+ result = []
180
+
181
+ while queue.any?
182
+ current_id = queue.shift
183
+ next if visited.include?(current_id)
184
+
185
+ visited << current_id
186
+
187
+ deps = @db.find_dependencies(current_id, direction: direction)
188
+ deps = deps.select { |d| types.include?(d.type) }
189
+
190
+ deps.each do |dep|
191
+ related_id = direction == :incoming ? dep.source_id : dep.target_id
192
+ next if visited.include?(related_id)
193
+
194
+ task = @db.find_task(related_id)
195
+ if task
196
+ result << task
197
+ queue << related_id
198
+ end
199
+ end
200
+ end
201
+
202
+ result
203
+ end
204
+
205
+ def find_longest_path(task_id, memo)
206
+ return memo[task_id] if memo.key?(task_id)
207
+
208
+ task = @db.find_task(task_id)
209
+ return [] unless task
210
+
211
+ deps = @db.find_dependencies(task_id, direction: :outgoing)
212
+ blocking_deps = deps.select(&:blocking?)
213
+
214
+ if blocking_deps.empty?
215
+ memo[task_id] = [task]
216
+ return [task]
217
+ end
218
+
219
+ longest_child_path = blocking_deps.map do |dep|
220
+ find_longest_path(dep.target_id, memo)
221
+ end.max_by(&:size) || []
222
+
223
+ memo[task_id] = [task] + longest_child_path
224
+ memo[task_id]
225
+ end
226
+
227
+ def find_orphans
228
+ @db.list_tasks.select do |task|
229
+ task.parent_id && !@db.find_task(task.parent_id)
230
+ end
231
+ end
232
+
233
+ def find_potential_bottlenecks
234
+ bottlenecks = []
235
+
236
+ @db.list_tasks(status: "open").each do |task|
237
+ incoming = @db.find_dependencies(task.id, direction: :incoming).count
238
+ outgoing = @db.find_dependencies(task.id, direction: :outgoing).count
239
+
240
+ if incoming >= 3 || outgoing >= 3
241
+ bottlenecks << {
242
+ id: task.id,
243
+ title: task.title,
244
+ incoming_deps: incoming,
245
+ outgoing_deps: outgoing
246
+ }
247
+ end
248
+ end
249
+
250
+ bottlenecks.sort_by { |b| -(b[:incoming_deps] + b[:outgoing_deps]) }
251
+ end
252
+
253
+ def node_color(task)
254
+ status_color = COLORS[:status][task.status.to_sym]
255
+ return status_color if status_color
256
+
257
+ priority_colors = {
258
+ 0 => COLORS[:priority][:critical],
259
+ 1 => COLORS[:priority][:high],
260
+ 2 => COLORS[:priority][:medium],
261
+ 3 => COLORS[:priority][:low]
262
+ }
263
+ priority_colors[task.priority] || COLORS[:priority][:backlog]
264
+ end
265
+
266
+ def edge_style(dep)
267
+ type_key = dep.type.tr("-", "_").to_sym
268
+ color = COLORS[:edge][type_key]
269
+ return "" unless color
270
+
271
+ style = case dep.type
272
+ when "blocks" then "bold"
273
+ when "parent-child", "related", "discovered-from" then "dashed"
274
+ else "solid"
275
+ end
276
+ style = "dotted" if %w[related discovered-from].include?(dep.type)
277
+
278
+ %(color="#{color}", style=#{style})
279
+ end
280
+
281
+ def truncate(str, length)
282
+ return str if str.length <= length
283
+
284
+ "#{str[0, length - 3]}..."
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ # Generates hash-based IDs to prevent merge conflicts when multiple
5
+ # agents or branches work simultaneously. The ID format is "tf-XXXX"
6
+ # where XXXX is a truncated hash derived from a UUID.
7
+ class IdGenerator
8
+ DEFAULT_PREFIX = "tf"
9
+ MIN_HASH_LENGTH = 4
10
+ MAX_HASH_LENGTH = 8
11
+
12
+ class << self
13
+ def generate(prefix: DEFAULT_PREFIX, existing_ids: [], min_length: MIN_HASH_LENGTH)
14
+ loop do
15
+ uuid = SecureRandom.uuid
16
+ hash = Digest::SHA256.hexdigest(uuid)[0, max_length_needed(existing_ids, min_length)]
17
+ id = "#{prefix}-#{hash}"
18
+
19
+ return id unless existing_ids.include?(id)
20
+ end
21
+ end
22
+
23
+ def generate_child_id(parent_id, child_index)
24
+ "#{parent_id}.#{child_index}"
25
+ end
26
+
27
+ def parent_id(child_id)
28
+ return nil unless child_id.include?(".")
29
+
30
+ child_id.split(".")[0..-2].join(".")
31
+ end
32
+
33
+ def valid?(id)
34
+ return false if id.nil? || id.empty?
35
+
36
+ id.match?(/^[a-z]+-[a-f0-9]{#{MIN_HASH_LENGTH},#{MAX_HASH_LENGTH}}(\.\d+)*$/i)
37
+ end
38
+
39
+ def content_hash(data)
40
+ Digest::SHA256.hexdigest(Oj.dump(data, mode: :compat))[0, 16]
41
+ end
42
+
43
+ private
44
+
45
+ def max_length_needed(existing_ids, min_length)
46
+ return min_length if existing_ids.empty?
47
+
48
+ [min_length, MAX_HASH_LENGTH].max
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class BaseResource < FastMcp::Resource
7
+ class << self
8
+ def with_database
9
+ TrakFlow.ensure_initialized!
10
+
11
+ db = Storage::Database.new
12
+ db.connect
13
+
14
+ jsonl = Storage::Jsonl.new
15
+ jsonl.import(db) if jsonl.exists?
16
+
17
+ yield db
18
+ ensure
19
+ db&.close
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class DependencyGraph < BaseResource
7
+ uri "trak_flow://graph/dependencies"
8
+ resource_name "Dependency Graph"
9
+ description "Full dependency graph of all tasks"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ tasks = db.list_tasks
15
+ nodes = tasks.map { |t| { id: t.id, title: t.title, status: t.status } }
16
+
17
+ edges = []
18
+ tasks.each do |task|
19
+ db.find_dependencies(task.id, direction: :outgoing).each do |dep|
20
+ edges << { source: dep.source_id, target: dep.target_id, type: dep.type }
21
+ end
22
+ end
23
+
24
+ result = { nodes: nodes, edges: edges }
25
+ Oj.dump(result, mode: :compat, indent: 2)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class LabelList < BaseResource
7
+ uri "trak_flow://labels"
8
+ resource_name "Label List"
9
+ description "List of all unique labels in the system"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ labels = db.all_labels
15
+ Oj.dump(labels, mode: :compat, indent: 2)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class PlanById < BaseResource
7
+ uri "trak_flow://plans/{id}"
8
+ resource_name "Plan by ID"
9
+ description "Get a Plan with its steps (child tasks)"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ plan = db.find_task!(params[:id])
15
+ steps = db.find_plan_tasks(plan.id)
16
+
17
+ result = {
18
+ plan: plan.to_h,
19
+ steps: steps.map(&:to_h)
20
+ }
21
+ Oj.dump(result, mode: :compat, indent: 2)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class PlanList < BaseResource
7
+ uri "trak_flow://plans"
8
+ resource_name "Plan List"
9
+ description "List of all Plans (workflow blueprints)"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ plans = db.find_plans
15
+ Oj.dump(plans.map(&:to_h), mode: :compat, indent: 2)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class TaskById < BaseResource
7
+ uri "trak_flow://tasks/{id}"
8
+ resource_name "Task by ID"
9
+ description "Get a specific task by its ID, including labels, dependencies, and comments"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ task = db.find_task!(params[:id])
15
+ labels = db.find_labels(task.id)
16
+ deps = db.find_dependencies(task.id)
17
+ comments = db.find_comments(task.id)
18
+
19
+ result = {
20
+ task: task.to_h,
21
+ labels: labels.map(&:name),
22
+ dependencies: deps.map(&:to_h),
23
+ comments: comments.map(&:to_h)
24
+ }
25
+ Oj.dump(result, mode: :compat, indent: 2)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class TaskList < BaseResource
7
+ uri "trak_flow://tasks"
8
+ resource_name "Task List"
9
+ description "List of all tasks with their status and priority"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ tasks = db.list_tasks({})
15
+ Oj.dump(tasks.map(&:to_h), mode: :compat, indent: 2)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class TaskNext < BaseResource
7
+ uri "trak_flow://tasks/next"
8
+ resource_name "Next Actionable Task"
9
+ description "Get the next actionable task (highest priority with no blockers)"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ tasks = db.ready_tasks
15
+ next_task = tasks.first
16
+
17
+ if next_task
18
+ labels = db.find_labels(next_task.id)
19
+ result = { task: next_task.to_h, labels: labels.map(&:name) }
20
+ else
21
+ result = { task: nil, message: "No ready tasks found" }
22
+ end
23
+
24
+ Oj.dump(result, mode: :compat, indent: 2)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class WorkflowById < BaseResource
7
+ uri "trak_flow://workflows/{id}"
8
+ resource_name "Workflow by ID"
9
+ description "Get a Workflow with its tasks"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ workflow = db.find_task!(params[:id])
15
+ tasks = db.find_workflow_tasks(workflow.id)
16
+
17
+ result = {
18
+ workflow: workflow.to_h,
19
+ tasks: tasks.map(&:to_h)
20
+ }
21
+ Oj.dump(result, mode: :compat, indent: 2)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrakFlow
4
+ module Mcp
5
+ module Resources
6
+ class WorkflowList < BaseResource
7
+ uri "trak_flow://workflows"
8
+ resource_name "Workflow List"
9
+ description "List of all Workflows (running instances of Plans)"
10
+ mime_type "application/json"
11
+
12
+ def content
13
+ self.class.with_database do |db|
14
+ workflows = db.find_workflows
15
+ Oj.dump(workflows.map(&:to_h), mode: :compat, indent: 2)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end