coderouter-cli 2.9.3__py3-none-any.whl → 2.10.0__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.
@@ -0,0 +1,573 @@
1
+ """v2.10-A: ``coderouter vscode-init`` — one-shot VSCode workspace scaffolder.
2
+
3
+ Writes ``.vscode/settings.json`` with ``terminal.integrated.env.*`` so a
4
+ Claude Code session launched from VSCode's integrated terminal
5
+ auto-points at CodeRouter. Optionally emits a direnv ``.envrc`` in the
6
+ same run.
7
+
8
+ The design contract:
9
+
10
+ * stdlib only — no new runtime deps (keeps the strict-5 rule in
11
+ ``pyproject.toml`` intact).
12
+ * Never overwrite unrelated keys in an existing ``settings.json``. Only
13
+ ``terminal.integrated.env.osx`` / ``.linux`` / ``.windows`` are
14
+ touched, and inside them only the CodeRouter-managed env vars.
15
+ * When an existing target value differs from the desired one, refuse
16
+ without ``--force`` and print a unified diff so the operator decides.
17
+ * ``--dry-run`` is byte-identical to the write path minus the actual
18
+ ``os.replace`` at the end.
19
+ * All writes are atomic (tmp file + ``os.replace``) so a partial write
20
+ cannot corrupt an existing ``settings.json``. This matters more here
21
+ than in most CodeRouter modules because the file we're editing is
22
+ workspace-critical: a broken JSON blob would silently break every
23
+ VSCode terminal env until a human debugged it.
24
+
25
+ Files touched (opt-in per flag):
26
+
27
+ * ``{target}/.vscode/settings.json`` — always
28
+ * ``{target}/.envrc`` — ``--with-envrc``
29
+
30
+ Non-Claude-Code extensions (Cline / Roo / Kilo / Continue) are handled
31
+ by the cheat sheet printed to stdout at end of run and the fuller
32
+ ``docs/guides/vscode.md`` — this CLI deliberately does NOT reach into
33
+ those extensions' settings, because each ships its own config schema
34
+ that changes with its own release cadence.
35
+
36
+ CLI plumbing lives in :mod:`coderouter.cli` (``_run_vscode_init``); this
37
+ module is import-clean so tests can exercise ``run_vscode_init``
38
+ directly without argparse in the way.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import os
45
+ from dataclasses import dataclass
46
+ from difflib import unified_diff
47
+ from pathlib import Path
48
+
49
+ # v2.10-A: The README, quickstart, and every backend guide teach 8088 as
50
+ # the CodeRouter port. ``coderouter serve`` still defaults to 4000 (a
51
+ # pre-README default that predates the docs unification); vscode-init
52
+ # sides with the docs so that the settings.json it writes matches every
53
+ # tutorial the user is likely to be reading. Operators who deviate can
54
+ # pass ``--port`` explicitly.
55
+ DEFAULT_PORT = 8088
56
+
57
+ # Bogus token — CodeRouter's chat ingress does not validate ANTHROPIC_AUTH_TOKEN
58
+ # (real API keys are managed on the ``providers.yaml`` side). Using a
59
+ # fixed sentinel here means we never accidentally embed the operator's
60
+ # real token into a workspace file.
61
+ DEFAULT_TOKEN = "dummy" # noqa: S105 — intentional placeholder, not a secret
62
+
63
+ # VSCode reads terminal env from a separate key per host OS. Writing all
64
+ # three means the same ``.vscode/`` folder works whether the developer
65
+ # opens it on macOS, Linux, or WSL/Windows without a second scaffolder
66
+ # run. The keys are stable across VSCode versions (documented public
67
+ # API since 1.x).
68
+ _TERMINAL_ENV_KEYS: tuple[str, ...] = (
69
+ "terminal.integrated.env.osx",
70
+ "terminal.integrated.env.linux",
71
+ "terminal.integrated.env.windows",
72
+ )
73
+
74
+ # Env vars this scaffolder owns inside the terminal-env blocks. Anything
75
+ # NOT in this set that already exists under one of the terminal-env
76
+ # keys is preserved verbatim on merge (e.g. the user's PATH tweak).
77
+ _MANAGED_ENV_KEYS: tuple[str, ...] = (
78
+ "ANTHROPIC_BASE_URL",
79
+ "ANTHROPIC_AUTH_TOKEN",
80
+ "CODEROUTER_MODE",
81
+ )
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class FileOutcome:
86
+ """The outcome of touching (or planning to touch) one file.
87
+
88
+ ``action`` values:
89
+
90
+ * ``created`` — file didn't exist, we wrote it
91
+ * ``updated`` — file existed, we changed it
92
+ * ``unchanged`` — file already had the desired content
93
+ * ``conflict`` — existing content differs and ``--force`` was
94
+ not set; the file was NOT written
95
+ * ``skipped`` — hard error prevented processing (unparseable
96
+ JSON, wrong type, permission denied); NOT written
97
+ """
98
+
99
+ path: Path
100
+ action: str
101
+ diff: str = ""
102
+ reason: str = ""
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class VSCodeInitResult:
107
+ outcomes: list[FileOutcome]
108
+
109
+ @property
110
+ def has_conflicts(self) -> bool:
111
+ return any(o.action == "conflict" for o in self.outcomes)
112
+
113
+ @property
114
+ def has_skipped(self) -> bool:
115
+ return any(o.action == "skipped" for o in self.outcomes)
116
+
117
+
118
+ def run_vscode_init(
119
+ target: Path,
120
+ *,
121
+ port: int = DEFAULT_PORT,
122
+ profile: str | None = None,
123
+ with_envrc: bool = False,
124
+ dry_run: bool = False,
125
+ force: bool = False,
126
+ ) -> VSCodeInitResult:
127
+ """Scaffold VSCode workspace settings for CodeRouter.
128
+
129
+ Parameters
130
+ ----------
131
+ target:
132
+ Workspace root — the folder that VSCode opens. ``.vscode/`` and
133
+ (if ``with_envrc``) ``.envrc`` are placed under this directory.
134
+ Must already exist as a directory; we do not create the
135
+ workspace root itself.
136
+ port:
137
+ CodeRouter port for ``ANTHROPIC_BASE_URL``. Defaults to 8088
138
+ (docs-standard); override to match a non-default ``serve``.
139
+ profile:
140
+ Optional ``CODEROUTER_MODE`` value to preload. When set, the
141
+ VSCode terminal gets ``CODEROUTER_MODE=<profile>`` too.
142
+ with_envrc:
143
+ When true, also write ``.envrc`` (direnv). Idempotent and
144
+ conflict-aware just like ``settings.json``.
145
+ dry_run:
146
+ When true, compute all the outcomes and diffs but skip the
147
+ actual ``os.replace``. Guarantees byte-parity with the write
148
+ path — see the module docstring for the invariant.
149
+ force:
150
+ When true, existing conflicting values are overwritten. Without
151
+ this flag, a conflict is reported with a diff and the file is
152
+ left untouched.
153
+
154
+ Raises
155
+ ------
156
+ FileNotFoundError:
157
+ The target directory does not exist. All other issues (bad
158
+ JSON, permission denied, existing conflicts) are captured as
159
+ ``FileOutcome`` entries with the appropriate action.
160
+ """
161
+ target = Path(target).expanduser().resolve()
162
+ if not target.is_dir():
163
+ raise FileNotFoundError(f"target directory does not exist: {target}")
164
+
165
+ base_url = f"http://localhost:{port}"
166
+ outcomes: list[FileOutcome] = []
167
+
168
+ # 1. .vscode/settings.json (always).
169
+ outcomes.append(
170
+ _apply_settings_json(
171
+ target / ".vscode" / "settings.json",
172
+ base_url=base_url,
173
+ profile=profile,
174
+ force=force,
175
+ dry_run=dry_run,
176
+ )
177
+ )
178
+
179
+ # 2. .envrc (opt-in via --with-envrc).
180
+ if with_envrc:
181
+ outcomes.append(
182
+ _apply_envrc(
183
+ target / ".envrc",
184
+ base_url=base_url,
185
+ profile=profile,
186
+ force=force,
187
+ dry_run=dry_run,
188
+ )
189
+ )
190
+
191
+ return VSCodeInitResult(outcomes=outcomes)
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # settings.json
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ def _apply_settings_json(
200
+ path: Path,
201
+ *,
202
+ base_url: str,
203
+ profile: str | None,
204
+ force: bool,
205
+ dry_run: bool,
206
+ ) -> FileOutcome:
207
+ desired_env: dict[str, str] = {
208
+ "ANTHROPIC_BASE_URL": base_url,
209
+ "ANTHROPIC_AUTH_TOKEN": DEFAULT_TOKEN,
210
+ }
211
+ if profile:
212
+ desired_env["CODEROUTER_MODE"] = profile
213
+
214
+ old_text = ""
215
+ if path.exists():
216
+ try:
217
+ old_text = path.read_text(encoding="utf-8")
218
+ except OSError as exc:
219
+ return FileOutcome(
220
+ path=path,
221
+ action="skipped",
222
+ reason=f"cannot read existing file: {exc}",
223
+ )
224
+ try:
225
+ existing = json.loads(old_text) if old_text.strip() else {}
226
+ except json.JSONDecodeError as exc:
227
+ return FileOutcome(
228
+ path=path,
229
+ action="skipped",
230
+ reason=(
231
+ "existing settings.json is not valid JSON "
232
+ f"({exc.msg} at line {exc.lineno}). Fix or move it "
233
+ "aside and re-run."
234
+ ),
235
+ )
236
+ if not isinstance(existing, dict):
237
+ return FileOutcome(
238
+ path=path,
239
+ action="skipped",
240
+ reason=(
241
+ "existing settings.json is not a JSON object "
242
+ f"(got {type(existing).__name__}). Fix or move it "
243
+ "aside and re-run."
244
+ ),
245
+ )
246
+ else:
247
+ existing = {}
248
+
249
+ merged, conflicts = _merge_terminal_env(existing, desired_env, force=force)
250
+
251
+ if conflicts and not force:
252
+ # Report the diff between existing and what --force WOULD write
253
+ # so the operator can decide whether the change is desired.
254
+ force_merged, _ = _merge_terminal_env(existing, desired_env, force=True)
255
+ diff = _json_diff(existing, force_merged, path)
256
+ return FileOutcome(
257
+ path=path,
258
+ action="conflict",
259
+ diff=diff,
260
+ reason=(
261
+ "existing settings.json has different value(s) for "
262
+ f"{', '.join(sorted(conflicts))}. Re-run with --force to overwrite."
263
+ ),
264
+ )
265
+
266
+ new_text = _dump_json(merged)
267
+ if new_text == old_text:
268
+ return FileOutcome(path=path, action="unchanged")
269
+
270
+ diff = _text_diff(old_text, new_text, path)
271
+ if not dry_run:
272
+ try:
273
+ path.parent.mkdir(parents=True, exist_ok=True)
274
+ _atomic_write(path, new_text)
275
+ except OSError as exc:
276
+ return FileOutcome(
277
+ path=path,
278
+ action="skipped",
279
+ reason=f"write failed: {exc}",
280
+ diff=diff,
281
+ )
282
+
283
+ return FileOutcome(
284
+ path=path,
285
+ action="updated" if old_text else "created",
286
+ diff=diff,
287
+ )
288
+
289
+
290
+ def _merge_terminal_env(
291
+ existing: dict,
292
+ desired_env: dict[str, str],
293
+ *,
294
+ force: bool,
295
+ ) -> tuple[dict, set[str]]:
296
+ """Merge our env vars into a settings.json dict.
297
+
298
+ Preserves unrelated top-level keys (``editor.fontSize`` etc.) AND
299
+ unrelated env keys inside the three ``terminal.integrated.env.*``
300
+ blocks. When ``force`` is false, an existing conflicting value for
301
+ one of our managed keys is left in place and its dotted path
302
+ (e.g. ``terminal.integrated.env.osx.ANTHROPIC_BASE_URL``) is added
303
+ to the returned conflict set.
304
+
305
+ Returns
306
+ -------
307
+ merged:
308
+ The merged dict, safe to serialize with :func:`_dump_json`.
309
+ conflicts:
310
+ Set of dotted paths whose existing value differs from the
311
+ desired one and were NOT overwritten (because ``force=False``).
312
+ """
313
+ # Deep copy via json round-trip — the input dict may share nested
314
+ # references with the caller and we must not mutate it.
315
+ merged = json.loads(json.dumps(existing))
316
+ conflicts: set[str] = set()
317
+
318
+ for os_key in _TERMINAL_ENV_KEYS:
319
+ sub = merged.get(os_key)
320
+ if not isinstance(sub, dict):
321
+ # Overwrite any non-dict value at this key — VSCode would
322
+ # ignore it anyway. Preserve nothing since the previous
323
+ # value was already broken.
324
+ sub = {}
325
+
326
+ for env_key, env_val in desired_env.items():
327
+ if env_key in sub and sub[env_key] != env_val and not force:
328
+ conflicts.add(f"{os_key}.{env_key}")
329
+ # Leave sub[env_key] unchanged.
330
+ continue
331
+ sub[env_key] = env_val
332
+
333
+ merged[os_key] = sub
334
+
335
+ return merged, conflicts
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # .envrc
340
+ # ---------------------------------------------------------------------------
341
+
342
+
343
+ def _apply_envrc(
344
+ path: Path,
345
+ *,
346
+ base_url: str,
347
+ profile: str | None,
348
+ force: bool,
349
+ dry_run: bool,
350
+ ) -> FileOutcome:
351
+ new_text = _render_envrc(base_url=base_url, profile=profile)
352
+ old_text = ""
353
+ if path.exists():
354
+ try:
355
+ old_text = path.read_text(encoding="utf-8")
356
+ except OSError as exc:
357
+ return FileOutcome(
358
+ path=path,
359
+ action="skipped",
360
+ reason=f"cannot read existing file: {exc}",
361
+ )
362
+
363
+ if new_text == old_text:
364
+ return FileOutcome(path=path, action="unchanged")
365
+
366
+ diff = _text_diff(old_text, new_text, path)
367
+
368
+ if old_text and not force:
369
+ return FileOutcome(
370
+ path=path,
371
+ action="conflict",
372
+ diff=diff,
373
+ reason=(
374
+ ".envrc already exists and its contents differ. "
375
+ "Re-run with --force to overwrite. "
376
+ "(Tip: keep secrets in a separate .envrc.local and "
377
+ "``source_env_if_exists`` from .envrc.)"
378
+ ),
379
+ )
380
+
381
+ if not dry_run:
382
+ try:
383
+ _atomic_write(path, new_text)
384
+ except OSError as exc:
385
+ return FileOutcome(
386
+ path=path,
387
+ action="skipped",
388
+ reason=f"write failed: {exc}",
389
+ diff=diff,
390
+ )
391
+
392
+ return FileOutcome(
393
+ path=path,
394
+ action="updated" if old_text else "created",
395
+ diff=diff,
396
+ )
397
+
398
+
399
+ def _render_envrc(*, base_url: str, profile: str | None) -> str:
400
+ """Build the .envrc contents. Kept split for test targeting."""
401
+ lines = [
402
+ "# Auto-generated by `coderouter vscode-init`. Safe to edit.",
403
+ "# direnv will export these when you `cd` into this directory.",
404
+ "# Run `direnv allow` once after generation.",
405
+ "",
406
+ f'export ANTHROPIC_BASE_URL="{base_url}"',
407
+ f'export ANTHROPIC_AUTH_TOKEN="{DEFAULT_TOKEN}"',
408
+ ]
409
+ if profile:
410
+ lines.append(f'export CODEROUTER_MODE="{profile}"')
411
+ lines.append("") # trailing newline
412
+ return "\n".join(lines)
413
+
414
+
415
+ # ---------------------------------------------------------------------------
416
+ # Rendering helpers
417
+ # ---------------------------------------------------------------------------
418
+
419
+
420
+ def format_result(result: VSCodeInitResult, *, dry_run: bool, port: int) -> str:
421
+ """Human-readable rendering of the outcomes for CLI stdout.
422
+
423
+ The output has three sections:
424
+
425
+ 1. Per-file outcome lines (with unified diffs where relevant).
426
+ 2. A cheat sheet for the extensions this scaffolder does NOT wire
427
+ (Cline / Roo / Kilo / Continue) — the user copy-pastes.
428
+ 3. A trailing hint about the fuller ``docs/guides/vscode.md``.
429
+
430
+ ``dry_run`` toggles the "(dry-run — nothing written)" reminder.
431
+ """
432
+ lines: list[str] = []
433
+ marker_for = {
434
+ "created": " new ",
435
+ "updated": " diff ",
436
+ "unchanged": " nop ",
437
+ "conflict": "CONFLICT",
438
+ "skipped": " SKIP ",
439
+ }
440
+ for o in result.outcomes:
441
+ marker = marker_for.get(o.action, f" {o.action} ")
442
+ lines.append(f"[{marker}] {o.path}")
443
+ if o.reason:
444
+ lines.append(f" → {o.reason}")
445
+ if o.diff and o.action in ("created", "updated", "conflict"):
446
+ lines.append("")
447
+ lines.extend(o.diff.rstrip("\n").splitlines())
448
+ lines.append("")
449
+
450
+ if dry_run:
451
+ lines.append("")
452
+ lines.append("(dry-run — no files were written. Re-run without --dry-run to apply.)")
453
+
454
+ lines.append("")
455
+ lines.append(_render_cheat_sheet(port))
456
+ return "\n".join(lines)
457
+
458
+
459
+ def _render_cheat_sheet(port: int) -> str:
460
+ """Emit the copy-paste snippets shown after every ``vscode-init`` run.
461
+
462
+ The Continue.dev snippet is built with :func:`json.dumps` rather
463
+ than hand-formatted string concatenation to guarantee the output
464
+ is syntactically valid JSON. An earlier hand-formatted version
465
+ accidentally leaked an extra ``}`` from mismatched f-string /
466
+ plain-string brace escaping — the JSON round-trip closes that
467
+ class of bug and the test in ``tests/test_vscode_init.py`` pins
468
+ the invariant.
469
+ """
470
+ continue_snippet = _render_continue_snippet(port)
471
+ return (
472
+ "─── Other extensions (copy/paste) ─────────────────────────────\n"
473
+ "\n"
474
+ " Cline / Roo Code / Kilo Code:\n"
475
+ " API Provider: OpenAI Compatible\n"
476
+ f" Base URL: http://localhost:{port}/v1\n"
477
+ " API Key: dummy (any non-empty value)\n"
478
+ " Model ID: anything (CodeRouter routes via default_profile)\n"
479
+ "\n"
480
+ " Continue.dev — add to the models array in ~/.continue/config.json:\n"
481
+ f"{continue_snippet}\n"
482
+ "\n"
483
+ "Full guide: docs/guides/vscode.md in the CodeRouter repo.\n"
484
+ )
485
+
486
+
487
+ def _render_continue_snippet(port: int) -> str:
488
+ """Return one indented pretty-printed JSON block for Continue.dev.
489
+
490
+ Split out from :func:`_render_cheat_sheet` so tests can
491
+ ``json.loads`` the exact bytes users copy-paste and verify the
492
+ snippet is valid.
493
+ """
494
+ model_entry: dict[str, str] = {
495
+ "title": "CodeRouter",
496
+ "provider": "openai",
497
+ "model": "any-id",
498
+ "apiBase": f"http://localhost:{port}/v1",
499
+ "apiKey": "dummy",
500
+ }
501
+ pretty = json.dumps(model_entry, indent=2, ensure_ascii=False)
502
+ # Indent every line by 4 spaces so it aligns under the "─── Other
503
+ # extensions" header exactly like the Cline block above.
504
+ return "\n".join(" " + line for line in pretty.splitlines())
505
+
506
+
507
+ def exit_code_for(result: VSCodeInitResult) -> int:
508
+ """Map the outcome set to a shell exit code.
509
+
510
+ * 0 — every file is clean or a no-op.
511
+ * 2 — at least one conflict (needs ``--force``); nothing was
512
+ written for the conflicting file(s), but other files may
513
+ have been.
514
+ * 1 — a hard error prevented processing (unparseable JSON, write
515
+ failure). Treated as more severe than a conflict.
516
+ """
517
+ if result.has_skipped:
518
+ return 1
519
+ if result.has_conflicts:
520
+ return 2
521
+ return 0
522
+
523
+
524
+ # ---------------------------------------------------------------------------
525
+ # Low-level I/O helpers
526
+ # ---------------------------------------------------------------------------
527
+
528
+
529
+ def _dump_json(obj: object) -> str:
530
+ """Deterministic pretty JSON for settings.json.
531
+
532
+ ``indent=2`` matches VSCode's own formatter default, ``ensure_ascii=False``
533
+ keeps CJK env values (e.g. a profile name) readable in the file.
534
+ A trailing newline is appended so POSIX tools don't complain.
535
+ """
536
+ return json.dumps(obj, indent=2, ensure_ascii=False) + "\n"
537
+
538
+
539
+ def _atomic_write(path: Path, text: str) -> None:
540
+ """Write ``text`` to ``path`` atomically (tmp + os.replace).
541
+
542
+ A partial write cannot corrupt the target file: the tmp file lives
543
+ on the same filesystem (same parent directory) so ``os.replace`` is
544
+ an atomic inode swap on POSIX. On Windows ``os.replace`` is also
545
+ atomic since Python 3.3.
546
+
547
+ The tmp file uses the parent + ``<name>.tmp`` pattern rather than
548
+ ``Path.with_suffix`` because ``.envrc`` has no stem/suffix in the
549
+ ``Path`` sense and ``with_suffix`` would silently misbehave.
550
+ """
551
+ tmp = path.parent / (path.name + ".tmp")
552
+ tmp.write_text(text, encoding="utf-8")
553
+ os.replace(tmp, path)
554
+
555
+
556
+ def _text_diff(old: str, new: str, path: Path) -> str:
557
+ """Unified diff between two text buffers, labelled by file name."""
558
+ return "".join(
559
+ unified_diff(
560
+ old.splitlines(keepends=True),
561
+ new.splitlines(keepends=True),
562
+ fromfile=f"a/{path.name}",
563
+ tofile=f"b/{path.name}",
564
+ n=3,
565
+ )
566
+ )
567
+
568
+
569
+ def _json_diff(old: dict, new: dict, path: Path) -> str:
570
+ """Diff between two JSON dicts, formatted as ``_dump_json`` would."""
571
+ old_text = _dump_json(old) if old else ""
572
+ new_text = _dump_json(new)
573
+ return _text_diff(old_text, new_text, path)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.9.3
3
+ Version: 2.10.0
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
@@ -40,7 +40,7 @@ Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
40
40
  Provides-Extra: doctor
41
41
  Requires-Dist: ruamel-yaml>=0.18.6; extra == 'doctor'
42
42
  Provides-Extra: repair
43
- Requires-Dist: json-repair>=0.30; extra == 'repair'
43
+ Requires-Dist: json-repair>=0.60.1; extra == 'repair'
44
44
  Description-Content-Type: text/markdown
45
45
 
46
46
  <h1 align="center">CodeRouter</h1>
@@ -132,6 +132,8 @@ ANTHROPIC_BASE_URL=http://localhost:8088 ANTHROPIC_AUTH_TOKEN=dummy claude
132
132
 
133
133
  これだけ。Claude Code はいつも通り動きますが、裏ではローカルの Ollama が答えています。
134
134
 
135
+ **VSCode の統合ターミナルから使う場合** は、環境変数の書き忘れを避けるため `coderouter vscode-init` が便利です。プロジェクトルートで 1 回叩くと `.vscode/settings.json` に `terminal.integrated.env.*` をマージ書き込みするので、以後 VSCode のターミナルで `claude` と打つだけで通ります。Cline / Roo Code / Continue.dev の設定コピペも含めて → [VSCode 連携ガイド](./docs/guides/vscode.md)
136
+
135
137
  ---
136
138
 
137
139
  ## 自分に必要?
@@ -281,6 +283,7 @@ providers:
281
283
  | 無料で回す | [無料枠ガイド](./docs/guides/free-tier-guide.md) |
282
284
  | llama.cpp / vllm を GUI で起動 | [Launcher ガイド](./docs/backends/launcher.md) |
283
285
  | 言語税を計測・回避する | [言語税ガイド](./docs/guides/language-tax.md) |
286
+ | VSCode / Cline / Continue から使う | [VSCode 連携ガイド](./docs/guides/vscode.md) |
284
287
  | 別の PC から安全に繋ぐ | [リモートアクセスガイド](./docs/guides/remote-access.md) |
285
288
  | 詰まった | [トラブルシューティング](./docs/guides/troubleshooting.md) |
286
289
  | 設計を知りたい | [アーキテクチャ詳細](./docs/concepts/architecture.md) |
@@ -1,6 +1,6 @@
1
1
  coderouter/__init__.py,sha256=ghdjPrLtnRzY8fyQ4CJZI1UJKADyNTLtA3G7se8H7Ns,696
2
2
  coderouter/__main__.py,sha256=-LCgxJnvgUV240HjQKv7ly-mn2NuKHpC4nCpvTHjeSU,130
3
- coderouter/cli.py,sha256=7zxt6riKPTpHGiDms9SVaPbe-n4ytfCrhIVtuPII-oU,30173
3
+ coderouter/cli.py,sha256=rEo-G9fyIBXnRWmVCHxWBlfzII2CWMcFvhRJW5h2oBU,34650
4
4
  coderouter/cli_stats.py,sha256=CCjzc1G4hTRHZ2gG1XhxhDpUkJnnl3NXbcbp1T18jpg,29894
5
5
  coderouter/cost.py,sha256=32h6uzb4nxh2eA5d2Hn3kD9yJbtis6CFDAbeIy5KRkM,7431
6
6
  coderouter/doctor.py,sha256=x0OqWUMGPzYaX2poWSH2UA90sEr4Z69-gVYLXYQFCFU,85483
@@ -10,12 +10,14 @@ coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
10
10
  coderouter/gguf_introspect.py,sha256=KeE00CfbYRa4gSTrzuwC6zVuHi20IzLC4nUQa98BEXI,10387
11
11
  coderouter/hardware.py,sha256=gn3_9qbVcGRR81yKMn1lJE_8-YDRau0LxIH_M-f7pxE,8356
12
12
  coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,9360
13
+ coderouter/launcher_devices.py,sha256=SoGpzknRqvnmvg_IeXy1TkeE7aLc_MOFqLzXdd3CjVs,19275
13
14
  coderouter/launcher_speculative.py,sha256=kWaHNmhzBsbWuM_lRGDlbWkgJ1suuxBWQNTKhJOC-yg,12644
14
15
  coderouter/launcher_swap.py,sha256=W7qpdB-8Npr8yHGfx0N3SgZ2vDJ6BR9JuxW0I3TaZ-4,23783
15
16
  coderouter/logging.py,sha256=5rfi4_R4aoeeUYDc642dgs61wA0aBX7wvubyB-k3Uyo,58074
16
17
  coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
17
18
  coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
18
19
  coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcxPpFiJGlzjk,4609
20
+ coderouter/vscode_init.py,sha256=fAcxJmwGXX8u0ylW0GHGACe_Co9VL6wdAKNFw7-ulwg,19952
19
21
  coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
20
22
  coderouter/adapters/anthropic_native.py,sha256=CT9Hitun-c3z83YHT2zZXgwZpY3_t6eRnOfAmNud2aw,23610
21
23
  coderouter/adapters/base.py,sha256=ykKMaiIVVGr23oFFXilK2iP_YhI8-CtsEiJyFXaC9bE,10396
@@ -25,7 +27,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
25
27
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
26
28
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
27
29
  coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
28
- coderouter/config/schemas.py,sha256=FJ0bc23tGbOYA2iQLKmyZN3A8R0vZzSfTY4PT8a5f1s,118933
30
+ coderouter/config/schemas.py,sha256=X2LeonDTTIkesbqzmmo3QYcD1dwmjGiCalMyS5X-A1o,120986
29
31
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
30
32
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
31
33
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -43,7 +45,7 @@ coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLu
43
45
  coderouter/ingress/anthropic_routes.py,sha256=1ThXzx8VgxRV7UCi3NF5ey7kbr0ADPKTXyQuYS5gK1w,27063
44
46
  coderouter/ingress/app.py,sha256=6e_qNlYZIyjLKhY20t0ZNL-x5Fv8R8oaIH_HCdxa9Ic,20921
45
47
  coderouter/ingress/dashboard_routes.py,sha256=1SqNNrVTfQiZcx5n2MvjAzWirittX0e61tCt-MKp4Ag,25617
46
- coderouter/ingress/launcher_routes.py,sha256=up9kNAjq1FHgv4_I298u7SkWLcv5T-RLhDcDYbxIp-s,85199
48
+ coderouter/ingress/launcher_routes.py,sha256=lU2EILVAi-lvHBHIxyixsMTRZlp-Rv7-2sw-eW4ccYg,119687
47
49
  coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
48
50
  coderouter/ingress/openai_routes.py,sha256=TrugsQNJfNPgKKLzfT1aXzrs4emWYGE4XebEBuaZoWk,12505
49
51
  coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
@@ -66,11 +68,11 @@ coderouter/state/request_log.py,sha256=mNxQJ9IDGpUtT5rZbCo_-CTyhxmvig7C2Gfvdn9rc
66
68
  coderouter/state/store.py,sha256=h-rsMJq8GILsOfCP94nI40cuHaj4Vqycsm9UNN77REI,7445
67
69
  coderouter/state/suggest_rules.py,sha256=FvdhEvao5NvdKp9zs8AkcoFKHY4yqqXY2HekvSjpDFA,16670
68
70
  coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8IYOtG8,1788
69
- coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
71
+ coderouter/translation/anthropic.py,sha256=uEScq-icLn6ZXFWaxsKROv7feaq3zD0ZogjbTJ3Y7gQ,12578
70
72
  coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
71
73
  coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
72
- coderouter_cli-2.9.3.dist-info/METADATA,sha256=ofXxvu6VaPHY-an5OdALTE8qZKIdJvfFK28heY_3ZOM,15994
73
- coderouter_cli-2.9.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
74
- coderouter_cli-2.9.3.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
75
- coderouter_cli-2.9.3.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
76
- coderouter_cli-2.9.3.dist-info/RECORD,,
74
+ coderouter_cli-2.10.0.dist-info/METADATA,sha256=quMG0dkYOOZpkWxidZQIxREB-vHl3-U7VktyA00bE4Y,16578
75
+ coderouter_cli-2.10.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
76
+ coderouter_cli-2.10.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
77
+ coderouter_cli-2.10.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
78
+ coderouter_cli-2.10.0.dist-info/RECORD,,