@flydocs/cli 0.5.0-beta.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.
- package/README.md +96 -0
- package/dist/cli.js +2666 -0
- package/package.json +32 -0
- package/template/.claude/CLAUDE.md +90 -0
- package/template/.claude/agents/README.md +19 -0
- package/template/.claude/agents/implementation-agent.md +29 -0
- package/template/.claude/agents/pm-agent.md +29 -0
- package/template/.claude/agents/research-agent.md +25 -0
- package/template/.claude/agents/review-agent.md +29 -0
- package/template/.claude/commands/activate.md +10 -0
- package/template/.claude/commands/attach.md +9 -0
- package/template/.claude/commands/block.md +10 -0
- package/template/.claude/commands/capture.md +10 -0
- package/template/.claude/commands/close.md +10 -0
- package/template/.claude/commands/flydocs-setup.md +598 -0
- package/template/.claude/commands/flydocs-update.md +27 -0
- package/template/.claude/commands/implement.md +10 -0
- package/template/.claude/commands/new-project.md +11 -0
- package/template/.claude/commands/project-update.md +10 -0
- package/template/.claude/commands/refine.md +10 -0
- package/template/.claude/commands/review.md +10 -0
- package/template/.claude/commands/start-session.md +10 -0
- package/template/.claude/commands/status.md +10 -0
- package/template/.claude/commands/validate.md +10 -0
- package/template/.claude/commands/wrap-session.md +10 -0
- package/template/.claude/settings.json +49 -0
- package/template/.claude/skills/README.md +293 -0
- package/template/.claude/skills/flydocs-cloud/SKILL.md +96 -0
- package/template/.claude/skills/flydocs-cloud/cursor-rule.mdc +50 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign_cycle.py +44 -0
- package/template/.claude/skills/flydocs-cloud/scripts/assign_milestone.py +44 -0
- package/template/.claude/skills/flydocs-cloud/scripts/comment.py +39 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_issue.py +100 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_milestone.py +46 -0
- package/template/.claude/skills/flydocs-cloud/scripts/create_project.py +40 -0
- package/template/.claude/skills/flydocs-cloud/scripts/estimate.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/flydocs_api.py +277 -0
- package/template/.claude/skills/flydocs-cloud/scripts/get_issue.py +77 -0
- package/template/.claude/skills/flydocs-cloud/scripts/link.py +47 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_cycles.py +35 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_issues.py +105 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_milestones.py +40 -0
- package/template/.claude/skills/flydocs-cloud/scripts/list_projects.py +45 -0
- package/template/.claude/skills/flydocs-cloud/scripts/priority.py +38 -0
- package/template/.claude/skills/flydocs-cloud/scripts/project_update.py +59 -0
- package/template/.claude/skills/flydocs-cloud/scripts/transition.py +67 -0
- package/template/.claude/skills/flydocs-cloud/scripts/update_description.py +47 -0
- package/template/.claude/skills/flydocs-cloud/scripts/update_issue.py +111 -0
- package/template/.claude/skills/flydocs-context-graph/SKILL.md +87 -0
- package/template/.claude/skills/flydocs-context-graph/schema.md +78 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_build.py +299 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_context.py +338 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_query.py +191 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_session.py +161 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_update.py +194 -0
- package/template/.claude/skills/flydocs-context-graph/scripts/graph_utils.py +118 -0
- package/template/.claude/skills/flydocs-estimates/SKILL.md +384 -0
- package/template/.claude/skills/flydocs-estimates/references/provider-costs.md +152 -0
- package/template/.claude/skills/flydocs-figma/SKILL.md +377 -0
- package/template/.claude/skills/flydocs-figma/references/PROMPTING.md +108 -0
- package/template/.claude/skills/flydocs-figma/references/TROUBLESHOOTING.md +112 -0
- package/template/.claude/skills/flydocs-local/SKILL.md +103 -0
- package/template/.claude/skills/flydocs-local/cursor-rule.mdc +43 -0
- package/template/.claude/skills/flydocs-local/scripts/assign.py +20 -0
- package/template/.claude/skills/flydocs-local/scripts/comment.py +27 -0
- package/template/.claude/skills/flydocs-local/scripts/create_issue.py +44 -0
- package/template/.claude/skills/flydocs-local/scripts/estimate.py +37 -0
- package/template/.claude/skills/flydocs-local/scripts/flydocs_api.py +272 -0
- package/template/.claude/skills/flydocs-local/scripts/get_issue.py +20 -0
- package/template/.claude/skills/flydocs-local/scripts/link.py +41 -0
- package/template/.claude/skills/flydocs-local/scripts/list_issues.py +34 -0
- package/template/.claude/skills/flydocs-local/scripts/priority.py +37 -0
- package/template/.claude/skills/flydocs-local/scripts/project_update.py +67 -0
- package/template/.claude/skills/flydocs-local/scripts/status_summary.py +16 -0
- package/template/.claude/skills/flydocs-local/scripts/transition.py +24 -0
- package/template/.claude/skills/flydocs-local/scripts/update_description.py +35 -0
- package/template/.claude/skills/flydocs-local/scripts/update_issue.py +84 -0
- package/template/.claude/skills/flydocs-workflow/SKILL.md +85 -0
- package/template/.claude/skills/flydocs-workflow/cursor-rule.mdc +53 -0
- package/template/.claude/skills/flydocs-workflow/reference/comment-templates.md +131 -0
- package/template/.claude/skills/flydocs-workflow/reference/golden-rules.md +76 -0
- package/template/.claude/skills/flydocs-workflow/reference/priority-estimates.md +28 -0
- package/template/.claude/skills/flydocs-workflow/reference/status-workflow.md +50 -0
- package/template/.claude/skills/flydocs-workflow/session.md +128 -0
- package/template/.claude/skills/flydocs-workflow/stages/activate.md +46 -0
- package/template/.claude/skills/flydocs-workflow/stages/capture.md +50 -0
- package/template/.claude/skills/flydocs-workflow/stages/close.md +32 -0
- package/template/.claude/skills/flydocs-workflow/stages/implement.md +124 -0
- package/template/.claude/skills/flydocs-workflow/stages/refine.md +51 -0
- package/template/.claude/skills/flydocs-workflow/stages/review.md +86 -0
- package/template/.claude/skills/flydocs-workflow/stages/validate.md +90 -0
- package/template/.claude/skills/flydocs-workflow/templates/bug.md +95 -0
- package/template/.claude/skills/flydocs-workflow/templates/chore.md +75 -0
- package/template/.claude/skills/flydocs-workflow/templates/feature.md +93 -0
- package/template/.claude/skills/flydocs-workflow/templates/idea.md +84 -0
- package/template/.cursor/agents/implementation-agent.md +28 -0
- package/template/.cursor/agents/pm-agent.md +27 -0
- package/template/.cursor/agents/research-agent.md +23 -0
- package/template/.cursor/agents/review-agent.md +27 -0
- package/template/.cursor/hooks.json +29 -0
- package/template/.cursor/mcp.json +16 -0
- package/template/.env.example +44 -0
- package/template/.flydocs/config.json +104 -0
- package/template/.flydocs/hooks/auto-approve.py +71 -0
- package/template/.flydocs/hooks/post-edit.py +72 -0
- package/template/.flydocs/hooks/prefer-scripts.py +89 -0
- package/template/.flydocs/hooks/prompt-submit.py +277 -0
- package/template/.flydocs/scripts/generate_manifest.py +287 -0
- package/template/.flydocs/scripts/skill_manager.py +541 -0
- package/template/.flydocs/templates/README.md +46 -0
- package/template/.flydocs/templates/bug.md +166 -0
- package/template/.flydocs/templates/chore.md +110 -0
- package/template/.flydocs/templates/design-system/README.md +27 -0
- package/template/.flydocs/templates/design-system/component-patterns.md +92 -0
- package/template/.flydocs/templates/design-system/token-mapping.md +168 -0
- package/template/.flydocs/templates/feature.md +173 -0
- package/template/.flydocs/templates/idea.md +122 -0
- package/template/.flydocs/templates/instructions.md +228 -0
- package/template/.flydocs/templates/quick-capture.md +35 -0
- package/template/.flydocs/templates/scripts/check-design-system.template.mjs +179 -0
- package/template/.flydocs/version +1 -0
- package/template/AGENTS.md +95 -0
- package/template/CHANGELOG.md +271 -0
- package/template/flydocs/README.md +186 -0
- package/template/flydocs/context/project.md +51 -0
- package/template/flydocs/design-system/README.md +126 -0
- package/template/flydocs/design-system/component-patterns.md +173 -0
- package/template/flydocs/design-system/token-mapping.md +114 -0
- package/template/flydocs/knowledge/INDEX.md +100 -0
- package/template/flydocs/knowledge/README.md +62 -0
- package/template/flydocs/knowledge/product/personas.md +79 -0
- package/template/flydocs/knowledge/product/user-flows.md +88 -0
- package/template/manifest.json +221 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Query the context graph via BFS traversal.
|
|
3
|
+
|
|
4
|
+
Returns related nodes for a given starting node, filtered by depth and
|
|
5
|
+
relationship type. Output as compressed markdown or JSON.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 .claude/skills/flydocs-context-graph/scripts/graph_query.py \\
|
|
9
|
+
--node decision:001 [--depth 2] [--rel EXTENDS] [--reverse] [--format md|json]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from collections import deque
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
18
|
+
from graph_utils import find_project_root, load_graph, output_json, fail
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def bfs_traverse(graph, start_node, max_depth, rel_filter, reverse):
|
|
22
|
+
"""BFS traversal from start_node, returning discovered nodes with paths.
|
|
23
|
+
|
|
24
|
+
Returns list of (node_id, depth, edge_rel, via_node) tuples.
|
|
25
|
+
"""
|
|
26
|
+
nodes = graph.get("nodes", {})
|
|
27
|
+
edges = graph.get("edges", [])
|
|
28
|
+
|
|
29
|
+
if start_node not in nodes:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
# Build adjacency from edges
|
|
33
|
+
adjacency = {}
|
|
34
|
+
for edge in edges:
|
|
35
|
+
if rel_filter and edge["rel"] not in rel_filter:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
if reverse:
|
|
39
|
+
src, dst = edge["to"], edge["from"]
|
|
40
|
+
else:
|
|
41
|
+
src, dst = edge["from"], edge["to"]
|
|
42
|
+
|
|
43
|
+
if src not in adjacency:
|
|
44
|
+
adjacency[src] = []
|
|
45
|
+
adjacency[src].append((dst, edge["rel"], edge.get("weight", 1.0)))
|
|
46
|
+
|
|
47
|
+
# BFS
|
|
48
|
+
visited = {start_node}
|
|
49
|
+
queue = deque()
|
|
50
|
+
results = []
|
|
51
|
+
|
|
52
|
+
for neighbor, rel, weight in adjacency.get(start_node, []):
|
|
53
|
+
if neighbor not in visited:
|
|
54
|
+
queue.append((neighbor, 1, rel, start_node))
|
|
55
|
+
visited.add(neighbor)
|
|
56
|
+
|
|
57
|
+
while queue:
|
|
58
|
+
node_id, depth, rel, via = queue.popleft()
|
|
59
|
+
results.append((node_id, depth, rel, via))
|
|
60
|
+
|
|
61
|
+
if depth < max_depth:
|
|
62
|
+
for neighbor, next_rel, weight in adjacency.get(node_id, []):
|
|
63
|
+
if neighbor not in visited:
|
|
64
|
+
queue.append((neighbor, depth + 1, next_rel, node_id))
|
|
65
|
+
visited.add(neighbor)
|
|
66
|
+
|
|
67
|
+
return results
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_markdown(graph, start_node, results):
|
|
71
|
+
"""Format traversal results as compressed markdown context block."""
|
|
72
|
+
nodes = graph.get("nodes", {})
|
|
73
|
+
start_info = nodes.get(start_node, {})
|
|
74
|
+
label = start_info.get("label", start_node)
|
|
75
|
+
|
|
76
|
+
lines = [f"## Context for: {label}"]
|
|
77
|
+
lines.append("")
|
|
78
|
+
|
|
79
|
+
if not results:
|
|
80
|
+
lines.append("No related nodes found.")
|
|
81
|
+
return "\n".join(lines)
|
|
82
|
+
|
|
83
|
+
# Group results by node type
|
|
84
|
+
by_type = {}
|
|
85
|
+
for node_id, depth, rel, via in results:
|
|
86
|
+
node = nodes.get(node_id, {})
|
|
87
|
+
node_type = node.get("type", "unknown")
|
|
88
|
+
if node_type not in by_type:
|
|
89
|
+
by_type[node_type] = []
|
|
90
|
+
by_type[node_type].append((node_id, node, rel, depth))
|
|
91
|
+
|
|
92
|
+
# Type display order
|
|
93
|
+
type_labels = {
|
|
94
|
+
"decision": "Decisions",
|
|
95
|
+
"skill": "Skills",
|
|
96
|
+
"module": "Modules",
|
|
97
|
+
"issue": "Issues",
|
|
98
|
+
"session": "Sessions",
|
|
99
|
+
"concept": "Concepts",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for node_type in ["decision", "skill", "module", "issue", "session", "concept"]:
|
|
103
|
+
entries = by_type.get(node_type, [])
|
|
104
|
+
if not entries:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
type_label = type_labels.get(node_type, node_type.title())
|
|
108
|
+
lines.append(f"**{type_label}:**")
|
|
109
|
+
|
|
110
|
+
for node_id, node, rel, depth in entries:
|
|
111
|
+
node_label = node.get("label", node_id)
|
|
112
|
+
status = node.get("status", "")
|
|
113
|
+
status_str = f" [{status}]" if status else ""
|
|
114
|
+
lines.append(f"- {node_id}: {node_label} ({rel}){status_str}")
|
|
115
|
+
|
|
116
|
+
lines.append("")
|
|
117
|
+
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_json(graph, start_node, results):
|
|
122
|
+
"""Format traversal results as JSON."""
|
|
123
|
+
nodes = graph.get("nodes", {})
|
|
124
|
+
|
|
125
|
+
related = []
|
|
126
|
+
for node_id, depth, rel, via in results:
|
|
127
|
+
node = nodes.get(node_id, {})
|
|
128
|
+
related.append({
|
|
129
|
+
"id": node_id,
|
|
130
|
+
"type": node.get("type", "unknown"),
|
|
131
|
+
"label": node.get("label", node_id),
|
|
132
|
+
"relationship": rel,
|
|
133
|
+
"depth": depth,
|
|
134
|
+
"via": via,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"node": start_node,
|
|
139
|
+
"label": nodes.get(start_node, {}).get("label", start_node),
|
|
140
|
+
"related": related,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main():
|
|
145
|
+
parser = argparse.ArgumentParser(description="Query the context graph")
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--node", required=True,
|
|
148
|
+
help="Starting node ID (e.g., decision:001, skill:typescript-strict)"
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"--depth", type=int, default=2,
|
|
152
|
+
help="BFS traversal depth (default: 2)"
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--rel", action="append", dest="rels",
|
|
156
|
+
help="Filter by relationship type (can specify multiple)"
|
|
157
|
+
)
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--reverse", action="store_true",
|
|
160
|
+
help="Follow edges in reverse direction"
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--format", choices=["md", "json"], default="md", dest="fmt",
|
|
164
|
+
help="Output format (default: md)"
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--root", type=str, default=None,
|
|
168
|
+
help="Project root (default: auto-detect)"
|
|
169
|
+
)
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
|
|
172
|
+
root = Path(args.root) if args.root else find_project_root()
|
|
173
|
+
if not root:
|
|
174
|
+
fail("Could not find project root (no .flydocs/ directory found)")
|
|
175
|
+
|
|
176
|
+
graph = load_graph(root)
|
|
177
|
+
|
|
178
|
+
if args.node not in graph.get("nodes", {}):
|
|
179
|
+
fail(f"Node not found: {args.node}")
|
|
180
|
+
|
|
181
|
+
rel_filter = set(args.rels) if args.rels else None
|
|
182
|
+
results = bfs_traverse(graph, args.node, args.depth, rel_filter, args.reverse)
|
|
183
|
+
|
|
184
|
+
if args.fmt == "json":
|
|
185
|
+
output_json(format_json(graph, args.node, results))
|
|
186
|
+
else:
|
|
187
|
+
print(format_markdown(graph, args.node, results))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Record session outcomes in the context graph.
|
|
3
|
+
|
|
4
|
+
Called during session wrap to create a session node with edges to issues
|
|
5
|
+
worked on and decisions produced. Supports cross-session continuity by
|
|
6
|
+
building the temporal layer that graph_context.py traverses.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 .claude/skills/flydocs-context-graph/scripts/graph_session.py \
|
|
10
|
+
--summary "Completed Phase 1, started Phase 2" \
|
|
11
|
+
[--issue FLY-56] [--issue FLY-174] \
|
|
12
|
+
[--decision 006] \
|
|
13
|
+
[--date 2026-02-17] \
|
|
14
|
+
[--root PATH]
|
|
15
|
+
|
|
16
|
+
Multiple --issue and --decision flags can be provided.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import date
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
25
|
+
from graph_utils import (
|
|
26
|
+
find_project_root,
|
|
27
|
+
load_graph,
|
|
28
|
+
save_graph,
|
|
29
|
+
output_json,
|
|
30
|
+
fail,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Session nodes older than this get reduced weight in graph_context.py
|
|
35
|
+
DEFAULT_RETENTION_DAYS = 30
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate_session_id(session_date):
|
|
39
|
+
"""Generate a session ID like session:2026-02-17-a.
|
|
40
|
+
|
|
41
|
+
If a session with the same date exists, increment the sequence letter.
|
|
42
|
+
"""
|
|
43
|
+
base = f"session:{session_date}"
|
|
44
|
+
return base
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def find_next_session_id(graph, session_date):
|
|
48
|
+
"""Find the next available session ID for a given date."""
|
|
49
|
+
nodes = graph.get("nodes", {})
|
|
50
|
+
base = f"session:{session_date}"
|
|
51
|
+
|
|
52
|
+
# Check if base ID exists, if so try a, b, c...
|
|
53
|
+
if base not in nodes:
|
|
54
|
+
return base
|
|
55
|
+
|
|
56
|
+
for suffix in "abcdefghijklmnopqrstuvwxyz":
|
|
57
|
+
candidate = f"{base}-{suffix}"
|
|
58
|
+
if candidate not in nodes:
|
|
59
|
+
return candidate
|
|
60
|
+
|
|
61
|
+
# Fallback
|
|
62
|
+
return f"{base}-z2"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
parser = argparse.ArgumentParser(
|
|
67
|
+
description="Record session outcomes in the context graph"
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--summary", required=True,
|
|
71
|
+
help="Brief session summary (1-2 sentences)"
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--issue", action="append", dest="issues", default=[],
|
|
75
|
+
help="Issue identifier worked on (can specify multiple)"
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--decision", action="append", dest="decisions", default=[],
|
|
79
|
+
help="ADR number produced/referenced (can specify multiple)"
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--date", type=str, default=None,
|
|
83
|
+
help="Session date (YYYY-MM-DD, default: today)"
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--root", type=str, default=None,
|
|
87
|
+
help="Project root (default: auto-detect)"
|
|
88
|
+
)
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
|
|
91
|
+
root = Path(args.root) if args.root else find_project_root()
|
|
92
|
+
if not root:
|
|
93
|
+
fail("Could not find project root (no .flydocs/ directory found)")
|
|
94
|
+
|
|
95
|
+
graph = load_graph(root)
|
|
96
|
+
|
|
97
|
+
session_date = args.date or date.today().isoformat()
|
|
98
|
+
session_id = find_next_session_id(graph, session_date)
|
|
99
|
+
|
|
100
|
+
# Create session node
|
|
101
|
+
graph["nodes"][session_id] = {
|
|
102
|
+
"type": "session",
|
|
103
|
+
"label": args.summary,
|
|
104
|
+
"date": session_date,
|
|
105
|
+
"manual": True,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
edges_added = []
|
|
109
|
+
|
|
110
|
+
# Create WORKED_ON edges to issues
|
|
111
|
+
for issue_ref in args.issues:
|
|
112
|
+
issue_node = f"issue:{issue_ref}"
|
|
113
|
+
|
|
114
|
+
# Create issue node if it doesn't exist
|
|
115
|
+
if issue_node not in graph["nodes"]:
|
|
116
|
+
graph["nodes"][issue_node] = {
|
|
117
|
+
"type": "issue",
|
|
118
|
+
"label": issue_ref,
|
|
119
|
+
"manual": True,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
edge = {
|
|
123
|
+
"from": session_id,
|
|
124
|
+
"to": issue_node,
|
|
125
|
+
"rel": "WORKED_ON",
|
|
126
|
+
"weight": 1.0,
|
|
127
|
+
"manual": True,
|
|
128
|
+
}
|
|
129
|
+
graph["edges"].append(edge)
|
|
130
|
+
edges_added.append(f"{session_id} --WORKED_ON--> {issue_node}")
|
|
131
|
+
|
|
132
|
+
# Create PRODUCED edges to decisions
|
|
133
|
+
for decision_num in args.decisions:
|
|
134
|
+
decision_node = f"decision:{decision_num.zfill(3)}"
|
|
135
|
+
|
|
136
|
+
edge = {
|
|
137
|
+
"from": session_id,
|
|
138
|
+
"to": decision_node,
|
|
139
|
+
"rel": "PRODUCED",
|
|
140
|
+
"weight": 1.0,
|
|
141
|
+
"manual": True,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Only add if target exists
|
|
145
|
+
if decision_node in graph["nodes"]:
|
|
146
|
+
graph["edges"].append(edge)
|
|
147
|
+
edges_added.append(f"{session_id} --PRODUCED--> {decision_node}")
|
|
148
|
+
|
|
149
|
+
save_graph(root, graph)
|
|
150
|
+
|
|
151
|
+
output_json({
|
|
152
|
+
"success": True,
|
|
153
|
+
"sessionId": session_id,
|
|
154
|
+
"date": session_date,
|
|
155
|
+
"summary": args.summary,
|
|
156
|
+
"edges": edges_added,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Incrementally add or remove nodes and edges in the context graph.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 .claude/skills/flydocs-context-graph/scripts/graph_update.py \\
|
|
6
|
+
add-node <ID> --type <TYPE> [--label STR] [--path STR]
|
|
7
|
+
|
|
8
|
+
python3 .claude/skills/flydocs-context-graph/scripts/graph_update.py \\
|
|
9
|
+
remove-node <ID>
|
|
10
|
+
|
|
11
|
+
python3 .claude/skills/flydocs-context-graph/scripts/graph_update.py \\
|
|
12
|
+
add-edge <FROM> <TO> <REL> [--weight N] [--manual]
|
|
13
|
+
|
|
14
|
+
python3 .claude/skills/flydocs-context-graph/scripts/graph_update.py \\
|
|
15
|
+
remove-edge <FROM> <TO> <REL>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
23
|
+
from graph_utils import (
|
|
24
|
+
find_project_root,
|
|
25
|
+
load_graph,
|
|
26
|
+
save_graph,
|
|
27
|
+
output_json,
|
|
28
|
+
fail,
|
|
29
|
+
VALID_NODE_TYPES,
|
|
30
|
+
VALID_REL_TYPES,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cmd_add_node(args, graph):
|
|
35
|
+
"""Add a node to the graph."""
|
|
36
|
+
node_id = args.id
|
|
37
|
+
if args.type not in VALID_NODE_TYPES:
|
|
38
|
+
fail(f"Invalid node type: {args.type}. Valid: {', '.join(sorted(VALID_NODE_TYPES))}")
|
|
39
|
+
|
|
40
|
+
node = {"type": args.type}
|
|
41
|
+
if args.label:
|
|
42
|
+
node["label"] = args.label
|
|
43
|
+
else:
|
|
44
|
+
# Default label from ID
|
|
45
|
+
node["label"] = node_id.split(":", 1)[-1] if ":" in node_id else node_id
|
|
46
|
+
if args.path:
|
|
47
|
+
node["path"] = args.path
|
|
48
|
+
if args.status:
|
|
49
|
+
node["status"] = args.status
|
|
50
|
+
if args.date:
|
|
51
|
+
node["date"] = args.date
|
|
52
|
+
|
|
53
|
+
node["manual"] = True
|
|
54
|
+
|
|
55
|
+
graph["nodes"][node_id] = node
|
|
56
|
+
return {"success": True, "action": "add-node", "node": node_id}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_remove_node(args, graph):
|
|
60
|
+
"""Remove a node and its connected edges from the graph."""
|
|
61
|
+
node_id = args.id
|
|
62
|
+
if node_id not in graph["nodes"]:
|
|
63
|
+
fail(f"Node not found: {node_id}")
|
|
64
|
+
|
|
65
|
+
del graph["nodes"][node_id]
|
|
66
|
+
|
|
67
|
+
# Remove connected edges
|
|
68
|
+
before = len(graph["edges"])
|
|
69
|
+
graph["edges"] = [
|
|
70
|
+
e for e in graph["edges"]
|
|
71
|
+
if e["from"] != node_id and e["to"] != node_id
|
|
72
|
+
]
|
|
73
|
+
removed_edges = before - len(graph["edges"])
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"success": True,
|
|
77
|
+
"action": "remove-node",
|
|
78
|
+
"removed": node_id,
|
|
79
|
+
"removedEdges": removed_edges,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_add_edge(args, graph):
|
|
84
|
+
"""Add an edge to the graph."""
|
|
85
|
+
if args.rel not in VALID_REL_TYPES:
|
|
86
|
+
fail(f"Invalid relationship: {args.rel}. Valid: {', '.join(sorted(VALID_REL_TYPES))}")
|
|
87
|
+
|
|
88
|
+
# Validate nodes exist
|
|
89
|
+
if args.source not in graph["nodes"]:
|
|
90
|
+
fail(f"Source node not found: {args.source}")
|
|
91
|
+
if args.target not in graph["nodes"]:
|
|
92
|
+
fail(f"Target node not found: {args.target}")
|
|
93
|
+
|
|
94
|
+
# Check for duplicate
|
|
95
|
+
for edge in graph["edges"]:
|
|
96
|
+
if edge["from"] == args.source and edge["to"] == args.target and edge["rel"] == args.rel:
|
|
97
|
+
fail(f"Edge already exists: {args.source} --{args.rel}--> {args.target}")
|
|
98
|
+
|
|
99
|
+
edge = {
|
|
100
|
+
"from": args.source,
|
|
101
|
+
"to": args.target,
|
|
102
|
+
"rel": args.rel,
|
|
103
|
+
"weight": args.weight,
|
|
104
|
+
}
|
|
105
|
+
if args.manual:
|
|
106
|
+
edge["manual"] = True
|
|
107
|
+
|
|
108
|
+
graph["edges"].append(edge)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"success": True,
|
|
112
|
+
"action": "add-edge",
|
|
113
|
+
"edge": f"{args.source} --{args.rel}--> {args.target}",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_remove_edge(args, graph):
|
|
118
|
+
"""Remove an edge from the graph."""
|
|
119
|
+
before = len(graph["edges"])
|
|
120
|
+
graph["edges"] = [
|
|
121
|
+
e for e in graph["edges"]
|
|
122
|
+
if not (e["from"] == args.source and e["to"] == args.target and e["rel"] == args.rel)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
if len(graph["edges"]) == before:
|
|
126
|
+
fail(f"Edge not found: {args.source} --{args.rel}--> {args.target}")
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"success": True,
|
|
130
|
+
"action": "remove-edge",
|
|
131
|
+
"removed": f"{args.source} --{args.rel}--> {args.target}",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
parser = argparse.ArgumentParser(description="Update the context graph")
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--root", type=str, default=None,
|
|
139
|
+
help="Project root (default: auto-detect)"
|
|
140
|
+
)
|
|
141
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
142
|
+
|
|
143
|
+
# add-node
|
|
144
|
+
p_add_node = subparsers.add_parser("add-node", help="Add a node")
|
|
145
|
+
p_add_node.add_argument("id", help="Node ID (e.g., module:auth)")
|
|
146
|
+
p_add_node.add_argument("--type", required=True, help="Node type")
|
|
147
|
+
p_add_node.add_argument("--label", help="Human-readable label")
|
|
148
|
+
p_add_node.add_argument("--path", help="File path relative to project root")
|
|
149
|
+
p_add_node.add_argument("--status", help="Node status")
|
|
150
|
+
p_add_node.add_argument("--date", help="Date for temporal nodes (ISO format)")
|
|
151
|
+
|
|
152
|
+
# remove-node
|
|
153
|
+
p_rm_node = subparsers.add_parser("remove-node", help="Remove a node")
|
|
154
|
+
p_rm_node.add_argument("id", help="Node ID to remove")
|
|
155
|
+
|
|
156
|
+
# add-edge
|
|
157
|
+
p_add_edge = subparsers.add_parser("add-edge", help="Add an edge")
|
|
158
|
+
p_add_edge.add_argument("source", help="Source node ID")
|
|
159
|
+
p_add_edge.add_argument("target", help="Target node ID")
|
|
160
|
+
p_add_edge.add_argument("rel", help="Relationship type (e.g., EXTENDS)")
|
|
161
|
+
p_add_edge.add_argument("--weight", type=float, default=0.8, help="Edge weight 0.0-1.0")
|
|
162
|
+
p_add_edge.add_argument("--manual", action="store_true", help="Mark as manually added")
|
|
163
|
+
|
|
164
|
+
# remove-edge
|
|
165
|
+
p_rm_edge = subparsers.add_parser("remove-edge", help="Remove an edge")
|
|
166
|
+
p_rm_edge.add_argument("source", help="Source node ID")
|
|
167
|
+
p_rm_edge.add_argument("target", help="Target node ID")
|
|
168
|
+
p_rm_edge.add_argument("rel", help="Relationship type")
|
|
169
|
+
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
|
|
172
|
+
root = Path(args.root) if args.root else find_project_root()
|
|
173
|
+
if not root:
|
|
174
|
+
fail("Could not find project root (no .flydocs/ directory found)")
|
|
175
|
+
|
|
176
|
+
graph = load_graph(root)
|
|
177
|
+
|
|
178
|
+
if args.command == "add-node":
|
|
179
|
+
result = cmd_add_node(args, graph)
|
|
180
|
+
elif args.command == "remove-node":
|
|
181
|
+
result = cmd_remove_node(args, graph)
|
|
182
|
+
elif args.command == "add-edge":
|
|
183
|
+
result = cmd_add_edge(args, graph)
|
|
184
|
+
elif args.command == "remove-edge":
|
|
185
|
+
result = cmd_remove_edge(args, graph)
|
|
186
|
+
else:
|
|
187
|
+
fail(f"Unknown command: {args.command}")
|
|
188
|
+
|
|
189
|
+
save_graph(root, graph)
|
|
190
|
+
output_json(result)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
main()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared utilities for context graph scripts."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
GRAPH_REL_PATH = os.path.join("flydocs", "context", "graph.json")
|
|
13
|
+
|
|
14
|
+
VALID_NODE_TYPES = {"skill", "decision", "issue", "module", "session", "concept"}
|
|
15
|
+
VALID_REL_TYPES = {
|
|
16
|
+
"EXTENDS", "IMPLEMENTS", "DELEGATES_TO", "PRECEDES", "MODIFIES",
|
|
17
|
+
"WORKED_ON", "PRODUCED", "RELATES_TO", "SUPERSEDES", "BLOCKS",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_project_root(start=None):
|
|
22
|
+
"""Walk up from start (or cwd) to find the directory containing .flydocs/."""
|
|
23
|
+
current = Path(start) if start else Path.cwd()
|
|
24
|
+
for parent in [current] + list(current.parents):
|
|
25
|
+
if (parent / ".flydocs").is_dir():
|
|
26
|
+
return parent
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def graph_path(root):
|
|
31
|
+
"""Return the absolute path to graph.json."""
|
|
32
|
+
return os.path.join(str(root), GRAPH_REL_PATH)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def empty_graph():
|
|
36
|
+
"""Return a new empty graph structure."""
|
|
37
|
+
return {
|
|
38
|
+
"version": 1,
|
|
39
|
+
"updated": datetime.now(timezone.utc).isoformat(),
|
|
40
|
+
"nodes": {},
|
|
41
|
+
"edges": [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_graph(root):
|
|
46
|
+
"""Load graph.json from the project root. Returns empty graph if missing."""
|
|
47
|
+
path = graph_path(root)
|
|
48
|
+
if not os.path.exists(path):
|
|
49
|
+
return empty_graph()
|
|
50
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
51
|
+
return json.load(f)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_graph(root, graph):
|
|
55
|
+
"""Write graph.json to the project root. Creates parent dirs if needed."""
|
|
56
|
+
path = graph_path(root)
|
|
57
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
58
|
+
graph["updated"] = datetime.now(timezone.utc).isoformat()
|
|
59
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
60
|
+
json.dump(graph, f, indent=2)
|
|
61
|
+
f.write("\n")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def output_json(data):
|
|
65
|
+
"""Print JSON to stdout."""
|
|
66
|
+
print(json.dumps(data, indent=2))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fail(message):
|
|
70
|
+
"""Print error to stderr and exit 1."""
|
|
71
|
+
print(message, file=sys.stderr)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_frontmatter(text):
|
|
76
|
+
"""Parse YAML frontmatter from a SKILL.md file (no PyYAML dependency).
|
|
77
|
+
|
|
78
|
+
Returns dict with 'name', 'description', 'triggers' keys or None.
|
|
79
|
+
"""
|
|
80
|
+
match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
|
|
81
|
+
if not match:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
block = match.group(1)
|
|
85
|
+
result = {}
|
|
86
|
+
current_key = None
|
|
87
|
+
current_list = None
|
|
88
|
+
|
|
89
|
+
for line in block.split("\n"):
|
|
90
|
+
# List item
|
|
91
|
+
if re.match(r"^\s+-\s+", line) and current_key:
|
|
92
|
+
value = re.sub(r"^\s+-\s+", "", line).strip()
|
|
93
|
+
if current_list is None:
|
|
94
|
+
current_list = []
|
|
95
|
+
current_list.append(value)
|
|
96
|
+
result[current_key] = current_list
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Key: value
|
|
100
|
+
kv = re.match(r"^(\w+)\s*:\s*(.*)", line)
|
|
101
|
+
if kv:
|
|
102
|
+
if current_list is not None:
|
|
103
|
+
current_list = None
|
|
104
|
+
current_key = kv.group(1)
|
|
105
|
+
value = kv.group(2).strip().rstrip("|").rstrip(">").strip()
|
|
106
|
+
if value:
|
|
107
|
+
result[current_key] = value
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Continuation of block scalar
|
|
111
|
+
if current_key and current_key in result and isinstance(result[current_key], str):
|
|
112
|
+
result[current_key] = result[current_key] + " " + line.strip()
|
|
113
|
+
|
|
114
|
+
# Clean up description whitespace
|
|
115
|
+
if "description" in result and isinstance(result["description"], str):
|
|
116
|
+
result["description"] = re.sub(r"\s+", " ", result["description"]).strip()
|
|
117
|
+
|
|
118
|
+
return result
|