juno-code 1.0.37 → 1.0.40

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.
@@ -23,6 +23,22 @@
23
23
 
24
24
  set -euo pipefail # Exit on error, undefined variable, or pipe failure
25
25
 
26
+ # Handle --help early (before any debug output)
27
+ for arg in "$@"; do
28
+ if [[ "$arg" == "-h" ]] || [[ "$arg" == "--help" ]]; then
29
+ echo "Usage: install_requirements.sh [OPTIONS]"
30
+ echo ""
31
+ echo "Options:"
32
+ echo " --check-updates Only check for updates without installing"
33
+ echo " --force-update Force update check and upgrade packages"
34
+ echo " -h, --help Show this help message"
35
+ echo ""
36
+ echo "Environment Variables:"
37
+ echo " VERSION_CHECK_INTERVAL_HOURS Hours between automatic update checks (default: 24)"
38
+ exit 0
39
+ fi
40
+ done
41
+
26
42
  # DEBUG OUTPUT: Show that install_requirements.sh is being executed
27
43
  # User feedback: "Add a one line printing from .sh file as well so we could debug it"
28
44
  echo "[DEBUG] install_requirements.sh is being executed from: $(pwd)" >&2
@@ -41,6 +57,12 @@ REQUIRED_PACKAGES=("juno-kanban" "roundtable-ai")
41
57
  # Slack integration dependencies (optional, only installed when Slack scripts are used)
42
58
  SLACK_PACKAGES=("slack_sdk" "python-dotenv")
43
59
 
60
+ # Version check cache configuration
61
+ # This ensures we don't check PyPI on every run (performance optimization per Task RTafs5)
62
+ VERSION_CHECK_CACHE_DIR="${HOME}/.juno_code"
63
+ VERSION_CHECK_CACHE_FILE="${VERSION_CHECK_CACHE_DIR}/.version_check_cache"
64
+ VERSION_CHECK_INTERVAL_HOURS=24 # Check for updates once per day
65
+
44
66
  # Logging functions
