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.
- package/dist/bin/cli.js +201 -143
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +200 -142
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/index.js +32 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32 -3
- package/dist/index.mjs.map +1 -1
- package/dist/templates/services/__pycache__/codex.cpython-38.pyc +0 -0
- package/dist/templates/services/claude.py +14 -0
- package/dist/templates/services/codex.py +254 -16
- package/package.json +1 -1
|
Binary file
|
|
@@ -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
|
-
|
|
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 = "
|
|
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,
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|