juno-code 1.0.31 → 1.0.33

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.
@@ -95,6 +95,8 @@ Examples:
95
95
  %(prog)s -p "Complex task" -m claude-opus-4-20250514 --tool Read --tool Write
96
96
  %(prog)s -p "Multi-tool task" --allowed-tools Bash Edit Read Write
97
97
  %(prog)s -p "Restricted task" --disallowed-tools Bash WebSearch
98
+ %(prog)s --continue -p "Continue previous conversation"
99
+ %(prog)s --resume abc123 -p "Resume session abc123"
98
100
 
99
101
  Default Tools (enabled by default when no --allowed-tools specified):
100
102
  Task, Bash, Glob, Grep, ExitPlanMode, Read, Edit, Write, NotebookEdit,
@@ -210,6 +212,14 @@ Environment Variables:
210
212
  help="Continue the most recent conversation"
211
213
  )
212
214
 
215
+ parser.add_argument(
216
+ "-r", "--resume",
217
+ type=str,
218
+ dest="resume_session",
219
+ metavar="SESSION_ID",
220
+ help="Resume a conversation by session ID (e.g., claude --resume abc123)"
221
+ )
222
+
213
223
  parser.add_argument(
214
224
  "--agents",
215
225
  type=str,
@@ -286,6 +296,10 @@ Environment Variables:
286
296
  if args.continue_conversation:
287
297
  cmd.append("--continue")
288
298
 
299
+ # Add resume flag if specified
300
+ if args.resume_session:
301
+ cmd.extend(["--resume", args.resume_session])
302
+
289
303
  # Add agents configuration if specified
290
304
  if args.agents:
291
305
  cmd.extend(["--agents", args.agents])
@@ -8,16 +8,25 @@ import argparse
8
8
  import os
9
9
  import subprocess
10
10
  import sys
11
- from typing import List
11
+ import json
12
+ from datetime import datetime
13
+ from typing import List, Optional
12
14
 
13
15
 
14
16
  class CodexService:
15
17
  """Service wrapper for OpenAI Codex CLI"""
16
18
 
17
19
  # Default configuration
18
- DEFAULT_MODEL = "gpt-5-codex"
20
+ DEFAULT_MODEL = "codex-5.1-max"
19
21
  DEFAULT_AUTO_INSTRUCTION = """You are an AI coding assistant. Follow the instructions provided and generate high-quality code."""
20
22
 
23
+ # Model shorthand mappings (colon-prefixed names expand to full model IDs)
24
+ MODEL_SHORTHANDS = {
25
+ ":codex": "codex-5.1-codex-max",
26
+ ":gpt-5": "gpt-5",
27
+ ":mini": "gpt-5-codex-mini",
28
+ }
29
+
21
30
  def __init__(self):
22
31
  self.model_name = self.DEFAULT_MODEL
23
32
  self.auto_instruction = self.DEFAULT_AUTO_INSTRUCTION
@@ -26,6 +35,17 @@ class CodexService:
26
35
  self.additional_args: List[str] = []
27
36
  self.verbose = False
28
37
 
38
+ def expand_model_shorthand(self, model: str) -> str:
39
+ """
40
+ Expand model shorthand names to full model IDs.
41
+
42
+ If the model starts with ':', look it up in MODEL_SHORTHANDS.
43
+ Otherwise, return the model name as-is.
44
+ """
45
+ if model.startswith(":"):
46
+ return self.MODEL_SHORTHANDS.get(model, model)
47
+ return model
48
+
29
49
  def check_codex_installed(self) -> bool:
30
50
  """Check if codex CLI is installed and available"""
31
51
  try:
@@ -49,6 +69,13 @@ Examples:
49
69
  %(prog)s -p "Write a hello world function"
50
70
  %(prog)s -pp prompt.txt --cd /path/to/project
51
71
  %(prog)s -p "Add tests" -m gpt-4 -c custom_arg=value
72
+ %(prog)s -p "Optimize code" -m :codex # uses codex-5.1-codex-max
73
+
74
+ Environment Variables:
75
+ CODEX_MODEL Model name (supports shorthand, default: codex-5.1-max)
76
+ CODEX_HIDE_STREAM_TYPES Comma-separated list of streaming msg types to hide
77
+ Default: turn_diff,token_count,exec_command_output_delta
78
+ JUNO_CODE_HIDE_STREAM_TYPES Same as CODEX_HIDE_STREAM_TYPES (alias)
52
79
  """
53
80
  )
54
81
 
@@ -75,8 +102,8 @@ Examples:
75
102
  parser.add_argument(
76
103
  "-m", "--model",
77
104
  type=str,
78
- default=self.DEFAULT_MODEL,
79
- help=f"Model name. Default: {self.DEFAULT_MODEL}"
105
+ default=os.environ.get("CODEX_MODEL", self.DEFAULT_MODEL),
106
+ help=f"Model name. Supports shorthand (e.g., ':codex', ':gpt-5', ':mini') or full model ID. Default: {self.DEFAULT_MODEL} (env: CODEX_MODEL)"
80
107
  )
81
108
 
82
109
  parser.add_argument(
@@ -163,34 +190,242 @@ Examples:
163
190
 
164
191
  return cmd
165
192
 
193
+ def _format_msg_pretty(self, obj: dict) -> Optional[str]:
194
+ """
195
+ Pretty format for specific msg types to be human readable while
196
+ preserving a compact JSON header line that includes the msg.type.
197
+
198
+ - agent_message: render 'message' field as multi-line text
199
+ - agent_reasoning: render 'text' field as multi-line text
200
+ - exec_command_end: only output 'formatted_output' (suppress other fields)
201
+ - token_count: fully suppressed (no final summary emission)
202
+
203
+ Returns a string to print, or None to fall back to raw printing.
204
+ """
205
+ try:
206
+ msg = obj.get("msg") or {}
207
+ msg_type = (msg.get("type") or "").strip()
208
+ now = datetime.now().strftime("%I:%M:%S %p")
209
+
210
+ # agent_message → show 'message' human-readable
211
+ if msg_type == "agent_message":
212
+ content = msg.get("message", "")
213
+ header = {"type": msg_type, "datetime": now}
214
+ if "\n" in content:
215
+ return json.dumps(header, ensure_ascii=False) + "\nmessage:\n" + content
216
+ header["message"] = content
217
+ return json.dumps(header, ensure_ascii=False)
218
+
219
+ # agent_reasoning → show 'text' human-readable
220
+ if msg_type == "agent_reasoning":
221
+ content = msg.get("text", "")
222
+ header = {"type": msg_type, "datetime": now}
223
+ if "\n" in content:
224
+ return json.dumps(header, ensure_ascii=False) + "\ntext:\n" + content
225
+ header["text"] = content
226
+ return json.dumps(header, ensure_ascii=False)
227
+
228
+ # exec_command_end → only show 'formatted_output'
229
+ if msg_type == "exec_command_end":
230
+ formatted_output = msg.get("formatted_output", "")
231
+ header = {"type": msg_type, "datetime": now}
232
+ if "\n" in formatted_output:
233
+ return json.dumps(header, ensure_ascii=False) + "\nformatted_output:\n" + formatted_output
234
+ header["formatted_output"] = formatted_output
235
+ return json.dumps(header, ensure_ascii=False)
236
+
237
+ return None
238
+ except Exception:
239
+ return None
240
+
166
241
  def run_codex(self, cmd: List[str], verbose: bool = False) -> int:
167
- """Execute the codex command and stream output"""
242
+ """Execute the codex command and stream output with filtering and pretty-printing
243
+
244
+ Robustness improvements:
245
+ - Attempts to parse JSON even if the line has extra prefix/suffix noise
246
+ - Falls back to string suppression for known noisy types if JSON parsing fails
247
+ - Never emits token_count or exec_command_output_delta even on malformed lines
248
+ """
168
249
  if verbose:
169
250
  print(f"Executing: {' '.join(cmd)}", file=sys.stderr)
170
251
  print("-" * 80, file=sys.stderr)
171
252
 
253
+ # Resolve hidden stream types (ENV configurable)
254
+ default_hidden = {"turn_diff", "token_count", "exec_command_output_delta"}
255
+ env_hide_1 = os.environ.get("CODEX_HIDE_STREAM_TYPES", "")
256
+ env_hide_2 = os.environ.get("JUNO_CODE_HIDE_STREAM_TYPES", "")
257
+ hide_types = set(default_hidden)
258
+ for env_val in (env_hide_1, env_hide_2):
259
+ if env_val:
260
+ parts = [p.strip() for p in env_val.split(",") if p.strip()]
261
+ hide_types.update(parts)
262
+
263
+ # We fully suppress all token_count events (do not emit even at end)
264
+ last_token_count = None
265
+
172
266
  try:
173
267
  # Run the command and stream output
174
- # Use line buffering (bufsize=1) to ensure each JSON line is output immediately
175
268
  process = subprocess.Popen(
176
269
  cmd,
177
270
  stdout=subprocess.PIPE,
178
271
  stderr=subprocess.PIPE,
179
272
  text=True,
180
- bufsize=1, # Line buffering for immediate output
273
+ bufsize=1,
181
274
  universal_newlines=True
182
275
  )
183
276
 
184
- # Stream stdout line by line
185
- # Codex outputs text format by default (not JSON), so we just pass it through
186
277
  if process.stdout:
187
- for line in process.stdout:
188
- # Output text as-is with immediate flush for real-time streaming
189
- print(line, end='', flush=True)
190
-
191
- # Wait for process to complete
278
+ for raw_line in process.stdout:
279
+ line = raw_line.rstrip("\n")
280
+ if not line:
281
+ continue
282
+ # Try to parse NDJSON and filter by msg.type
283
+ try:
284
+ obj = None
285
+ s = line.strip()
286
+
287
+ # Direct JSON line
288
+ if s.startswith("{") and s.endswith("}"):
289
+ try:
290
+ obj = json.loads(s)
291
+ except Exception:
292
+ obj = None
293
+
294
+ # Attempt to extract JSON object substring if noise surrounds it
295
+ if obj is None:
296
+ lbrace = s.find("{")
297
+ rbrace = s.rfind("}")
298
+ if lbrace != -1 and rbrace != -1 and rbrace > lbrace:
299
+ candidate = s[lbrace:rbrace + 1]
300
+ try:
301
+ obj = json.loads(candidate)
302
+ s = candidate # normalized JSON text
303
+ except Exception:
304
+ obj = None
305
+
306
+ def handle_obj(obj_dict: dict):
307
+ nonlocal last_token_count
308
+ msg = obj_dict.get("msg") or {}
309
+ msg_type_inner = (msg.get("type") or "").strip()
310
+
311
+ if msg_type_inner == "token_count":
312
+ last_token_count = obj_dict
313
+ return # suppress
314
+
315
+ if msg_type_inner and msg_type_inner in hide_types:
316
+ return # suppress
317
+
318
+ pretty_line_inner = self._format_msg_pretty(obj_dict)
319
+ if pretty_line_inner is not None:
320
+ print(pretty_line_inner, flush=True)
321
+ else:
322
+ # print normalized JSON
323
+ print(json.dumps(obj_dict, ensure_ascii=False), flush=True)
324
+
325
+ if isinstance(obj, dict):
326
+ handle_obj(obj)
327
+ continue
328
+
329
+ # If line appears to contain multiple concatenated JSON objects,
330
+ # split by top-level brace balancing while respecting strings
331
+ def split_concatenated_json(text: str):
332
+ objs = []
333
+ buf = []
334
+ depth = 0
335
+ in_str = False
336
+ esc = False
337
+ started = False
338
+ for ch in text:
339
+ if in_str:
340
+ buf.append(ch)
341
+ if esc:
342
+ esc = False
343
+ elif ch == '\\':
344
+ esc = True
345
+ elif ch == '"':
346
+ in_str = False
347
+ continue
348
+ # not in string
349
+ if ch == '"':
350
+ in_str = True
351
+ buf.append(ch)
352
+ continue
353
+ if ch == '{':
354
+ depth += 1
355
+ started = True
356
+ buf.append(ch)
357
+ continue
358
+ if ch == '}':
359
+ depth -= 1
360
+ buf.append(ch)
361
+ if started and depth == 0:
362
+ candidate = ''.join(buf).strip().strip("'\"")
363
+ if candidate:
364
+ objs.append(candidate)
365
+ buf = []
366
+ started = False
367
+ continue
368
+ # accumulate only if currently collecting an object
369
+ if started:
370
+ buf.append(ch)
371
+ return objs
372
+
373
+ parts = split_concatenated_json(s)
374
+ if parts:
375
+ for part in parts:
376
+ try:
377
+ sub = json.loads(part)
378
+ if isinstance(sub, dict):
379
+ handle_obj(sub)
380
+ else:
381
+ low = part.lower()
382
+ if (
383
+ '"token_count"' in low
384
+ or '"exec_command_output_delta"' in low
385
+ or '"turn_diff"' in low
386
+ ):
387
+ continue
388
+ print(part, flush=True)
389
+ except Exception:
390
+ low = part.lower()
391
+ if (
392
+ '"token_count"' in low
393
+ or '"exec_command_output_delta"' in low
394
+ or '"turn_diff"' in low
395
+ ):
396
+ continue
397
+ print(part, flush=True)
398
+ continue
399
+ else:
400
+ # If not valid JSON, suppress known noisy tokens by string search
401
+ lower = s.lower()
402
+ if (
403
+ '"type"' in lower
404
+ and (
405
+ '"token_count"' in lower
406
+ or '"exec_command_output_delta"' in lower
407
+ or '"turn_diff"' in lower
408
+ )
409
+ ):
410
+ # Best-effort suppression for malformed lines containing noisy types
411
+ continue
412
+ # Not JSON and not noisy → passthrough
413
+ print(raw_line, end="", flush=True)
414
+ except Exception:
415
+ # On parsing error, last-gasp suppression by substring match
416
+ if (
417
+ '"token_count"' in raw_line
418
+ or '"exec_command_output_delta"' in raw_line
419
+ or '"turn_diff"' in raw_line
420
+ ):
421
+ continue
422
+ print(raw_line, end="", flush=True)
423
+
424
+ # Wait for process completion
192
425
  process.wait()
193
426
 
427
+ # Do not emit token_count summary; fully suppressed per user feedback
428
+
194
429
  # Print stderr if there were errors
195
430
  if process.stderr and process.returncode != 0:
196
431
  stderr_output = process.stderr.read()
@@ -201,9 +436,11 @@ Examples:
201
436
 
202
437
  except KeyboardInterrupt:
203
438
  print("\nInterrupted by user", file=sys.stderr)
204
- if process:
439
+ try:
205
440
  process.terminate()
206
441
  process.wait()
442
+ except Exception:
443
+ pass
207
444
  return 130
208
445
  except Exception as e:
209
446
  print(f"Error executing codex: {e}", file=sys.stderr)
@@ -237,7 +474,8 @@ Examples:
237
474
 
238
475
  # Set configuration from arguments
239
476
  self.project_path = os.path.abspath(args.cd)
240
- self.model_name = args.model
477
+ # Expand model shorthand
478
+ self.model_name = self.expand_model_shorthand(args.model)
241
479
  self.auto_instruction = args.auto_instruction
242
480
 
243
481
  # 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.31",
3
+ "version": "1.0.33",
4
4
  "description": "TypeScript CLI tool for AI subagent orchestration with code automation",
5
5
  "keywords": [
6
6
  "ai",