logion-cli 0.1.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.
- cli/__init__.py +2 -0
- cli/_config.py +51 -0
- cli/_confirm.py +16 -0
- cli/_context.py +17 -0
- cli/_course_bundle.py +46 -0
- cli/_course_capabilities.py +580 -0
- cli/_credentials.py +104 -0
- cli/_errors.py +82 -0
- cli/_first_run.py +90 -0
- cli/_harness/__init__.py +68 -0
- cli/_harness/base.py +106 -0
- cli/_harness/claude_code.py +168 -0
- cli/_harness/codex.py +79 -0
- cli/_harness/custom.py +55 -0
- cli/_harness/hermes.py +93 -0
- cli/_harness/opencode.py +255 -0
- cli/_local_state.py +1053 -0
- cli/_options.py +36 -0
- cli/_output.py +47 -0
- cli/_parser.py +73 -0
- cli/_recall_calibration.py +90 -0
- cli/_recall_ranker.py +74 -0
- cli/_taxonomy.py +120 -0
- cli/_update_policy.py +152 -0
- cli/_utils.py +16 -0
- cli/_version.py +26 -0
- cli/commands/__init__.py +2 -0
- cli/commands/admin.py +535 -0
- cli/commands/bounties.py +490 -0
- cli/commands/course_reviews/__init__.py +6 -0
- cli/commands/course_reviews/_download_handler.py +104 -0
- cli/commands/course_reviews/_render.py +129 -0
- cli/commands/course_reviews/handlers.py +197 -0
- cli/commands/course_reviews/parser.py +93 -0
- cli/commands/courses/__init__.py +6 -0
- cli/commands/courses/_capability_render.py +183 -0
- cli/commands/courses/_cmd_help.py +18 -0
- cli/commands/courses/_purchase.py +76 -0
- cli/commands/courses/_review_helpers.py +93 -0
- cli/commands/courses/_taxonomy_data.py +173 -0
- cli/commands/courses/_upload_bundle_validation.py +28 -0
- cli/commands/courses/_uploads_push.py +243 -0
- cli/commands/courses/capabilities.py +250 -0
- cli/commands/courses/capability_frontmatter.py +150 -0
- cli/commands/courses/handlers.py +50 -0
- cli/commands/courses/mutations.py +217 -0
- cli/commands/courses/parser.py +66 -0
- cli/commands/courses/parser_capabilities.py +95 -0
- cli/commands/courses/parser_sections.py +239 -0
- cli/commands/courses/parser_uploads.py +84 -0
- cli/commands/courses/parser_utils.py +65 -0
- cli/commands/courses/publication.py +60 -0
- cli/commands/courses/report_usage.py +131 -0
- cli/commands/courses/reviews.py +237 -0
- cli/commands/courses/taxonomy_handler.py +61 -0
- cli/commands/courses/taxonomy_suggest.py +197 -0
- cli/commands/courses/uploads.py +142 -0
- cli/commands/courses/versions.py +65 -0
- cli/commands/credits/__init__.py +6 -0
- cli/commands/credits/_helpers.py +153 -0
- cli/commands/credits/handlers.py +218 -0
- cli/commands/credits/parser.py +115 -0
- cli/commands/docs/__init__.py +6 -0
- cli/commands/docs/handlers.py +137 -0
- cli/commands/docs/parser.py +27 -0
- cli/commands/health/__init__.py +6 -0
- cli/commands/health/handlers.py +26 -0
- cli/commands/health/parser.py +20 -0
- cli/commands/identity/__init__.py +6 -0
- cli/commands/identity/_autopost.py +97 -0
- cli/commands/identity/_closing_copy.py +89 -0
- cli/commands/identity/_companion.py +232 -0
- cli/commands/identity/_companion_source.py +135 -0
- cli/commands/identity/_harness_select.py +85 -0
- cli/commands/identity/_onboarding_helpers.py +168 -0
- cli/commands/identity/handlers.py +173 -0
- cli/commands/identity/onboarding.py +246 -0
- cli/commands/identity/parser.py +72 -0
- cli/commands/listings/__init__.py +6 -0
- cli/commands/listings/handlers.py +135 -0
- cli/commands/listings/parser.py +57 -0
- cli/commands/notifications/__init__.py +6 -0
- cli/commands/notifications/handlers.py +120 -0
- cli/commands/notifications/parser.py +49 -0
- cli/commands/payments/__init__.py +6 -0
- cli/commands/payments/_orders_helpers.py +114 -0
- cli/commands/payments/handlers.py +138 -0
- cli/commands/payments/parser.py +97 -0
- cli/commands/recall/__init__.py +7 -0
- cli/commands/recall/handlers.py +87 -0
- cli/commands/recall/parser.py +70 -0
- cli/commands/referrals/__init__.py +6 -0
- cli/commands/referrals/_helpers.py +63 -0
- cli/commands/referrals/handlers.py +100 -0
- cli/commands/referrals/parser.py +65 -0
- cli/commands/reports/__init__.py +6 -0
- cli/commands/reports/handlers.py +57 -0
- cli/commands/reports/parser.py +52 -0
- cli/commands/skills/__init__.py +7 -0
- cli/commands/skills/_agent_symlink.py +161 -0
- cli/commands/skills/_finalize.py +112 -0
- cli/commands/skills/_inspect_handler.py +218 -0
- cli/commands/skills/_install_helpers.py +186 -0
- cli/commands/skills/_query_handlers.py +83 -0
- cli/commands/skills/_search_handler.py +136 -0
- cli/commands/skills/_update_handler.py +110 -0
- cli/commands/skills/_verify_handler.py +109 -0
- cli/commands/skills/handlers.py +202 -0
- cli/commands/skills/parser.py +154 -0
- cli/commands/workspace.py +406 -0
- cli/docs/README.md +5 -0
- cli/docs/__init__.py +1 -0
- cli/docs/bounties-and-referrals.md +18 -0
- cli/docs/concepts.md +47 -0
- cli/docs/creating-courses.md +25 -0
- cli/docs/credits-and-purchases.md +30 -0
- cli/docs/credits-terms.md +23 -0
- cli/docs/getting-started.md +95 -0
- cli/docs/marketplace-loop.md +108 -0
- cli/docs/privacy.md +30 -0
- cli/docs/referral-terms.md +24 -0
- cli/docs/reviews.md +47 -0
- cli/docs/safety.md +28 -0
- cli/docs/terms.md +54 -0
- cli/main.py +84 -0
- cli/templates/__init__.py +2 -0
- cli/templates/course_capabilities.template.yaml +189 -0
- cli/templates/course_license_apache-2.0.template.txt +30 -0
- cli/templates/course_license_logion-standard-course-v1.template.txt +49 -0
- cli/templates/course_license_mit.template.txt +21 -0
- logion_cli-0.1.0.dist-info/METADATA +49 -0
- logion_cli-0.1.0.dist-info/RECORD +135 -0
- logion_cli-0.1.0.dist-info/WHEEL +4 -0
- logion_cli-0.1.0.dist-info/entry_points.txt +4 -0
- logion_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
cli/_harness/hermes.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""Hermes Agent harness adapter.
|
|
3
|
+
|
|
4
|
+
Hermes stores its configuration in ``~/.hermes/config.yaml`` (YAML, not
|
|
5
|
+
JSON) and loads skills from ``~/.hermes/skills/``. Permission gating is
|
|
6
|
+
controlled by ``approvals.mode`` (``manual`` / ``smart`` / ``off``), and
|
|
7
|
+
Hermes *does* keep a per-command allow list — ``command_allowlist`` in
|
|
8
|
+
config.yaml — that bypasses approval in future sessions:
|
|
9
|
+
|
|
10
|
+
command_allowlist:
|
|
11
|
+
- rm
|
|
12
|
+
- systemctl
|
|
13
|
+
|
|
14
|
+
Crucially, those entries are keyed on the **command name** (approving
|
|
15
|
+
``rm -rf`` "always" stores ``rm``), not a full command line. Logion's
|
|
16
|
+
autopost grant is deliberately sub-command-scoped — *exactly*
|
|
17
|
+
``logion courses report-usage``, nothing broader (see ``base.py``).
|
|
18
|
+
That scope is not expressible in ``command_allowlist``: the only entry
|
|
19
|
+
we could add is ``logion``, which would pre-approve **every** ``logion``
|
|
20
|
+
command. Over-granting like that defeats the point of the grant.
|
|
21
|
+
|
|
22
|
+
Therefore the autopost grant is a **no-op** for Hermes: ``grant`` and
|
|
23
|
+
``revoke`` report ``already=True`` without writing anything, and
|
|
24
|
+
``is_granted`` always returns ``False`` — not because Hermes lacks an
|
|
25
|
+
allow list, but because its allow list cannot express a least-privilege,
|
|
26
|
+
sub-command-scoped grant. The companion skill directory is still
|
|
27
|
+
correct so the symlink step works. (``--yolo`` / ``HERMES_YOLO_MODE``
|
|
28
|
+
exist but bypass *all* approvals globally — never something Logion sets.)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from cli._harness.base import GrantResult, HarnessAdapter
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HermesAdapter(HarnessAdapter):
|
|
39
|
+
"""Hermes agent harness.
|
|
40
|
+
|
|
41
|
+
Companion install is supported (symlink into ``~/.hermes/skills``);
|
|
42
|
+
autopost grant is a no-op because Hermes's ``command_allowlist`` is
|
|
43
|
+
keyed on command name and cannot express a sub-command-scoped grant
|
|
44
|
+
without over-granting all ``logion`` commands (see module docstring).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name = "hermes"
|
|
48
|
+
display_name = "Hermes"
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
project_dir: Path | None = None, # noqa: ARG002
|
|
54
|
+
home_dir: Path | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._home_dir = home_dir
|
|
57
|
+
|
|
58
|
+
def _home(self) -> Path:
|
|
59
|
+
return self._home_dir if self._home_dir is not None else Path.home()
|
|
60
|
+
|
|
61
|
+
def skill_dir(self) -> Path:
|
|
62
|
+
return self._home() / ".hermes" / "skills"
|
|
63
|
+
|
|
64
|
+
def is_present(self) -> bool:
|
|
65
|
+
import shutil
|
|
66
|
+
|
|
67
|
+
return (self._home() / ".hermes").is_dir() or (
|
|
68
|
+
shutil.which("hermes") is not None
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def config_path(self, scope: str) -> Path: # noqa: ARG002
|
|
72
|
+
return self._home() / ".hermes" / "config.yaml"
|
|
73
|
+
|
|
74
|
+
def is_granted(self, scope: str) -> bool: # noqa: ARG002
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def grant(self, scope: str) -> GrantResult:
|
|
78
|
+
return GrantResult(
|
|
79
|
+
self.name,
|
|
80
|
+
scope,
|
|
81
|
+
self.config_path(scope),
|
|
82
|
+
changed=False,
|
|
83
|
+
already=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def revoke(self, scope: str) -> GrantResult:
|
|
87
|
+
return GrantResult(
|
|
88
|
+
self.name,
|
|
89
|
+
scope,
|
|
90
|
+
self.config_path(scope),
|
|
91
|
+
changed=False,
|
|
92
|
+
already=True,
|
|
93
|
+
)
|
cli/_harness/opencode.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""OpenCode harness adapter.
|
|
3
|
+
|
|
4
|
+
OpenCode stores its configuration in ``opencode.json`` (JSON with
|
|
5
|
+
comments — JSONC) and loads skills from ``~/.config/opencode/skills/``.
|
|
6
|
+
Permission gating uses a ``permission`` object where each tool name
|
|
7
|
+
maps to either a string action (``"allow"``, ``"ask"``, ``"deny"``) or
|
|
8
|
+
a granular object of pattern → action pairs.
|
|
9
|
+
|
|
10
|
+
For the autopost grant, this adapter writes a bash permission rule:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
"permission": {
|
|
14
|
+
"bash": {
|
|
15
|
+
"logion courses report-usage*": "allow"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Config file locations (precedence: project > global):
|
|
21
|
+
- ``project`` → ``<cwd>/opencode.json``
|
|
22
|
+
- ``global`` → ``~/.config/opencode/opencode.json``
|
|
23
|
+
|
|
24
|
+
(OpenCode also reads a project ``.opencode/opencode.json``, but this
|
|
25
|
+
adapter writes the top-level ``<cwd>/opencode.json`` only; document the
|
|
26
|
+
path actually used to avoid drift with :meth:`config_path`.)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import shutil
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from cli._harness.base import (
|
|
37
|
+
AUTOPOST_COMMAND,
|
|
38
|
+
GrantResult,
|
|
39
|
+
HarnessAdapter,
|
|
40
|
+
HarnessConfigError,
|
|
41
|
+
)
|
|
42
|
+
from cli._local_state import _atomic_write_text
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _autopost_pattern() -> str:
|
|
46
|
+
"""Render :data:`AUTOPOST_COMMAND` as an OpenCode bash pattern.
|
|
47
|
+
|
|
48
|
+
``("logion", "courses", "report-usage")`` →
|
|
49
|
+
``logion courses report-usage*``.
|
|
50
|
+
"""
|
|
51
|
+
return " ".join(AUTOPOST_COMMAND) + "*"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OpenCodeAdapter(HarnessAdapter):
|
|
55
|
+
"""Grants the autopost command in OpenCode's ``opencode.json``.
|
|
56
|
+
|
|
57
|
+
OpenCode uses a JSON config (JSONC) with a ``permission`` object
|
|
58
|
+
where bash commands are matched by pattern. This adapter inserts
|
|
59
|
+
exactly the autopost grant as a bash permission rule.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
name = "opencode"
|
|
63
|
+
display_name = "OpenCode"
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
project_dir: Path | None = None,
|
|
69
|
+
home_dir: Path | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
self._project_dir = project_dir
|
|
72
|
+
self._home_dir = home_dir
|
|
73
|
+
|
|
74
|
+
def _project(self) -> Path:
|
|
75
|
+
return (
|
|
76
|
+
self._project_dir if self._project_dir is not None else Path.cwd()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _home(self) -> Path:
|
|
80
|
+
return self._home_dir if self._home_dir is not None else Path.home()
|
|
81
|
+
|
|
82
|
+
def config_path(self, scope: str) -> Path:
|
|
83
|
+
from cli._harness.base import VALID_SCOPES
|
|
84
|
+
|
|
85
|
+
if scope not in VALID_SCOPES:
|
|
86
|
+
raise ValueError(f"unknown scope: {scope!r}")
|
|
87
|
+
# Global config lives under ~/.config/opencode/ (matching
|
|
88
|
+
# skill_dir()/is_present()); the project config is opencode.json
|
|
89
|
+
# at the project root. A bare ~/opencode.json (the old global
|
|
90
|
+
# path) is not where OpenCode reads its settings, so the grant
|
|
91
|
+
# would have been written to a file OpenCode never loads.
|
|
92
|
+
if scope == "global":
|
|
93
|
+
return self._home() / ".config" / "opencode" / "opencode.json"
|
|
94
|
+
return self._project() / "opencode.json"
|
|
95
|
+
|
|
96
|
+
def skill_dir(self) -> Path:
|
|
97
|
+
return self._home() / ".config" / "opencode" / "skills"
|
|
98
|
+
|
|
99
|
+
def is_present(self) -> bool:
|
|
100
|
+
if (self._home() / ".config" / "opencode").is_dir():
|
|
101
|
+
return True
|
|
102
|
+
return shutil.which("opencode") is not None
|
|
103
|
+
|
|
104
|
+
# -- config read/write -------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def _read_config(self, path: Path) -> dict[str, Any]:
|
|
107
|
+
if not path.is_file():
|
|
108
|
+
return {}
|
|
109
|
+
try:
|
|
110
|
+
raw = path.read_text(encoding="utf-8")
|
|
111
|
+
except OSError as exc:
|
|
112
|
+
raise HarnessConfigError(
|
|
113
|
+
f"cannot read {path} — refusing to overwrite: {exc}"
|
|
114
|
+
) from exc
|
|
115
|
+
# Strip JSONC comments before parsing.
|
|
116
|
+
cleaned = _strip_jsonc_comments(raw)
|
|
117
|
+
try:
|
|
118
|
+
data = json.loads(cleaned)
|
|
119
|
+
except json.JSONDecodeError as exc:
|
|
120
|
+
raise HarnessConfigError(
|
|
121
|
+
f"cannot parse {path} — refusing to overwrite: {exc}"
|
|
122
|
+
) from exc
|
|
123
|
+
if not isinstance(data, dict):
|
|
124
|
+
raise HarnessConfigError(
|
|
125
|
+
f"{path} is not a JSON object — refusing to overwrite"
|
|
126
|
+
)
|
|
127
|
+
return data
|
|
128
|
+
|
|
129
|
+
def _write_config(self, path: Path, data: dict[str, Any]) -> None:
|
|
130
|
+
_atomic_write_text(
|
|
131
|
+
path,
|
|
132
|
+
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _bash_perms(
|
|
136
|
+
self, config: dict[str, Any], path: Path
|
|
137
|
+
) -> dict[str, str]:
|
|
138
|
+
perms = config.setdefault("permission", {})
|
|
139
|
+
if not isinstance(perms, dict):
|
|
140
|
+
raise HarnessConfigError(
|
|
141
|
+
f"{path}: 'permission' is not an object — refusing to edit"
|
|
142
|
+
)
|
|
143
|
+
bash = perms.setdefault("bash", {})
|
|
144
|
+
if isinstance(bash, str):
|
|
145
|
+
# ``"bash": "allow"`` → upgrade to granular object so we
|
|
146
|
+
# can add our specific pattern without losing the global.
|
|
147
|
+
bash = {"*": bash}
|
|
148
|
+
perms["bash"] = bash
|
|
149
|
+
if not isinstance(bash, dict):
|
|
150
|
+
raise HarnessConfigError(
|
|
151
|
+
f"{path}: 'permission.bash' is not an object — "
|
|
152
|
+
"refusing to edit"
|
|
153
|
+
)
|
|
154
|
+
return bash
|
|
155
|
+
|
|
156
|
+
# -- grant / revoke / query -------------------------------------------
|
|
157
|
+
|
|
158
|
+
def is_granted(self, scope: str) -> bool:
|
|
159
|
+
path = self.config_path(scope)
|
|
160
|
+
config = self._read_config(path)
|
|
161
|
+
perms = config.get("permission")
|
|
162
|
+
if not isinstance(perms, dict):
|
|
163
|
+
return False
|
|
164
|
+
bash = perms.get("bash")
|
|
165
|
+
if not isinstance(bash, dict):
|
|
166
|
+
return False
|
|
167
|
+
return bash.get(_autopost_pattern()) == "allow"
|
|
168
|
+
|
|
169
|
+
def grant(self, scope: str) -> GrantResult:
|
|
170
|
+
path = self.config_path(scope)
|
|
171
|
+
config = self._read_config(path)
|
|
172
|
+
bash = self._bash_perms(config, path)
|
|
173
|
+
pattern = _autopost_pattern()
|
|
174
|
+
if bash.get(pattern) == "allow":
|
|
175
|
+
return GrantResult(
|
|
176
|
+
self.name, scope, path, changed=False, already=True
|
|
177
|
+
)
|
|
178
|
+
bash[pattern] = "allow"
|
|
179
|
+
self._write_config(path, config)
|
|
180
|
+
return GrantResult(self.name, scope, path, changed=True, already=False)
|
|
181
|
+
|
|
182
|
+
def revoke(self, scope: str) -> GrantResult:
|
|
183
|
+
path = self.config_path(scope)
|
|
184
|
+
if not path.is_file():
|
|
185
|
+
return GrantResult(
|
|
186
|
+
self.name, scope, path, changed=False, already=True
|
|
187
|
+
)
|
|
188
|
+
config = self._read_config(path)
|
|
189
|
+
bash = self._bash_perms(config, path)
|
|
190
|
+
pattern = _autopost_pattern()
|
|
191
|
+
if bash.get(pattern) != "allow":
|
|
192
|
+
return GrantResult(
|
|
193
|
+
self.name, scope, path, changed=False, already=True
|
|
194
|
+
)
|
|
195
|
+
del bash[pattern]
|
|
196
|
+
self._write_config(path, config)
|
|
197
|
+
return GrantResult(self.name, scope, path, changed=True, already=False)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _strip_jsonc_comments(text: str) -> str:
|
|
201
|
+
"""Remove ``//`` line comments and ``/* */`` block comments.
|
|
202
|
+
|
|
203
|
+
Tracks string literals (single and double quotes) so ``//`` inside
|
|
204
|
+
strings (e.g. URLs like ``https://...``) is preserved.
|
|
205
|
+
"""
|
|
206
|
+
result: list[str] = []
|
|
207
|
+
i = 0
|
|
208
|
+
n = len(text)
|
|
209
|
+
in_string = False
|
|
210
|
+
string_char = ""
|
|
211
|
+
|
|
212
|
+
while i < n:
|
|
213
|
+
ch = text[i]
|
|
214
|
+
|
|
215
|
+
if in_string:
|
|
216
|
+
result.append(ch)
|
|
217
|
+
if ch == "\\" and i + 1 < n:
|
|
218
|
+
# Escape — keep the next char literal.
|
|
219
|
+
result.append(text[i + 1])
|
|
220
|
+
i += 2
|
|
221
|
+
continue
|
|
222
|
+
if ch == string_char:
|
|
223
|
+
in_string = False
|
|
224
|
+
i += 1
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Not in a string.
|
|
228
|
+
if ch in ('"', "'"):
|
|
229
|
+
in_string = True
|
|
230
|
+
string_char = ch
|
|
231
|
+
result.append(ch)
|
|
232
|
+
i += 1
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if ch == "/" and i + 1 < n:
|
|
236
|
+
nxt = text[i + 1]
|
|
237
|
+
if nxt == "/":
|
|
238
|
+
# Line comment — skip to end of line.
|
|
239
|
+
while i < n and text[i] != "\n":
|
|
240
|
+
i += 1
|
|
241
|
+
continue
|
|
242
|
+
if nxt == "*":
|
|
243
|
+
# Block comment — skip to */.
|
|
244
|
+
i += 2
|
|
245
|
+
while i + 1 < n and not (
|
|
246
|
+
text[i] == "*" and text[i + 1] == "/"
|
|
247
|
+
):
|
|
248
|
+
i += 1
|
|
249
|
+
i += 2 # skip the */
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
result.append(ch)
|
|
253
|
+
i += 1
|
|
254
|
+
|
|
255
|
+
return "".join(result)
|