juno-code 1.0.44 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/cli.js +658 -50
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +658 -50
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -4
- package/dist/index.mjs.map +1 -1
- package/dist/templates/scripts/__pycache__/attachment_downloader.cpython-38.pyc +0 -0
- package/dist/templates/scripts/__pycache__/github.cpython-38.pyc +0 -0
- package/dist/templates/scripts/__pycache__/slack_fetch.cpython-38.pyc +0 -0
- package/dist/templates/scripts/__pycache__/slack_state.cpython-38.pyc +0 -0
- package/dist/templates/scripts/attachment_downloader.py +405 -0
- package/dist/templates/scripts/github.py +282 -7
- package/dist/templates/scripts/hooks/session_counter.sh +328 -0
- package/dist/templates/scripts/kanban.sh +22 -4
- package/dist/templates/scripts/log_scanner.sh +790 -0
- package/dist/templates/scripts/slack_fetch.py +232 -20
- package/dist/templates/services/claude.py +50 -1
- package/dist/templates/services/codex.py +5 -4
- package/dist/templates/skills/claude/.gitkeep +0 -0
- package/dist/templates/skills/claude/plan-kanban-tasks/SKILL.md +25 -0
- package/dist/templates/skills/claude/ralph-loop/SKILL.md +43 -0
- package/dist/templates/skills/claude/ralph-loop/references/first_check.md +20 -0
- package/dist/templates/skills/claude/ralph-loop/references/implement.md +99 -0
- package/dist/templates/skills/claude/ralph-loop/scripts/kanban.sh +293 -0
- package/dist/templates/skills/claude/understand-project/SKILL.md +39 -0
- package/dist/templates/skills/codex/.gitkeep +0 -0
- package/dist/templates/skills/codex/ralph-loop/SKILL.md +43 -0
- package/dist/templates/skills/codex/ralph-loop/references/first_check.md +20 -0
- package/dist/templates/skills/codex/ralph-loop/references/implement.md +99 -0
- package/dist/templates/skills/codex/ralph-loop/scripts/kanban.sh +293 -0
- package/package.json +3 -2
|
@@ -12,12 +12,20 @@ Features:
|
|
|
12
12
|
- Persistent state tracking (NDJSON-based) to prevent duplicate processing
|
|
13
13
|
- Tag-based identification using tag_id for O(1) lookups (no fuzzy matching)
|
|
14
14
|
- Environment-based configuration with secure token management
|
|
15
|
+
- File attachment downloading (saves to .juno_task/attachments/github/)
|
|
15
16
|
|
|
16
17
|
Usage:
|
|
17
18
|
python github.py fetch --repo owner/repo
|
|
19
|
+
python github.py fetch --repo owner/repo --download-attachments
|
|
18
20
|
python github.py respond --tag github-input
|
|
19
21
|
python github.py sync --repo owner/repo --once
|
|
20
22
|
|
|
23
|
+
Environment Variables:
|
|
24
|
+
GITHUB_TOKEN GitHub personal access token (required)
|
|
25
|
+
GITHUB_REPO Default repository (format: owner/repo)
|
|
26
|
+
JUNO_DOWNLOAD_ATTACHMENTS Enable/disable file downloads (default: true)
|
|
27
|
+
JUNO_MAX_ATTACHMENT_SIZE Max file size in bytes (default: 50MB)
|
|
28
|
+
|
|
21
29
|
Version: 1.0.0
|
|
22
30
|
Package: juno-code@1.x.x
|
|
23
31
|
Auto-installed by: ScriptInstaller
|
|
@@ -53,6 +61,46 @@ shutdown_requested = False
|
|
|
53
61
|
# Configure logging
|
|
54
62
|
logger = logging.getLogger(__name__)
|
|
55
63
|
|
|
64
|
+
# Import attachment downloader for file handling
|
|
65
|
+
script_dir = Path(__file__).parent
|
|
66
|
+
sys.path.insert(0, str(script_dir))
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from attachment_downloader import (
|
|
70
|
+
AttachmentDownloader,
|
|
71
|
+
format_attachments_section,
|
|
72
|
+
is_attachments_enabled
|
|
73
|
+
)
|
|
74
|
+
ATTACHMENTS_AVAILABLE = True
|
|
75
|
+
except ImportError:
|
|
76
|
+
ATTACHMENTS_AVAILABLE = False
|
|
77
|
+
# Define stub functions if attachment_downloader not available
|
|
78
|
+
def is_attachments_enabled():
|
|
79
|
+
return False
|
|
80
|
+
def format_attachments_section(paths):
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# GitHub Attachment URL Patterns
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
# Regex patterns to extract attachment URLs from issue body and comments
|
|
89
|
+
GITHUB_ATTACHMENT_PATTERNS = [
|
|
90
|
+
# GitHub user-attachments/files (file uploads with numeric ID)
|
|
91
|
+
r'https://github\.com/user-attachments/files/\d+/[^\s\)\"\'\]]+',
|
|
92
|
+
# GitHub user-attachments/assets (new format with UUID)
|
|
93
|
+
r'https://github\.com/user-attachments/assets/[a-f0-9-]+/[^\s\)\"\'\]]+',
|
|
94
|
+
# User images (screenshots, drag-drop uploads)
|
|
95
|
+
r'https://user-images\.githubusercontent\.com/\d+/[^\s\)\"\'\]]+',
|
|
96
|
+
# Private user images
|
|
97
|
+
r'https://private-user-images\.githubusercontent\.com/\d+/[^\s\)\"\'\]]+',
|
|
98
|
+
# Repository assets
|
|
99
|
+
r'https://github\.com/[^/]+/[^/]+/assets/\d+/[^\s\)\"\'\]]+',
|
|
100
|
+
# Objects storage
|
|
101
|
+
r'https://objects\.githubusercontent\.com/[^\s\)\"\'\]]+',
|
|
102
|
+
]
|
|
103
|
+
|
|
56
104
|
|
|
57
105
|
# =============================================================================
|
|
58
106
|
# State Management Classes
|
|
@@ -911,6 +959,138 @@ def validate_repo_format(repo: str) -> bool:
|
|
|
911
959
|
return True
|
|
912
960
|
|
|
913
961
|
|
|
962
|
+
# =============================================================================
|
|
963
|
+
# Attachment Extraction and Download Functions
|
|
964
|
+
# =============================================================================
|
|
965
|
+
|
|
966
|
+
def extract_attachment_urls(body: str, comments: Optional[List[Dict[str, Any]]] = None) -> List[str]:
|
|
967
|
+
"""
|
|
968
|
+
Extract attachment URLs from issue body and comments.
|
|
969
|
+
|
|
970
|
+
GitHub doesn't have a dedicated attachment API - files are embedded as URLs
|
|
971
|
+
in the issue body or comments when users drag-drop or paste images.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
body: Issue body text
|
|
975
|
+
comments: Optional list of comment dicts
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
Deduplicated list of attachment URLs
|
|
979
|
+
"""
|
|
980
|
+
urls = set()
|
|
981
|
+
|
|
982
|
+
def extract_from_text(text: str, source: str = "text") -> None:
|
|
983
|
+
if not text:
|
|
984
|
+
logger.debug(f"extract_attachment_urls: Empty {source}, skipping")
|
|
985
|
+
return
|
|
986
|
+
for pattern in GITHUB_ATTACHMENT_PATTERNS:
|
|
987
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
988
|
+
if matches:
|
|
989
|
+
logger.debug(f"extract_attachment_urls: Pattern '{pattern[:50]}...' matched {len(matches)} URL(s) in {source}")
|
|
990
|
+
for url in matches:
|
|
991
|
+
logger.info(f" Detected attachment URL: {url}")
|
|
992
|
+
urls.update(matches)
|
|
993
|
+
|
|
994
|
+
# Extract from body
|
|
995
|
+
logger.debug(f"extract_attachment_urls: Scanning body ({len(body) if body else 0} chars)")
|
|
996
|
+
extract_from_text(body, "body")
|
|
997
|
+
|
|
998
|
+
# Extract from comments
|
|
999
|
+
if comments:
|
|
1000
|
+
logger.debug(f"extract_attachment_urls: Scanning {len(comments)} comments")
|
|
1001
|
+
for i, comment in enumerate(comments):
|
|
1002
|
+
extract_from_text(comment.get('body', ''), f"comment[{i}]")
|
|
1003
|
+
|
|
1004
|
+
if urls:
|
|
1005
|
+
logger.info(f"extract_attachment_urls: Found {len(urls)} unique attachment URL(s)")
|
|
1006
|
+
else:
|
|
1007
|
+
logger.debug("extract_attachment_urls: No attachment URLs found")
|
|
1008
|
+
|
|
1009
|
+
return list(urls)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def download_github_attachments(
|
|
1013
|
+
urls: List[str],
|
|
1014
|
+
token: str,
|
|
1015
|
+
repo: str,
|
|
1016
|
+
issue_number: int,
|
|
1017
|
+
downloader: 'AttachmentDownloader'
|
|
1018
|
+
) -> List[str]:
|
|
1019
|
+
"""
|
|
1020
|
+
Download GitHub attachment files.
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
urls: List of attachment URLs
|
|
1024
|
+
token: GitHub token for authentication
|
|
1025
|
+
repo: Repository in owner/repo format
|
|
1026
|
+
issue_number: Issue number
|
|
1027
|
+
downloader: AttachmentDownloader instance
|
|
1028
|
+
|
|
1029
|
+
Returns:
|
|
1030
|
+
List of local file paths
|
|
1031
|
+
"""
|
|
1032
|
+
if not urls:
|
|
1033
|
+
return []
|
|
1034
|
+
|
|
1035
|
+
downloaded_paths = []
|
|
1036
|
+
headers = {
|
|
1037
|
+
'Authorization': f'token {token}',
|
|
1038
|
+
'Accept': 'application/octet-stream'
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
# Sanitize repo for directory name
|
|
1042
|
+
repo_dir = repo.replace('/', '_')
|
|
1043
|
+
target_dir = downloader.base_dir / 'github' / repo_dir
|
|
1044
|
+
|
|
1045
|
+
for url in urls:
|
|
1046
|
+
# Extract filename from URL
|
|
1047
|
+
try:
|
|
1048
|
+
from urllib.parse import urlparse, unquote
|
|
1049
|
+
parsed = urlparse(url)
|
|
1050
|
+
path_parts = parsed.path.split('/')
|
|
1051
|
+
filename = unquote(path_parts[-1]) if path_parts else None
|
|
1052
|
+
|
|
1053
|
+
# Handle URLs without clear filename
|
|
1054
|
+
if not filename or filename in ['/', '']:
|
|
1055
|
+
# Use hash of URL as filename
|
|
1056
|
+
import hashlib
|
|
1057
|
+
url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
|
|
1058
|
+
# Try to extract extension from URL path
|
|
1059
|
+
ext = ''
|
|
1060
|
+
for part in reversed(path_parts):
|
|
1061
|
+
if '.' in part:
|
|
1062
|
+
ext = '.' + part.split('.')[-1]
|
|
1063
|
+
break
|
|
1064
|
+
filename = f"attachment_{url_hash}{ext}"
|
|
1065
|
+
|
|
1066
|
+
except Exception as e:
|
|
1067
|
+
logger.warning(f"Error parsing URL {url}: {e}")
|
|
1068
|
+
continue
|
|
1069
|
+
|
|
1070
|
+
metadata = {
|
|
1071
|
+
'source': 'github',
|
|
1072
|
+
'repo': repo,
|
|
1073
|
+
'issue_number': issue_number,
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
path, error = downloader.download_file(
|
|
1077
|
+
url=url,
|
|
1078
|
+
target_dir=target_dir,
|
|
1079
|
+
filename_prefix=f"issue_{issue_number}",
|
|
1080
|
+
original_filename=filename,
|
|
1081
|
+
headers=headers,
|
|
1082
|
+
metadata=metadata
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
if path:
|
|
1086
|
+
downloaded_paths.append(path)
|
|
1087
|
+
logger.info(f"Downloaded GitHub attachment: {filename}")
|
|
1088
|
+
else:
|
|
1089
|
+
logger.warning(f"Failed to download {url}: {error}")
|
|
1090
|
+
|
|
1091
|
+
return downloaded_paths
|
|
1092
|
+
|
|
1093
|
+
|
|
914
1094
|
def extract_task_ids_from_text(text: str) -> List[str]:
|
|
915
1095
|
"""
|
|
916
1096
|
Extract task IDs from text using [task_id]...[/task_id] format.
|
|
@@ -1097,7 +1277,8 @@ def create_kanban_task_from_issue(
|
|
|
1097
1277
|
issue: Dict[str, Any],
|
|
1098
1278
|
repo: str,
|
|
1099
1279
|
kanban_script: str,
|
|
1100
|
-
dry_run: bool = False
|
|
1280
|
+
dry_run: bool = False,
|
|
1281
|
+
attachment_paths: Optional[List[str]] = None
|
|
1101
1282
|
) -> Optional[str]:
|
|
1102
1283
|
"""
|
|
1103
1284
|
Create kanban task from GitHub issue.
|
|
@@ -1107,6 +1288,7 @@ def create_kanban_task_from_issue(
|
|
|
1107
1288
|
repo: Repository in format "owner/repo"
|
|
1108
1289
|
kanban_script: Path to kanban.sh script
|
|
1109
1290
|
dry_run: If True, don't actually create the task
|
|
1291
|
+
attachment_paths: Optional list of downloaded attachment file paths
|
|
1110
1292
|
|
|
1111
1293
|
Returns:
|
|
1112
1294
|
Task ID if created, None if failed
|
|
@@ -1122,6 +1304,10 @@ def create_kanban_task_from_issue(
|
|
|
1122
1304
|
task_body = f"# {issue['title']}\n\n"
|
|
1123
1305
|
task_body += issue['body'] or "(No description)"
|
|
1124
1306
|
|
|
1307
|
+
# Append attachment paths if any
|
|
1308
|
+
if attachment_paths:
|
|
1309
|
+
task_body += format_attachments_section(attachment_paths)
|
|
1310
|
+
|
|
1125
1311
|
# Build tags - all metadata goes here for token efficiency
|
|
1126
1312
|
tags = [
|
|
1127
1313
|
'github-input',
|
|
@@ -1140,10 +1326,16 @@ def create_kanban_task_from_issue(
|
|
|
1140
1326
|
# Add tag_id as a tag
|
|
1141
1327
|
tags.append(tag_id)
|
|
1142
1328
|
|
|
1329
|
+
# Add has-attachments tag if files were downloaded
|
|
1330
|
+
if attachment_paths:
|
|
1331
|
+
tags.append('has-attachments')
|
|
1332
|
+
|
|
1143
1333
|
if dry_run:
|
|
1144
1334
|
logger.info(f"[DRY RUN] Would create task with tag_id: {tag_id}")
|
|
1145
1335
|
logger.debug(f"[DRY RUN] Body: {task_body[:200]}...")
|
|
1146
1336
|
logger.debug(f"[DRY RUN] Tags: {', '.join(tags)}")
|
|
1337
|
+
if attachment_paths:
|
|
1338
|
+
logger.debug(f"[DRY RUN] Attachments: {len(attachment_paths)} files")
|
|
1147
1339
|
return "dry-run-task-id"
|
|
1148
1340
|
|
|
1149
1341
|
try:
|
|
@@ -1337,7 +1529,8 @@ def create_kanban_task_from_comment(
|
|
|
1337
1529
|
repo: str,
|
|
1338
1530
|
kanban_script: str,
|
|
1339
1531
|
related_task_ids: List[str],
|
|
1340
|
-
dry_run: bool = False
|
|
1532
|
+
dry_run: bool = False,
|
|
1533
|
+
attachment_paths: Optional[List[str]] = None
|
|
1341
1534
|
) -> Optional[str]:
|
|
1342
1535
|
"""
|
|
1343
1536
|
Create kanban task from a GitHub issue comment (user reply).
|
|
@@ -1349,6 +1542,7 @@ def create_kanban_task_from_comment(
|
|
|
1349
1542
|
kanban_script: Path to kanban.sh script
|
|
1350
1543
|
related_task_ids: List of previous task IDs from the thread
|
|
1351
1544
|
dry_run: If True, don't actually create the task
|
|
1545
|
+
attachment_paths: Optional list of downloaded attachment file paths
|
|
1352
1546
|
|
|
1353
1547
|
Returns:
|
|
1354
1548
|
Task ID if created, None if failed
|
|
@@ -1367,6 +1561,10 @@ def create_kanban_task_from_comment(
|
|
|
1367
1561
|
task_body = f"# Reply to Issue #{issue_number}: {issue['title']}\n\n"
|
|
1368
1562
|
task_body += comment['body'] or "(No content)"
|
|
1369
1563
|
|
|
1564
|
+
# Append attachment paths if any
|
|
1565
|
+
if attachment_paths:
|
|
1566
|
+
task_body += format_attachments_section(attachment_paths)
|
|
1567
|
+
|
|
1370
1568
|
# Add previous task_id references if any
|
|
1371
1569
|
if related_task_ids:
|
|
1372
1570
|
task_body += f"\n\n[task_id]{','.join(related_task_ids)}[/task_id]"
|
|
@@ -1388,11 +1586,17 @@ def create_kanban_task_from_comment(
|
|
|
1388
1586
|
# Add comment tag_id
|
|
1389
1587
|
tags.append(comment_tag_id)
|
|
1390
1588
|
|
|
1589
|
+
# Add has-attachments tag if files were downloaded
|
|
1590
|
+
if attachment_paths:
|
|
1591
|
+
tags.append('has-attachments')
|
|
1592
|
+
|
|
1391
1593
|
if dry_run:
|
|
1392
1594
|
logger.info(f"[DRY RUN] Would create task for comment #{comment_id} on issue #{issue_number}")
|
|
1393
1595
|
logger.debug(f"[DRY RUN] Body: {task_body[:200]}...")
|
|
1394
1596
|
logger.debug(f"[DRY RUN] Tags: {', '.join(tags)}")
|
|
1395
1597
|
logger.debug(f"[DRY RUN] Related task IDs: {related_task_ids}")
|
|
1598
|
+
if attachment_paths:
|
|
1599
|
+
logger.debug(f"[DRY RUN] Attachments: {len(attachment_paths)} files")
|
|
1396
1600
|
return "dry-run-task-id"
|
|
1397
1601
|
|
|
1398
1602
|
try:
|
|
@@ -1436,7 +1640,10 @@ def process_issue_comments(
|
|
|
1436
1640
|
repo: str,
|
|
1437
1641
|
kanban_script: str,
|
|
1438
1642
|
comment_state_mgr: 'CommentStateManager',
|
|
1439
|
-
dry_run: bool = False
|
|
1643
|
+
dry_run: bool = False,
|
|
1644
|
+
download_attachments: bool = False,
|
|
1645
|
+
downloader: Optional['AttachmentDownloader'] = None,
|
|
1646
|
+
token: Optional[str] = None
|
|
1440
1647
|
) -> Tuple[int, int]:
|
|
1441
1648
|
"""
|
|
1442
1649
|
Process comments on an issue, creating kanban tasks for user replies.
|
|
@@ -1454,6 +1661,9 @@ def process_issue_comments(
|
|
|
1454
1661
|
kanban_script: Path to kanban.sh script
|
|
1455
1662
|
comment_state_mgr: CommentStateManager for tracking processed comments
|
|
1456
1663
|
dry_run: If True, don't actually create tasks
|
|
1664
|
+
download_attachments: If True, download file attachments from comments
|
|
1665
|
+
downloader: AttachmentDownloader instance for handling downloads
|
|
1666
|
+
token: GitHub token for authenticated downloads
|
|
1457
1667
|
|
|
1458
1668
|
Returns:
|
|
1459
1669
|
Tuple of (processed_count, created_count)
|
|
@@ -1500,8 +1710,30 @@ def process_issue_comments(
|
|
|
1500
1710
|
logger.info(f" Comment #{comment_id} (@{comment_author}): User reply detected")
|
|
1501
1711
|
processed += 1
|
|
1502
1712
|
|
|
1713
|
+
# Handle attachments if enabled
|
|
1714
|
+
attachment_paths = []
|
|
1715
|
+
if download_attachments and downloader and token:
|
|
1716
|
+
attachment_urls = extract_attachment_urls(comment_body)
|
|
1717
|
+
if attachment_urls:
|
|
1718
|
+
logger.info(f" Found {len(attachment_urls)} attachment(s) in comment")
|
|
1719
|
+
if not dry_run:
|
|
1720
|
+
attachment_paths = download_github_attachments(
|
|
1721
|
+
urls=attachment_urls,
|
|
1722
|
+
token=token,
|
|
1723
|
+
repo=repo,
|
|
1724
|
+
issue_number=issue_number,
|
|
1725
|
+
downloader=downloader
|
|
1726
|
+
)
|
|
1727
|
+
if attachment_paths:
|
|
1728
|
+
logger.info(f" Downloaded {len(attachment_paths)} attachment(s)")
|
|
1729
|
+
else:
|
|
1730
|
+
logger.warning(f" Failed to download some attachments")
|
|
1731
|
+
else:
|
|
1732
|
+
logger.info(f" [DRY RUN] Would download {len(attachment_urls)} attachment(s)")
|
|
1733
|
+
|
|
1503
1734
|
task_id = create_kanban_task_from_comment(
|
|
1504
|
-
comment, issue, repo, kanban_script, all_task_ids, dry_run
|
|
1735
|
+
comment, issue, repo, kanban_script, all_task_ids, dry_run,
|
|
1736
|
+
attachment_paths=attachment_paths
|
|
1505
1737
|
)
|
|
1506
1738
|
|
|
1507
1739
|
if task_id:
|
|
@@ -1593,12 +1825,24 @@ def handle_fetch(args: argparse.Namespace) -> int:
|
|
|
1593
1825
|
# Check if we should include comments
|
|
1594
1826
|
include_comments = getattr(args, 'include_comments', True) # Default to True
|
|
1595
1827
|
|
|
1828
|
+
# Initialize attachment downloader if enabled
|
|
1829
|
+
downloader = None
|
|
1830
|
+
download_attachments = getattr(args, 'download_attachments', True) and is_attachments_enabled()
|
|
1831
|
+
if download_attachments and ATTACHMENTS_AVAILABLE:
|
|
1832
|
+
attachments_dir = project_dir / '.juno_task' / 'attachments'
|
|
1833
|
+
downloader = AttachmentDownloader(base_dir=str(attachments_dir))
|
|
1834
|
+
logger.info(f"Attachment downloads enabled: {attachments_dir}")
|
|
1835
|
+
elif download_attachments and not ATTACHMENTS_AVAILABLE:
|
|
1836
|
+
logger.warning("Attachment downloads requested but attachment_downloader module not available")
|
|
1837
|
+
download_attachments = False
|
|
1838
|
+
|
|
1596
1839
|
if args.dry_run:
|
|
1597
1840
|
logger.info("Running in DRY RUN mode - no tasks will be created")
|
|
1598
1841
|
|
|
1599
1842
|
logger.info(f"Monitoring repository: {repo}")
|
|
1600
1843
|
logger.info(f"Filters: labels={args.labels or 'None'} assignee={args.assignee or 'None'} state={args.state}")
|
|
1601
1844
|
logger.info(f"Include comments/replies: {include_comments}")
|
|
1845
|
+
logger.info(f"Download attachments: {download_attachments}")
|
|
1602
1846
|
logger.info(f"Mode: {'once' if args.once else 'continuous'}")
|
|
1603
1847
|
if since:
|
|
1604
1848
|
logger.info(f"Incremental sync since: {since}")
|
|
@@ -1642,7 +1886,29 @@ def handle_fetch(args: argparse.Namespace) -> int:
|
|
|
1642
1886
|
for issue in new_issues:
|
|
1643
1887
|
logger.info(f" Issue #{issue['number']} (@{issue['user']['login']}): {issue['title']}")
|
|
1644
1888
|
|
|
1645
|
-
|
|
1889
|
+
# Handle attachments if enabled
|
|
1890
|
+
attachment_paths = []
|
|
1891
|
+
if download_attachments and downloader:
|
|
1892
|
+
attachment_urls = extract_attachment_urls(issue.get('body', ''))
|
|
1893
|
+
if attachment_urls:
|
|
1894
|
+
logger.info(f" Found {len(attachment_urls)} attachment(s) in issue body")
|
|
1895
|
+
if not args.dry_run:
|
|
1896
|
+
attachment_paths = download_github_attachments(
|
|
1897
|
+
urls=attachment_urls,
|
|
1898
|
+
token=token,
|
|
1899
|
+
repo=repo,
|
|
1900
|
+
issue_number=issue['number'],
|
|
1901
|
+
downloader=downloader
|
|
1902
|
+
)
|
|
1903
|
+
if attachment_paths:
|
|
1904
|
+
logger.info(f" Downloaded {len(attachment_paths)} attachment(s)")
|
|
1905
|
+
else:
|
|
1906
|
+
logger.info(f" [DRY RUN] Would download {len(attachment_urls)} attachment(s)")
|
|
1907
|
+
|
|
1908
|
+
task_id = create_kanban_task_from_issue(
|
|
1909
|
+
issue, repo, kanban_script, args.dry_run,
|
|
1910
|
+
attachment_paths=attachment_paths
|
|
1911
|
+
)
|
|
1646
1912
|
|
|
1647
1913
|
if task_id:
|
|
1648
1914
|
if not args.dry_run:
|
|
@@ -1659,7 +1925,9 @@ def handle_fetch(args: argparse.Namespace) -> int:
|
|
|
1659
1925
|
'created_at': issue['created_at'],
|
|
1660
1926
|
'updated_at': issue['updated_at'],
|
|
1661
1927
|
'issue_url': issue['url'],
|
|
1662
|
-
'issue_html_url': issue['html_url']
|
|
1928
|
+
'issue_html_url': issue['html_url'],
|
|
1929
|
+
'attachment_count': len(attachment_paths),
|
|
1930
|
+
'attachment_paths': attachment_paths
|
|
1663
1931
|
}, task_id)
|
|
1664
1932
|
|
|
1665
1933
|
logger.info(f" ✓ Created kanban task: {task_id}")
|
|
@@ -1692,7 +1960,10 @@ def handle_fetch(args: argparse.Namespace) -> int:
|
|
|
1692
1960
|
repo,
|
|
1693
1961
|
kanban_script,
|
|
1694
1962
|
comment_state_mgr,
|
|
1695
|
-
args.dry_run
|
|
1963
|
+
args.dry_run,
|
|
1964
|
+
download_attachments=download_attachments,
|
|
1965
|
+
downloader=downloader,
|
|
1966
|
+
token=token
|
|
1696
1967
|
)
|
|
1697
1968
|
|
|
1698
1969
|
if replies_created > 0:
|
|
@@ -2294,6 +2565,8 @@ Notes:
|
|
|
2294
2565
|
fetch_parser.add_argument('--verbose', '-v', action='store_true', help='Enable DEBUG level logging')
|
|
2295
2566
|
fetch_parser.add_argument('--include-comments', dest='include_comments', action='store_true', default=True, help='Include user replies/comments (default: True)')
|
|
2296
2567
|
fetch_parser.add_argument('--no-comments', dest='include_comments', action='store_false', help='Skip processing user replies/comments')
|
|
2568
|
+
fetch_parser.add_argument('--download-attachments', dest='download_attachments', action='store_true', default=True, help='Download file attachments (default: True)')
|
|
2569
|
+
fetch_parser.add_argument('--no-attachments', dest='download_attachments', action='store_false', help='Skip downloading file attachments')
|
|
2297
2570
|
|
|
2298
2571
|
# Respond subcommand
|
|
2299
2572
|
respond_parser = subparsers.add_parser('respond', help='Post comments on GitHub issues for completed tasks')
|
|
@@ -2322,6 +2595,8 @@ Notes:
|
|
|
2322
2595
|
sync_parser.add_argument('--reset-tracker', action='store_true', help='Reset response tracker (WARNING: will re-send all responses)')
|
|
2323
2596
|
sync_parser.add_argument('--include-comments', dest='include_comments', action='store_true', default=True, help='Include user replies/comments (default: True)')
|
|
2324
2597
|
sync_parser.add_argument('--no-comments', dest='include_comments', action='store_false', help='Skip processing user replies/comments')
|
|
2598
|
+
sync_parser.add_argument('--download-attachments', dest='download_attachments', action='store_true', default=True, help='Download file attachments (default: True)')
|
|
2599
|
+
sync_parser.add_argument('--no-attachments', dest='download_attachments', action='store_false', help='Skip downloading file attachments')
|
|
2325
2600
|
|
|
2326
2601
|
# Push subcommand
|
|
2327
2602
|
push_parser = subparsers.add_parser('push', help='Create GitHub issues from kanban tasks without issues')
|