juno-code 1.0.36 → 1.0.38

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,263 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # slack_respond.sh
4
+ #
5
+ # Purpose: Send kanban agent responses back to Slack
6
+ #
7
+ # This script activates the Python virtual environment and runs the
8
+ # slack_respond.py script to send completed task responses back to
9
+ # Slack as threaded replies.
10
+ #
11
+ # Usage:
12
+ # ./.juno_task/scripts/slack_respond.sh
13
+ # ./.juno_task/scripts/slack_respond.sh --tag slack-input
14
+ # ./.juno_task/scripts/slack_respond.sh --dry-run --verbose
15
+ # ./.juno_task/scripts/slack_respond.sh --reset-tracker
16
+ #
17
+ # Environment Variables:
18
+ # SLACK_BOT_TOKEN Slack bot token (required, starts with xoxb-)
19
+ # JUNO_DEBUG=true Show debug messages
20
+ # JUNO_VERBOSE=true Show informational messages
21
+ #
22
+ # Created by: juno-code init command
23
+ # Date: Auto-generated during project initialization
24
+
25
+ set -euo pipefail
26
+
27
+ # Debug output
28
+ if [ "${JUNO_DEBUG:-false}" = "true" ]; then
29
+ echo "[DEBUG] slack_respond.sh is being executed from: $(pwd)" >&2
30
+ fi
31
+
32
+ # Color output
33
+ RED='\033[0;31m'
34
+ GREEN='\033[0;32m'
35
+ YELLOW='\033[1;33m'
36
+ BLUE='\033[0;34m'
37
+ NC='\033[0m'
38
+
39
+ # Configuration
40
+ VENV_DIR=".venv_juno"
41
+ SCRIPTS_DIR=".juno_task/scripts"
42
+ INSTALL_SCRIPT="${SCRIPTS_DIR}/install_requirements.sh"
43
+ SLACK_RESPOND_SCRIPT="${SCRIPTS_DIR}/slack_respond.py"
44
+
45
+ # Logging functions
46
+ log_info() {
47
+ if [ "${JUNO_VERBOSE:-false}" = "true" ]; then
48
+ echo -e "${BLUE}[SLACK_RESPOND]${NC} $1"
49
+ fi
50
+ }
51
+
52
+ log_success() {
53
+ if [ "${JUNO_VERBOSE:-false}" = "true" ]; then
54
+ echo -e "${GREEN}[SLACK_RESPOND]${NC} $1"
55
+ fi
56
+ }
57
+
58
+ log_warning() {
59
+ if [ "${JUNO_VERBOSE:-false}" = "true" ]; then
60
+ echo -e "${YELLOW}[SLACK_RESPOND]${NC} $1"
61
+ fi
62
+ }
63
+
64
+ log_error() {
65
+ echo -e "${RED}[SLACK_RESPOND]${NC} $1" >&2
66
+ }
67
+
68
+ # Check if we're in .venv_juno
69
+ is_in_venv_juno() {
70
+ if [ -n "${VIRTUAL_ENV:-}" ]; then
71
+ if [[ "${VIRTUAL_ENV:-}" == *"/.venv_juno" ]] || [[ "${VIRTUAL_ENV:-}" == *".venv_juno"* ]]; then
72
+ return 0
73
+ fi
74
+ if [ "$(basename "${VIRTUAL_ENV:-}")" = ".venv_juno" ]; then
75
+ return 0
76
+ fi
77
+ fi
78
+ return 1
79
+ }
80
+
81
+ # Activate virtual environment
82
+ activate_venv() {
83
+ local venv_path="$1"
84
+
85
+ if [ ! -d "$venv_path" ]; then
86
+ log_error "Virtual environment not found: $venv_path"
87
+ return 1
88
+ fi
89
+
90
+ if [ -f "$venv_path/bin/activate" ]; then
91
+ # shellcheck disable=SC1091
92
+ source "$venv_path/bin/activate"
93
+ log_success "Activated virtual environment: $venv_path"
94
+ return 0
95
+ else
96
+ log_error "Activation script not found: $venv_path/bin/activate"
97
+ return 1
98
+ fi
99
+ }
100
+
101
+ # Ensure Python environment is ready
102
+ ensure_python_environment() {
103
+ log_info "Checking Python environment..."
104
+
105
+ if is_in_venv_juno; then
106
+ log_success "Already inside .venv_juno virtual environment"
107
+ return 0
108
+ fi
109
+
110
+ if [ -d "$VENV_DIR" ]; then
111
+ log_info "Found existing virtual environment: $VENV_DIR"
112
+ if activate_venv "$VENV_DIR"; then
113
+ return 0
114
+ else
115
+ log_error "Failed to activate virtual environment"
116
+ return 1
117
+ fi
118
+ fi
119
+
120
+ log_warning "Virtual environment not found: $VENV_DIR"
121
+ log_info "Running install_requirements.sh to create virtual environment..."
122
+
123
+ if [ ! -f "$INSTALL_SCRIPT" ]; then
124
+ log_error "Install script not found: $INSTALL_SCRIPT"
125
+ log_error "Please run 'juno-code init' to initialize the project"
126
+ return 1
127
+ fi
128
+
129
+ chmod +x "$INSTALL_SCRIPT"
130
+
131
+ if bash "$INSTALL_SCRIPT"; then
132
+ log_success "Python environment setup completed successfully"
133
+ if [ -d "$VENV_DIR" ]; then
134
+ activate_venv "$VENV_DIR"
135
+ fi
136
+ return 0
137
+ else
138
+ log_error "Failed to run install_requirements.sh"
139
+ return 1
140
+ fi
141
+ }
142
+
143
+ # Check for required Slack dependencies
144
+ check_slack_deps() {
145
+ log_info "Checking Slack SDK dependencies..."
146
+
147
+ if python3 -c "import slack_sdk; import dotenv" 2>/dev/null; then
148
+ log_success "Slack SDK dependencies available"
149
+ return 0
150
+ fi
151
+
152
+ log_warning "Slack SDK not installed. Installing..."
153
+ pip install slack_sdk python-dotenv >/dev/null 2>&1 || {
154
+ log_error "Failed to install Slack SDK dependencies"
155
+ log_error "Please run: pip install slack_sdk python-dotenv"
156
+ return 1
157
+ }
158
+
159
+ log_success "Slack SDK installed successfully"
160
+ return 0
161
+ }
162
+
163
+ # Get script directory and project root
164
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
165
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )"
166
+
167
+ cd "$PROJECT_ROOT"
168
+
169
+ # Show help for Slack environment setup
170
+ show_env_help() {
171
+ echo ""
172
+ echo "========================================================================"
173
+ echo "Slack Integration - Environment Setup"
174
+ echo "========================================================================"
175
+ echo ""
176
+ echo "Required Environment Variables:"
177
+ echo " SLACK_BOT_TOKEN Your Slack bot token (starts with xoxb-)"
178
+ echo ""
179
+ echo "Optional Environment Variables:"
180
+ echo " LOG_LEVEL DEBUG, INFO, WARNING, ERROR (default: INFO)"
181
+ echo ""
182
+ echo "Configuration Methods:"
183
+ echo " 1. Environment variables:"
184
+ echo " export SLACK_BOT_TOKEN=xoxb-your-token-here"
185
+ echo ""
186
+ echo " 2. .env file (in project root):"
187
+ echo " SLACK_BOT_TOKEN=xoxb-your-token-here"
188
+ echo ""
189
+ echo " 3. .juno_task/.env file (project-specific):"
190
+ echo " SLACK_BOT_TOKEN=xoxb-your-token-here"
191
+ echo ""
192
+ echo "To generate a Slack bot token:"
193
+ echo " 1. Go to https://api.slack.com/apps and create a new app"
194
+ echo " 2. Under 'OAuth & Permissions', add these scopes:"
195
+ echo " - channels:history, channels:read (public channels)"
196
+ echo " - groups:history, groups:read (private channels)"
197
+ echo " - users:read (user info)"
198
+ echo " - chat:write (send messages)"
199
+ echo " 3. Install the app to your workspace"
200
+ echo " 4. Copy the 'Bot User OAuth Token' (starts with xoxb-)"
201
+ echo ""
202
+ echo " Full tutorial: https://api.slack.com/tutorials/tracks/getting-a-token"
203
+ echo ""
204
+ echo "========================================================================"
205
+ echo ""
206
+ }
207
+
208
+ # Main function
209
+ main() {
210
+ log_info "=== Slack Respond Wrapper ==="
211
+
212
+ # Ensure Python environment
213
+ if ! ensure_python_environment; then
214
+ log_error "Failed to setup Python environment"
215
+ exit 1
216
+ fi
217
+
218
+ # Check Slack dependencies
219
+ if ! check_slack_deps; then
220
+ exit 1
221
+ fi
222
+
223
+ # Load .env file if it exists
224
+ if [ -f ".env" ]; then
225
+ # shellcheck disable=SC1091
226
+ set -a
227
+ source .env
228
+ set +a
229
+ log_success "Loaded environment from .env"
230
+ fi
231
+
232
+ # Also check .juno_task/.env
233
+ if [ -f ".juno_task/.env" ]; then
234
+ # shellcheck disable=SC1091
235
+ set -a
236
+ source .juno_task/.env
237
+ set +a
238
+ log_success "Loaded environment from .juno_task/.env"
239
+ fi
240
+
241
+ # Check for SLACK_BOT_TOKEN
242
+ if [ -z "${SLACK_BOT_TOKEN:-}" ]; then
243
+ log_error "SLACK_BOT_TOKEN not set!"
244
+ show_env_help
245
+ exit 1
246
+ fi
247
+
248
+ # Validate token format
249
+ if [[ ! "${SLACK_BOT_TOKEN:-}" =~ ^xoxb- ]]; then
250
+ log_warning "SLACK_BOT_TOKEN does not start with 'xoxb-' - this may be an invalid bot token"
251
+ log_info "Bot tokens from Slack should start with 'xoxb-'"
252
+ log_info "To generate a valid token, visit: https://api.slack.com/tutorials/tracks/getting-a-token"
253
+ fi
254
+
255
+ log_success "Python environment ready!"
256
+ log_success "Slack token configured"
257
+
258
+ # Execute slack_respond.py
259
+ log_info "Executing slack_respond.py: $*"
260
+ exec python3 "$SLACK_RESPOND_SCRIPT" "$@"
261
+ }
262
+
263
+ main "$@"
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Slack State Manager - Persistent state management for Slack-Kanban integration.
4
+
5
+ This module handles:
6
+ - Loading/saving message state from NDJSON file
7
+ - Tracking processed Slack messages with their kanban task IDs
8
+ - Tracking sent responses to avoid duplicates
9
+ - Deduplication of messages
10
+
11
+ NDJSON Format (Newline-Delimited JSON):
12
+ Each line is a complete JSON object for atomic append operations.
13
+
14
+ State Files:
15
+ - slack.ndjson: Processed Slack messages with task mappings
16
+ - responses_sent.ndjson: Responses already sent back to Slack
17
+
18
+ Usage:
19
+ from slack_state import SlackStateManager, ResponseStateManager
20
+
21
+ # Track processed messages
22
+ state = SlackStateManager('.juno_task/slack/slack.ndjson')
23
+ if not state.is_processed(message_ts):
24
+ # Process message...
25
+ state.mark_processed(message_ts, task_id, message_data)
26
+
27
+ # Track sent responses
28
+ responses = ResponseStateManager('.juno_task/slack/responses_sent.ndjson')
29
+ if not responses.was_response_sent(task_id, message_ts):
30
+ # Send response...
31
+ responses.record_sent(task_id, message_ts, channel_id)
32
+ """
33
+
34
+ import json
35
+ import logging
36
+ from datetime import datetime, timezone
37
+ from pathlib import Path
38
+ from typing import Dict, List, Optional, Any
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class SlackStateManager:
44
+ """
45
+ Manages persistent state for Slack message processing.
46
+
47
+ Tracks which Slack messages have been processed and their associated
48
+ kanban task IDs for later response matching.
49
+ """
50
+
51
+ def __init__(self, state_file_path: str):
52
+ """
53
+ Initialize SlackStateManager.
54
+
55
+ Args:
56
+ state_file_path: Path to NDJSON state file (e.g., .juno_task/slack/slack.ndjson)
57
+ """
58
+ self.state_file = Path(state_file_path)
59
+ self.messages: List[Dict[str, Any]] = []
60
+ self.message_ts_set: set = set()
61
+ self.last_ts: Optional[str] = None
62
+ self._load_state()
63
+
64
+ def _load_state(self) -> None:
65
+ """Load existing state from NDJSON file."""
66
+ if not self.state_file.exists():
67
+ logger.info(f"State file does not exist, will create: {self.state_file}")
68
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
69
+ self.messages = []
70
+ self.message_ts_set = set()
71
+ self.last_ts = None
72
+ return
73
+
74
+ try:
75
+ self.messages = []
76
+ self.message_ts_set = set()
77
+ with open(self.state_file, 'r', encoding='utf-8') as f:
78
+ for line in f:
79
+ line = line.strip()
80
+ if line:
81
+ msg = json.loads(line)
82
+ self.messages.append(msg)
83
+ ts = msg.get('ts')
84
+ if ts:
85
+ self.message_ts_set.add(ts)
86
+
87
+ # Get last timestamp
88
+ if self.messages:
89
+ self.last_ts = max(msg.get('ts', '0') for msg in self.messages)
90
+ logger.info(
91
+ f"Loaded {len(self.messages)} messages from {self.state_file}, "
92
+ f"last_ts={self.last_ts}"
93
+ )
94
+ else:
95
+ self.last_ts = None
96
+ logger.info(f"State file empty: {self.state_file}")
97
+
98
+ except Exception as e:
99
+ logger.error(f"Error loading state from {self.state_file}: {e}")
100
+ self.messages = []
101
+ self.message_ts_set = set()
102
+ self.last_ts = None
103
+
104
+ def get_last_timestamp(self) -> Optional[str]:
105
+ """
106
+ Get the timestamp of the last processed message.
107
+
108
+ Returns:
109
+ Last message timestamp or None if no messages processed
110
+ """
111
+ return self.last_ts
112
+
113
+ def is_processed(self, message_ts: str) -> bool:
114
+ """
115
+ Check if a message has already been processed.
116
+
117
+ Args:
118
+ message_ts: Slack message timestamp
119
+
120
+ Returns:
121
+ True if already processed, False otherwise
122
+ """
123
+ return message_ts in self.message_ts_set
124
+
125
+ def mark_processed(
126
+ self,
127
+ message_ts: str,
128
+ task_id: str,
129
+ message_data: Dict[str, Any]
130
+ ) -> bool:
131
+ """
132
+ Mark a message as processed and store task mapping.
133
+
134
+ Args:
135
+ message_ts: Slack message timestamp (unique identifier)
136
+ task_id: Kanban task ID created for this message
137
+ message_data: Additional message data (text, author, channel, etc.)
138
+
139
+ Returns:
140
+ True if message was new and recorded, False if duplicate
141
+ """
142
+ if message_ts in self.message_ts_set:
143
+ logger.debug(f"Duplicate message ts={message_ts}, skipping")
144
+ return False
145
+
146
+ # Build state entry
147
+ entry = {
148
+ 'ts': message_ts,
149
+ 'task_id': task_id,
150
+ 'processed_at': datetime.now(timezone.utc).isoformat(),
151
+ **message_data
152
+ }
153
+
154
+ try:
155
+ # Append to file (atomic)
156
+ with open(self.state_file, 'a', encoding='utf-8') as f:
157
+ f.write(json.dumps(entry, ensure_ascii=False) + '\n')
158
+
159
+ # Update in-memory state
160
+ self.messages.append(entry)
161
+ self.message_ts_set.add(message_ts)
162
+ if not self.last_ts or message_ts > self.last_ts:
163
+ self.last_ts = message_ts
164
+
165
+ logger.debug(f"Recorded message ts={message_ts} -> task_id={task_id}")
166
+ return True
167
+
168
+ except Exception as e:
169
+ logger.error(f"Error appending to {self.state_file}: {e}")
170
+ return False
171
+
172
+ def get_task_id_for_message(self, message_ts: str) -> Optional[str]:
173
+ """
174
+ Get the kanban task ID for a given message.
175
+
176
+ Args:
177
+ message_ts: Slack message timestamp
178
+
179
+ Returns:
180
+ Task ID or None if not found
181
+ """
182
+ for msg in self.messages:
183
+ if msg.get('ts') == message_ts:
184
+ return msg.get('task_id')
185
+ return None
186
+
187
+ def get_message_for_task(self, task_id: str) -> Optional[Dict[str, Any]]:
188
+ """
189
+ Get the message data for a given kanban task ID.
190
+
191
+ Args:
192
+ task_id: Kanban task ID
193
+
194
+ Returns:
195
+ Message data dict or None if not found
196
+ """
197
+ for msg in self.messages:
198
+ if msg.get('task_id') == task_id:
199
+ return msg
200
+ return None
201
+
202
+ def get_message_count(self) -> int:
203
+ """Get total number of processed messages."""
204
+ return len(self.messages)
205
+
206
+ def get_messages_since(self, since_ts: str) -> List[Dict[str, Any]]:
207
+ """
208
+ Get all messages since a given timestamp.
209
+
210
+ Args:
211
+ since_ts: Timestamp to filter from
212
+
213
+ Returns:
214
+ List of messages after since_ts
215
+ """
216
+ return [msg for msg in self.messages if msg.get('ts', '0') > since_ts]
217
+
218
+
219
+ class ResponseStateManager:
220
+ """
221
+ Manages state for tracking sent responses.
222
+
223
+ Prevents duplicate responses by tracking which task/message combinations
224
+ have already received a response.
225
+ """
226
+
227
+ def __init__(self, state_file_path: str):
228
+ """
229
+ Initialize ResponseStateManager.
230
+
231
+ Args:
232
+ state_file_path: Path to NDJSON state file
233
+ """
234
+ self.state_file = Path(state_file_path)
235
+ self.sent_responses: List[Dict[str, Any]] = []
236
+ self.sent_keys: set = set() # (task_id, message_ts) tuples
237
+ self._load_state()
238
+
239
+ def _load_state(self) -> None:
240
+ """Load existing state from NDJSON file."""
241
+ if not self.state_file.exists():
242
+ logger.info(f"Response state file does not exist, will create: {self.state_file}")
243
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
244
+ self.sent_responses = []
245
+ self.sent_keys = set()
246
+ return
247
+
248
+ try:
249
+ self.sent_responses = []
250
+ self.sent_keys = set()
251
+ with open(self.state_file, 'r', encoding='utf-8') as f:
252
+ for line in f:
253
+ line = line.strip()
254
+ if line:
255
+ entry = json.loads(line)
256
+ self.sent_responses.append(entry)
257
+ task_id = entry.get('task_id')
258
+ message_ts = entry.get('message_ts')
259
+ if task_id and message_ts:
260
+ self.sent_keys.add((task_id, message_ts))
261
+
262
+ logger.info(f"Loaded {len(self.sent_responses)} sent responses from {self.state_file}")
263
+
264
+ except Exception as e:
265
+ logger.error(f"Error loading response state from {self.state_file}: {e}")
266
+ self.sent_responses = []
267
+ self.sent_keys = set()
268
+
269
+ def was_response_sent(self, task_id: str, message_ts: str) -> bool:
270
+ """
271
+ Check if a response was already sent for this task/message.
272
+
273
+ Args:
274
+ task_id: Kanban task ID
275
+ message_ts: Slack message timestamp
276
+
277
+ Returns:
278
+ True if already sent, False otherwise
279
+ """
280
+ return (task_id, message_ts) in self.sent_keys
281
+
282
+ def record_sent(
283
+ self,
284
+ task_id: str,
285
+ message_ts: str,
286
+ channel_id: str,
287
+ response_ts: Optional[str] = None
288
+ ) -> bool:
289
+ """
290
+ Record that a response was sent.
291
+
292
+ Args:
293
+ task_id: Kanban task ID
294
+ message_ts: Original Slack message timestamp
295
+ channel_id: Slack channel ID
296
+ response_ts: Timestamp of the response message (optional)
297
+
298
+ Returns:
299
+ True if recorded, False if duplicate or error
300
+ """
301
+ key = (task_id, message_ts)
302
+ if key in self.sent_keys:
303
+ logger.debug(f"Response already recorded for task={task_id}, ts={message_ts}")
304
+ return False
305
+
306
+ entry = {
307
+ 'task_id': task_id,
308
+ 'message_ts': message_ts,
309
+ 'channel_id': channel_id,
310
+ 'sent_at': datetime.now(timezone.utc).isoformat(),
311
+ }
312
+ if response_ts:
313
+ entry['response_ts'] = response_ts
314
+
315
+ try:
316
+ # Append to file (atomic)
317
+ with open(self.state_file, 'a', encoding='utf-8') as f:
318
+ f.write(json.dumps(entry, ensure_ascii=False) + '\n')
319
+
320
+ # Update in-memory state
321
+ self.sent_responses.append(entry)
322
+ self.sent_keys.add(key)
323
+
324
+ logger.debug(f"Recorded sent response for task={task_id}, ts={message_ts}")
325
+ return True
326
+
327
+ except Exception as e:
328
+ logger.error(f"Error recording response to {self.state_file}: {e}")
329
+ return False
330
+
331
+ def get_sent_count(self) -> int:
332
+ """Get total number of sent responses."""
333
+ return len(self.sent_responses)
334
+
335
+ def reset_state(self) -> None:
336
+ """
337
+ Clear all state (WARNING: will cause re-sending).
338
+
339
+ Use with caution - should only be called after user confirmation.
340
+ """
341
+ if self.state_file.exists():
342
+ self.state_file.unlink()
343
+ logger.warning(f"Deleted response state file: {self.state_file}")
344
+
345
+ self.sent_responses = []
346
+ self.sent_keys = set()
347
+ logger.warning("Response state reset - all responses may be re-sent")
348
+
349
+
350
+ if __name__ == '__main__':
351
+ # Simple test/demo
352
+ import sys
353
+
354
+ logging.basicConfig(level=logging.DEBUG)
355
+
356
+ # Test SlackStateManager
357
+ print("Testing SlackStateManager...")
358
+ state = SlackStateManager('/tmp/test_slack_state.ndjson')
359
+ print(f" Message count: {state.get_message_count()}")
360
+ print(f" Last timestamp: {state.get_last_timestamp()}")
361
+
362
+ if not state.is_processed('1234567890.123456'):
363
+ state.mark_processed(
364
+ '1234567890.123456',
365
+ 'task_abc123',
366
+ {'text': 'Test message', 'author': 'test_user', 'channel': 'general'}
367
+ )
368
+ print(" Marked test message as processed")
369
+
370
+ print(f" Task ID for test: {state.get_task_id_for_message('1234567890.123456')}")
371
+
372
+ # Test ResponseStateManager
373
+ print("\nTesting ResponseStateManager...")
374
+ responses = ResponseStateManager('/tmp/test_responses_sent.ndjson')
375
+ print(f" Sent count: {responses.get_sent_count()}")
376
+
377
+ if not responses.was_response_sent('task_abc123', '1234567890.123456'):
378
+ responses.record_sent('task_abc123', '1234567890.123456', 'C1234567890')
379
+ print(" Recorded test response")
380
+
381
+ print(f" Was sent: {responses.was_response_sent('task_abc123', '1234567890.123456')}")
382
+
383
+ print("\nAll tests passed!")
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "juno-code",
3
- "version": "1.0.36",
4
- "description": "TypeScript CLI tool for AI subagent orchestration with code automation",
3
+ "version": "1.0.38",
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
+ "Ralph",
7
+ "Ralph Wiggum",
8
+ "Claude code",
9
+ "backlog ralph",
10
+ "claude lopp",
6
11
  "ai",
7
12
  "cli",
8
13
  "typescript",
@@ -82,6 +87,7 @@
82
87
  "handlebars": "^4.7.8",
83
88
  "ink": "^4.4.1",
84
89
  "js-yaml": "^4.1.0",
90
+ "juno-code": "file:develop_package",
85
91
  "react": "^18.2.0",
86
92
  "semver": "^7.5.4",
87
93
  "supports-color": "^9.4.0",