janet-cli 0.2.7__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.
@@ -0,0 +1,264 @@
1
+ """Server-Sent Events (SSE) watcher for real-time ticket updates."""
2
+
3
+ import json
4
+ import signal
5
+ import sys
6
+ from typing import Dict, List, Optional, Callable
7
+
8
+ import httpx
9
+
10
+ from janet.config.manager import ConfigManager
11
+ from janet.markdown.generator import MarkdownGenerator
12
+ from janet.sync.file_manager import FileManager
13
+ from janet.utils.console import console
14
+
15
+
16
+ class SSEWatcher:
17
+ """Watch for ticket changes via Server-Sent Events."""
18
+
19
+ def __init__(
20
+ self,
21
+ config_manager: ConfigManager,
22
+ projects: List[Dict],
23
+ org_name: str,
24
+ sync_dir: str,
25
+ org_members: Optional[List[Dict]] = None,
26
+ on_update: Optional[Callable[[str, str], None]] = None,
27
+ project_statuses: Optional[Dict[str, List[str]]] = None,
28
+ ):
29
+ """
30
+ Initialize SSE watcher.
31
+
32
+ Args:
33
+ config_manager: Configuration manager instance
34
+ projects: List of project dictionaries to watch
35
+ org_name: Organization name
36
+ sync_dir: Sync directory path
37
+ org_members: Organization members for name resolution
38
+ on_update: Optional callback when ticket is updated (ticket_key, action)
39
+ project_statuses: Dict mapping project_identifier to list of valid statuses
40
+ """
41
+ self.config_manager = config_manager
42
+ self.config = config_manager.get()
43
+ self.projects = {p["id"]: p for p in projects}
44
+ self.projects_list = projects # Keep list for README generation
45
+ self.org_name = org_name
46
+ self.sync_dir = sync_dir
47
+ self.file_manager = FileManager(sync_dir)
48
+ self.markdown_generator = MarkdownGenerator()
49
+ self.org_members = org_members
50
+ self.on_update = on_update
51
+ self.project_statuses = project_statuses or {}
52
+ self._running = False
53
+
54
+ # Build SSE URL
55
+ self.sse_url = f"{self.config.api.base_url}/api/v1/cli/events"
56
+
57
+ def _get_headers(self) -> Dict[str, str]:
58
+ """Get headers for SSE connection."""
59
+ from janet.auth.token_manager import TokenManager
60
+
61
+ token_manager = TokenManager(self.config_manager)
62
+ access_token = token_manager.get_access_token()
63
+
64
+ return {
65
+ "Authorization": f"Bearer {access_token}",
66
+ "X-Organization-ID": self.config.selected_organization.id,
67
+ "Accept": "text/event-stream",
68
+ }
69
+
70
+ def _handle_event(self, event: Dict) -> None:
71
+ """
72
+ Handle incoming SSE event.
73
+
74
+ Args:
75
+ event: Event dictionary
76
+ """
77
+ event_type = event.get("type")
78
+ project_id = event.get("projectId")
79
+ ticket_id = event.get("ticketId")
80
+ ticket_data = event.get("ticketData", {})
81
+
82
+ # Skip if not for a project we're watching
83
+ if project_id and project_id not in self.projects:
84
+ return
85
+
86
+ # Get project info
87
+ project = self.projects.get(project_id, {})
88
+ project_name = project.get("project_name", "Unknown")
89
+
90
+ if event_type == "connected":
91
+ project_count = len(self.projects)
92
+ project_names = ", ".join([p.get("project_name", p.get("project_identifier", "")) for p in self.projects.values()][:3])
93
+ if project_count > 3:
94
+ project_names += f" +{project_count - 3} more"
95
+ console.print(f"[green]Watching {project_count} project(s) in {self.org_name}[/green] ({project_names})")
96
+ return
97
+
98
+ if event_type in ("ticket-change", "ticket-created"):
99
+ ticket_key = ticket_data.get("ticket_key", "")
100
+ if not ticket_key:
101
+ return
102
+
103
+ # Generate markdown and write to file
104
+ try:
105
+ # Extract attachments from event data if present
106
+ attachments = None
107
+ if ticket_data.get("attachments"):
108
+ attachments = {
109
+ "direct_attachments": ticket_data.get("attachments", []),
110
+ "indirect_attachments": []
111
+ }
112
+ markdown = self.markdown_generator.generate(ticket_data, self.org_members, attachments)
113
+ self.file_manager.write_ticket(
114
+ org_name=self.org_name,
115
+ project_name=project_name,
116
+ ticket_key=ticket_key,
117
+ content=markdown,
118
+ )
119
+
120
+ action = "Created" if event_type == "ticket-created" else "Updated"
121
+ updated_fields = ticket_data.get("updated_fields", [])
122
+ # Transform field names for display (backend uses "labels", CLI uses "tags")
123
+ display_fields = ["tags" if f == "labels" else f for f in updated_fields]
124
+ if display_fields and event_type == "ticket-change":
125
+ console.print(f"[green] {ticket_key}[/green] ({', '.join(display_fields)})")
126
+ else:
127
+ console.print(f"[green] {ticket_key}[/green] ({action.lower()})")
128
+
129
+ if self.on_update:
130
+ self.on_update(ticket_key, action.lower())
131
+
132
+ except Exception as e:
133
+ console.print(f"[red] Failed to update {ticket_key}: {e}[/red]")
134
+
135
+ elif event_type == "ticket-deleted":
136
+ ticket_key = event.get("ticketKey", "")
137
+ if not ticket_key:
138
+ return
139
+
140
+ # Delete or archive local file
141
+ try:
142
+ deleted = self.file_manager.delete_ticket(
143
+ org_name=self.org_name,
144
+ project_name=project_name,
145
+ ticket_key=ticket_key,
146
+ )
147
+ if deleted:
148
+ console.print(f"[yellow] {ticket_key}[/yellow] (deleted)")
149
+ if self.on_update:
150
+ self.on_update(ticket_key, "deleted")
151
+
152
+ except Exception as e:
153
+ console.print(f"[red] Failed to delete {ticket_key}: {e}[/red]")
154
+
155
+ elif event_type == "column-change":
156
+ # Update project statuses and regenerate README
157
+ project_identifier = event.get("projectIdentifier", "")
158
+ columns = event.get("columns", [])
159
+
160
+ if project_identifier and columns:
161
+ # Extract status values in order
162
+ statuses = [col.get("status_value", "") for col in sorted(columns, key=lambda x: x.get("column_order", 0))]
163
+ self.project_statuses[project_identifier] = statuses
164
+
165
+ # Regenerate README with updated statuses
166
+ try:
167
+ from janet.sync.readme_generator import ReadmeGenerator
168
+ from pathlib import Path
169
+
170
+ # Calculate total tickets from projects
171
+ total_tickets = sum(p.get("ticket_count", 0) for p in self.projects_list)
172
+
173
+ readme_gen = ReadmeGenerator()
174
+ readme_gen.write_readme(
175
+ sync_dir=Path(self.sync_dir),
176
+ org_name=self.org_name,
177
+ projects=self.projects_list,
178
+ total_tickets=total_tickets,
179
+ project_statuses=self.project_statuses,
180
+ )
181
+ console.print(f"[cyan] README updated[/cyan] ({project_identifier} statuses changed)")
182
+ except Exception as e:
183
+ console.print(f"[red] Failed to update README: {e}[/red]")
184
+
185
+ def watch(self) -> None:
186
+ """
187
+ Start watching for events.
188
+
189
+ This method blocks until interrupted (Ctrl+C).
190
+ """
191
+ self._running = True
192
+
193
+ # Setup signal handler for graceful shutdown
194
+ def signal_handler(sig, frame):
195
+ console.print("\n[yellow]Stopping watch...[/yellow]")
196
+ self._running = False
197
+ sys.exit(0)
198
+
199
+ signal.signal(signal.SIGINT, signal_handler)
200
+ signal.signal(signal.SIGTERM, signal_handler)
201
+
202
+ console.print("[cyan]Watching for changes... (Ctrl+C to stop)[/cyan]")
203
+ console.print("[dim]Ticket updates from the platform will sync to your local markdown files in real-time.[/dim]\n")
204
+
205
+ while self._running:
206
+ try:
207
+ self._connect_and_stream()
208
+ except httpx.ConnectError as e:
209
+ console.print(f"[red]Connection error: {e}[/red]")
210
+ console.print("[yellow]Reconnecting in 5 seconds...[/yellow]")
211
+ import time
212
+ time.sleep(5)
213
+ except Exception as e:
214
+ error_str = str(e)
215
+ # Check if this is an auth error that requires re-login
216
+ if "401" in error_str or "Unauthorized" in error_str:
217
+ console.print("[red]Authentication failed. Attempting to refresh token...[/red]")
218
+ try:
219
+ from janet.auth.token_manager import TokenManager
220
+ token_manager = TokenManager(self.config_manager)
221
+ token_manager.refresh_access_token()
222
+ console.print("[green]Token refreshed successfully.[/green]")
223
+ except Exception as refresh_error:
224
+ console.print(f"[red]Token refresh failed: {refresh_error}[/red]")
225
+ console.print("[yellow]Please run 'janet login' to re-authenticate.[/yellow]")
226
+ self._running = False
227
+ break
228
+ # Expected disconnection from load balancer timeout - reconnect silently
229
+ elif "incomplete chunked read" in error_str or "closed" in error_str.lower():
230
+ console.print("[dim]Connection closed, reconnecting...[/dim]")
231
+ else:
232
+ console.print(f"[yellow]Connection interrupted: {e}[/yellow]")
233
+ import time
234
+ time.sleep(2) # Shorter delay for expected disconnections
235
+
236
+ def _connect_and_stream(self) -> None:
237
+ """Connect to SSE endpoint and process stream."""
238
+ headers = self._get_headers()
239
+
240
+ with httpx.Client(timeout=None) as client:
241
+ with client.stream("GET", self.sse_url, headers=headers) as response:
242
+ if response.status_code == 401:
243
+ raise Exception("401 Unauthorized - token may have expired")
244
+ if response.status_code != 200:
245
+ raise Exception(f"SSE connection failed: {response.status_code}")
246
+
247
+ for line in response.iter_lines():
248
+ if not self._running:
249
+ break
250
+
251
+ if not line:
252
+ continue
253
+
254
+ # SSE format: "data: {...json...}"
255
+ if line.startswith("data: "):
256
+ try:
257
+ event_data = json.loads(line[6:])
258
+ self._handle_event(event_data)
259
+ except json.JSONDecodeError:
260
+ pass # Ignore malformed events
261
+
262
+ # Ignore comments (keepalive pings)
263
+ elif line.startswith(":"):
264
+ pass
janet/sync/sync_engine.py CHANGED
@@ -127,15 +127,23 @@ class SyncEngine:
127
127
  # Fetch organization members for name resolution (once per project)
