pantheon-opencode 1.5.0-beta.2 → 1.5.0-beta.4

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.
Files changed (38) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/CHANGELOG.md +835 -0
  3. package/README.md +103 -15
  4. package/README.pt-BR.md +57 -13
  5. package/package.json +8 -3
  6. package/plugin.json +1 -1
  7. package/pyproject.toml +57 -0
  8. package/scripts/code_mode_server.py +403 -42
  9. package/scripts/install/opencode.mjs +42 -0
  10. package/scripts/install/plugin.mjs +3 -33
  11. package/scripts/manifest-inventory.mjs +410 -0
  12. package/scripts/mcp_persistence_server.py +516 -163
  13. package/scripts/package-evidence.mjs +242 -0
  14. package/scripts/probe-pantheon-cost.mjs +10 -1
  15. package/scripts/release-beta-version.mjs +61 -60
  16. package/scripts/sync-tui.mjs +76 -54
  17. package/scripts/test-opencode-v1-v2-sandbox.sh +79 -161
  18. package/scripts/validation-policy.mjs +7 -7
  19. package/scripts/version-check.mjs +113 -108
  20. package/scripts/versioning.mjs +131 -88
  21. package/src/agents/zeus.md +2 -2
  22. package/src/mcp/code_mode_server.py +403 -42
  23. package/src/mcp/mcp_persistence_server.py +516 -163
  24. package/src/pantheon/background-job-board.ts +14 -0
  25. package/src/pantheon/compaction-handoff.ts +304 -0
  26. package/src/pantheon/cost-command.ts +62 -42
  27. package/src/pantheon/delegate-manager.ts +109 -12
  28. package/src/pantheon/native-probe.ts +234 -0
  29. package/src/pantheon/native-task-status.ts +139 -0
  30. package/src/pantheon/provider-capabilities.ts +221 -0
  31. package/src/pantheon/provider-stream-tracker.ts +182 -0
  32. package/src/pantheon/task-result-guard.ts +107 -0
  33. package/src/pantheon/token-opt.ts +18 -110
  34. package/src/pantheon/tool-ceiling.ts +159 -0
  35. package/src/pantheon/v2-events.ts +10 -1
  36. package/src/plugin.ts +19 -1
  37. package/src/plugins/tui/package-lock.json +2 -2
  38. package/src/plugins/tui/package.json +1 -1
@@ -16,6 +16,8 @@ Or via MCP client (stdio transport):
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import hashlib
20
+ import json
19
21
  import logging
20
22
  import os
21
23
  import shutil
@@ -38,21 +40,47 @@ SCRIPT_TIMEOUT: int = 30
38
40
  # executor for longer than 5 minutes.
39
41
  MAX_SCRIPT_TIMEOUT: int = 300
40
42
 
43
+ # ── Common status contract ────────────────────────────────────────────────────
44
+ # Mirrors the nine-code `NativeTaskStatus` contract introduced in B3-05. Every
45
+ # code-mode result is labelled with one of these statuses so callers can apply
46
+ # the same retry/skip/escalate strategy across the whole platform.
47
+ CONTRACT_STATUSES: frozenset[str] = frozenset(
48
+ {
49
+ "OK",
50
+ "UNSUPPORTED",
51
+ "UNAVAILABLE",
52
+ "INVALID_INPUT",
53
+ "INVALID_STATE",
54
+ "CONFLICT",
55
+ "CORRUPT_DATA",
56
+ "TIMEOUT",
57
+ "ESCALATE",
58
+ }
59
+ )
60
+
61
+ # Explicit installation mode: a script only runs when it is listed in this
62
+ # manifest with a matching SHA-256. No manifest → nothing executes.
63
+ MANIFEST_FILENAME: str = "manifest.json"
64
+ MANIFEST_VERSION: int = 1
65
+ _SHA256_HEX_LEN: int = 64
66
+
41
67
  # ── Env Allowlist ──────────────────────────────────────────────────────────────
42
68
  # Only these variables are passed to script subprocesses. Everything else
43
69
  # (secrets, cloud credentials, etc.) is stripped to limit blast radius.
