juno-code 1.0.44 → 1.0.45
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/dist/bin/cli.js +18 -8
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +18 -8
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +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/slack_fetch.py +232 -20
- package/dist/templates/services/claude.py +48 -0
- package/package.json +1 -1
|
@@ -11,18 +11,22 @@ Features:
|
|
|
11
11
|
- Default --once mode for cron/scheduled jobs
|
|
12
12
|
- Persistent state tracking (no duplicate processing)
|
|
13
13
|
- Automatic kanban task creation with slack-input tag
|
|
14
|
+
- File attachment downloading (saves to .juno_task/attachments/slack/)
|
|
14
15
|
- Graceful shutdown on SIGINT/SIGTERM
|
|
15
16
|
|
|
16
17
|
Usage:
|
|
17
18
|
python slack_fetch.py --channel bug-reports --once
|
|
18
19
|
python slack_fetch.py --channel feature-requests --continuous
|
|
19
20
|
python slack_fetch.py --channel general --dry-run --verbose
|
|
21
|
+
python slack_fetch.py --channel uploads --download-attachments
|
|
20
22
|
|
|
21
23
|
Environment Variables:
|
|
22
|
-
SLACK_BOT_TOKEN
|
|
23
|
-
SLACK_CHANNEL
|
|
24
|
-
CHECK_INTERVAL_SECONDS
|
|
25
|
-
LOG_LEVEL
|
|
24
|
+
SLACK_BOT_TOKEN Slack bot token (required, starts with xoxb-)
|
|
25
|
+
SLACK_CHANNEL Default channel to monitor
|
|
26
|
+
CHECK_INTERVAL_SECONDS Polling interval in seconds (default: 60)
|
|
27
|
+
LOG_LEVEL DEBUG, INFO, WARNING, ERROR (default: INFO)
|
|
28
|
+
JUNO_DOWNLOAD_ATTACHMENTS Enable/disable file downloads (default: true)
|
|
29
|
+
JUNO_MAX_ATTACHMENT_SIZE Max file size in bytes (default: 50MB)
|
|
26
30
|
"""
|
|
27
31
|
|
|
28
32
|
import argparse
|
|
@@ -35,7 +39,7 @@ import sys
|
|
|
35
39
|
import time
|
|
36
40
|
from datetime import datetime
|
|
37
41
|
from pathlib import Path
|
|
38
|
-
from typing import Dict, List, Optional, Any
|
|
42
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
39
43
|
|
|
40
44
|
try:
|
|
41
45
|
from dotenv import load_dotenv
|
|
@@ -46,15 +50,32 @@ except ImportError as e:
|
|
|
46
50
|
print("Please run: pip install slack_sdk python-dotenv")
|
|
47
51
|
sys.exit(1)
|
|
48
52
|
|
|
49
|
-
# Import local
|
|
53
|
+
# Import local modules
|
|
54
|
+
script_dir = Path(__file__).parent
|
|
55
|
+
sys.path.insert(0, str(script_dir))
|
|
56
|
+
|
|
50
57
|
try:
|
|
51
58
|
from slack_state import SlackStateManager
|
|
52
59
|
except ImportError:
|
|
53
60
|
# Fallback: try importing from same directory
|
|
54
|
-
script_dir = Path(__file__).parent
|
|
55
|
-
sys.path.insert(0, str(script_dir))
|
|
56
61
|
from slack_state import SlackStateManager
|
|
57
62
|
|
|
63
|
+
# Import attachment downloader for file handling
|
|
64
|
+
try:
|
|
65
|
+
from attachment_downloader import (
|
|
66
|
+
AttachmentDownloader,
|
|
67
|
+
format_attachments_section,
|
|
68
|
+
is_attachments_enabled
|
|
69
|
+
)
|
|
70
|
+
ATTACHMENTS_AVAILABLE = True
|
|
71
|
+
except ImportError:
|
|
72
|
+
ATTACHMENTS_AVAILABLE = False
|
|
73
|
+
# Define stub functions if attachment_downloader not available
|
|
74
|
+
def is_attachments_enabled():
|
|
75
|
+
return False
|
|
76
|
+
def format_attachments_section(paths):
|
|
77
|
+
return ""
|
|
78
|
+
|
|
58
79
|
|
|
59
80
|
# Global shutdown flag
|
|
60
81
|
shutdown_requested = False
|
|
@@ -235,6 +256,119 @@ def sanitize_tag(tag: str) -> str:
|
|
|
235
256
|
return tag
|
|
236
257
|
|
|
237
258
|
|
|
259
|
+
# =============================================================================
|
|
260
|
+
# File Attachment Handling
|
|
261
|
+
# =============================================================================
|
|
262
|
+
|
|
263
|
+
def extract_files_from_message(message: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
264
|
+
"""
|
|
265
|
+
Extract file attachment information from a Slack message.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
message: Message dict from conversations.history API
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of file info dicts with keys: id, name, url_private_download, size, mimetype
|
|
272
|
+
"""
|
|
273
|
+
files = message.get('files', [])
|
|
274
|
+
if not files:
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
extracted = []
|
|
278
|
+
for file_info in files:
|
|
279
|
+
# Skip external file links (not uploaded to Slack)
|
|
280
|
+
if file_info.get('mode') == 'external':
|
|
281
|
+
logger.debug(f"Skipping external file: {file_info.get('name')}")
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
# Skip files that are tombstoned (deleted)
|
|
285
|
+
if file_info.get('mode') == 'tombstone':
|
|
286
|
+
logger.debug(f"Skipping deleted file: {file_info.get('id')}")
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
extracted.append({
|
|
290
|
+
'id': file_info.get('id'),
|
|
291
|
+
'name': file_info.get('name', f"file_{file_info.get('id')}"),
|
|
292
|
+
'url_private_download': file_info.get('url_private_download'),
|
|
293
|
+
'url_private': file_info.get('url_private'),
|
|
294
|
+
'size': file_info.get('size', 0),
|
|
295
|
+
'mimetype': file_info.get('mimetype', 'application/octet-stream'),
|
|
296
|
+
'filetype': file_info.get('filetype', 'unknown'),
|
|
297
|
+
'title': file_info.get('title', '')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
return extracted
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def download_message_files(
|
|
304
|
+
files: List[Dict[str, Any]],
|
|
305
|
+
bot_token: str,
|
|
306
|
+
channel_id: str,
|
|
307
|
+
message_ts: str,
|
|
308
|
+
downloader: 'AttachmentDownloader'
|
|
309
|
+
) -> List[str]:
|
|
310
|
+
"""
|
|
311
|
+
Download all files from a Slack message.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
files: List of file info dicts from extract_files_from_message()
|
|
315
|
+
bot_token: Slack bot token for authorization
|
|
316
|
+
channel_id: Channel ID for directory organization
|
|
317
|
+
message_ts: Message timestamp for filename prefix
|
|
318
|
+
downloader: AttachmentDownloader instance
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of local file paths (empty for failures)
|
|
322
|
+
"""
|
|
323
|
+
if not files:
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
downloaded_paths = []
|
|
327
|
+
headers = {'Authorization': f'Bearer {bot_token}'}
|
|
328
|
+
|
|
329
|
+
# Sanitize message_ts for use in filename (remove dots)
|
|
330
|
+
ts_prefix = message_ts.replace('.', '_')
|
|
331
|
+
|
|
332
|
+
target_dir = downloader.base_dir / 'slack' / channel_id
|
|
333
|
+
|
|
334
|
+
for file_info in files:
|
|
335
|
+
# Prefer url_private_download, fallback to url_private
|
|
336
|
+
url = file_info.get('url_private_download') or file_info.get('url_private')
|
|
337
|
+
if not url:
|
|
338
|
+
logger.warning(f"No download URL for file {file_info.get('id')}")
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
original_filename = file_info.get('name', 'unnamed')
|
|
342
|
+
|
|
343
|
+
metadata = {
|
|
344
|
+
'source': 'slack',
|
|
345
|
+
'source_id': file_info.get('id'),
|
|
346
|
+
'message_ts': message_ts,
|
|
347
|
+
'channel_id': channel_id,
|
|
348
|
+
'mime_type': file_info.get('mimetype'),
|
|
349
|
+
'filetype': file_info.get('filetype'),
|
|
350
|
+
'title': file_info.get('title', ''),
|
|
351
|
+
'original_size': file_info.get('size', 0)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
path, error = downloader.download_file(
|
|
355
|
+
url=url,
|
|
356
|
+
target_dir=target_dir,
|
|
357
|
+
filename_prefix=ts_prefix,
|
|
358
|
+
original_filename=original_filename,
|
|
359
|
+
headers=headers,
|
|
360
|
+
metadata=metadata
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if path:
|
|
364
|
+
downloaded_paths.append(path)
|
|
365
|
+
logger.info(f"Downloaded Slack file: {original_filename}")
|
|
366
|
+
else:
|
|
367
|
+
logger.warning(f"Failed to download {original_filename}: {error}")
|
|
368
|
+
|
|
369
|
+
return downloaded_paths
|
|
370
|
+
|
|
371
|
+
|
|
238
372
|
def create_kanban_task(
|
|
239
373
|
message_text: str,
|
|
240
374
|
author_name: str,
|
|
@@ -310,7 +444,10 @@ def process_messages(
|
|
|
310
444
|
client: WebClient,
|
|
311
445
|
state_mgr: SlackStateManager,
|
|
312
446
|
kanban_script: str,
|
|
313
|
-
dry_run: bool = False
|
|
447
|
+
dry_run: bool = False,
|
|
448
|
+
bot_token: Optional[str] = None,
|
|
449
|
+
downloader: Optional['AttachmentDownloader'] = None,
|
|
450
|
+
download_attachments: bool = True
|
|
314
451
|
) -> int:
|
|
315
452
|
"""
|
|
316
453
|
Process new messages: create kanban tasks and update state.
|
|
@@ -323,6 +460,9 @@ def process_messages(
|
|
|
323
460
|
state_mgr: SlackStateManager instance
|
|
324
461
|
kanban_script: Path to kanban.sh script
|
|
325
462
|
dry_run: If True, don't create tasks
|
|
463
|
+
bot_token: Slack bot token for downloading attachments
|
|
464
|
+
downloader: AttachmentDownloader instance for file downloads
|
|
465
|
+
download_attachments: Whether to download file attachments
|
|
326
466
|
|
|
327
467
|
Returns:
|
|
328
468
|
Number of messages processed
|
|
@@ -341,8 +481,11 @@ def process_messages(
|
|
|
341
481
|
user_id = msg.get('user', 'unknown')
|
|
342
482
|
text = msg.get('text', '')
|
|
343
483
|
|
|
344
|
-
#
|
|
345
|
-
|
|
484
|
+
# Check if message has files (allows processing messages with only attachments)
|
|
485
|
+
has_files = bool(msg.get('files'))
|
|
486
|
+
|
|
487
|
+
# Skip empty messages (unless they have files)
|
|
488
|
+
if not text.strip() and not has_files:
|
|
346
489
|
logger.debug(f"Skipping empty message ts={ts}")
|
|
347
490
|
continue
|
|
348
491
|
|
|
@@ -362,11 +505,40 @@ def process_messages(
|
|
|
362
505
|
|
|
363
506
|
logger.info(f"New message from {author_name}: {text[:50]}{'...' if len(text) > 50 else ''}")
|
|
364
507
|
|
|
508
|
+
# Handle file attachments
|
|
509
|
+
attachment_paths = []
|
|
510
|
+
if download_attachments and ATTACHMENTS_AVAILABLE and downloader and bot_token:
|
|
511
|
+
files = extract_files_from_message(msg)
|
|
512
|
+
if files:
|
|
513
|
+
logger.info(f"Found {len(files)} file(s) attached to message")
|
|
514
|
+
if not dry_run:
|
|
515
|
+
attachment_paths = download_message_files(
|
|
516
|
+
files=files,
|
|
517
|
+
bot_token=bot_token,
|
|
518
|
+
channel_id=channel_id,
|
|
519
|
+
message_ts=ts,
|
|
520
|
+
downloader=downloader
|
|
521
|
+
)
|
|
522
|
+
if attachment_paths:
|
|
523
|
+
logger.info(f"Downloaded {len(attachment_paths)} file(s)")
|
|
524
|
+
else:
|
|
525
|
+
logger.info(f"[DRY RUN] Would download {len(files)} file(s)")
|
|
526
|
+
|
|
527
|
+
# Build task text with attachment paths
|
|
528
|
+
task_text = text
|
|
529
|
+
if attachment_paths:
|
|
530
|
+
task_text += format_attachments_section(attachment_paths)
|
|
531
|
+
|
|
365
532
|
# Create kanban task
|
|
366
533
|
# Create author tag with sanitization (colons not allowed in kanban tags)
|
|
367
534
|
author_tag = sanitize_tag(f'author_{author_name}')
|
|
368
535
|
tags = ['slack-input', author_tag]
|
|
369
|
-
|
|
536
|
+
|
|
537
|
+
# Add has-attachments tag if files were downloaded
|
|
538
|
+
if attachment_paths:
|
|
539
|
+
tags.append('has-attachments')
|
|
540
|
+
|
|
541
|
+
task_id = create_kanban_task(task_text, author_name, tags, kanban_script, dry_run)
|
|
370
542
|
|
|
371
543
|
if task_id:
|
|
372
544
|
# Record in state
|
|
@@ -377,7 +549,9 @@ def process_messages(
|
|
|
377
549
|
'date': date_str,
|
|
378
550
|
'channel': channel_name,
|
|
379
551
|
'channel_id': channel_id,
|
|
380
|
-
'thread_ts': msg.get('thread_ts', ts)
|
|
552
|
+
'thread_ts': msg.get('thread_ts', ts),
|
|
553
|
+
'attachment_count': len(attachment_paths),
|
|
554
|
+
'attachment_paths': attachment_paths
|
|
381
555
|
}
|
|
382
556
|
|
|
383
557
|
if not dry_run:
|
|
@@ -408,7 +582,7 @@ def find_kanban_script(project_dir: Path) -> Optional[str]:
|
|
|
408
582
|
SLACK_TOKEN_DOCS_URL = "https://api.slack.com/tutorials/tracks/getting-a-token"
|
|
409
583
|
|
|
410
584
|
|
|
411
|
-
def validate_slack_environment() ->
|
|
585
|
+
def validate_slack_environment() -> Tuple[Optional[str], Optional[str], List[str]]:
|
|
412
586
|
"""
|
|
413
587
|
Validate Slack environment variables are properly configured.
|
|
414
588
|
|
|
@@ -486,6 +660,7 @@ Generating a Slack Bot Token:
|
|
|
486
660
|
- channels:history, channels:read (public channels)
|
|
487
661
|
- groups:history, groups:read (private channels)
|
|
488
662
|
- users:read (user info)
|
|
663
|
+
- files:read (download file attachments)
|
|
489
664
|
- chat:write (for slack_respond.py)
|
|
490
665
|
3. Install the app to your workspace
|
|
491
666
|
4. Copy the "Bot User OAuth Token" (starts with xoxb-)
|
|
@@ -583,9 +758,21 @@ def main_loop(args: argparse.Namespace) -> int:
|
|
|
583
758
|
if args.dry_run:
|
|
584
759
|
logger.info("Running in DRY RUN mode - no tasks will be created")
|
|
585
760
|
|
|
761
|
+
# Initialize attachment downloader if enabled
|
|
762
|
+
downloader = None
|
|
763
|
+
download_attachments = getattr(args, 'download_attachments', True) and is_attachments_enabled()
|
|
764
|
+
if download_attachments and ATTACHMENTS_AVAILABLE:
|
|
765
|
+
attachments_dir = project_dir / '.juno_task' / 'attachments'
|
|
766
|
+
downloader = AttachmentDownloader(base_dir=str(attachments_dir))
|
|
767
|
+
logger.info(f"Attachment downloads enabled: {attachments_dir}")
|
|
768
|
+
elif download_attachments and not ATTACHMENTS_AVAILABLE:
|
|
769
|
+
logger.warning("Attachment downloads requested but attachment_downloader module not available")
|
|
770
|
+
download_attachments = False
|
|
771
|
+
|
|
586
772
|
logger.info(f"Monitoring channel #{channel} (ID: {channel_id})")
|
|
587
773
|
logger.info(f"Check interval: {check_interval} seconds")
|
|
588
774
|
logger.info(f"Mode: {'once' if args.once else 'continuous'}")
|
|
775
|
+
logger.info(f"Download attachments: {download_attachments}")
|
|
589
776
|
logger.info("-" * 70)
|
|
590
777
|
|
|
591
778
|
# Main loop
|
|
@@ -614,7 +801,10 @@ def main_loop(args: argparse.Namespace) -> int:
|
|
|
614
801
|
client,
|
|
615
802
|
state_mgr,
|
|
616
803
|
kanban_script,
|
|
617
|
-
dry_run=args.dry_run
|
|
804
|
+
dry_run=args.dry_run,
|
|
805
|
+
bot_token=bot_token,
|
|
806
|
+
downloader=downloader,
|
|
807
|
+
download_attachments=download_attachments
|
|
618
808
|
)
|
|
619
809
|
total_processed += processed
|
|
620
810
|
logger.info(f"Processed {processed} messages (total: {total_processed})")
|
|
@@ -655,20 +845,26 @@ def main() -> int:
|
|
|
655
845
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
656
846
|
epilog="""
|
|
657
847
|
Examples:
|
|
658
|
-
%(prog)s --channel bug-reports
|
|
848
|
+
%(prog)s --channel bug-reports # Run once (default)
|
|
659
849
|
%(prog)s --channel feature-requests --continuous # Continuous monitoring
|
|
660
850
|
%(prog)s --channel general --dry-run --verbose # Test mode
|
|
851
|
+
%(prog)s --channel uploads --download-attachments # Explicit attachment download
|
|
661
852
|
|
|
662
853
|
Environment Variables:
|
|
663
|
-
SLACK_BOT_TOKEN
|
|
664
|
-
SLACK_CHANNEL
|
|
665
|
-
CHECK_INTERVAL_SECONDS
|
|
666
|
-
LOG_LEVEL
|
|
854
|
+
SLACK_BOT_TOKEN Slack bot token (required)
|
|
855
|
+
SLACK_CHANNEL Default channel to monitor
|
|
856
|
+
CHECK_INTERVAL_SECONDS Polling interval (default: 60)
|
|
857
|
+
LOG_LEVEL DEBUG, INFO, WARNING, ERROR (default: INFO)
|
|
858
|
+
JUNO_DOWNLOAD_ATTACHMENTS Enable/disable file downloads (default: true)
|
|
859
|
+
JUNO_MAX_ATTACHMENT_SIZE Max file size in bytes (default: 50MB)
|
|
667
860
|
|
|
668
861
|
Notes:
|
|
669
862
|
- Messages are tagged with 'slack-input' and 'author_<name>'
|
|
863
|
+
- Messages with attachments also get 'has-attachments' tag
|
|
670
864
|
- State is persisted to .juno_task/slack/slack.ndjson
|
|
865
|
+
- Attachments saved to .juno_task/attachments/slack/<channel_id>/
|
|
671
866
|
- Use Ctrl+C for graceful shutdown
|
|
867
|
+
- Required OAuth scope for file downloads: files:read
|
|
672
868
|
"""
|
|
673
869
|
)
|
|
674
870
|
|
|
@@ -692,6 +888,22 @@ Notes:
|
|
|
692
888
|
help='Run continuously with polling'
|
|
693
889
|
)
|
|
694
890
|
|
|
891
|
+
# Attachment handling options
|
|
892
|
+
attachment_group = parser.add_mutually_exclusive_group()
|
|
893
|
+
attachment_group.add_argument(
|
|
894
|
+
'--download-attachments',
|
|
895
|
+
dest='download_attachments',
|
|
896
|
+
action='store_true',
|
|
897
|
+
default=True,
|
|
898
|
+
help='Download file attachments from messages (DEFAULT)'
|
|
899
|
+
)
|
|
900
|
+
attachment_group.add_argument(
|
|
901
|
+
'--no-attachments',
|
|
902
|
+
dest='download_attachments',
|
|
903
|
+
action='store_false',
|
|
904
|
+
help='Skip downloading file attachments'
|
|
905
|
+
)
|
|
906
|
+
|
|
695
907
|
parser.add_argument(
|
|
696
908
|
'--dry-run',
|
|
697
909
|
action='store_true',
|
|
@@ -388,6 +388,54 @@ Environment Variables:
|
|
|
388
388
|
simplified["content"] = text_content
|
|
389
389
|
return json.dumps(simplified, ensure_ascii=False)
|
|
390
390
|
|
|
391
|
+
# For progress events, handle bash_progress and skip hook_progress
|
|
392
|
+
elif data.get("type") == "progress":
|
|
393
|
+
progress_data = data.get("data", {})
|
|
394
|
+
progress_type = progress_data.get("type", "")
|
|
395
|
+
|
|
396
|
+
# Skip hook_progress events (not interested)
|
|
397
|
+
if progress_type == "hook_progress":
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
# Display bash_progress events with [Progress] tag
|
|
401
|
+
if progress_type == "bash_progress":
|
|
402
|
+
# Extract relevant fields from bash_progress
|
|
403
|
+
output_text = progress_data.get("output", "")
|
|
404
|
+
elapsed_time = progress_data.get("elapsedTimeSeconds", 0)
|
|
405
|
+
total_lines = progress_data.get("totalLines", 0)
|
|
406
|
+
|
|
407
|
+
# Create simplified output with datetime and counter
|
|
408
|
+
simplified = {
|
|
409
|
+
"type": "progress",
|
|
410
|
+
"progress_type": "bash_progress",
|
|
411
|
+
"datetime": now,
|
|
412
|
+
"counter": f"#{self.message_counter}",
|
|
413
|
+
"elapsed": f"{elapsed_time}s",
|
|
414
|
+
"lines": total_lines
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
# Check if output has newlines
|
|
418
|
+
if '\n' in output_text:
|
|
419
|
+
# Multi-line output: print metadata, then raw output
|
|
420
|
+
metadata_json = json.dumps(simplified, ensure_ascii=False)
|
|
421
|
+
return metadata_json + "\n[Progress] output:\n" + output_text
|
|
422
|
+
else:
|
|
423
|
+
# Single-line output: normal JSON with [Progress] tag
|
|
424
|
+
simplified["output"] = output_text
|
|
425
|
+
# Add [Progress] tag to the output
|
|
426
|
+
output_json = json.dumps(simplified, ensure_ascii=False)
|
|
427
|
+
return f"[Progress] {output_json}"
|
|
428
|
+
|
|
429
|
+
# For other progress types, display with datetime and counter
|
|
430
|
+
simplified = {
|
|
431
|
+
"type": "progress",
|
|
432
|
+
"progress_type": progress_type,
|
|
433
|
+
"datetime": now,
|
|
434
|
+
"counter": f"#{self.message_counter}",
|
|
435
|
+
"data": progress_data
|
|
436
|
+
}
|
|
437
|
+
return json.dumps(simplified, ensure_ascii=False)
|
|
438
|
+
|
|
391
439
|
# For assistant messages, show simplified output
|
|
392
440
|
elif data.get("type") == "assistant":
|
|
393
441
|
message = data.get("message", {})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juno-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "Ralph Wiggum meet Kanban! Ralph style execution for [Claude Code, Codex, Gemini, Cursor]. One task per iteration, automatic progress tracking, and git commits. Set it and let it run.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ralph",
|