janet-cli 0.2.8__py3-none-any.whl → 0.2.33__py3-none-any.whl
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.
- janet/__init__.py +1 -1
- janet/api/client.py +36 -0
- janet/api/projects.py +20 -0
- janet/api/tickets.py +144 -1
- janet/auth/callback_server.py +5 -4
- janet/auth/oauth_flow.py +0 -1
- janet/auth/token_manager.py +31 -5
- janet/cli.py +512 -10
- janet/config/models.py +13 -1
- janet/markdown/generator.py +74 -21
- janet/markdown/yjs_converter.py +103 -17
- janet/sync/readme_generator.py +186 -90
- janet/sync/sse_watcher.py +264 -0
- janet/sync/sync_engine.py +14 -5
- janet_cli-0.2.33.dist-info/METADATA +356 -0
- janet_cli-0.2.33.dist-info/RECORD +34 -0
- janet_cli-0.2.8.dist-info/METADATA +0 -215
- janet_cli-0.2.8.dist-info/RECORD +0 -33
- {janet_cli-0.2.8.dist-info → janet_cli-0.2.33.dist-info}/WHEEL +0 -0
- {janet_cli-0.2.8.dist-info → janet_cli-0.2.33.dist-info}/entry_points.txt +0 -0
- {janet_cli-0.2.8.dist-info → janet_cli-0.2.33.dist-info}/licenses/LICENSE +0 -0
- {janet_cli-0.2.8.dist-info → janet_cli-0.2.33.dist-info}/top_level.txt +0 -0
janet/markdown/generator.py
CHANGED
|
@@ -44,8 +44,8 @@ class MarkdownGenerator:
|
|
|
44
44
|
# 2. Metadata
|
|
45
45
|
sections.append(self._generate_metadata(ticket, organization_members))
|
|
46
46
|
|
|
47
|
-
# 3. Description
|
|
48
|
-
sections.append(self._generate_description(ticket))
|
|
47
|
+
# 3. Description (pass attachments for inline image handling)
|
|
48
|
+
sections.append(self._generate_description(ticket, attachments))
|
|
49
49
|
|
|
50
50
|
# 4. Comments
|
|
51
51
|
if ticket.get("comments"):
|
|
@@ -84,6 +84,9 @@ class MarkdownGenerator:
|
|
|
84
84
|
# Assignees
|
|
85
85
|
assignees = ticket.get("assignees", [])
|
|
86
86
|
if assignees:
|
|
87
|
+
# Handle case where assignees is a string instead of a list
|
|
88
|
+
if isinstance(assignees, str):
|
|
89
|
+
assignees = [assignees]
|
|
87
90
|
assignee_names = [
|
|
88
91
|
self._resolve_user_name(email, organization_members) for email in assignees
|
|
89
92
|
]
|
|
@@ -96,39 +99,61 @@ class MarkdownGenerator:
|
|
|
96
99
|
creator_name = self._resolve_user_name(creator, organization_members)
|
|
97
100
|
lines.append(f"- **Creator:** {creator_name}")
|
|
98
101
|
|
|
102
|
+
# Tags/Labels
|
|
103
|
+
labels = ticket.get("labels", [])
|
|
104
|
+
if labels:
|
|
105
|
+
# Handle case where labels is a string instead of a list
|
|
106
|
+
if isinstance(labels, str):
|
|
107
|
+
labels = [labels]
|
|
108
|
+
lines.append(f"- **Tags:** {', '.join(labels)}")
|
|
109
|
+
else:
|
|
110
|
+
lines.append("- **Tags:** None")
|
|
111
|
+
|
|
99
112
|
# Dates
|
|
100
113
|
created_at = ticket.get("created_at", "")
|
|
101
114
|
updated_at = ticket.get("updated_at", "")
|
|
102
115
|
lines.append(f"- **Created:** {self._format_date(created_at)}")
|
|
103
116
|
lines.append(f"- **Updated:** {self._format_date(updated_at)}")
|
|
104
117
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
lines.append(
|
|
118
|
+
# Due Date (always shown)
|
|
119
|
+
due_date = ticket.get("due_date")
|
|
120
|
+
if due_date:
|
|
121
|
+
lines.append(f"- **Due Date:** {self._format_date(due_date)}")
|
|
122
|
+
else:
|
|
123
|
+
lines.append("- **Due Date:** None")
|
|
111
124
|
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
# Story Points (always shown)
|
|
126
|
+
story_points = ticket.get("story_points")
|
|
127
|
+
if story_points:
|
|
128
|
+
lines.append(f"- **Story Points:** {story_points}")
|
|
129
|
+
else:
|
|
130
|
+
lines.append("- **Story Points:** None")
|
|
114
131
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
# Sprint (always shown)
|
|
133
|
+
sprint = ticket.get("sprint")
|
|
134
|
+
if sprint:
|
|
135
|
+
lines.append(f"- **Sprint:** {sprint}")
|
|
136
|
+
else:
|
|
137
|
+
lines.append("- **Sprint:** None")
|
|
119
138
|
|
|
120
139
|
lines.append("") # Empty line after metadata
|
|
121
140
|
return "\n".join(lines)
|
|
122
141
|
|
|
123
|
-
def _generate_description(self, ticket: Dict) -> str:
|
|
142
|
+
def _generate_description(self, ticket: Dict, attachments: Optional[Dict] = None) -> str:
|
|
124
143
|
"""Generate description section."""
|
|
125
144
|
lines = ["## Description\n"]
|
|
126
145
|
|
|
127
146
|
yjs_binary = ticket.get("description_yjs_binary")
|
|
128
147
|
plain_text = ticket.get("description")
|
|
129
148
|
|
|
130
|
-
#
|
|
131
|
-
|
|
149
|
+
# Combine all attachments for image matching
|
|
150
|
+
all_attachments = []
|
|
151
|
+
if attachments:
|
|
152
|
+
all_attachments.extend(attachments.get("direct_attachments", []))
|
|
153
|
+
all_attachments.extend(attachments.get("indirect_attachments", []))
|
|
154
|
+
|
|
155
|
+
# Convert Yjs binary to markdown with attachment info for images
|
|
156
|
+
markdown = self.yjs_converter.convert(yjs_binary, plain_text, all_attachments)
|
|
132
157
|
lines.append(markdown)
|
|
133
158
|
|
|
134
159
|
lines.append("") # Empty line after description
|
|
@@ -170,6 +195,11 @@ class MarkdownGenerator:
|
|
|
170
195
|
|
|
171
196
|
lines.append(f"- **Type:** {attachment.get('mime_type', 'Unknown')}")
|
|
172
197
|
|
|
198
|
+
# Format file size
|
|
199
|
+
file_size = attachment.get("file_size_bytes") or attachment.get("file_size")
|
|
200
|
+
if file_size:
|
|
201
|
+
lines.append(f"- **Size:** {self._format_file_size(file_size)}")
|
|
202
|
+
|
|
173
203
|
uploader = attachment.get("uploaded_by", "Unknown")
|
|
174
204
|
uploader_name = self._resolve_user_name(uploader, organization_members)
|
|
175
205
|
lines.append(f"- **Uploaded by:** {uploader_name}")
|
|
@@ -245,13 +275,13 @@ class MarkdownGenerator:
|
|
|
245
275
|
|
|
246
276
|
def _format_date(self, iso_string: str) -> str:
|
|
247
277
|
"""
|
|
248
|
-
Format ISO timestamp to human-readable date.
|
|
278
|
+
Format ISO timestamp to human-readable date with timezone.
|
|
249
279
|
|
|
250
280
|
Args:
|
|
251
281
|
iso_string: ISO 8601 timestamp
|
|
252
282
|
|
|
253
283
|
Returns:
|
|
254
|
-
Formatted date string
|
|
284
|
+
Formatted date string with UTC timezone
|
|
255
285
|
"""
|
|
256
286
|
if not iso_string:
|
|
257
287
|
return "Unknown"
|
|
@@ -265,8 +295,31 @@ class MarkdownGenerator:
|
|
|
265
295
|
# No microseconds
|
|
266
296
|
dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
|
|
267
297
|
|
|
268
|
-
# Format: Jan 15, 2024 10:30 AM
|
|
269
|
-
return dt.strftime("%b %d, %Y %I:%M %p")
|
|
298
|
+
# Format: Jan 15, 2024 10:30 AM UTC
|
|
299
|
+
return dt.strftime("%b %d, %Y %I:%M %p") + " UTC"
|
|
270
300
|
except Exception:
|
|
271
301
|
# Fallback: return as-is
|
|
272
302
|
return iso_string
|
|
303
|
+
|
|
304
|
+
def _format_file_size(self, size_bytes) -> str:
|
|
305
|
+
"""
|
|
306
|
+
Format file size in human-readable format.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
size_bytes: File size in bytes
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Formatted size string (e.g., "1.5 MB")
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
size = int(size_bytes)
|
|
316
|
+
if size < 1024:
|
|
317
|
+
return f"{size} B"
|
|
318
|
+
elif size < 1024 * 1024:
|
|
319
|
+
return f"{size / 1024:.1f} KB"
|
|
320
|
+
elif size < 1024 * 1024 * 1024:
|
|
321
|
+
return f"{size / (1024 * 1024):.1f} MB"
|
|
322
|
+
else:
|
|
323
|
+
return f"{size / (1024 * 1024 * 1024):.1f} GB"
|
|
324
|
+
except (ValueError, TypeError):
|
|
325
|
+
return "Unknown"
|
janet/markdown/yjs_converter.py
CHANGED
|
@@ -16,33 +16,38 @@ class YjsConverter:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
def convert(
|
|
19
|
-
self,
|
|
19
|
+
self,
|
|
20
|
+
yjs_binary_base64: Optional[str],
|
|
21
|
+
plain_text_fallback: Optional[str] = None,
|
|
22
|
+
attachments: Optional[List[dict]] = None,
|
|
20
23
|
) -> str:
|
|
21
24
|
"""
|
|
22
25
|
Convert Yjs binary to markdown.
|
|
23
26
|
|
|
24
27
|
Args:
|
|
25
28
|
yjs_binary_base64: Base64-encoded Yjs binary
|
|
26
|
-
plain_text_fallback: Plain text description
|
|
29
|
+
plain_text_fallback: Plain text description to use as fallback
|
|
30
|
+
attachments: List of attachment dicts with ai_description for inline image handling
|
|
27
31
|
|
|
28
32
|
Returns:
|
|
29
33
|
Markdown string
|
|
30
34
|
"""
|
|
31
|
-
|
|
32
|
-
if not yjs_binary_base64:
|
|
33
|
-
return "*No description provided*"
|
|
35
|
+
self._attachments = attachments or []
|
|
34
36
|
|
|
35
|
-
try
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
# If we have Yjs binary, try to convert it
|
|
38
|
+
if yjs_binary_base64:
|
|
39
|
+
try:
|
|
40
|
+
markdown = self._convert_with_pycrdt(yjs_binary_base64)
|
|
41
|
+
if markdown and markdown.strip():
|
|
42
|
+
return markdown
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.warning(f"Yjs conversion failed, falling back to plain text: {e}")
|
|
45
|
+
|
|
46
|
+
# Fall back to plain text if Yjs conversion failed or returned empty
|
|
47
|
+
if plain_text_fallback and plain_text_fallback.strip():
|
|
48
|
+
return plain_text_fallback.strip()
|
|
49
|
+
|
|
50
|
+
return "*No description provided*"
|
|
46
51
|
|
|
47
52
|
def _convert_with_pycrdt(self, base64_str: str) -> str:
|
|
48
53
|
"""
|
|
@@ -190,17 +195,98 @@ class YjsConverter:
|
|
|
190
195
|
lines.append("```")
|
|
191
196
|
lines.append("")
|
|
192
197
|
|
|
198
|
+
elif node_name == "image":
|
|
199
|
+
# Handle image nodes - show placeholder with AI description if available
|
|
200
|
+
url = node.attributes.get("url", "") or node.attributes.get("src", "")
|
|
201
|
+
name = node.attributes.get("name", "")
|
|
202
|
+
caption = node.attributes.get("caption", "")
|
|
203
|
+
|
|
204
|
+
# Try to find matching attachment by URL
|
|
205
|
+
ai_description = None
|
|
206
|
+
filename = name or "image"
|
|
207
|
+
|
|
208
|
+
for attachment in self._attachments:
|
|
209
|
+
att_url = attachment.get("url", "") or attachment.get("file_url", "")
|
|
210
|
+
att_filename = attachment.get("original_filename", "")
|
|
211
|
+
# Match by URL or filename
|
|
212
|
+
if (url and att_url and url in att_url) or \
|
|
213
|
+
(name and att_filename and name in att_filename) or \
|
|
214
|
+
(att_url and att_url in url):
|
|
215
|
+
ai_description = attachment.get("ai_description")
|
|
216
|
+
filename = att_filename or filename
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
lines.append(f"[Image: {filename}]")
|
|
220
|
+
if ai_description:
|
|
221
|
+
lines.append(f"*{ai_description}*")
|
|
222
|
+
elif caption:
|
|
223
|
+
lines.append(f"*{caption}*")
|
|
224
|
+
lines.append("")
|
|
225
|
+
|
|
193
226
|
elif node_name == "hardBreak":
|
|
194
227
|
# Hard breaks are inline - ignore them as we add blank lines between blocks
|
|
195
228
|
pass
|
|
196
229
|
|
|
230
|
+
elif node_name == "table":
|
|
231
|
+
# Handle table - recurse into rows
|
|
232
|
+
for child in children:
|
|
233
|
+
process_node(child, indent, add_blank_line=False)
|
|
234
|
+
lines.append("")
|
|
235
|
+
|
|
236
|
+
elif node_name == "tableRow":
|
|
237
|
+
# Collect cell contents for this row
|
|
238
|
+
row_cells = []
|
|
239
|
+
for child in children:
|
|
240
|
+
if isinstance(child, XmlElement) and child.tag in ("tableCell", "tableHeader"):
|
|
241
|
+
cell_text = "".join(
|
|
242
|
+
str(c) for c in child.children if isinstance(c, XmlText)
|
|
243
|
+
).strip()
|
|
244
|
+
row_cells.append(cell_text or " ")
|
|
245
|
+
if row_cells:
|
|
246
|
+
lines.append("| " + " | ".join(row_cells) + " |")
|
|
247
|
+
|
|
248
|
+
elif node_name in ("tableCell", "tableHeader"):
|
|
249
|
+
# Handled by tableRow
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
elif node_name == "file":
|
|
253
|
+
# File attachment block
|
|
254
|
+
filename = node.attributes.get("name", "file")
|
|
255
|
+
lines.append(f"[File: {filename}]")
|
|
256
|
+
lines.append("")
|
|
257
|
+
|
|
258
|
+
elif node_name == "video":
|
|
259
|
+
# Video block
|
|
260
|
+
url = node.attributes.get("url", "")
|
|
261
|
+
name = node.attributes.get("name", "video")
|
|
262
|
+
lines.append(f"[Video: {name}]")
|
|
263
|
+
lines.append("")
|
|
264
|
+
|
|
265
|
+
elif node_name == "audio":
|
|
266
|
+
# Audio block
|
|
267
|
+
url = node.attributes.get("url", "")
|
|
268
|
+
name = node.attributes.get("name", "audio")
|
|
269
|
+
lines.append(f"[Audio: {name}]")
|
|
270
|
+
lines.append("")
|
|
271
|
+
|
|
272
|
+
elif node_name in ("divider", "horizontalRule"):
|
|
273
|
+
lines.append("---")
|
|
274
|
+
lines.append("")
|
|
275
|
+
|
|
276
|
+
elif node_name == "alert":
|
|
277
|
+
# Alert/callout block
|
|
278
|
+
alert_type = node.attributes.get("type", "info")
|
|
279
|
+
if text_content:
|
|
280
|
+
lines.append(f"> **{alert_type.upper()}:** {text_content}")
|
|
281
|
+
lines.append("")
|
|
282
|
+
|
|
197
283
|
# Handle empty paragraphs or unknown blocks
|
|
198
284
|
elif text_content:
|
|
199
285
|
lines.append(text_content)
|
|
200
286
|
lines.append("")
|
|
201
287
|
|
|
202
288
|
# Always process children for unknown types
|
|
203
|
-
elif node_name not in ("hardBreak",):
|
|
289
|
+
elif node_name not in ("hardBreak", "tableCell", "tableHeader"):
|
|
204
290
|
for child in children:
|
|
205
291
|
process_node(child, indent, add_blank_line=True)
|
|
206
292
|
|
janet/sync/readme_generator.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import List, Dict
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ReadmeGenerator:
|
|
@@ -14,132 +14,226 @@ class ReadmeGenerator:
|
|
|
14
14
|
projects: List[Dict],
|
|
15
15
|
total_tickets: int,
|
|
16
16
|
sync_time: datetime,
|
|
17
|
+
project_statuses: Optional[Dict[str, List[str]]] = None,
|
|
17
18
|
) -> str:
|
|
18
19
|
"""
|
|
19
20
|
Generate README content for ticket directory.
|
|
21
|
+
Written as instructions FOR the AI coding agent.
|
|
20
22
|
|
|
21
23
|
Args:
|
|
22
24
|
org_name: Organization name
|
|
23
25
|
projects: List of synced projects
|
|
24
26
|
total_tickets: Total number of tickets synced
|
|
25
27
|
sync_time: Timestamp of sync
|
|
28
|
+
project_statuses: Dict mapping project_identifier to list of valid statuses
|
|
26
29
|
|
|
27
30
|
Returns:
|
|
28
31
|
README markdown content
|
|
29
32
|
"""
|
|
30
33
|
sections = []
|
|
31
34
|
|
|
32
|
-
# Header
|
|
33
|
-
sections.append(f"#
|
|
35
|
+
# Header - addressing the AI agent directly
|
|
36
|
+
sections.append(f"# Project Tickets - {org_name}\n")
|
|
34
37
|
sections.append(
|
|
35
|
-
"This directory contains
|
|
36
|
-
"
|
|
38
|
+
"This directory contains project management tickets from Janet AI. "
|
|
39
|
+
"Use these tickets to understand requirements, track work, and stay aligned with project goals.\n"
|
|
37
40
|
)
|
|
38
41
|
|
|
39
|
-
#
|
|
40
|
-
sections.append("##
|
|
41
|
-
sections.append(
|
|
42
|
-
"These markdown files are a local mirror of your tickets from "
|
|
43
|
-
"[Janet AI](https://tryjanet.ai), an AI-native project management platform. "
|
|
44
|
-
"Each ticket has been exported as a markdown file containing:\n"
|
|
45
|
-
)
|
|
46
|
-
sections.append("- **Metadata** - Status, priority, assignees, dates, labels")
|
|
47
|
-
sections.append("- **Description** - Full ticket description")
|
|
48
|
-
sections.append("- **Comments** - All comments with timestamps")
|
|
49
|
-
sections.append("- **Attachments** - Attachment metadata and descriptions")
|
|
50
|
-
sections.append("- **Child Tasks** - Sub-tasks if applicable\n")
|
|
51
|
-
|
|
52
|
-
# Why is this here?
|
|
53
|
-
sections.append("## Why is this here?\n")
|
|
54
|
-
sections.append(
|
|
55
|
-
"AI coding assistants work best when they have full context about your project. "
|
|
56
|
-
"By having your tickets in your workspace, AI agents can:\n"
|
|
57
|
-
)
|
|
58
|
-
sections.append("- Reference specific tickets while writing code")
|
|
59
|
-
sections.append("- Understand requirements and acceptance criteria")
|
|
60
|
-
sections.append("- Answer questions about project priorities and status")
|
|
61
|
-
sections.append("- Suggest implementations based on ticket descriptions")
|
|
62
|
-
sections.append("- Link code changes to relevant tickets\n")
|
|
63
|
-
|
|
64
|
-
# Directory structure
|
|
65
|
-
sections.append("## Directory Structure\n")
|
|
66
|
-
sections.append("```")
|
|
67
|
-
sections.append(f"{org_name}/")
|
|
68
|
-
for project in projects[:10]: # Show first 10 projects
|
|
69
|
-
key = project.get("project_identifier", "")
|
|
70
|
-
name = project.get("project_name", "")
|
|
71
|
-
count = project.get("ticket_count", 0)
|
|
72
|
-
sections.append(f"├── {name}/")
|
|
73
|
-
sections.append(f"│ ├── {key}-1.md")
|
|
74
|
-
sections.append(f"│ ├── {key}-2.md")
|
|
75
|
-
sections.append(f"│ └── ... ({count} tickets)")
|
|
76
|
-
|
|
77
|
-
if len(projects) > 10:
|
|
78
|
-
sections.append(f"└── ... ({len(projects) - 10} more projects)")
|
|
79
|
-
sections.append("```\n")
|
|
80
|
-
|
|
81
|
-
# Sync info
|
|
82
|
-
sections.append("## Sync Information\n")
|
|
42
|
+
# Context for the AI
|
|
43
|
+
sections.append("## Context\n")
|
|
83
44
|
sections.append(f"- **Organization:** {org_name}")
|
|
84
|
-
sections.append(f"- **Projects
|
|
45
|
+
sections.append(f"- **Projects:** {len(projects)}")
|
|
85
46
|
sections.append(f"- **Total Tickets:** {total_tickets}")
|
|
86
47
|
sections.append(
|
|
87
48
|
f"- **Last Synced:** {sync_time.strftime('%B %d, %Y at %I:%M %p')}\n"
|
|
88
49
|
)
|
|
89
50
|
|
|
90
|
-
# Projects
|
|
51
|
+
# Projects list with statuses
|
|
91
52
|
if projects:
|
|
92
|
-
sections.append("
|
|
53
|
+
sections.append("## Projects\n")
|
|
93
54
|
for project in projects:
|
|
94
55
|
key = project.get("project_identifier", "")
|
|
95
56
|
name = project.get("project_name", "")
|
|
96
57
|
count = project.get("ticket_count", 0)
|
|
97
|
-
sections.append(f"
|
|
58
|
+
sections.append(f"### {key} - {name}\n")
|
|
59
|
+
sections.append(f"- **Tickets:** {count}")
|
|
60
|
+
|
|
61
|
+
# Show valid statuses for this project
|
|
62
|
+
if project_statuses and key in project_statuses:
|
|
63
|
+
statuses = project_statuses[key]
|
|
64
|
+
if statuses:
|
|
65
|
+
sections.append(f"- **Valid Statuses:** {', '.join(statuses)}")
|
|
66
|
+
sections.append("")
|
|
67
|
+
|
|
68
|
+
# Ticket format explanation
|
|
69
|
+
sections.append("## Ticket Format\n")
|
|
70
|
+
sections.append("Each `.md` file represents a ticket with:")
|
|
71
|
+
sections.append("- **Title** - Ticket key and summary (e.g., `PROJ-123: Add login feature`)")
|
|
72
|
+
sections.append("- **Metadata** - Status, priority, type, assignees, dates, sprint, story points, labels")
|
|
73
|
+
sections.append("- **Description** - Full requirements and details")
|
|
74
|
+
sections.append("- **Comments** - Discussion and updates")
|
|
75
|
+
sections.append("- **Attachments** - Files attached to the ticket with type, size, and AI-generated descriptions")
|
|
76
|
+
sections.append("- **Child Tasks** - Sub-tasks if applicable\n")
|
|
77
|
+
|
|
78
|
+
# Instructions for the AI
|
|
79
|
+
sections.append("## How to Use These Tickets\n")
|
|
80
|
+
sections.append("When working on this codebase:")
|
|
81
|
+
sections.append("1. **Reference tickets** - Read relevant tickets before implementing features")
|
|
82
|
+
sections.append("2. **Check status** - Verify ticket status before starting work")
|
|
83
|
+
sections.append("3. **Follow requirements** - Use ticket descriptions as specifications")
|
|
84
|
+
sections.append("4. **Note priorities** - High/Critical tickets should be addressed first\n")
|
|
85
|
+
|
|
86
|
+
# CLI commands for the AI - comprehensive reference
|
|
87
|
+
sections.append("## CLI Commands Reference\n")
|
|
88
|
+
sections.append("Use the `janet` CLI to create and update tickets.\n")
|
|
89
|
+
|
|
90
|
+
# Get context
|
|
91
|
+
sections.append("### Get Project Context\n")
|
|
92
|
+
sections.append("```bash")
|
|
93
|
+
sections.append("janet context --json")
|
|
94
|
+
sections.append("```")
|
|
95
|
+
sections.append("Returns available projects with their keys. Use this to find valid project keys.\n")
|
|
96
|
+
|
|
97
|
+
# Create ticket
|
|
98
|
+
sections.append("### Create a Ticket\n")
|
|
99
|
+
sections.append("```bash")
|
|
100
|
+
sections.append('janet ticket create "Title" --project <PROJECT_KEY> [options]')
|
|
101
|
+
sections.append("```\n")
|
|
102
|
+
sections.append("**Required:**")
|
|
103
|
+
sections.append("- `--project`, `-p` - Project key (e.g., MAIN, BACK, FRONT)\n")
|
|
104
|
+
sections.append("**Optional:**")
|
|
105
|
+
sections.append("- `--description`, `-d` - Detailed description")
|
|
106
|
+
sections.append("- `--status`, `-s` - Status (see valid statuses per project above)")
|
|
107
|
+
sections.append("- `--priority` - Low, Medium, High, Critical")
|
|
108
|
+
sections.append("- `--type`, `-t` - Task, Bug, Story, Epic")
|
|
109
|
+
sections.append("- `--assignee`, `-a` - Assignee email (can use multiple times)")
|
|
110
|
+
sections.append("- `--tag` - Label/tag (can use multiple times)")
|
|
111
|
+
sections.append("- `--json` - Output as JSON\n")
|
|
112
|
+
sections.append("**Example:**")
|
|
113
|
+
sections.append("```bash")
|
|
114
|
+
sections.append('janet ticket create "Fix authentication bug" \\')
|
|
115
|
+
sections.append(' --project BACK \\')
|
|
116
|
+
sections.append(' --description "Users are getting logged out unexpectedly" \\')
|
|
117
|
+
sections.append(' --priority High \\')
|
|
118
|
+
sections.append(' --type Bug \\')
|
|
119
|
+
sections.append(' --tag backend \\')
|
|
120
|
+
sections.append(' --tag auth')
|
|
121
|
+
sections.append("```\n")
|
|
122
|
+
|
|
123
|
+
# Update ticket
|
|
124
|
+
sections.append("### Update a Ticket\n")
|
|
125
|
+
sections.append("```bash")
|
|
126
|
+
sections.append('janet ticket update <TICKET_KEY> [options]')
|
|
127
|
+
sections.append("```\n")
|
|
128
|
+
sections.append("**Options:**")
|
|
129
|
+
sections.append("- `--status`, `-s` - New status (see valid statuses per project above)")
|
|
130
|
+
sections.append("- `--title` - New title")
|
|
131
|
+
sections.append("- `--description`, `-d` - New description")
|
|
132
|
+
sections.append("- `--priority` - Low, Medium, High, Critical")
|
|
133
|
+
sections.append("- `--type`, `-t` - Task, Bug, Story, Epic")
|
|
134
|
+
sections.append("- `--assignee`, `-a` - New assignee(s)")
|
|
135
|
+
sections.append("- `--tag` - New tag(s)")
|
|
136
|
+
sections.append("- `--json` - Output as JSON\n")
|
|
137
|
+
sections.append("**Examples:**")
|
|
138
|
+
sections.append("```bash")
|
|
139
|
+
sections.append("# Starting work on a ticket")
|
|
140
|
+
sections.append('janet ticket update MAIN-123 --status "In Progress"')
|
|
141
|
+
sections.append("")
|
|
142
|
+
sections.append("# Marking ticket as complete")
|
|
143
|
+
sections.append('janet ticket update MAIN-123 --status "Done"')
|
|
144
|
+
sections.append("")
|
|
145
|
+
sections.append("# Ticket is blocked by external dependency")
|
|
146
|
+
sections.append('janet ticket update MAIN-123 --status "Blocked"')
|
|
147
|
+
sections.append("")
|
|
148
|
+
sections.append("# Update priority and add description")
|
|
149
|
+
sections.append('janet ticket update MAIN-123 --priority Critical --description "Causing production issues"')
|
|
150
|
+
sections.append("```\n")
|
|
151
|
+
|
|
152
|
+
# Project selection guidance
|
|
153
|
+
sections.append("### When to Ask User for Project\n")
|
|
154
|
+
sections.append("If the user doesn't specify which project to use:")
|
|
155
|
+
sections.append("1. Check if there's only one project - use it automatically")
|
|
156
|
+
sections.append("2. If multiple projects exist, ask the user which project to use")
|
|
157
|
+
sections.append("3. Show available projects from the list above\n")
|
|
158
|
+
|
|
159
|
+
# Valid field values - now dynamic per project
|
|
160
|
+
sections.append("### Valid Field Values\n")
|
|
161
|
+
|
|
162
|
+
# Show statuses per project
|
|
163
|
+
if project_statuses and projects:
|
|
164
|
+
sections.append("**Status (varies by project):**")
|
|
165
|
+
for project in projects:
|
|
166
|
+
key = project.get("project_identifier", "")
|
|
167
|
+
if key in project_statuses and project_statuses[key]:
|
|
168
|
+
sections.append(f"- {key}: {', '.join(project_statuses[key])}")
|
|
98
169
|
sections.append("")
|
|
170
|
+
else:
|
|
171
|
+
sections.append("**Status:** Run `janet context --json` to see valid statuses per project\n")
|
|
99
172
|
|
|
100
|
-
|
|
101
|
-
sections.append("
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
sections.append(
|
|
107
|
-
sections.append(
|
|
108
|
-
sections.append('"Look at ticket CS-42 and implement the authentication flow"')
|
|
109
|
-
sections.append('"What are the high priority tickets in the Software project?"')
|
|
110
|
-
sections.append('"Which tickets are assigned to me?"')
|
|
111
|
-
sections.append('"Implement the feature described in HL-15"')
|
|
112
|
-
sections.append('```\n')
|
|
113
|
-
|
|
114
|
-
# Keeping in sync
|
|
115
|
-
sections.append("## Keeping Tickets in Sync\n")
|
|
116
|
-
sections.append("To update tickets with latest changes from Janet AI:\n")
|
|
173
|
+
sections.append("**Priority:** Low, Medium, High, Critical")
|
|
174
|
+
sections.append("**Type:** Task, Bug, Story, Epic\n")
|
|
175
|
+
|
|
176
|
+
# Common workflow examples
|
|
177
|
+
sections.append("## Common Workflow Examples\n")
|
|
178
|
+
|
|
179
|
+
sections.append("### Example 1: Bug Fix Workflow")
|
|
180
|
+
sections.append("When you identify and fix a bug:\n")
|
|
117
181
|
sections.append("```bash")
|
|
118
|
-
sections.append("
|
|
182
|
+
sections.append("# 1. Create a bug ticket")
|
|
183
|
+
sections.append('janet ticket create "Fix null pointer in user service" \\')
|
|
184
|
+
sections.append(' --project BACK \\')
|
|
185
|
+
sections.append(' --type Bug \\')
|
|
186
|
+
sections.append(' --priority High \\')
|
|
187
|
+
sections.append(' --description "getUserById throws NPE when user not found"')
|
|
188
|
+
sections.append("")
|
|
189
|
+
sections.append("# 2. Start working on it")
|
|
190
|
+
sections.append('janet ticket update BACK-42 --status "In Progress"')
|
|
191
|
+
sections.append("")
|
|
192
|
+
sections.append("# 3. Mark as done after fixing")
|
|
193
|
+
sections.append('janet ticket update BACK-42 --status "Done"')
|
|
119
194
|
sections.append("```\n")
|
|
120
|
-
sections.append(
|
|
121
|
-
"This will update all changed tickets and add new ones. "
|
|
122
|
-
"Run this regularly to keep your local tickets up to date.\n"
|
|
123
|
-
)
|
|
124
195
|
|
|
125
|
-
|
|
126
|
-
sections.append("
|
|
127
|
-
sections.append(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
sections.append(
|
|
132
|
-
sections.append(
|
|
133
|
-
sections.append("
|
|
134
|
-
sections.append(
|
|
135
|
-
sections.append(
|
|
136
|
-
sections.append("
|
|
137
|
-
|
|
196
|
+
sections.append("### Example 2: Feature Implementation")
|
|
197
|
+
sections.append("When implementing a new feature:\n")
|
|
198
|
+
sections.append("```bash")
|
|
199
|
+
sections.append("# Create a story for the feature")
|
|
200
|
+
sections.append('janet ticket create "Add dark mode support" \\')
|
|
201
|
+
sections.append(' --project FRONT \\')
|
|
202
|
+
sections.append(' --type Story \\')
|
|
203
|
+
sections.append(' --priority Medium \\')
|
|
204
|
+
sections.append(' --description "Implement dark mode toggle in settings" \\')
|
|
205
|
+
sections.append(' --tag ui \\')
|
|
206
|
+
sections.append(' --tag feature')
|
|
207
|
+
sections.append("```\n")
|
|
208
|
+
|
|
209
|
+
sections.append("### Example 3: Using JSON Output")
|
|
210
|
+
sections.append("When you need to parse the response programmatically:\n")
|
|
211
|
+
sections.append("```bash")
|
|
212
|
+
sections.append("# Get project info as JSON")
|
|
213
|
+
sections.append("janet context --json")
|
|
214
|
+
sections.append("")
|
|
215
|
+
sections.append("# Create ticket and capture the ticket key")
|
|
216
|
+
sections.append('janet ticket create "Automated task" --project MAIN --json')
|
|
217
|
+
sections.append("```\n")
|
|
218
|
+
|
|
219
|
+
# Directory structure
|
|
220
|
+
sections.append("## File Structure\n")
|
|
221
|
+
sections.append("```")
|
|
222
|
+
sections.append(f"{org_name}/")
|
|
223
|
+
for project in projects[:5]: # Show first 5 projects
|
|
224
|
+
key = project.get("project_identifier", "")
|
|
225
|
+
name = project.get("project_name", "")
|
|
226
|
+
sections.append(f"└── {name}/")
|
|
227
|
+
sections.append(f" ├── {key}-1.md")
|
|
228
|
+
sections.append(f" └── ...")
|
|
229
|
+
if len(projects) > 5:
|
|
230
|
+
sections.append(f"└── ... ({len(projects) - 5} more projects)")
|
|
231
|
+
sections.append("```\n")
|
|
138
232
|
|
|
139
233
|
# Footer
|
|
140
234
|
sections.append("---\n")
|
|
141
235
|
sections.append(
|
|
142
|
-
f"*
|
|
236
|
+
f"*Synced from [Janet AI](https://app.tryjanet.ai) "
|
|
143
237
|
f"on {sync_time.strftime('%B %d, %Y at %I:%M %p')}*\n"
|
|
144
238
|
)
|
|
145
239
|
|
|
@@ -151,6 +245,7 @@ class ReadmeGenerator:
|
|
|
151
245
|
org_name: str,
|
|
152
246
|
projects: List[Dict],
|
|
153
247
|
total_tickets: int,
|
|
248
|
+
project_statuses: Optional[Dict[str, List[str]]] = None,
|
|
154
249
|
) -> Path:
|
|
155
250
|
"""
|
|
156
251
|
Write README.md to sync directory.
|
|
@@ -160,12 +255,13 @@ class ReadmeGenerator:
|
|
|
160
255
|
org_name: Organization name
|
|
161
256
|
projects: List of synced projects
|
|
162
257
|
total_tickets: Total number of tickets synced
|
|
258
|
+
project_statuses: Dict mapping project_identifier to list of valid statuses
|
|
163
259
|
|
|
164
260
|
Returns:
|
|
165
261
|
Path to created README
|
|
166
262
|
"""
|
|
167
263
|
sync_time = datetime.utcnow()
|
|
168
|
-
readme_content = self.generate(org_name, projects, total_tickets, sync_time)
|
|
264
|
+
readme_content = self.generate(org_name, projects, total_tickets, sync_time, project_statuses)
|
|
169
265
|
|
|
170
266
|
# Write to root of sync directory
|
|
171
267
|
readme_path = sync_dir / "README.md"
|