coderouter-cli 2.7.8__py3-none-any.whl → 2.7.9__py3-none-any.whl

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.
@@ -18,15 +18,24 @@ CodeRouter merely performs the one conversion. Following the
18
18
  ``openai_compat`` precedent, a *single* adapter class fronts *multiple*
19
19
  target agents, dispatched on the ``agent`` field.
20
20
 
21
- Implemented agents (Phase 1a + 1d)
22
- ==================================
21
+ Implemented agents (Phase 1a + 1b + 1d)
22
+ ========================================
23
23
 
24
- Two targets are implemented here:
24
+ Three targets are implemented here:
25
25
 
26
26
  * ``claude`` (Claude Code CLI, Phase 1a) — the most stable CLI, the most
27
27
  fine-grained safety controls, and the only one that emits
28
28
  ``total_cost_usd`` directly (making it the reference implementation for
29
29
  the parser / cost path).
30
+ * ``codex`` (codex CLI, Phase 1b) — headless one-shot via ``codex exec
31
+ --json``, prompt on stdin (like claude). The CLI is pre-1.0 and emits
32
+ defensively-parsed JSONL (one event per line); usage comes from
33
+ ``turn.completed`` events, normalized per design §5.1.6 with
34
+ ``cached_input_tokens`` kept as a *subset* of ``input_tokens`` (unlike
35
+ claude, which folds its cache buckets into ``prompt_tokens``).
36
+ ``--skip-git-repo-check`` and ``--ephemeral`` are always passed — the
37
+ isolated workdir is not a git repo, and the adapter never wants
38
+ session persistence.
30
39
  * ``grok`` (grok CLI, Phase 1d) — headless one-shot via ``--prompt-file`` +
31
40
  ``--output-format json``. The CLI emits no token/cost figures, so usage
32
41
  is reported as zeros (cost stays 0 unless the operator sets
@@ -34,9 +43,9 @@ Two targets are implemented here:
34
43
  so a user-level cross-session memory setting cannot leak state between
35
44
  requests.
36
45
 
37
- The remaining agents (codex / gemini) are declared in the config schema so
38
- providers.yaml is forward-compatible, but constructing an adapter for them
39
- raises a clear ``AdapterError`` until their phase (1b / 1c) lands.
46
+ The remaining agent (gemini) is declared in the config schema so
47
+ providers.yaml is forward-compatible, but constructing an adapter for it
48
+ raises a clear ``AdapterError`` until its phase (1c) lands.
40
49
 
41
50
  Security (design §6, non-negotiable)
42
51
  ====================================
@@ -44,10 +53,11 @@ Security (design §6, non-negotiable)
44
53
  * **allowlist argv only** — the child is launched with
45
54
  :func:`asyncio.create_subprocess_exec` and a list argv. ``shell=True`` is
46
55
  never used; the prompt is never subject to shell interpretation (claude
47
- reads it from stdin; grok reads it from a private ``0600`` prompt file
48
- inside the resolved workdir its ``-p`` requires the prompt as an argv
49
- value, and argv would both hit Linux's ~128KiB ``MAX_ARG_STRLEN`` on huge
50
- prompts and leak the text into ``ps`` output).
56
+ and codex read it from stdin codex's argv carries a trailing ``-``
57
+ sentinel making that explicit; grok reads it from a private ``0600``
58
+ prompt file inside the resolved workdir its ``-p`` requires the prompt
59
+ as an argv value, and argv would both hit Linux's ~128KiB
60
+ ``MAX_ARG_STRLEN`` on huge prompts and leak the text into ``ps`` output).
51
61
  * **default read-only** — ``allow_file_writes=False`` /
52
62
  ``sandbox_mode="read_only"`` are the defaults, mapped to claude's
53
63
  ``--permission-mode plan``. Writes require explicit opt-in and the sandbox
@@ -127,6 +137,17 @@ _GROK_SANDBOX_ARGS = {
127
137
  "full_auto": ["--sandbox", "workspace", "--always-approve"],
128
138
  }
129
139
 
140
+ # sandbox_mode → codex ``-s/--sandbox`` value (design §5.4, codex-cli
141
+ # 0.144.1, verified via facts-codex.md). ``codex exec`` has NO approval
142
+ # flag at all (non-interactive, so there is no prompt to approve/skip), so
143
+ # ``full_auto`` collapses onto the same ``workspace-write`` value as
144
+ # ``edit`` — there is nothing further to "auto" beyond granting writes.
145
+ _CODEX_SANDBOX_ARGS = {
146
+ "read_only": ["-s", "read-only"],
147
+ "edit": ["-s", "workspace-write"],
148
+ "full_auto": ["-s", "workspace-write"],
149
+ }
150
+
130
151
 