128
128
  org_members = self._fetch_org_members()
129
129
 
130
- # Batch fetch all full ticket details at once
130
+ # Batch fetch all full ticket details using unlimited CLI endpoint
131
131
  ticket_ids = [t.get("id") for t in tickets if t.get("id")]
132
132
 
133
133
  console.print(f" Fetching full details for {len(ticket_ids)} tickets...")
134
- full_tickets_list = self.ticket_api.batch_fetch(ticket_ids)
134
+ full_tickets_list = self.ticket_api.cli_batch_fetch(ticket_ids)
135
135
 
136
136
  # Create lookup map by ticket ID
137
137
  full_tickets_map = {t.get("id"): t for t in full_tickets_list}
138
138
 
139
+ # Batch fetch all attachments in one call
140
+ console.print(f" Fetching attachments...")
141
+ attachments_map = {}
142
+ try:
143
+ attachments_map = self.ticket_api.batch_fetch_attachments(ticket_ids)
144
+ except Exception as e:
145
+ console.print(f" [yellow]Warning: Could not batch fetch attachments: {e}[/yellow]")
146
+
139
147
  # Sync each ticket with progress bar
140
148
  synced_count = 0
141
149
  with Progress(
@@ -157,8 +165,11 @@ class SyncEngine:
157
165
  full_ticket = full_tickets_map.get(ticket_id, {})
158
166
  merged_ticket = {**full_ticket, **ticket}
159
167
 
168
+ # Get pre-fetched attachments for this ticket
169
+ ticket_attachments = attachments_map.get(ticket_id)
170
+
160
171
  self._sync_single_ticket_fast(
161
- merged_ticket, org_name, project_name, org_members
172
+ merged_ticket, org_name, project_name, org_members, ticket_attachments
162
173
  )
163
174
  synced_count += 1
164
175
  except Exception as e:
@@ -176,6 +187,7 @@ class SyncEngine:
176
187
  org_name: str,
177
188
  project_name: str,
178
189
  org_members: Optional[List[Dict]] = None,
190
+ attachments: Optional[Dict] = None,
179
191
  ) -> None:
