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.
@@ -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
- # Optional fields
106
- if ticket.get("story_points"):
107
- lines.append(f"- **Story Points:** {ticket['story_points']}")
108
-
109
- if ticket.get("due_date"):
110
- lines.append(f"- **Due Date:** {self._format_date(ticket['due_date'])}")
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
- if ticket.get("sprint"):
113
- lines.append(f"- **Sprint:** {ticket['sprint']}")
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
- if ticket.get("labels"):
116
- labels = ticket["labels"]
117
- if labels:
118
- lines.append(f"- **Labels:** {', '.join(labels)}")
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
- # Convert Yjs binary to markdown
131
- markdown = self.yjs_converter.convert(yjs_binary, plain_text)
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"
@@ -16,33 +16,38 @@ class YjsConverter:
16
16
  """
17
17
 
18
18
  def convert(
19
- self, yjs_binary_base64: Optional[str], plain_text_fallback: Optional[str] = None
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 (NOT USED - only for compatibility)
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
- # If no Yjs binary, show placeholder
32
- if not yjs_binary_base64:
33
- return "*No description provided*"
35
+ self._attachments = attachments or []
34
36
 
35
- try:
36
- # Try pycrdt conversion
37
- markdown = self._convert_with_pycrdt(yjs_binary_base64)
38
- if markdown and markdown.strip():
39
- return markdown
40
- else:
41
- return "*[Empty description]*"
42
- except Exception as e:
43
- # Show error instead of falling back
44
- logger.error(f"Yjs conversion failed: {e}")
45
- return f"*[Yjs conversion failed: {e}]*"
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
 
@@ -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"# Janet AI Tickets - {org_name}\n")
35
+ # Header - addressing the AI agent directly
36
+ sections.append(f"# Project Tickets - {org_name}\n")
34
37
  sections.append(
35
- "This directory contains your Janet AI tickets synced as markdown files "
36
- "for use with AI coding agents like Claude Code, Cursor, and GitHub Copilot.\n"
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
- # What is this?
40
- sections.append("## What is this?\n")
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 Synced:** {len(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 summary
51
+ # Projects list with statuses
91
52
  if projects:
92
- sections.append("### Projects\n")
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"- **{key}** - {name} ({count} tickets)")
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
- # How to use
101
- sections.append("## How to Use with AI Coding Agents\n")
102
- sections.append(
103
- "When working with AI assistants (Claude Code, Cursor, etc.), "
104
- "you can reference these tickets:\n"
105
- )
106
- sections.append('```bash')
107
- sections.append('# Example prompts:')
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("janet sync")
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
- # About Janet AI
126
- sections.append("## About Janet AI\n")
127
- sections.append(
128
- "[Janet AI](https://tryjanet.ai) is an AI-native project management platform "
129
- "designed for modern software teams. It provides:\n"
130
- )
131
- sections.append("- AI-powered ticket creation and updates")
132
- sections.append("- Intelligent ticket prioritization")
133
- sections.append("- Automated ticket summaries and evaluations")
134
- sections.append("- Real-time collaboration")
135
- sections.append("- GitHub integration")
136
- sections.append("- Discord/Slack integration")
137
- sections.append("- Meeting and document context linking\n")
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"*Generated by [Janet CLI](https://github.com/janet-ai/janet-cli) v0.2.0 "
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"