131
152
  def _chunk_text(text: str, size: int = _STREAM_CHUNK_CHARS) -> Iterator[str]:
132
153
  """Split ``text`` into ``size``-char pieces for the pseudo-stream."""
@@ -135,7 +156,7 @@ def _chunk_text(text: str, size: int = _STREAM_CHUNK_CHARS) -> Iterator[str]:
135
156
 
136
157
 
137
158
  class AgentCliAdapter(BaseAdapter):
138
- """Invoke an external coding-agent CLI one-shot (claude + grok).
159
+ """Invoke an external coding-agent CLI one-shot (claude + codex + grok).
139
160
 
140
161
  The ``agent`` field selects the argv builder / output parser via the
141
162
  dispatch tables built in :meth:`__init__`, mirroring how
@@ -145,10 +166,10 @@ class AgentCliAdapter(BaseAdapter):
145
166
  def __init__(self, config: ProviderConfig) -> None:
146
167
  """Bind to a ``ProviderConfig`` and reject unsupported agents.
147
168
 
148
- Constructing an adapter for an agent other than ``claude`` / ``grok``
149
- raises a non-retryable :class:`AdapterError` — the other targets are
150
- declared in the schema but not implemented until their phase
151
- (design §9).
169
+ Constructing an adapter for an agent other than ``claude`` /
170
+ ``codex`` / ``grok`` raises a non-retryable :class:`AdapterError` —
171
+ the remaining target (gemini) is declared in the schema but not
172
+ implemented until its phase (design §9).
152
173
  """
153
174
  super().__init__(config)
154
175
  if config.agent_cli is None: # pragma: no cover - schema enforces this
@@ -158,30 +179,34 @@ class AgentCliAdapter(BaseAdapter):
158
179
  retryable=False,
159
180
  )
160
181
  self.acfg: AgentCliConfig = config.agent_cli
161
- if self.acfg.agent not in ("claude", "grok"):
182
+ if self.acfg.agent not in ("claude", "codex", "grok"):
162
183
  raise AdapterError(
163
184
  f"agent {self.acfg.agent!r} is not implemented yet "
164
- f"(implemented: claude, grok). Wait for Phase 1b/1c.",
185
+ f"(implemented: claude, codex, grok). Wait for Phase 1c.",
165
186
  provider=config.name,
166
187
  retryable=False,
167
188
  )
168
189
  # agent → argv builder / output parser dispatch tables. claude landed
169
- # in Phase 1a, grok in Phase 1d; Phase 1b/1c add codex / gemini.
190
+ # in Phase 1a, codex in Phase 1b, grok in Phase 1d; Phase 1c adds
191
+ # gemini.
170
192
  self._builders = {
171
193
  "claude": self._build_claude_argv,
194
+ "codex": self._build_codex_argv,
172
195
  "grok": self._build_grok_argv,
173
196
  }
174
197
  self._parsers = {
175
198
  "claude": self._parse_claude,
199
+ "codex": self._parse_codex,
176
200
  "grok": self._parse_grok,
177
201
  }
178
- # Prompt delivery is per-agent: claude reads its print-mode prompt
179
- # from stdin (10MB cap), which keeps argv free of the (potentially
202
+ # Prompt delivery is per-agent: claude and codex read their prompt
203
+ # from stdin (10MB cap; codex's argv carries a trailing "-" sentinel
204
+ # making that explicit), which keeps argv free of the (potentially
180
205
  # huge) prompt text. grok's ``-p`` REQUIRES the prompt as its argv
181
206
  # value (piped stdin is only appended as extra context, verified on
182
207
  # v0.2.93), so grok gets the prompt via ``--prompt-file`` instead —
183
208
  # see ``_write_prompt_file`` for the rationale.
184
- self._uses_stdin = self.acfg.agent == "claude"
209
+ self._uses_stdin = self.acfg.agent in ("claude", "codex")
185
210
 
186
211
  # ------------------------------------------------------------------
187
212
  # BaseAdapter contract
@@ -465,6 +490,178 @@ class AgentCliAdapter(BaseAdapter):
465
490
  usage["duration_ms"] = data["duration_ms"]
466
491
  return usage
467
492
 
