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.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/CHANGELOG.md +69 -0
- data/COMMITS.md +196 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +281 -0
- data/README.md +479 -0
- data/Rakefile +16 -0
- data/bin/tf +6 -0
- data/bin/tf_mcp +81 -0
- data/docs/.keep +0 -0
- data/docs/api/database.md +434 -0
- data/docs/api/ruby-library.md +349 -0
- data/docs/api/task-model.md +341 -0
- data/docs/assets/stylesheets/extra.css +53 -0
- data/docs/assets/trak_flow.jpg +0 -0
- data/docs/cli/admin-commands.md +369 -0
- data/docs/cli/dependency-commands.md +321 -0
- data/docs/cli/label-commands.md +222 -0
- data/docs/cli/overview.md +163 -0
- data/docs/cli/plan-commands.md +344 -0
- data/docs/cli/task-commands.md +333 -0
- data/docs/core-concepts/dependencies.md +232 -0
- data/docs/core-concepts/labels.md +217 -0
- data/docs/core-concepts/overview.md +178 -0
- data/docs/core-concepts/plans-workflows.md +264 -0
- data/docs/core-concepts/tasks.md +205 -0
- data/docs/getting-started/configuration.md +120 -0
- data/docs/getting-started/installation.md +79 -0
- data/docs/getting-started/quick-start.md +245 -0
- data/docs/index.md +169 -0
- data/docs/mcp/integration.md +302 -0
- data/docs/mcp/overview.md +206 -0
- data/docs/mcp/resources.md +284 -0
- data/docs/mcp/tools.md +457 -0
- data/examples/basic_usage.rb +365 -0
- data/examples/cli_demo.sh +314 -0
- data/examples/mcp/Gemfile +9 -0
- data/examples/mcp/Gemfile.lock +226 -0
- data/examples/mcp/http_demo.rb +232 -0
- data/examples/mcp/stdio_demo.rb +146 -0
- data/lib/trak_flow/cli/admin_commands.rb +136 -0
- data/lib/trak_flow/cli/config_commands.rb +260 -0
- data/lib/trak_flow/cli/dep_commands.rb +71 -0
- data/lib/trak_flow/cli/label_commands.rb +76 -0
- data/lib/trak_flow/cli/main_commands.rb +386 -0
- data/lib/trak_flow/cli/plan_commands.rb +185 -0
- data/lib/trak_flow/cli/workflow_commands.rb +133 -0
- data/lib/trak_flow/cli.rb +110 -0
- data/lib/trak_flow/config/defaults.yml +114 -0
- data/lib/trak_flow/config/section.rb +74 -0
- data/lib/trak_flow/config.rb +276 -0
- data/lib/trak_flow/graph/dependency_graph.rb +288 -0
- data/lib/trak_flow/id_generator.rb +52 -0
- data/lib/trak_flow/mcp/resources/base_resource.rb +25 -0
- data/lib/trak_flow/mcp/resources/dependency_graph.rb +31 -0
- data/lib/trak_flow/mcp/resources/label_list.rb +21 -0
- data/lib/trak_flow/mcp/resources/plan_by_id.rb +27 -0
- data/lib/trak_flow/mcp/resources/plan_list.rb +21 -0
- data/lib/trak_flow/mcp/resources/task_by_id.rb +31 -0
- data/lib/trak_flow/mcp/resources/task_list.rb +21 -0
- data/lib/trak_flow/mcp/resources/task_next.rb +30 -0
- data/lib/trak_flow/mcp/resources/workflow_by_id.rb +27 -0
- data/lib/trak_flow/mcp/resources/workflow_list.rb +21 -0
- data/lib/trak_flow/mcp/server.rb +140 -0
- data/lib/trak_flow/mcp/tools/base_tool.rb +29 -0
- data/lib/trak_flow/mcp/tools/comment_add.rb +33 -0
- data/lib/trak_flow/mcp/tools/dep_add.rb +34 -0
- data/lib/trak_flow/mcp/tools/dep_remove.rb +25 -0
- data/lib/trak_flow/mcp/tools/label_add.rb +28 -0
- data/lib/trak_flow/mcp/tools/label_remove.rb +25 -0
- data/lib/trak_flow/mcp/tools/plan_add_step.rb +35 -0
- data/lib/trak_flow/mcp/tools/plan_create.rb +33 -0
- data/lib/trak_flow/mcp/tools/plan_run.rb +58 -0
- data/lib/trak_flow/mcp/tools/plan_start.rb +58 -0
- data/lib/trak_flow/mcp/tools/task_block.rb +27 -0
- data/lib/trak_flow/mcp/tools/task_close.rb +26 -0
- data/lib/trak_flow/mcp/tools/task_create.rb +51 -0
- data/lib/trak_flow/mcp/tools/task_defer.rb +27 -0
- data/lib/trak_flow/mcp/tools/task_start.rb +25 -0
- data/lib/trak_flow/mcp/tools/task_update.rb +36 -0
- data/lib/trak_flow/mcp/tools/workflow_discard.rb +28 -0
- data/lib/trak_flow/mcp/tools/workflow_summarize.rb +34 -0
- data/lib/trak_flow/mcp.rb +38 -0
- data/lib/trak_flow/models/comment.rb +71 -0
- data/lib/trak_flow/models/dependency.rb +96 -0
- data/lib/trak_flow/models/label.rb +90 -0
- data/lib/trak_flow/models/task.rb +188 -0
- data/lib/trak_flow/storage/database.rb +638 -0
- data/lib/trak_flow/storage/jsonl.rb +259 -0
- data/lib/trak_flow/time_parser.rb +15 -0
- data/lib/trak_flow/version.rb +5 -0
- data/lib/trak_flow.rb +100 -0
- data/mkdocs.yml +143 -0
- 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
|