180
192
  """
181
193
  Sync a single ticket (optimized - no individual API calls).
@@ -185,6 +197,7 @@ class SyncEngine:
185
197
  org_name: Organization name
186
198
  project_name: Project name
187
199
  org_members: Organization members for name resolution
200
+ attachments: Pre-fetched attachments dict (from batch fetch)
188
201
  """
189
202
  ticket_id = ticket.get("id")
190
203
 
@@ -209,10 +222,6 @@ class SyncEngine:
209
222
  if "ticket_key" not in ticket:
210
223
  ticket["ticket_key"] = ticket_key
211
224
 
212
- # Skip attachment fetching to reduce API calls
213
- # Attachments info is usually in the ticket data already
214
- attachments = None
215
-
216
225
  # Generate markdown
217
226
  markdown = self.markdown_generator.generate(
218
227
  ticket, org_members, attachments
@@ -0,0 +1,356 @@
1
+ Metadata-Version: 2.4
2
+ Name: janet-cli
3
+ Version: 0.2.33
4
+ Summary: CLI tool to sync Janet AI tickets to local markdown files
5
+ Author-email: Janet AI <support@janet-ai.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/janet-ai/janet-cli
8
+ Project-URL: Repository, https://github.com/janet-ai/janet-cli
9
+ Project-URL: Issues, https://github.com/janet-ai/janet-cli/issues
10
+ Keywords: cli,janet,tickets,markdown,sync
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: typer>=0.12.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: pydantic-settings>=2.0.0
27
+ Requires-Dist: rich>=13.0.0
28
+ Requires-Dist: platformdirs>=4.0.0
29
+ Requires-Dist: keyring>=25.0.0
30
+ Requires-Dist: python-dateutil>=2.8.0
31
+ Requires-Dist: pycrdt>=0.9.0
32
+ Requires-Dist: InquirerPy>=0.3.4
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
36
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
37
+ Requires-Dist: httpx-mock>=0.15.0; extra == "dev"
38
+ Requires-Dist: black>=24.0.0; extra == "dev"
39
+ Requires-Dist: mypy>=1.8.0; extra == "dev"
40
+ Requires-Dist: ruff>=0.2.0; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # Janet AI CLI
44
+
45
+ > Sync your Janet AI tickets to local markdown files and enable AI coding agents to create and manage tickets.
46
+
47
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
48
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
49
+
50
+ ## What is Janet AI?
51
+
52
+ [Janet AI](https://tryjanet.ai) is an AI-native project management platform for modern software teams. The Janet CLI allows developers to:
53
+
54
+ - **Sync tickets** to local markdown files with real-time updates as changes are made on the platform
55
+ - **Create tickets** directly from the command line or via AI agents
56
+ - **Update tickets** without leaving your terminal
57
+
58
+ ## Why Use the CLI?
59
+
60
+ AI coding assistants work better when they understand your project's tickets, requirements, and priorities. With Janet CLI, AI agents like Claude Code and Cursor can:
61
+
62
+ - Reference specific tickets while writing code
63
+ - Create new tickets when discovering bugs or needed features
64
+ - Update ticket status as work progresses
65
+ - Understand requirements and acceptance criteria
66
+ - Answer questions about project priorities
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ pip install janet-cli
72
+ ```
73
+
74
+ ## Quick Start
75
+
76
+ ```bash
77
+ # 1. Authenticate
78
+ janet login
79
+
80
+ # 2. Sync tickets and watch for real-time updates
81
+ janet sync
82
+ ```
83
+
84
+ ## Using with AI Coding Agents
85
+
86
+ ### Claude Code
87
+
88
+ Add Janet CLI to your Claude Code workflow:
89
+
90
+ ```bash
91
+ # In your project directory
92
+ janet sync
93
+ ```
94
+
95
+ Claude Code can now reference, create, and update tickets. Example natural language prompts:
96
+
97
+ **Referencing tickets:**
98
+ - "Look at ticket BACK-42 and implement the authentication flow"
99
+ - "What are the high priority tickets in the Backend project?"
100
+ - "Show me all tickets assigned to me"
101
+
102
+ **Creating tickets:**
103
+ - "Create a high-priority bug ticket in BACK for the null pointer exception we just found in auth.py"
104
+ - "Make a new feature ticket in FRONT for dark mode support with the description from design-doc.md"
105
+ - "Create a ticket to add unit tests for the payment service, assign it to dev@example.com"
106
+
107
+ **Updating tickets:**
108
+ - "Update BACK-42 status to In Progress since I'm working on it now"
109
+ - "Mark FRONT-15 as Done and add a comment that it's deployed to staging"
110
+ - "Change the priority of BACK-38 to Critical"
111
+
112
+ Claude Code can run these commands directly:
113
+
114
+ ```bash
115
+ janet context --json # Discover available projects
116
+ janet ticket create "Bug: null pointer in auth" -p BACK --json
117
+ janet ticket update BACK-42 --status "In Progress"
118
+ ```
119
+
120
+ ### Cursor
121
+
122
+ 1. Sync tickets to your workspace:
123
+ ```bash
124
+ janet sync
125
+ ```
126
+
127
+ 2. Add to `.cursorrules` or system prompt:
128
+ ```
129
+ Project tickets are in ./janet-tickets/ as markdown files.
130
+ Use `janet ticket create` to create new tickets.
131
+ Use `janet ticket update` to update ticket status.
132
+ Use `janet context --json` to see available projects.
133
+ ```
134
+
135
+ 3. Use natural language prompts:
136
+
137
+ **Referencing tickets:**
138
+ - "Read ticket BACK-42 and help me implement the OAuth flow"
139
+ - "What bugs are currently open in the Frontend project?"
140
+
141
+ **Creating tickets:**
142
+ - "Create a ticket in BACK for refactoring the database layer, high priority"
143
+ - "Make a bug ticket for the memory leak we found, include the stack trace"
144
+
145
+ **Updating tickets:**
146
+ - "Update ticket FRONT-28 to In Review status"
147
+ - "Mark BACK-51 as complete"
148
+
149
+ ### GitHub Copilot / Other Agents
150
+
151
+ Any AI agent with terminal access can use the CLI:
152
+
153
+ ```bash
154
+ # Get context about the workspace
155
+ janet context --json
156
+
157
+ # Create tickets programmatically
158
+ janet ticket create "Title" -p PROJECT --json
159
+
160
+ # Update tickets
161
+ janet ticket update PROJ-123 --status "Done" --json
162
+ ```
163
+
164
+ ## Real-Time Sync
165
+
166
+ After syncing, the CLI stays connected to receive real-time updates:
167
+
168
+ ```bash
169
+ janet sync
170
+ ```
171
+
172
+ Output:
173
+ ```
174
+ ✓ Sync complete!
175
+ Projects: 2
176
+ Tickets: 42
177
+
178
+ Tickets saved to: ./janet-tickets
179
+
180
+ Watching for changes... (Ctrl+C to stop)
181
+
182
+ Watching 2 project(s) in My Org (Backend, Frontend)
183
+
184
+ BACK-123 (status)
185
+ BACK-124 (created)
186
+ FRONT-45 (title, description)
187
+ ```
188
+
189
+ Real-time updates sync changes whenever tickets are created, updated, or deleted—whether from the web UI, API, or other CLI sessions. Press Ctrl+C to stop watching.
190
+
191
+ ## File Organization
192
+
193
+ Tickets are organized in a clear hierarchy:
194
+
195
+ ```
196
+ janet-tickets/
197
+ ├── README.md # Context for AI agents
198
+ └── My Organization/
199
+ ├── Backend/
200
+ │ ├── BACK-1.md
201
+ │ ├── BACK-2.md
202
+ │ └── BACK-42.md
203
+ └── Frontend/
204
+ ├── FRONT-1.md
205
+ └── FRONT-15.md
206
+ ```
207
+
208
+ ## Markdown Format
209
+
210
+ Each ticket is exported with complete information:
211
+
212
+ ```markdown
213
+ # PROJ-42: Add user authentication
214
+
215
+ ## Metadata
216
+ - **Status:** In Progress
217
+ - **Priority:** High
218
+ - **Type:** Feature
219
+ - **Assignees:** John Doe, Jane Smith
220
+ - **Created:** Jan 07, 2026 10:30 AM
221
+ - **Updated:** Jan 07, 2026 02:45 PM
222
+ - **Labels:** backend, security
223
+
224
+ ## Description
225
+
226
+ We need to implement OAuth authentication...
227
+
228
+ ### Requirements
229
+ - Support multiple auth providers
230
+ - Handle token refresh
231
+ - Secure token storage
232
+
233
+ ## Comments (2)
234
+
235
+ ### John Doe - Jan 07, 2026 11:00 AM
236
+
237
+ Started working on the OAuth flow.
238
+
239
+ ### Jane Smith - Jan 07, 2026 01:30 PM
240
+
241
+ Looks good! Add tests when done.
242
+ ```
243
+
244
+ ## Commands
245
+
246
+ ### Authentication
247
+
248
+ ```bash
249
+ janet login # Authenticate with Janet AI
250
+ janet logout # Clear credentials
251
+ janet auth status # Show authentication status
252
+ ```
253
+
254
+ ### Syncing Tickets
255
+
256
+ ```bash
257
+ janet sync # Sync and watch for real-time updates
258
+ janet sync --all # Sync all projects and watch
259
+ janet sync --dir ./tickets # Specify custom directory
260
+ janet sync --no-watch # Sync once and exit (no real-time updates)
261
+ ```
262
+
263
+ ### Creating Tickets
264
+
265
+ ```bash
266
+ janet ticket create "Title" --project PROJ # Basic creation
267
+ janet ticket create "Title" -p PROJ --priority High # With priority
268
+ janet ticket create "Title" -p PROJ --status "To Do" # With status
269
+ janet ticket create "Title" -p PROJ --type Bug # With type
270
+ janet ticket create "Title" -p PROJ --assignee dev@example.com
271
+ janet ticket create "Title" -p PROJ --tag backend --tag urgent
272
+ janet ticket create "Title" -p PROJ --json # JSON output for AI agents
273
+ cat desc.md | janet ticket create "Title" -p PROJ # Pipe description
274
+
275
+ # Full ticket with all fields
276
+ janet ticket create "Implement OAuth2 authentication" \
277
+ --project BACK \
278
+ --description "Add Google and GitHub OAuth providers with token refresh" \
279
+ --status "To Do" \
280
+ --priority High \
281
+ --type Feature \
282
+ --assignee dev@example.com \
283
+ --tag backend \
284
+ --tag security \
285
+ --tag authentication
286
+ ```
287
+
288
+ ### Updating Tickets
289
+
290
+ ```bash
291
+ janet ticket update PROJ-123 --status "In Progress" # Update status
292
+ janet ticket update PROJ-123 --priority Critical # Update priority
293
+ janet ticket update PROJ-123 --title "New title" # Update title
294
+ janet ticket update PROJ-123 --assignee dev@example.com
295
+ janet ticket update PROJ-123 --json # JSON output
296
+
297
+ # Update multiple fields at once
298
+ janet ticket update BACK-42 \
299
+ --title "Fixed: OAuth authentication flow" \
300
+ --description "Implemented token refresh and error handling" \
301
+ --status "In Review" \
302
+ --priority High \
303
+ --assignee reviewer@example.com
304
+ ```
305
+
306
+ ### Context (for AI Agents)
307
+
308
+ ```bash
309
+ janet context # Human-readable context
310
+ janet context --json # JSON output for AI agents/scripts
311
+ ```
312
+
313
+ ### Organization Management
314
+
315
+ ```bash
316
+ janet org list # List your organizations
317
+ janet org select <org-id> # Switch organization
318
+ janet org current # Show current organization
319
+ ```
320
+
321
+ ### Project Management
322
+
323
+ ```bash
324
+ janet project list # List projects in current org
325
+ ```
326
+
327
+ ### Status & Configuration
328
+
329
+ ```bash
330
+ janet status # Show overall status
331
+ janet config show # Display configuration
332
+ janet config path # Show config file location
333
+ janet config reset # Reset to defaults
334
+ janet --version # Show CLI version
335
+ ```
336
+
337
+ ## Configuration
338
+
339
+ The CLI stores configuration at:
340
+ - **macOS:** `~/Library/Application Support/janet-cli/config.json`
341
+ - **Linux:** `~/.config/janet-cli/config.json`
342
+ - **Windows:** `%APPDATA%\janet-cli\config.json`
343
+
344
+ ## Requirements
345
+
346
+ - Python 3.8 or higher
347
+ - Janet AI account ([sign up](https://tryjanet.ai))
348
+
349
+ ## License
350
+
351
+ MIT License - see [LICENSE](LICENSE) file for details.
352
+
353
+ ## Links
354
+
355
+ - [Janet AI](https://tryjanet.ai) - AI-native project management
356
+ - [Documentation](https://docs.tryjanet.ai)