493
+ # ------------------------------------------------------------------
494
+ # codex argv builder + output parser (Phase 1b)
495
+ # ------------------------------------------------------------------
496
+
497
+ def _build_codex_argv(self, workdir: str, prompt_file: str | None = None) -> list[str]:
498
+ """Assemble the ``codex exec`` argv (design §5.1.5 / §5.4, verified
499
+ against codex-cli 0.144.1 — see ``_codex/facts-codex.md``).
500
+
501
+ Shape::
502
+
503
+ codex exec --json --skip-git-repo-check --ephemeral
504
+ -m <model> -C <workdir> -s <read-only|workspace-write> -
505
+
506
+ The prompt is fed on stdin (not argv), so it never appears here and
507
+ ``prompt_file`` is ignored (it exists only to keep the builder
508
+ signature uniform across agents) — the trailing ``-`` makes the
509
+ stdin intent explicit to ``codex exec``, which otherwise treats a
510
+ bare invocation with no PROMPT arg the same way but reads more
511
+ ambiguously in a fixed argv list. ``--skip-git-repo-check`` is
512
+ ALWAYS passed because the isolated workdir is not a git repository
513
+ (without it the CLI exits 1). ``--ephemeral`` is ALWAYS passed so no
514
+ session state persists to disk, matching the adapter's stateless
515
+ one-shot ethos (the same rationale as grok's ``--no-memory``). codex
516
+ has no ``--max-turns`` equivalent, so ``AgentCliConfig.max_turns`` is
517
+ silently ignored here (documented in the schema).
518
+ """
519
+ del prompt_file # codex takes the prompt on stdin, not from a file.
520
+ model = self.acfg.model or self.config.model
521
+ argv = [
522
+ self.acfg.command,
523
+ "exec",
524
+ "--json",
525
+ "--skip-git-repo-check",
526
+ "--ephemeral",
527
+ "-m",
528
+ model,
529
+ "-C",
530
+ workdir,
531
+ ]
532
+ argv += self._codex_sandbox_args()
533
+ argv += ["-"]
534
+ return argv
535
+
536
+ def _codex_sandbox_args(self) -> list[str]:
537
+ """Map ``sandbox_mode`` → codex ``-s/--sandbox`` flags, clamped.
538
+
539
+ Same clamp as claude/grok (design §5.4): when ``allow_file_writes``
540
+ is False the effective mode is forced to ``read_only`` regardless of
541
+ ``sandbox_mode``, so writes always require the explicit opt-in.
542
+ ``codex exec`` has no approval flag in 0.144.1 (non-interactive, so
543
+ there is nothing to approve), so ``full_auto`` maps onto the same
544
+ ``workspace-write`` value as ``edit`` —
545
+ ``--dangerously-bypass-approvals-and-sandbox`` is never used.
546
+ """
547
+ mode = self.acfg.sandbox_mode if self.acfg.allow_file_writes else "read_only"
548
+ return list(_CODEX_SANDBOX_ARGS[mode])
549
+
550
+ def _parse_codex(
551
+ self, stdout: bytes, stderr: bytes
552
+ ) -> tuple[str, dict[str, Any], dict[str, Any]]:
553
+ """Parse codex ``exec --json`` JSONL output (verified codex-cli
554
+ 0.144.1, ``_codex/facts-codex.md``).
555
+
556
+ Real-run shape (one JSON object per line, newline-delimited)::
557
+
558
+ {"type":"thread.started","thread_id":"<uuid>"}
559
+ {"type":"turn.started"}
560
+ {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"2"}}
561
+ {"type":"turn.completed","usage":{"input_tokens":13810,"cached_input_tokens":9984,"output_tokens":5,"reasoning_output_tokens":0}}
562
+
563
+ The CLI is pre-1.0 and its JSON schema is not frozen, so parsing is
564
+ deliberately defensive at every level: individual lines that fail to
565
+ parse (or parse to something other than a JSON object) are SKIPPED
566
+ rather than aborting the whole parse — stray non-JSON noise on
567
+ stdout should not sink an otherwise-valid answer. The final answer
568
+ is the LAST ``item.completed`` event whose ``item`` is an
569
+ ``agent_message`` with a string ``text``. If a completed answer was
570
+ found, it is returned even when a later ``error`` / ``turn.failed``
571
+ event also appears (a completed answer beats a trailing error); if
572
+ no answer was found, an ``error`` / ``turn.failed`` event (or the
573
+ total absence of any agent_message) raises a retryable
574
+ :class:`AdapterError`.
575
+ """
576
+ text = stdout.decode("utf-8", "replace")
577
+ if not text.strip():
578
+ raise AdapterError(
579
+ "codex produced no stdout to parse",
580
+ provider=self.name,
581
+ retryable=True,
582
+ )
583
+
584
+ final_text: str | None = None
585
+ thread_id: str | None = None
586
+ failure_event: dict[str, Any] | None = None
587
+ prompt_tokens = 0
588
+ completion_tokens = 0
589
+ cached_tokens = 0
590
+ reasoning_tokens = 0
591
+
592
+ for line in text.splitlines():
593
+ line = line.strip()
594
+ if not line:
595
+ continue
596
+ try:
597
+ event = json.loads(line)
598
+ except json.JSONDecodeError:
599
+ # Defensive against stray non-JSON noise (progress text that
600
+ # leaked onto stdout, partial writes, etc.) — skip the line.
601
+ continue
602
+ if not isinstance(event, dict):
603
+ continue
604
+
605
+ etype = event.get("type")
606
+ if etype == "thread.started":
607
+ tid = event.get("thread_id")
608
+ if isinstance(tid, str):
609
+ thread_id = tid
610
+ elif etype == "item.completed":
611
+ item = event.get("item")
612
+ if (
613
+ isinstance(item, dict)
614
+ and item.get("type") == "agent_message"
615
+ and isinstance(item.get("text"), str)
616
+ ):
617
+ final_text = item["text"]
618
+ elif etype == "turn.completed":
619
+ usage = event.get("usage")
620
+ usage = usage if isinstance(usage, dict) else {}
621
+
622
+ def _int(key: str, _usage: dict[str, Any] = usage) -> int:
623
+ value = _usage.get(key)
624
+ return int(value) if isinstance(value, (int, float)) else 0
625
+
626
+ prompt_tokens += _int("input_tokens")
627
+ completion_tokens += _int("output_tokens")
628
+ cached_tokens += _int("cached_input_tokens")
629
+ reasoning_tokens += _int("reasoning_output_tokens")
630
+ elif etype in ("error", "turn.failed"):
631
+ failure_event = event
632
+
633
+ if final_text is None:
634
+ if failure_event is not None:
635
+ raise AdapterError(
636
+ f"codex reported {failure_event.get('type')}: {failure_event!r}"[:500],
637
+ provider=self.name,
638
+ retryable=True,
639
+ )
640
+ raise AdapterError(
641
+ "codex JSONL output contained no agent_message",
642
+ provider=self.name,
643
+ retryable=True,
644
+ )
645
+
646
+ # cached_input_tokens is a SUBSET of input_tokens (not additive) —
647
+ # verified sample: input 13810 ⊇ cached 9984 — so it is preserved
648
+ # under prompt_tokens_details rather than folded into prompt_tokens
649
+ # (this differs from claude's normalization, design §5.1.6).
650
+ usage_out: dict[str, Any] = {
651
+ "prompt_tokens": prompt_tokens,
652
+ "completion_tokens": completion_tokens,
653
+ "total_tokens": prompt_tokens + completion_tokens,
654
+ }
655
+ if cached_tokens > 0:
656
+ usage_out["prompt_tokens_details"] = {"cached_tokens": cached_tokens}
657
+ if reasoning_tokens > 0:
658
+ usage_out["completion_tokens_details"] = {"reasoning_tokens": reasoning_tokens}
659
+
660
+ meta: dict[str, Any] = {}
661
+ if thread_id is not None:
662
+ meta["coderouter_session_id"] = thread_id
663
+ return final_text, usage_out, meta
664
+
468
665
  # ------------------------------------------------------------------