44
- _CODE_MODE_ENV_ALLOWLIST: frozenset[str] = frozenset({
45
- "PATH",
46
- "HOME",
47
- "LANG",
48
- "PANTHEON_HOME",
49
- "PANTHEON_PROJECT",
50
- "OPENAI_API_KEY",
51
- "OPENAI_BASE_URL",
52
- "EVAL_JUDGE_MODEL",
53
- "SHELL",
54
- "USER",
55
- })
70
+ _CODE_MODE_ENV_ALLOWLIST: frozenset[str] = frozenset(
71
+ {
72
+ "PATH",
73
+ "HOME",
74
+ "LANG",
75
+ "PANTHEON_HOME",
76
+ "PANTHEON_PROJECT",
77
+ "OPENAI_API_KEY",
78
+ "OPENAI_BASE_URL",
79
+ "EVAL_JUDGE_MODEL",
80
+ "SHELL",
81
+ "USER",
82
+ }
83
+ )
56
84
 
57
85
  # Optional extra vars that callers may opt-in to pass through. Populate
58
86
  # this set at startup if needed (e.g. ``CODE_MODE_EXTRA_ENV.add("MY_VAR")``).
@@ -127,6 +155,7 @@ def _detect_prlimit() -> str | None:
127
155
  return None
128
156
  return shutil.which("prlimit")
129
157
 
158
+
130
159
  # ── Scripts Directory Resolution ─────────────────────────────────────────────
131
160
  # Priority:
132
161
  # 1. /.opencode/.pantheon/code-mode/ (project install)
@@ -136,6 +165,49 @@ def _detect_prlimit() -> str | None:
136
165
  # fallback — package.json `files` includes .pantheon/code-mode/**)
137
166
 
138
167
 
168
+ def _is_within(path: Path, root: Path) -> bool:
169
+ """Return whether a resolved *path* is contained by a resolved *root*."""
170
+ try:
171
+ return os.path.commonpath((str(root), str(path))) == str(root)
172
+ except (OSError, ValueError):
173
+ return False
174
+
175
+
176
+ def _has_usable_scripts(scripts_dir: Path) -> bool:
177
+ """Return whether *scripts_dir* contains an executable code-mode target.
178
+
179
+ A directory is only a valid resolver candidate when it contains a regular
180
+ ``.py`` or ``.sh`` file that can pass the same direct-child and path
181
+ containment rules used by script execution. This prevents an empty
182
+ project overlay (or a directory containing only metadata) from masking a
183
+ lower-priority installation.
184
+ """
185
+ try:
186
+ if scripts_dir.is_symlink():
187
+ return False
188
+ resolved_dir = scripts_dir.resolve(strict=True)
189
+ if not resolved_dir.is_dir():
190
+ return False
191
+ for script_path in scripts_dir.iterdir():
192
+ if script_path.name.startswith("."):
193
+ continue
194
+ if script_path.suffix.lower() not in ALLOWED_EXTENSIONS:
195
+ continue
196
+ try:
197
+ resolved_script = script_path.resolve(strict=True)
198
+ except (OSError, RuntimeError):
199
+ continue
200
+ if (
201
+ resolved_script.is_file()
202
+ and resolved_script.parent == resolved_dir
203
+ and _is_within(resolved_script, resolved_dir)
204
+ ):
205
+ return True
206
+ except (OSError, RuntimeError):
207
+ return False
208
+ return False
209
+
210
+
139
211
  def _packaged_scripts_dir() -> Path | None:
140
212
  """Return the code-mode dir shipped inside the installed package, if any.
141
213
 
@@ -144,22 +216,27 @@ def _packaged_scripts_dir() -> Path | None:
144
216
  Returns None when no packaged copy exists.
145
217
  """
146
218
  try:
147
- here = Path(__file__).resolve()
148
- except Exception:
219
+ here = Path(__file__).resolve(strict=True)
220
+ except (OSError, RuntimeError):
149
221
  return None
150
222
  for parent in [here.parent, *here.parents]:
151
223
  try:
152
224
  candidate = parent / ".pantheon" / "code-mode"
153
225
  except Exception:
154
226
  continue
155
- try:
156
- if candidate.is_dir():
157
- return candidate
158
- except OSError:
159
- continue
227
+ if _has_usable_scripts(candidate):
228
+ return candidate
160
229
  return None
161
230
 
162
231
 
232
+ def _resolve_scripts_dir(candidates: list[Path]) -> Path | None:
233
+ """Select the first usable candidate, then try the packaged fallback."""
234
+ for candidate in candidates:
235
+ if _has_usable_scripts(candidate):
236
+ return candidate
237
+ return _packaged_scripts_dir()
238
+
239
+
163
240
  _PANTHEON_HOME: Path = pantheon_home()
