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.
- janet/__init__.py +1 -1
- janet/api/client.py +36 -0
- janet/api/projects.py +20 -0
- janet/api/tickets.py +168 -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 +16 -7
- janet_cli-0.2.33.dist-info/METADATA +356 -0
- janet_cli-0.2.33.dist-info/RECORD +34 -0
- janet_cli-0.2.7.dist-info/METADATA +0 -215
- janet_cli-0.2.7.dist-info/RECORD +0 -33
- {janet_cli-0.2.7.dist-info → janet_cli-0.2.33.dist-info}/WHEEL +0 -0
- {janet_cli-0.2.7.dist-info → janet_cli-0.2.33.dist-info}/entry_points.txt +0 -0
- {janet_cli-0.2.7.dist-info → janet_cli-0.2.33.dist-info}/licenses/LICENSE +0 -0
- {janet_cli-0.2.7.dist-info → janet_cli-0.2.33.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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.
|
|
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
48
|
+
[](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)
|