469
666
  # grok argv builder + output parser (Phase 1d)
470
667
  # ------------------------------------------------------------------
@@ -171,10 +171,10 @@ class AgentCliConfig(BaseModel):
171
171
  invokes an external coding-agent CLI (codex / gemini / grok / claude)
172
172
  in a single one-shot ``exec`` and returns the final answer as one
173
173
  ``prompt in → text out`` transformation. ``claude`` (Claude Code CLI,
174
- Phase 1a) and ``grok`` (grok CLI, Phase 1d) are implemented;
175
- codex / gemini are declared at the schema level so configs are
176
- forward-compatible, but the adapter rejects them until their phase
177
- lands.
174
+ Phase 1a), ``codex`` (codex CLI, Phase 1b) and ``grok`` (grok CLI,
175
+ Phase 1d) are implemented; ``gemini`` is declared at the schema level
176
+ so configs are forward-compatible, but the adapter rejects it until its
177
+ phase (1c) lands.
178
178
 
179
179
  Auth note (grok): the grok CLI uses OAuth credentials stored under
180
180
  ``~/.grok`` (``grok login``), which the adapter's HOME inheritance
@@ -182,6 +182,14 @@ class AgentCliConfig(BaseModel):
182
182
  ``GROK_CODE_XAI_API_KEY`` in ``passthrough_env`` (this is grok's key
183
183
  env var — NOT ``XAI_API_KEY``).