164
241
  _SCRIPTS_DIR_CANDIDATES: list[Path] = []
165
242
  _proj = pantheon_project()
@@ -168,15 +245,9 @@ if _proj is not None:
168
245
  _SCRIPTS_DIR_CANDIDATES.append(_proj / ".pantheon" / "code-mode")
169
246
  _SCRIPTS_DIR_CANDIDATES.append(_PANTHEON_HOME / ".pantheon" / "code-mode")
170
247
 
171
- SCRIPTS_DIR: Path = _PANTHEON_HOME / ".pantheon" / "code-mode" # default
172
- for _candidate in _SCRIPTS_DIR_CANDIDATES:
173
- if _candidate.is_dir():
174
- SCRIPTS_DIR = _candidate
175
- break
176
- else:
177
- _packaged = _packaged_scripts_dir()
178
- if _packaged is not None:
179
- SCRIPTS_DIR = _packaged
248
+ SCRIPTS_DIR: Path = _resolve_scripts_dir(_SCRIPTS_DIR_CANDIDATES) or (
249
+ _PANTHEON_HOME / ".pantheon" / "code-mode"
250
+ )
180
251
 
181
252
  # ── FastMCP App ───────────────────────────────────────────────────────────────
182
253
  mcp = FastMCP(
@@ -188,12 +259,19 @@ mcp = FastMCP(
188
259
  # ── Helpers ───────────────────────────────────────────────────────────────────
189
260
 
190
261
 
191
- def _validate_script_name(script_name: str) -> Path:
262
+ def _validate_script_name(script_name: str, scripts_dir: Path | None = None) -> Path:
192
263
  """Validate a script name and return its resolved path.
193
264
 
194
- Raises ValueError if the name is invalid, traverses paths, or
195
- has a disallowed extension.
265
+ Args:
266
+ script_name: Bare file name of a script in the code-mode directory.
267
+ scripts_dir: Directory to resolve against. Defaults to ``SCRIPTS_DIR``.
268
+
269
+ Raises:
270
+ ValueError: If the name is invalid, traverses paths, or has a
271
+ disallowed extension.
196
272
  """
273
+ scripts_dir = scripts_dir or SCRIPTS_DIR
274
+
197
275
  if not script_name:
198
276
  raise ValueError("Script name cannot be empty")
199
277
 
@@ -208,15 +286,232 @@ def _validate_script_name(script_name: str) -> Path:
208
286
  allowed = ", ".join(sorted(ALLOWED_EXTENSIONS))
209
287
  raise ValueError(f"Extension '{ext}' not allowed. Allowed: {allowed}")
210
288
 
211
- script_path = (SCRIPTS_DIR / name).resolve()
212
- if not str(script_path).startswith(str(SCRIPTS_DIR.resolve())):
289
+ if scripts_dir.is_symlink():
290
+ raise ValueError(f"Invalid script name: '{script_name}'")
291
+
292
+ try:
293
+ scripts_root = scripts_dir.resolve(strict=True)
294
+ script_path = (scripts_dir / name).resolve(strict=True)
295
+ except (FileNotFoundError, OSError, RuntimeError):
296
+ raise ValueError(f"Script '{script_name}' not found") from None
297
+
298
+ if (
299
+ not _is_within(script_path, scripts_root)
300
+ or script_path.parent != scripts_root
301
+ or not script_path.is_file()
302
+ ):
213
303
  raise ValueError(f"Invalid script name: '{script_name}'")
214
- if not script_path.exists():
215
- raise ValueError(f"Script '{script_name}' not found")
216
304
 
217
305
  return script_path
218
306
 
219
307
 
308
+ # ── Installation Mode: Manifest + SHA-256 ─────────────────────────────────────
309
+ # The manifest is the explicit opt-in. A script is executable only when its
310
+ # bare name is present in `manifest.json` AND the SHA-256 of the on-disk file
311
+ # matches the recorded digest. Any deviation fails closed with a contract
312
+ # status instead of executing.
313
+
314
+
315
+ class ManifestError(Exception):
316
+ """Raised when the code-mode manifest cannot authorise execution."""
317
+
318
+ def __init__(self, status: str, message: str) -> None:
319
+ super().__init__(message)
320
+ self.status = status
321
+ self.message = message
322
+
323
+
324
+ def _manifest_path(scripts_dir: Path | None = None) -> Path:
325
+ """Return the manifest path for *scripts_dir* (defaults to SCRIPTS_DIR)."""
326
+ return (scripts_dir or SCRIPTS_DIR) / MANIFEST_FILENAME
327
+
328
+
329
+ def _sha256_file(path: Path) -> str:
330
+ """Return the hex SHA-256 digest of *path* (streamed, binary-safe)."""
331
+ digest = hashlib.sha256()
332
+ with path.open("rb") as handle:
333
+ for chunk in iter(lambda: handle.read(65536), b""):
334
+ digest.update(chunk)
335
+ return digest.hexdigest()
336
+
337
+
338
+ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
339
+ """Load and validate the manifest, returning ``{script_name: sha256}``.
340
+
341
+ Raises:
342
+ ManifestError: With ``INVALID_STATE`` when the manifest is absent (no
343
+ explicit opt-in), or ``CORRUPT_DATA`` when it is unreadable or
344
+ malformed.
345
+ """
346
+ manifest_file = _manifest_path(scripts_dir)
347
+ if not manifest_file.is_file():
348
+ raise ManifestError(
349
+ "INVALID_STATE",
350
+ f"Code-mode manifest not found at {manifest_file}. No script executes "
351
+ "without explicit opt-in — run approve_code_script first.",
352
+ )
353
+
354
+ try:
355
+ raw = manifest_file.read_text(encoding="utf-8")
356
+ except OSError as exc:
357
+ raise ManifestError(
358
+ "CORRUPT_DATA", f"Code-mode manifest unreadable: {exc}"
359
+ ) from None
360
+
361
+ try:
362
+ data = json.loads(raw)
363
+ except (json.JSONDecodeError, ValueError):
364
+ raise ManifestError(
365
+ "CORRUPT_DATA",
366
+ f"Code-mode manifest is malformed JSON: {manifest_file}",
367
+ ) from None
368
+
369
+ if not isinstance(data, dict):
370
+ raise ManifestError(
371
+ "CORRUPT_DATA", "Code-mode manifest must be a JSON object."
372
+ )
373
+
374
+ scripts = data.get("scripts")
375
+ if not isinstance(scripts, dict):
376
+ raise ManifestError(
377
+ "CORRUPT_DATA",
378
+ "Code-mode manifest is malformed: missing 'scripts' object.",
379
+ )
380
+
381
+ approved: dict[str, str] = {}
382
+ for name, digest in scripts.items():
383
+ if (
384
+ not isinstance(name, str)
385
+ or not isinstance(digest, str)
386
+ or len(digest) != _SHA256_HEX_LEN
387
+ ):
388
+ raise ManifestError(
389
+ "CORRUPT_DATA",
390
+ f"Code-mode manifest entry for '{name}' is malformed "
391
+ "(expected a 64-character SHA-256 hex digest).",
392
+ )
393
+ approved[name] = digest.lower()
394
+ return approved
395
+
396
+
397
+ def _verify_approved(script_path: Path, scripts_dir: Path | None = None) -> None:
398
+ """Authorise *script_path* against the manifest.
399
+
400
+ Raises:
401
+ ManifestError: ``CONFLICT`` when the script is not listed and
402
+ ``CORRUPT_DATA`` when its hash does not match the recorded digest.
403
+ """
404
+ approved = _load_manifest(scripts_dir)
405
+ name = script_path.name
406
+ if name not in approved:
407
+ raise ManifestError(
408
+ "CONFLICT",
409
+ f"Script '{name}' is not approved in the code-mode manifest. "
410
+ "Run approve_code_script to opt in.",
411
+ )
412
+ actual = _sha256_file(script_path)
413
+ if actual != approved[name]:
414
+ raise ManifestError(
415
+ "CORRUPT_DATA",
416
+ f"Script '{name}' hash mismatch (expected {approved[name]}, got "
417
+ f"{actual}). Re-approve the script after verifying the change.",
418
+ )
419
+
420
+
421
+ def _approve_script(
422
+ script_name: str, scripts_dir: Path | None = None
423
+ ) -> tuple[str, str]:
424
+ """Add or refresh *script_name* in the manifest with its SHA-256.
425
+
426
+ Returns:
427
+ A ``(status, message)`` tuple; the status is a contract code and is
428
+ ``OK`` on success.
429
+ """
430
+ target_dir = scripts_dir or SCRIPTS_DIR
431
+ try:
432
+ script_path = _validate_script_name(script_name, target_dir)
433
+ except ValueError as exc:
434
+ return "INVALID_INPUT", str(exc)
435
+
436
+ try:
437
+ approved = _load_manifest(target_dir)
438
+ except ManifestError as exc:
439
+ if exc.status == "INVALID_STATE":
440
+ approved = {}
441
+ else:
442
+ return exc.status, exc.message
443
+
444
+ approved[script_path.name] = _sha256_file(script_path)
445
+ payload = {
446
+ "version": MANIFEST_VERSION,
447
+ "scripts": dict(sorted(approved.items())),
448
+ }
449
+ try:
450
+ _manifest_path(target_dir).write_text(
451
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
452
+ )
453
+ except OSError as exc:
454
+ return "UNAVAILABLE", f"Failed to write code-mode manifest: {exc}"
455
+
456
+ digest = approved[script_path.name]
457
+ return "OK", f"Approved '{script_path.name}' with SHA-256 {digest}"
458
+
459
+
460
+ def _generate_manifest(scripts_dir: Path | None = None) -> int:
461
+ """Write a manifest covering every valid script in *scripts_dir*.
462
+
463
+ Used by the installer to (re)seed the explicit opt-in for bundled scripts.
464
+
465
+ Returns:
466
+ The number of scripts recorded.
467
+ """
468
+ target_dir = scripts_dir or SCRIPTS_DIR
469
+ try:
470
+ resolved_dir = target_dir.resolve(strict=True)
471
+ except (OSError, RuntimeError):
472
+ return 0
473
+
474
+ approved: dict[str, str] = {}
475
+ if resolved_dir.is_dir() and not resolved_dir.is_symlink():
476
+ for candidate in sorted(resolved_dir.iterdir()):
477
+ if candidate.name.startswith("."):
478
+ continue
479
+ if candidate.suffix.lower() not in ALLOWED_EXTENSIONS:
480
+ continue
481
+ try:
482
+ resolved = candidate.resolve(strict=True)
483
+ except (OSError, RuntimeError):
484
+ continue
485
+ if (
486
+ resolved.is_file()
487
+ and resolved.parent == resolved_dir
488
+ and _is_within(resolved, resolved_dir)
489
+ ):
490
+ approved[candidate.name] = _sha256_file(resolved)
491
+
492
+ payload = {
493
+ "version": MANIFEST_VERSION,
494
+ "scripts": dict(sorted(approved.items())),
495
+ }
496
+ try:
497
+ _manifest_path(target_dir).write_text(
498
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
499
+ )
500
+ except OSError:
501
+ return 0
502
+ return len(approved)
503
+
504
+
505
+ def _contract_result(
506
+ status: str, message: str, json_output: bool
507
+ ) -> str | dict[str, Any]:
508
+ """Build a contract-labelled error result (text or structured JSON)."""
509
+ if json_output:
510
+ return {"status": status, "error": message}
511
+ return f"[{status}] {message}"
512
+
513
+
514
+
220
515
  def _format_output(
221
516
  stdout: str,
222
517
  stderr: str,
@@ -286,7 +581,9 @@ def _parse_frontmatter(script_path: Path) -> dict[str, Any]:
286
581
  break
287
582
  if not stripped.startswith("#"):
288
583
  return {} # non-comment line inside the block → not frontmatter
289
- body_lines.append(stripped[1:].lstrip() if stripped.startswith("# ") else stripped[1:])
584
+ body_lines.append(
585
+ stripped[1:].lstrip() if stripped.startswith("# ") else stripped[1:]
586
+ )
290
587
  else:
291
588
  return {} # no closing delimiter
292
589
 
@@ -338,10 +635,12 @@ def _build_result(
338
635
  metadata: dict[str, Any],
339
636
  json_output: bool,
340
637
  timeout_s: int,
638
+ status: str = "OK",
341
639
  ) -> str | dict[str, Any]:
342
640
  """Build the tool result in plain-text or structured JSON form."""
343
641
  if json_output:
344
642
  return {
643
+ "status": status,
345
644
  "stdout": stdout,
346
645
  "stderr": stderr,
347
646
  "exit_code": exit_code,
@@ -378,7 +677,9 @@ async def list_code_mode_scripts() -> str:
378
677
 
379
678
  @mcp.resource(
380
679
  "pantheon://code-mode/scripts/{script_name}",
381
- description="Content of a code-mode script by name, with parsed frontmatter metadata",
680
+ description=(
681
+ "Content of a code-mode script by name, with parsed frontmatter metadata"
682
+ ),
382
683
  )
383
684
  async def get_code_mode_script(script_name: str) -> str:
384
685
  """Return the source content of a code-mode script plus its metadata."""
@@ -425,12 +726,17 @@ async def execute_code_script(
425
726
  try:
426
727
  script_path = _validate_script_name(script_name)
427
728
  except ValueError as e:
428
- return str(e)
729
+ return _contract_result("INVALID_INPUT", str(e), json_output)
429
730
 
430
731
  metadata = _parse_frontmatter(script_path)
431
732
  denied = _validate_args(script_path, args)
432
733
  if denied is not None:
433
- return denied
734
+ return _contract_result("INVALID_INPUT", denied, json_output)
735
+
736
+ try:
737
+ _verify_approved(script_path)
738
+ except ManifestError as e:
739
+ return _contract_result(e.status, e.message, json_output)
434
740
 
435
741
  timeout_s = _script_timeout(script_path)
436
742
 
@@ -486,8 +792,15 @@ async def execute_code_script(
486
792
  await proc.wait()
487
793
  duration_ms = int((time.monotonic() - started) * 1000)
488
794
  return _build_result(
489
- "", "", -1, timed_out=True, duration_ms=duration_ms,
490
- metadata=metadata, json_output=json_output, timeout_s=timeout_s,
795
+ "",
796
+ "",
797
+ -1,
798
+ timed_out=True,
799
+ duration_ms=duration_ms,
800
+ metadata=metadata,
801
+ json_output=json_output,
802
+ timeout_s=timeout_s,
803
+ status="TIMEOUT",
491
804
  )
492
805
  except FileNotFoundError:
493
806
  return _build_result(
@@ -499,6 +812,7 @@ async def execute_code_script(
499
812
  metadata=metadata,
500
813
  json_output=json_output,
501
814
  timeout_s=timeout_s,
815
+ status="UNAVAILABLE",
502
816
  )
503
817
  except OSError as e:
504
818
  return _build_result(
@@ -510,10 +824,57 @@ async def execute_code_script(
510
824
  metadata=metadata,
511
825
  json_output=json_output,
512
826
  timeout_s=timeout_s,
827
+ status="UNAVAILABLE",
513
828
  )
514
829
 
515
830
 
831
+ @mcp.tool(
832
+ name="approve_code_script",
833
+ description="Approve a .sh/.py script for execution by adding it to the "
834
+ "code-mode manifest with its SHA-256. Execution is opt-in: only approved "
835
+ "scripts with a matching hash run. Set json_output=true for structured JSON.",
836
+ )
837
+ async def approve_code_script(
838
+ script_name: str,
839
+ json_output: bool = False,
840
+ ) -> str | dict[str, Any]:
841
+ """Approve (or re-approve) a code-mode script.
842
+
843
+ Args:
844
+ script_name: Name of the script in the code-mode directory.
845
+ json_output: When True, return ``{"status", "message"}`` instead of a
846
+ ``[STATUS] message`` string.
847
+
848
+ Returns:
849
+ A contract-labelled result. ``OK`` on success, ``INVALID_INPUT`` when
850
+ the script name is invalid, or ``CORRUPT_DATA``/``UNAVAILABLE`` when
851
+ the manifest cannot be read or written.
852
+ """
853
+ status, message = _approve_script(script_name)
854
+ if json_output:
855
+ return {"status": status, "message": message}
856
+ return f"[{status}] {message}"
857
+
858
+
516
859
  # ── Main Entrypoint ───────────────────────────────────────────────────────────
517
860
 
518
- if __name__ == "__main__":
861
+
862
+ def _cli() -> int:
863
+ """Handle CLI subcommands used by the installer; fall back to serving MCP."""
864
+ argv = sys.argv[1:]
865
+ if "--generate-manifest" in argv:
866
+ scripts_dir: Path | None = None
867
+ if "--scripts-dir" in argv:
868
+ index = argv.index("--scripts-dir")
869
+ if index + 1 < len(argv):
870
+ scripts_dir = Path(argv[index + 1])
871
+ count = _generate_manifest(scripts_dir)
872
+ print(f"code-mode manifest: {count} script(s) approved")
873
+ return 0
519
874
  mcp.run()
875
+ return 0
876
+
877
+
878
+ if __name__ == "__main__":
879
+ raise SystemExit(_cli())
880
+