45
67
  log_info() {
46
68
  echo -e "${BLUE}[INFO]${NC} $1"
@@ -70,6 +92,205 @@ check_package_installed() {
70
92
  return 1 # Package not installed
71
93
  }
72
94
 
95
+ # Function to get installed version of a package
96
+ get_installed_version() {
97
+ local package_name="$1"
98
+ local version=""
99
+
100
+ # Try python3 first, then python
101
+ version=$(python3 -m pip show "$package_name" 2>/dev/null | grep -i "^Version:" | awk '{print $2}')
102
+ if [ -z "$version" ]; then
103
+ version=$(python -m pip show "$package_name" 2>/dev/null | grep -i "^Version:" | awk '{print $2}')
104
+ fi
105
+
106
+ echo "$version"
107
+ }
108
+
109
+ # Function to get latest version from PyPI
110
+ get_pypi_latest_version() {
111
+ local package_name="$1"
112
+ local version=""
113
+
114
+ # Use curl to fetch from PyPI JSON API (lightweight and fast)
115
+ if command -v curl &>/dev/null; then
116
+ version=$(curl -s --max-time 5 "https://pypi.org/pypi/${package_name}/json" 2>/dev/null | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4)
117
+ fi
118
+
119
+ echo "$version"
120
+ }
121
+
122
+ # Function to check if version check cache is stale
123
+ is_version_check_stale() {
124
+ # Ensure cache directory exists
125
+ if [ ! -d "$VERSION_CHECK_CACHE_DIR" ]; then
126
+ mkdir -p "$VERSION_CHECK_CACHE_DIR"
127
+ return 0 # No cache, needs check
128
+ fi
129
+
130
+ # Check if cache file exists
131
+ if [ ! -f "$VERSION_CHECK_CACHE_FILE" ]; then
132
+ return 0 # No cache file, needs check
133
+ fi
134
+
135
+ # Get cache file modification time and current time
136
+ local cache_mtime
137
+ local current_time
138
+ local age_hours
139
+
140
+ # Cross-platform way to get file age
141
+ if [[ "$OSTYPE" == "darwin"* ]]; then
142
+ # macOS
143
+ cache_mtime=$(stat -f %m "$VERSION_CHECK_CACHE_FILE" 2>/dev/null || echo 0)
144
+ else
145
+ # Linux and others
146
+ cache_mtime=$(stat -c %Y "$VERSION_CHECK_CACHE_FILE" 2>/dev/null || echo 0)
147
+ fi
148
+
149
+ current_time=$(date +%s)
150
+ age_hours=$(( (current_time - cache_mtime) / 3600 ))
151
+
152
+ if [ "$age_hours" -ge "$VERSION_CHECK_INTERVAL_HOURS" ]; then
153
+ return 0 # Cache is stale, needs check
154
+ fi
155
+
156
+ return 1 # Cache is fresh, no check needed
157
+ }
158
+
159
+ # Function to update version check cache
160
+ update_version_check_cache() {
161
+ local package_name="$1"
162
+ local installed_version="$2"
163
+ local latest_version="$3"
164
+
165
+ # Ensure cache directory exists
166
+ mkdir -p "$VERSION_CHECK_CACHE_DIR"
167
+
168
+ # Update cache file with package info
169
+ local cache_line="${package_name}=${installed_version}:${latest_version}"
170
+
171
+ # Remove old entry for this package if exists, then add new entry
172
+ if [ -f "$VERSION_CHECK_CACHE_FILE" ]; then
173
+ grep -v "^${package_name}=" "$VERSION_CHECK_CACHE_FILE" > "${VERSION_CHECK_CACHE_FILE}.tmp" 2>/dev/null || true
174
+ mv "${VERSION_CHECK_CACHE_FILE}.tmp" "$VERSION_CHECK_CACHE_FILE"
175
+ fi
176
+
177
+ echo "$cache_line" >> "$VERSION_CHECK_CACHE_FILE"
178
+
179
+ # Touch the file to update modification time
180
+ touch "$VERSION_CHECK_CACHE_FILE"
181
+ }
182
+
183
+ # Function to check and upgrade a single package if needed
184
+ check_and_upgrade_package() {
185
+ local package_name="$1"
186
+ local force_check="${2:-false}"
187
+
188
+ # Only check if cache is stale or force_check is true
189
+ if [ "$force_check" != "true" ] && ! is_version_check_stale; then
190
+ log_info "Version check cache is fresh (checked within ${VERSION_CHECK_INTERVAL_HOURS}h)"
191
+ return 0
192
+ fi
193
+
194
+ log_info "Checking for updates for: $package_name"
195
+
196
+ local installed_version
197
+ local latest_version
198
+
199
+ installed_version=$(get_installed_version "$package_name")
200
+
201
+ if [ -z "$installed_version" ]; then
202
+ log_warning "$package_name is not installed"
203
+ return 1 # Package not installed
204
+ fi
205
+
206
+ latest_version=$(get_pypi_latest_version "$package_name")
207
+
208
+ if [ -z "$latest_version" ]; then
209
+ log_warning "Could not fetch latest version for $package_name from PyPI"
210
+ return 0 # Can't check, assume OK
211
+ fi
212
+
213
+ # Update cache
214
+ update_version_check_cache "$package_name" "$installed_version" "$latest_version"
215
+
216
+ if [ "$installed_version" = "$latest_version" ]; then
217
+ log_success "$package_name is up-to-date (v$installed_version)"
218
+ return 0
219
+ fi
220
+
221
+ log_warning "$package_name update available: $installed_version -> $latest_version"
222
+ return 2 # Update available
223
+ }
224
+
225
+ # Function to upgrade packages
226
+ upgrade_packages() {
227
+ local packages_to_upgrade=("$@")
228
+
229
+ if [ ${#packages_to_upgrade[@]} -eq 0 ]; then
230
+ return 0
231
+ fi
232
+
233
+ log_info "Upgrading packages: ${packages_to_upgrade[*]}"
234
+
235
+ # Determine which package manager to use (prefer uv for speed)
236
+ if command -v uv &>/dev/null; then
237
+ for package in "${packages_to_upgrade[@]}"; do
238
+ log_info "Upgrading $package with uv..."
239
+ if uv pip install --upgrade "$package" --quiet 2>/dev/null; then
240
+ log_success "Upgraded: $package"
241
+ else
242
+ log_warning "Failed to upgrade $package with uv, trying pip..."
243
+ python3 -m pip install --upgrade "$package" --quiet 2>/dev/null || true
244
+ fi
245
+ done
246
+ elif command -v pipx &>/dev/null && is_externally_managed_python && ! is_in_virtualenv; then
247
+ for package in "${packages_to_upgrade[@]}"; do
248
+ log_info "Upgrading $package with pipx..."
249
+ if pipx upgrade "$package" 2>/dev/null; then
250
+ log_success "Upgraded: $package"
251
+ else
252
+ log_warning "Failed to upgrade $package"
253
+ fi
254
+ done
255
+ else
256
+ for package in "${packages_to_upgrade[@]}"; do
257
+ log_info "Upgrading $package with pip..."
258
+ if python3 -m pip install --upgrade "$package" --quiet 2>/dev/null; then
259
+ log_success "Upgraded: $package"
260
+ else
261
+ log_warning "Failed to upgrade $package"
262
+ fi
263
+ done
264
+ fi
265
+ }
266
+
267
+ # Function to check all packages for updates (periodic check)
268
+ check_all_for_updates() {
269
+ local force_check="${1:-false}"
270
+ local packages_needing_upgrade=()
271
+
272
+ # Skip check if cache is fresh and not forcing
273
+ if [ "$force_check" != "true" ] && ! is_version_check_stale; then
274
+ return 0
275
+ fi
276
+
277
+ log_info "Performing periodic version check..."
278
+
279
+ for package in "${REQUIRED_PACKAGES[@]}"; do
280
+ check_and_upgrade_package "$package" "true"
281
+ local result=$?
282
+ if [ $result -eq 2 ]; then
283
+ packages_needing_upgrade+=("$package")
284
+ fi
285
+ done
286
+
287
+ if [ ${#packages_needing_upgrade[@]} -gt 0 ]; then
288
+ upgrade_packages "${packages_needing_upgrade[@]}"
289
+ else
290
+ log_success "All packages are up-to-date"
291
+ fi
292
+ }
293
+
73
294
  # Function to check if all requirements are satisfied
74
295
  check_all_requirements_satisfied() {
75
296
  local all_satisfied=true
@@ -366,10 +587,52 @@ install_with_pip() {
366
587
 
367
588
  # Main installation logic
368
589
  main() {
590
+ local force_update=false
591
+ local check_updates_only=false
592
+
593
+ # Parse command line arguments (--help is handled early, before debug output)
594
+ while [[ $# -gt 0 ]]; do
595
+ case $1 in
596
+ --force-update)
597
+ force_update=true
598
+ shift
599
+ ;;
600
+ --check-updates)
601
+ check_updates_only=true
602
+ shift
603
+ ;;
604
+ *)
605
+ shift
606
+ ;;
607
+ esac
608
+ done
609
+
369
610
  echo ""
370
611
  log_info "=== Python Requirements Installation ==="
371
612
  echo ""
372
613
 
614
+ # Handle --check-updates: just check and report, don't install
615
+ if [ "$check_updates_only" = true ]; then
616
+ log_info "Checking for updates..."
617
+ for package in "${REQUIRED_PACKAGES[@]}"; do
618
+ local installed_ver
619
+ local latest_ver
620
+ installed_ver=$(get_installed_version "$package")
621
+ latest_ver=$(get_pypi_latest_version "$package")
622
+
623
+ if [ -z "$installed_ver" ]; then
624
+ log_warning "$package is not installed"
625
+ elif [ -z "$latest_ver" ]; then
626
+ log_info "$package: v$installed_ver (could not check PyPI)"
627
+ elif [ "$installed_ver" = "$latest_ver" ]; then
628
+ log_success "$package: v$installed_ver (up-to-date)"
629
+ else
630
+ log_warning "$package: v$installed_ver -> v$latest_ver (update available)"
631
+ fi
632
+ done
633
+ exit 0
634
+ fi
635
+
373
636
  # Step 1: Check if all requirements are already satisfied
374
637
  log_info "Checking if requirements are already satisfied..."
375
638
 
@@ -378,9 +641,15 @@ main() {
378
641
  echo ""
379
642
  log_info "Installed packages:"
380
643
  for package in "${REQUIRED_PACKAGES[@]}"; do
381
- echo " ✓ $package"
644
+ local ver
645
+ ver=$(get_installed_version "$package")
646
+ echo " ✓ $package (v$ver)"
382
647
  done
383
648
  echo ""
649
+
650
+ # Step 1b: Periodic update check (only when cache is stale, or forced)
651
+ # This ensures dependencies stay up-to-date without degrading performance
652
+ check_all_for_updates "$force_update"
384
653
  exit 0
385
654
  fi
386
655
 
@@ -183,8 +183,10 @@ main() {
183
183
  log_success "Python environment ready!"
184
184
 
185
185
  # Execute juno-kanban with all passed arguments from project root
186
+ # Close stdin (redirect from /dev/null) to prevent hanging when called from tools
187
+ # that don't provide stdin (similar to Issue #42 hook fix)
186
188
  log_info "Executing juno-kanban: $*"
187
- exec juno-kanban "$@"
189
+ juno-kanban "$@" < /dev/null
188
190
  }
189
191
 
190
192
  # Run main function with all arguments
@@ -15,6 +15,8 @@
15
15
  # Example: ./.juno_task/scripts/run_until_completion.sh -b shell -s claude -m :opus
16
16
  # Example: ./.juno_task/scripts/run_until_completion.sh --pre-run "./slack/sync.sh" -s claude -i 5
17
17
  # Example: ./.juno_task/scripts/run_until_completion.sh --pre-run-hook START_ITERATION -s claude -i 5
18
+ # Example: ./.juno_task/scripts/run_until_completion.sh --pre-run-hook "SYNC_SLACK,VALIDATE" -s claude -i 5
19
+ # Example: ./.juno_task/scripts/run_until_completion.sh --pre-run-hook "HOOK1|HOOK2|HOOK3" -s claude -i 5
18
20
  #
19
21
  # Options (for run_until_completion.sh):
20
22
  # --pre-run <cmd> - Execute command before entering the main loop
@@ -27,7 +29,10 @@
27
29
  # The hook should be defined in config.json under "hooks"
28
30
  # with a "commands" array. All commands in the hook are
29
31
  # executed before the main loop.
30
- # Can be specified multiple times for multiple hooks.
32
+ # Multiple hooks can be specified by:
33
+ # - Using the flag multiple times: --pre-run-hook A --pre-run-hook B
34
+ # - Comma-separated: --pre-run-hook "A,B,C"
35
+ # - Pipe-separated: --pre-run-hook "A|B|C"
31
36
  #
32
37
  # All other arguments are forwarded to juno-code.
33
38
  # The script shows all stdout/stderr from juno-code in real-time.
@@ -86,7 +91,24 @@ parse_arguments() {
86
91
  echo "[ERROR] $1 requires a hook name argument" >&2
87
92
  exit 1
88
93
  fi
89
- PRE_RUN_HOOKS+=("$2")
94
+ # Support multiple hooks via comma or pipe separator
95
+ # e.g., --pre-run-hook "HOOK1,HOOK2" or --pre-run-hook "HOOK1|HOOK2"
96
+ local hook_value="$2"
97
+ if [[ "$hook_value" == *","* ]] || [[ "$hook_value" == *"|"* ]]; then
98
+ # Replace pipes with commas, then split on commas
99
+ local normalized="${hook_value//|/,}"
100
+ IFS=',' read -ra hook_names <<< "$normalized"
101
+ for hook_name in "${hook_names[@]}"; do
102
+ # Trim whitespace
103
+ hook_name="${hook_name#"${hook_name%%[![:space:]]*}"}"
104
+ hook_name="${hook_name%"${hook_name##*[![:space:]]}"}"
105
+ if [[ -n "$hook_name" ]]; then
106
+ PRE_RUN_HOOKS+=("$hook_name")
107
+ fi
108
+ done
109
+ else
110
+ PRE_RUN_HOOKS+=("$hook_value")
111
+ fi
90
112
  shift 2
91
113
  ;;
92
114
  *)
@@ -103,9 +125,27 @@ parse_arguments() {
103
125
  fi
104
126
 
105
127
  # Also check JUNO_PRE_RUN_HOOK environment variable
128
+ # Supports comma or pipe separated hooks: JUNO_PRE_RUN_HOOK="HOOK1,HOOK2|HOOK3"
106
129
  if [[ -n "${JUNO_PRE_RUN_HOOK:-}" ]]; then
107
- # Prepend env var hook (runs first)
108
- PRE_RUN_HOOKS=("$JUNO_PRE_RUN_HOOK" "${PRE_RUN_HOOKS[@]}")
130
+ local env_hooks=()
131
+ local hook_value="${JUNO_PRE_RUN_HOOK}"
132
+ if [[ "$hook_value" == *","* ]] || [[ "$hook_value" == *"|"* ]]; then
133
+ # Replace pipes with commas, then split on commas
134
+ local normalized="${hook_value//|/,}"
135
+ IFS=',' read -ra hook_names <<< "$normalized"
136
+ for hook_name in "${hook_names[@]}"; do
137
+ # Trim whitespace
138
+ hook_name="${hook_name#"${hook_name%%[![:space:]]*}"}"
139
+ hook_name="${hook_name%"${hook_name##*[![:space:]]}"}"
140
+ if [[ -n "$hook_name" ]]; then
141
+ env_hooks+=("$hook_name")
142
+ fi
143
+ done
144
+ else
145
+ env_hooks+=("$hook_value")
146
+ fi
147
+ # Prepend env var hooks (runs first)
148
+ PRE_RUN_HOOKS=("${env_hooks[@]}" "${PRE_RUN_HOOKS[@]}")
109
149
  fi
110
150
  }
111
151
 
@@ -493,7 +493,7 @@ Generating a Slack Bot Token:
493
493
  Full tutorial: """ + SLACK_TOKEN_DOCS_URL + """
494
494
 
495
495
  Example .env file:
496
- SLACK_BOT_TOKEN=xoxb-1234567890-1234567890123-abcdefghijklmnopqrstuvwxyz
496
+ SLACK_BOT_TOKEN=xoxb-YOUR-BOT-TOKEN-HERE
497
497
  SLACK_CHANNEL=bug-reports
498
498
  CHECK_INTERVAL_SECONDS=120
499
499
  LOG_LEVEL=INFO
@@ -71,7 +71,8 @@ def setup_logging(verbose: bool = False) -> None:
71
71
  def get_kanban_tasks(
72
72
  kanban_script: str,
73
73
  tag: Optional[str] = None,
74
- status: Optional[str] = None
74
+ status: Optional[str] = None,
75
+ limit: int = 10000
75
76
  ) -> List[Dict[str, Any]]:
76
77
  """
77
78
  Get kanban tasks from the kanban.sh script.
@@ -80,11 +81,12 @@ def get_kanban_tasks(
80
81
  kanban_script: Path to kanban.sh script
81
82
  tag: Optional tag to filter by
82
83
  status: Optional status to filter by
84
+ limit: Maximum number of tasks to retrieve (default: 10000 to ensure all tasks)
83
85
 
84
86
  Returns:
85
87
  List of task dicts
86
88
  """
87
- cmd = [kanban_script, 'list']
89
+ cmd = [kanban_script, 'list', '--limit', str(limit)]
88
90
 
89
91
  if tag:
90
92
  cmd.extend(['--tag', tag])
@@ -328,7 +330,7 @@ def send_slack_response(
328
330
  Response message timestamp if sent, None if failed
329
331
  """
330
332
  # Format the response with task ID
331
- formatted_response = f"**Task ID: {task_id}**\n\n{response_text}"
333
+ formatted_response = f"**[task_id]{task_id}[/task_id]**\n\n{response_text}"
332
334
 
333
335
  if dry_run:
334
336
  logger.info(f"[DRY RUN] Would send to channel {channel_id}, thread {thread_ts}:")
@@ -460,7 +462,7 @@ Generating a Slack Bot Token:
460
462
  Full tutorial: """ + SLACK_TOKEN_DOCS_URL + """
461
463
 
462
464
  Example .env file:
463
- SLACK_BOT_TOKEN=xoxb-1234567890-1234567890123-abcdefghijklmnopqrstuvwxyz
465
+ SLACK_BOT_TOKEN=xoxb-YOUR-BOT-TOKEN-HERE
464
466
  LOG_LEVEL=INFO
465
467
  """)
466
468
  print("=" * 70 + "\n")
@@ -650,7 +652,7 @@ Environment Variables:
650
652
  Notes:
651
653
  - Only sends responses for tasks with non-empty agent_response
652
654
  - Matches tasks to Slack messages by task_id or body text
653
- - Responses are prefixed with "**Task ID: {id}**"
655
+ - Responses are prefixed with "**[task_id]{id}[/task_id]**"
654
656
  - Tracks sent responses in .juno_task/slack/responses_sent.ndjson
655
657
  """
656
658
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juno-code",
3
- "version": "1.0.37",
3
+ "version": "1.0.40",
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",