184
184
 
185
+ Auth note (codex): the codex CLI uses a ChatGPT-plan OAuth login stored
186
+ under ``~/.codex`` (``codex login``), which the adapter's HOME
187
+ inheritance already covers — the credentials go stale after roughly 8
188
+ days and are auto-refreshed on use, so no extra config is needed for
189
+ interactive/subscription setups. For CI / API-key setups, list
190
+ ``CODEX_API_KEY`` (exec-only) or ``OPENAI_API_KEY`` (general) in
191
+ ``passthrough_env``.
192
+
185
193
  Follows the ``extra="forbid"`` convention used across this module so a
186
194
  typo'd key fails at config-load rather than being silently ignored.
187
195
  """
@@ -191,8 +199,9 @@ class AgentCliConfig(BaseModel):
191
199
  agent: Literal["codex", "gemini", "grok", "claude"] = Field(
192
200
  ...,
193
201
  description=(
194
- "External coding-agent CLI to invoke. 'claude' (Phase 1a) and "
195
- "'grok' (Phase 1d) are implemented; 'codex' / 'gemini' pending."
202
+ "External coding-agent CLI to invoke. 'claude' (Phase 1a), "
203
+ "'codex' (Phase 1b) and 'grok' (Phase 1d) are implemented; "
204
+ "'gemini' (Phase 1c) pending."
196
205
  ),
197
206
  )
198
207
  command: str | None = Field(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.7.8
3
+ Version: 2.7.9
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -16,7 +16,7 @@ coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,
16
16
  coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
17
17
  coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcxPpFiJGlzjk,4609
18
18
  coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
19
- coderouter/adapters/agent_cli.py,sha256=_-9DsSO-zQJI-bEldBhFgIBbAQd6kqMCqX7xNy8WqtQ,32992
19
+ coderouter/adapters/agent_cli.py,sha256=ChTheoyDz9SE86Htr-ZOwSW-M7XrbuUfed1daAlvXBI,42154
20
20
  coderouter/adapters/anthropic_native.py,sha256=CT9Hitun-c3z83YHT2zZXgwZpY3_t6eRnOfAmNud2aw,23610
21
21
  coderouter/adapters/base.py,sha256=ykKMaiIVVGr23oFFXilK2iP_YhI8-CtsEiJyFXaC9bE,10396
22
22
  coderouter/adapters/openai_compat.py,sha256=wcrd_UpV7N6ISpUyKvdFPGU-YjCrX5oMqGpjAFEeWfg,20356
@@ -25,7 +25,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
25
25
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
26
26
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
27
27
  coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
28
- coderouter/config/schemas.py,sha256=c5VJkcsuPHIKgTtcRQnXL-q52ollsWOISnDC8RuOX70,86528
28
+ coderouter/config/schemas.py,sha256=fuqMuO9bafAhztUMtA6CwEBjSKNFyW-CIeen3Q0pRlg,87047
29
29
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
30
30
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
31
31
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -69,8 +69,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
69
69
  coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
70
70
  coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
71
71
  coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
72
- coderouter_cli-2.7.8.dist-info/METADATA,sha256=S3XO80V0pWn7ifsjuoMOEYcU0Cnru5WIADYIq9-PQb4,15546
73
- coderouter_cli-2.7.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
74
- coderouter_cli-2.7.8.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
75
- coderouter_cli-2.7.8.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
76
- coderouter_cli-2.7.8.dist-info/RECORD,,
72
+ coderouter_cli-2.7.9.dist-info/METADATA,sha256=foZQpq3qpofluaktg5dWZBeLH9j7RUrD08n3wNS33p4,15546
73
+ coderouter_cli-2.7.9.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
74
+ coderouter_cli-2.7.9.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
75
+ coderouter_cli-2.7.9.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
76
+ coderouter_cli-2.7.9.dist-info/RECORD,,