juno-code 1.0.24 → 1.0.27

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/index.js CHANGED
@@ -75,7 +75,7 @@ var __export = (target, all) => {
75
75
  exports.version = void 0;
76
76
  var init_version = __esm({
77
77
  "src/version.ts"() {
78
- exports.version = "1.0.24";
78
+ exports.version = "1.0.27";
79
79
  }
80
80
  });
81
81
  function isHeadlessEnvironment() {
package/dist/index.mjs CHANGED
@@ -44,7 +44,7 @@ var __export = (target, all) => {
44
44
  var version;
45
45
  var init_version = __esm({
46
46
  "src/version.ts"() {
47
- version = "1.0.24";
47
+ version = "1.0.27";
48
48
  }
49
49
  });
50
50
  function isHeadlessEnvironment() {
@@ -10,13 +10,19 @@
10
10
  # Usage: ./.juno_task/scripts/kanban.sh [juno-kanban arguments]
11
11
  # Example: ./.juno_task/scripts/kanban.sh list --limit 5
12
12
  #
13
+ # Environment Variables:
14
+ # JUNO_DEBUG=true - Show [DEBUG] diagnostic messages
15
+ # JUNO_VERBOSE=true - Show [KANBAN] informational messages
16
+ # (Both default to false for silent operation)
17
+ #
13
18
  # Created by: juno-code init command
14
19
  # Date: Auto-generated during project initialization
15
20
 
16
21
  set -euo pipefail # Exit on error, undefined variable, or pipe failure
17
22
 
18
- # DEBUG OUTPUT: Show that kanban.sh is being executed (only if JUNO_VERBOSE=true)
19
- if [ "${JUNO_VERBOSE:-false}" = "true" ]; then
23
+ # DEBUG OUTPUT: Show that kanban.sh is being executed (only if JUNO_DEBUG=true)
24
+ # Note: JUNO_DEBUG is separate from JUNO_VERBOSE for fine-grained control
25
+ if [ "${JUNO_DEBUG:-false}" = "true" ]; then
20
26
  echo "[DEBUG] kanban.sh is being executed from: $(pwd)" >&2
21
27
  fi
22
28
 
@@ -22,6 +22,16 @@ class ClaudeService:
22
22
  DEFAULT_PERMISSION_MODE = "default"
23
23
  DEFAULT_AUTO_INSTRUCTION = """You are Claude Code, an AI coding assistant. Follow the instructions provided and generate high-quality code."""
24
24
 
25
+ # Model shorthand mappings (colon-prefixed names expand to full model IDs)
26
+ MODEL_SHORTHANDS = {
27
+ ":claude-haiku-4-5": "claude-haiku-4-5-20251001",
28
+ ":claude-sonnet-4-5": "claude-sonnet-4-5-20250929",
29
+ ":claude-opus-4": "claude-opus-4-20250514",
30
+ ":haiku": "claude-haiku-4-5-20251001",
31
+ ":sonnet": "claude-sonnet-4-5-20250929",
32
+ ":opus": "claude-opus-4-20250514",
33
+ }
34
+
25
35
  def __init__(self):
26
36
  self.model_name = self.DEFAULT_MODEL
27
37
  self.permission_mode = self.DEFAULT_PERMISSION_MODE
@@ -31,6 +41,24 @@ class ClaudeService:
31
41
  self.additional_args: List[str] = []
32
42
  self.message_counter = 0
33
43
  self.verbose = False
44
+ # User message truncation: -1 = no truncation, N = truncate to N lines
45
+ self.user_message_truncate = int(os.environ.get("CLAUDE_USER_MESSAGE_PRETTY_TRUNCATE", "4"))
46
+
47
+ def expand_model_shorthand(self, model: str) -> str:
48
+ """
49
+ Expand model shorthand names to full model IDs.
50
+
51
+ If the model starts with ':', look it up in MODEL_SHORTHANDS.
52
+ Otherwise, return the model name as-is.
53
+
54
+ Examples:
55
+ :claude-haiku-4-5 -> claude-haiku-4-5-20251001
56
+ :haiku -> claude-haiku-4-5-20251001
57
+ claude-sonnet-4-5-20250929 -> claude-sonnet-4-5-20250929 (unchanged)
58
+ """
59
+ if model.startswith(':'):
60
+ return self.MODEL_SHORTHANDS.get(model, model)
61
+ return model
34
62
 
35
63
  def check_claude_installed(self) -> bool:
36
64
  """Check if claude CLI is installed and available"""
@@ -54,15 +82,18 @@ class ClaudeService:
54
82
  Examples:
55
83
  %(prog)s -p "Write a hello world function"
56
84
  %(prog)s -pp prompt.txt --cd /path/to/project
57
- %(prog)s -p "Add tests" -m claude-opus-4-20250514 --tool "Bash Edit"
85
+ %(prog)s -p "Add tests" -m :opus --tool "Bash Edit"
86
+ %(prog)s -p "Quick task" -m :haiku
87
+ %(prog)s -p "Complex task" -m claude-opus-4-20250514
58
88
 
59
89
  Environment Variables:
60
- CLAUDE_PROJECT_PATH Project path (default: current directory)
61
- CLAUDE_MODEL Model name (default: claude-sonnet-4-5-20250929)
62
- CLAUDE_AUTO_INSTRUCTION Auto instruction to prepend to prompt
63
- CLAUDE_PERMISSION_MODE Permission mode (default: default)
64
- CLAUDE_PRETTY Pretty print JSON output (default: true)
65
- CLAUDE_VERBOSE Enable verbose output (default: false)
90
+ CLAUDE_PROJECT_PATH Project path (default: current directory)
91
+ CLAUDE_MODEL Model name (default: claude-sonnet-4-5-20250929)
92
+ CLAUDE_AUTO_INSTRUCTION Auto instruction to prepend to prompt
93
+ CLAUDE_PERMISSION_MODE Permission mode (default: default)
94
+ CLAUDE_PRETTY Pretty print JSON output (default: true)
95
+ CLAUDE_VERBOSE Enable verbose output (default: false)
96
+ CLAUDE_USER_MESSAGE_PRETTY_TRUNCATE Max lines for user messages in pretty mode (default: 4, -1: no truncation)
66
97
  """
67
98
  )
68
99
 
@@ -90,7 +121,7 @@ Environment Variables:
90
121
  "-m", "--model",
91
122
  type=str,
92
123
  default=os.environ.get("CLAUDE_MODEL", self.DEFAULT_MODEL),
93
- help=f"Model name (e.g. 'sonnet', 'opus', or full name). Default: {self.DEFAULT_MODEL} (env: CLAUDE_MODEL)"
124
+ help=f"Model name. Supports shorthand (e.g., ':haiku', ':sonnet', ':opus', ':claude-haiku-4-5') or full model ID (e.g., 'claude-haiku-4-5-20251001'). Default: {self.DEFAULT_MODEL} (env: CLAUDE_MODEL)"
94
125
  )
95
126
 
96
127
  parser.add_argument(
@@ -214,10 +245,20 @@ Environment Variables:
214
245
  """
215
246
  Format JSON line for pretty output.
216
247
  For type=assistant: show datetime, message content, and counter
248
+ For type=user: show datetime, message content (truncated based on env var), and counter
217
249
  For other types: show full message with datetime and counter
218
250
  Returns None if line should be skipped
219
251
 
220
252
  IMPORTANT: Always preserve the 'type' field so shell backend can parse events
253
+
254
+ MULTI-LINE HANDLING: When content/result fields contain \\n escape sequences,
255
+ the output shows the JSON metadata on one line, then the actual content/result
256
+ value is printed below with newlines properly rendered (similar to jq -r or @text).
257
+ This keeps JSON structure compact while making multi-line strings readable.
258
+
259
+ USER MESSAGE TRUNCATION: User messages are truncated based on CLAUDE_USER_MESSAGE_PRETTY_TRUNCATE
260
+ environment variable (default: 4 lines, -1: no truncation). When truncated, a [Truncated...]
261
+ indicator is added. This only applies to user messages in pretty mode.
221
262
  """
222
263
  try:
223
264
  data = json.loads(json_line)
@@ -226,8 +267,53 @@ Environment Variables:
226
267
  # Get current datetime in readable format
227
268
  now = datetime.now().strftime("%I:%M:%S %p")
228
269
 
270
+ # For user messages, show simplified output with truncation
271
+ if data.get("type") == "user":
272
+ message = data.get("message", {})
273
+ content_list = message.get("content", [])
274
+
275
+ # Extract text content
276
+ text_content = ""
277
+ for item in content_list:
278
+ if isinstance(item, dict) and item.get("type") == "text":
279
+ text_content = item.get("text", "")
280
+ break
281
+
282
+ # Apply truncation for user messages based on CLAUDE_USER_MESSAGE_PRETTY_TRUNCATE
283
+ # -1 means no truncation, otherwise truncate to N lines
284
+ if self.user_message_truncate != -1:
285
+ lines = text_content.split('\n')
286
+ if len(lines) > self.user_message_truncate:
287
+ # Truncate to N lines and add indicator
288
+ text_content = '\n'.join(lines[:self.user_message_truncate]) + '\n[Truncated...]'
289
+
290
+ # Create simplified output with datetime, content, and counter
291
+ simplified = {
292
+ "type": "user",
293
+ "datetime": now,
294
+ "counter": f"#{self.message_counter}"
295
+ }
296
+
297
+ # Check if content has newlines after potential truncation
298
+ if '\n' in text_content:
299
+ # Multi-line content: print JSON metadata, then raw content
300
+ metadata = {
301
+ "type": "user",
302
+ "datetime": now,
303
+ "counter": f"#{self.message_counter}"
304
+ }
305
+ # Print metadata as compact JSON on first line
306
+ output = json.dumps(metadata, ensure_ascii=False)
307
+ # Then print content label and raw multi-line text
308
+ output += "\ncontent:\n" + text_content
309
+ return output
310
+ else:
311
+ # Single-line content: normal JSON
312
+ simplified["content"] = text_content
313
+ return json.dumps(simplified, ensure_ascii=False)
314
+
229
315
  # For assistant messages, show simplified output
230
- if data.get("type") == "assistant":
316
+ elif data.get("type") == "assistant":
231
317
  message = data.get("message", {})
232
318
  content_list = message.get("content", [])
233
319
 
@@ -258,21 +344,82 @@ Environment Variables:
258
344
 
259
345
  # Add either content or tool_use data
260
346
  if tool_use_data:
261
- simplified["tool_use"] = tool_use_data
347
+ # Check if prompt field in tool_use.input has multi-line content
348
+ tool_input = tool_use_data.get("input", {})
349
+ prompt_field = tool_input.get("prompt", "")
350
+
351
+ if isinstance(prompt_field, str) and '\n' in prompt_field:
352
+ # Multi-line prompt: extract it and render separately
353
+ # Create a copy of tool_use_data with prompt removed
354
+ tool_use_copy = {
355
+ "name": tool_use_data.get("name", ""),
356
+ "input": {k: v for k, v in tool_input.items() if k != "prompt"}
357
+ }
358
+
359
+ simplified["tool_use"] = tool_use_copy
360
+
361
+ # Print metadata as compact JSON on first line
362
+ output = json.dumps(simplified, ensure_ascii=False)
363
+ # Then print prompt label and raw multi-line text
364
+ output += "\nprompt:\n" + prompt_field
365
+ return output
366
+ else:
367
+ # No multi-line prompt: normal JSON output for tool_use
368
+ simplified["tool_use"] = tool_use_data
369
+ return json.dumps(simplified, ensure_ascii=False)
262
370
  else:
263
- # For multi-line content, render it in a readable format
264
- # Check if content contains newline characters
265
- if text_content and '\n' in text_content:
266
- # Split into lines and create a readable multi-line representation
267
- simplified["content"] = text_content
268
- # Return a custom formatted output for multi-line content
269
- # that preserves the structure but makes it readable
270
- return json.dumps(simplified, indent=2, ensure_ascii=False)
371
+ # For content, check if it has newlines
372
+ if '\n' in text_content:
373
+ # Multi-line content: print JSON metadata, then raw content
374
+ metadata = {
375
+ "type": "assistant",
376
+ "datetime": now,
377
+ "counter": f"#{self.message_counter}"
378
+ }
379
+ # Print metadata as compact JSON on first line
380
+ output = json.dumps(metadata, ensure_ascii=False)
381
+ # Then print content label and raw multi-line text
382
+ output += "\ncontent:\n" + text_content
383
+ return output
271
384
  else:
385
+ # Single-line content: normal JSON
272
386
  simplified["content"] = text_content
273
-
274
- return json.dumps(simplified)
387
+ return json.dumps(simplified, ensure_ascii=False)
275
388
  else:
389
+ # For other message types, check if there's nested content to flatten
390
+ message = data.get("message", {})
391
+ content_list = message.get("content", [])
392
+
393
+ # Check if this is a message with nested tool_result or similar content
394
+ if content_list and isinstance(content_list, list) and len(content_list) > 0:
395
+ nested_item = content_list[0]
396
+ if isinstance(nested_item, dict) and nested_item.get("type") in ["tool_result"]:
397
+ # Flatten the nested structure by pulling nested fields to top level
398
+ flattened = {
399
+ "datetime": now,
400
+ "counter": f"#{self.message_counter}",
401
+ }
402
+
403
+ # Add tool_use_id if present
404
+ if "tool_use_id" in nested_item:
405
+ flattened["tool_use_id"] = nested_item["tool_use_id"]
406
+
407
+ # Add type from nested item
408
+ flattened["type"] = nested_item["type"]
409
+
410
+ # Handle content field with multiline support
411
+ nested_content = nested_item.get("content", "")
412
+ if isinstance(nested_content, str) and '\n' in nested_content:
413
+ # Multi-line content: separate metadata from content
414
+ # Print metadata as compact JSON
415
+ metadata_json = json.dumps(flattened, ensure_ascii=False)
416
+ # Then print content label and raw multi-line text
417
+ return metadata_json + "\ncontent:\n" + nested_content
418
+ else:
419
+ # Single-line content: normal JSON
420
+ flattened["content"] = nested_content
421
+ return json.dumps(flattened, ensure_ascii=False)
422
+
276
423
  # For other message types, show full message with datetime and counter
277
424
  # Type field is already present in data, so it's preserved
278
425
  output = {
@@ -280,7 +427,18 @@ Environment Variables:
280
427
  "counter": f"#{self.message_counter}",
281
428
  **data
282
429
  }
283
- return json.dumps(output)
430
+
431
+ # Check if 'result' field has multi-line content
432
+ if "result" in output and isinstance(output["result"], str) and '\n' in output["result"]:
433
+ # Multi-line result: separate metadata from content
434
+ result_value = output.pop("result")
435
+ # Print metadata as compact JSON
436
+ metadata_json = json.dumps(output, ensure_ascii=False)
437
+ # Then print result label and raw multi-line text
438
+ return metadata_json + "\nresult:\n" + result_value
439
+ else:
440
+ # Normal JSON output
441
+ return json.dumps(output, ensure_ascii=False)
284
442
 
285
443
  except json.JSONDecodeError:
286
444
  # If not valid JSON, return as-is
@@ -368,12 +526,13 @@ Environment Variables:
368
526
  )
369
527
  print("\nRun 'claude.py --help' for usage information.", file=sys.stderr)
370
528
  print("\nAvailable Environment Variables:", file=sys.stderr)
371
- print(" CLAUDE_PROJECT_PATH Project path (default: current directory)", file=sys.stderr)
372
- print(" CLAUDE_MODEL Model name (default: claude-sonnet-4-5-20250929)", file=sys.stderr)
373
- print(" CLAUDE_AUTO_INSTRUCTION Auto instruction to prepend to prompt", file=sys.stderr)
374
- print(" CLAUDE_PERMISSION_MODE Permission mode (default: default)", file=sys.stderr)
375
- print(" CLAUDE_PRETTY Pretty print JSON output (default: true)", file=sys.stderr)
376
- print(" CLAUDE_VERBOSE Enable verbose output (default: false)", file=sys.stderr)
529
+ print(" CLAUDE_PROJECT_PATH Project path (default: current directory)", file=sys.stderr)
530
+ print(" CLAUDE_MODEL Model name (default: claude-sonnet-4-5-20250929)", file=sys.stderr)
531
+ print(" CLAUDE_AUTO_INSTRUCTION Auto instruction to prepend to prompt", file=sys.stderr)
532
+ print(" CLAUDE_PERMISSION_MODE Permission mode (default: default)", file=sys.stderr)
533
+ print(" CLAUDE_PRETTY Pretty print JSON output (default: true)", file=sys.stderr)
534
+ print(" CLAUDE_VERBOSE Enable verbose output (default: false)", file=sys.stderr)
535
+ print(" CLAUDE_USER_MESSAGE_PRETTY_TRUNCATE Max lines for user messages in pretty mode (default: 4, -1: no truncation)", file=sys.stderr)
377
536
  return 1
378
537
 
379
538
  # Check if claude is installed
@@ -390,7 +549,8 @@ Environment Variables:
390
549
 
391
550
  # Set configuration from arguments
392
551
  self.project_path = os.path.abspath(args.cd)
393
- self.model_name = args.model
552
+ # Expand model shorthand (e.g., :haiku -> claude-haiku-4-5-20251001)
553
+ self.model_name = self.expand_model_shorthand(args.model)
394
554
  self.auto_instruction = args.auto_instruction
395
555
 
396
556
  # Get prompt from file or argument
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juno-code",
3
- "version": "1.0.24",
3
+ "version": "1.0.27",
4
4
  "description": "TypeScript CLI tool for AI subagent orchestration with code automation",
5
5
  "keywords": [
6
6
  "ai",