netizen-cli 0.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.
- netizen_cli/__init__.py +3 -0
- netizen_cli/__main__.py +4 -0
- netizen_cli/admin/__init__.py +1 -0
- netizen_cli/admin/auth.py +928 -0
- netizen_cli/admin/errors.py +9 -0
- netizen_cli/admin/port_config.py +115 -0
- netizen_cli/admin/presentation.py +257 -0
- netizen_cli/admin/queries.py +337 -0
- netizen_cli/admin/static/admin.css +260 -0
- netizen_cli/admin/static/admin.js +2898 -0
- netizen_cli/admin/static/index.html +327 -0
- netizen_cli/admin/transport.py +935 -0
- netizen_cli/admin/web.py +2717 -0
- netizen_cli/bindings.py +3215 -0
- netizen_cli/builtin_skills.py +93 -0
- netizen_cli/cards/__init__.py +105 -0
- netizen_cli/cards/callbacks.py +565 -0
- netizen_cli/cards/controls.py +2273 -0
- netizen_cli/cards/defaults.py +213 -0
- netizen_cli/cards/model_info.py +80 -0
- netizen_cli/cards/questions.py +220 -0
- netizen_cli/cards/reply.py +2247 -0
- netizen_cli/cards/scheduled.py +836 -0
- netizen_cli/channel/__init__.py +1 -0
- netizen_cli/channel/completion_mentions.py +60 -0
- netizen_cli/channel/input_preparation.py +644 -0
- netizen_cli/channel/messages.py +57 -0
- netizen_cli/channel/ports.py +52 -0
- netizen_cli/channel/question_inputs.py +51 -0
- netizen_cli/channel/reactions.py +293 -0
- netizen_cli/channel/reply_presenter.py +1505 -0
- netizen_cli/channel/topics.py +70 -0
- netizen_cli/channel_app.py +6593 -0
- netizen_cli/cli.py +287 -0
- netizen_cli/cli_data.py +536 -0
- netizen_cli/cli_packages.py +526 -0
- netizen_cli/cli_services.py +651 -0
- netizen_cli/cli_setup.py +242 -0
- netizen_cli/cli_update.py +303 -0
- netizen_cli/cli_update_restore.py +53 -0
- netizen_cli/cli_update_worker.py +333 -0
- netizen_cli/codex_runtime.py +7125 -0
- netizen_cli/completion_mention.py +16 -0
- netizen_cli/database_migrations.py +218 -0
- netizen_cli/defaults/__init__.py +5 -0
- netizen_cli/defaults/models.py +39 -0
- netizen_cli/defaults/service.py +232 -0
- netizen_cli/defaults/store.py +260 -0
- netizen_cli/deployment/__init__.py +1 -0
- netizen_cli/deployment/restart_worker.py +134 -0
- netizen_cli/deployment/update_executor.py +258 -0
- netizen_cli/deployment/update_protocol.py +281 -0
- netizen_cli/domain.py +416 -0
- netizen_cli/error_messages.py +124 -0
- netizen_cli/experience.py +531 -0
- netizen_cli/feishu_app_onboarding.py +187 -0
- netizen_cli/feishu_app_permissions.py +123 -0
- netizen_cli/git_status.py +63 -0
- netizen_cli/image_inputs.py +579 -0
- netizen_cli/instance.py +84 -0
- netizen_cli/lark_app.py +125 -0
- netizen_cli/main.py +903 -0
- netizen_cli/management/__init__.py +83 -0
- netizen_cli/management/blocking_io.py +352 -0
- netizen_cli/management/chat_labels.py +266 -0
- netizen_cli/management/coordination.py +32 -0
- netizen_cli/management/service.py +2187 -0
- netizen_cli/management/updates.py +214 -0
- netizen_cli/markdown_images.py +78 -0
- netizen_cli/message_content.py +786 -0
- netizen_cli/message_history.py +643 -0
- netizen_cli/message_preparation.py +60 -0
- netizen_cli/message_projection.py +923 -0
- netizen_cli/migrations/__init__.py +1 -0
- netizen_cli/migrations/schema.py +103 -0
- netizen_cli/migrations/v14.py +438 -0
- netizen_cli/model_settings.py +269 -0
- netizen_cli/package_resources.py +22 -0
- netizen_cli/projects.py +327 -0
- netizen_cli/prompt_projection.py +327 -0
- netizen_cli/quoted_context.py +312 -0
- netizen_cli/resources/config.example.yaml +35 -0
- netizen_cli/resources/skills/netizen-lark/SKILL.md +64 -0
- netizen_cli/resources/skills/netizen-user-guide/SKILL.md +37 -0
- netizen_cli/resources/skills/netizen-user-guide/references/user-guide.md +842 -0
- netizen_cli/result_images.py +123 -0
- netizen_cli/runtime/__init__.py +1 -0
- netizen_cli/runtime/contracts.py +792 -0
- netizen_cli/runtime/name_writes.py +67 -0
- netizen_cli/runtime/thread_naming.py +451 -0
- netizen_cli/schedules/__init__.py +1 -0
- netizen_cli/schedules/mcp.py +535 -0
- netizen_cli/schedules/models.py +394 -0
- netizen_cli/schedules/scheduler.py +374 -0
- netizen_cli/schedules/service.py +766 -0
- netizen_cli/schedules/store.py +771 -0
- netizen_cli/sdk_gap_adapter.py +1151 -0
- netizen_cli/service_launcher.py +583 -0
- netizen_cli/session_settings.py +126 -0
- netizen_cli/settings.py +216 -0
- netizen_cli/skill_references.py +40 -0
- netizen_cli/terminal_cleanup.py +155 -0
- netizen_cli/turn_activity.py +688 -0
- netizen_cli/turn_files.py +812 -0
- netizen_cli/turn_patch_children.py +254 -0
- netizen_cli/turn_plan_observer.py +315 -0
- netizen_cli/user_questions.py +106 -0
- netizen_cli-0.10.0.dist-info/METADATA +18 -0
- netizen_cli-0.10.0.dist-info/RECORD +112 -0
- netizen_cli-0.10.0.dist-info/WHEEL +5 -0
- netizen_cli-0.10.0.dist-info/entry_points.txt +2 -0
- netizen_cli-0.10.0.dist-info/top_level.txt +1 -0
netizen_cli/cli.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Lightweight public CLI. Query/help paths do not import the Channel runtime."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Sequence
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .instance import resolve_instance_root
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parser() -> argparse.ArgumentParser:
|
|
17
|
+
result = argparse.ArgumentParser(prog="netizen", description="Manage Netizen instances and this Python installation.")
|
|
18
|
+
result.add_argument("--version", action="version", version=f"netizen {__version__}")
|
|
19
|
+
result.add_argument("--root", dest="global_root", help="instance data root (not valid for update)")
|
|
20
|
+
result.add_argument("--json", dest="global_json", action="store_true", help="machine-readable result")
|
|
21
|
+
commands = result.add_subparsers(dest="command")
|
|
22
|
+
for name, description in (
|
|
23
|
+
("setup", "Prepare instance configuration and register its service; do not start"),
|
|
24
|
+
("start", "Start the bound service, or register retained instance data in this environment"),
|
|
25
|
+
("stop", "Stop and confirm exit; retain the service binding"),
|
|
26
|
+
("restart", "Restart using the existing service's Python environment"),
|
|
27
|
+
("status", "Show the selected instance's binding and runtime state"),
|
|
28
|
+
("logs", "Read the selected instance's logs"),
|
|
29
|
+
("doctor", "Read-only instance and installation diagnostics"),
|
|
30
|
+
("remove", "Remove an instance service; preserve data unless --purge"),
|
|
31
|
+
("_serve", "Internal service-manager entry; not an interactive command"),
|
|
32
|
+
):
|
|
33
|
+
command = commands.add_parser(name, help=description)
|
|
34
|
+
command.add_argument("--root", help="instance data root")
|
|
35
|
+
command.add_argument("--json", action="store_true", help="machine-readable result")
|
|
36
|
+
if name == "remove":
|
|
37
|
+
command.add_argument("--purge", action="store_true", help="also delete the displayed owned instance data")
|
|
38
|
+
command.add_argument("-y", "--yes", action="store_true", help="skip confirmation, not safety checks")
|
|
39
|
+
if name == "setup":
|
|
40
|
+
command.add_argument("--admin-port", type=_port)
|
|
41
|
+
if name == "logs":
|
|
42
|
+
command.add_argument("--lines", type=_lines, default=100)
|
|
43
|
+
update = commands.add_parser("update", help="Update this installation and restore its previously running services")
|
|
44
|
+
update.add_argument("--json", action="store_true", help="machine-readable result")
|
|
45
|
+
update.add_argument("--via", choices=("pip", "uv-pip", "uv-tool"),
|
|
46
|
+
help="resolve an ambiguous supported maintenance method; target checks still apply")
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _port(value: str) -> int:
|
|
51
|
+
number = int(value)
|
|
52
|
+
if not 1 <= number <= 65535:
|
|
53
|
+
raise argparse.ArgumentTypeError("port must be between 1 and 65535")
|
|
54
|
+
return number
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _lines(value: str) -> int:
|
|
58
|
+
number = int(value)
|
|
59
|
+
if not 1 <= number <= 10000:
|
|
60
|
+
raise argparse.ArgumentTypeError("lines must be between 1 and 10000")
|
|
61
|
+
return number
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _emit(value: dict, *, json_output: bool) -> None:
|
|
65
|
+
if json_output:
|
|
66
|
+
print(json.dumps(value, ensure_ascii=False, default=str))
|
|
67
|
+
else:
|
|
68
|
+
for key, item in value.items():
|
|
69
|
+
print(f"{key}: {json.dumps(item, ensure_ascii=False, default=str) if isinstance(item, (dict, list)) else item}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _status_value(status: object | None, root: Path) -> dict:
|
|
73
|
+
if status is None:
|
|
74
|
+
return {"root": str(root), "bound": False, "running": False, "ready": False}
|
|
75
|
+
value = asdict(status)
|
|
76
|
+
value["bound"] = True
|
|
77
|
+
value["root"] = str(root)
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _current_binding(root: Path):
|
|
82
|
+
from .cli_services import ServiceBinding
|
|
83
|
+
|
|
84
|
+
codex_home = os.environ.get("CODEX_HOME")
|
|
85
|
+
return ServiceBinding(root=root, python=Path(os.path.abspath(sys.executable)),
|
|
86
|
+
prefix=Path(sys.prefix).resolve(),
|
|
87
|
+
codex_home=Path(codex_home).expanduser().absolute() if codex_home else None)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _confirm_removal(root: Path, status: object | None, inventory: Sequence[Path],
|
|
91
|
+
*, purge: bool, yes: bool) -> None:
|
|
92
|
+
"""Always disclose the exact scope, even when confirmation is pre-authorized."""
|
|
93
|
+
print(f"Instance: {root}", file=sys.stderr)
|
|
94
|
+
print(f"Service: {_status_value(status, root)}", file=sys.stderr)
|
|
95
|
+
print("Remove: this instance's verified service definition and autostart registration", file=sys.stderr)
|
|
96
|
+
for path in inventory:
|
|
97
|
+
print(f"Delete: {path}", file=sys.stderr)
|
|
98
|
+
print("Preserve: instance data" if not purge else
|
|
99
|
+
"Preserve: root, ownership marker, unknown files, Projects and shared Codex state", file=sys.stderr)
|
|
100
|
+
if purge:
|
|
101
|
+
print("Deleted data is not automatically recoverable. Back up anything you need first.", file=sys.stderr)
|
|
102
|
+
if yes:
|
|
103
|
+
return
|
|
104
|
+
if not sys.stdin.isatty():
|
|
105
|
+
raise RuntimeError("confirmation required; review the scope and rerun with -y, or use an interactive terminal")
|
|
106
|
+
print("Proceed? [y/N] ", end="", file=sys.stderr, flush=True)
|
|
107
|
+
if sys.stdin.readline().strip().lower() not in {"y", "yes"}:
|
|
108
|
+
raise RuntimeError("cancelled; no service or data changes were performed")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _instance_command(args: argparse.Namespace, root: Path, *, json_output: bool,
|
|
112
|
+
report: dict) -> int:
|
|
113
|
+
from .cli_services import ServiceManager
|
|
114
|
+
|
|
115
|
+
manager = ServiceManager()
|
|
116
|
+
command = args.command
|
|
117
|
+
report.update(root=str(root), phase="preflight", completed=0,
|
|
118
|
+
total={"setup": 3, "start": 2, "stop": 1, "restart": 2,
|
|
119
|
+
"remove": 3}.get(command, 1), instance="unknown")
|
|
120
|
+
if command in {"status", "doctor", "logs"}:
|
|
121
|
+
if command == "logs":
|
|
122
|
+
output = manager.logs(root, lines=args.lines)
|
|
123
|
+
if output is not None:
|
|
124
|
+
_emit({"root": str(root), "logs": output}, json_output=json_output)
|
|
125
|
+
return 0
|
|
126
|
+
status = manager.inspect(root)
|
|
127
|
+
value = _status_value(status, root)
|
|
128
|
+
if command == "doctor":
|
|
129
|
+
value["cli"] = {"python": os.path.abspath(sys.executable),
|
|
130
|
+
"prefix": sys.prefix, "version": __version__}
|
|
131
|
+
value["note"] = "Read-only service diagnostics; does not migrate or open a running instance's database."
|
|
132
|
+
_emit(value, json_output=json_output)
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
from .cli_data import (begin_instance_setup, ensure_instance_root, initialize_instance_data,
|
|
136
|
+
instance_lifetime_lock, purge_instance_data, purge_inventory,
|
|
137
|
+
root_maintenance_lock, validate_prepared_instance)
|
|
138
|
+
from .deployment.restart_worker import assert_no_pending_restart
|
|
139
|
+
|
|
140
|
+
if command == "setup":
|
|
141
|
+
from .cli_setup import prepare_configuration, require_codex_login
|
|
142
|
+
|
|
143
|
+
existing = manager.inspect(root)
|
|
144
|
+
if existing is not None:
|
|
145
|
+
_emit({"phase": "setup", "status": "already_registered",
|
|
146
|
+
"instance": _status_value(existing, root),
|
|
147
|
+
"recommendation": (
|
|
148
|
+
"Existing binding and autostart setting retained; use start for this root to run it. "
|
|
149
|
+
"To repair interrupted registration, use remove (without --purge), then setup "
|
|
150
|
+
"for the same root from the intended Python environment. To switch environments, "
|
|
151
|
+
"use remove (without --purge), then start from the new environment."
|
|
152
|
+
)},
|
|
153
|
+
json_output=json_output)
|
|
154
|
+
return 0
|
|
155
|
+
require_codex_login()
|
|
156
|
+
ensure_instance_root(root)
|
|
157
|
+
with root_maintenance_lock(root) as maintenance_descriptor:
|
|
158
|
+
assert_no_pending_restart(root, lock_descriptor=maintenance_descriptor)
|
|
159
|
+
if manager.inspect(root) is not None:
|
|
160
|
+
raise RuntimeError("instance was registered concurrently; inspect its status before retrying")
|
|
161
|
+
with instance_lifetime_lock(root) as descriptor:
|
|
162
|
+
report.update(phase="prepare_instance")
|
|
163
|
+
begin_instance_setup(root, lifetime_descriptor=descriptor)
|
|
164
|
+
report.update(phase="prepare_configuration")
|
|
165
|
+
prepare_configuration(root, admin_port=args.admin_port)
|
|
166
|
+
report.update(phase="initialize", completed=1)
|
|
167
|
+
initialize_instance_data(root, lifetime_descriptor=descriptor)
|
|
168
|
+
report.update(phase="register", completed=2)
|
|
169
|
+
manager.register(_current_binding(root), lifetime_descriptor=descriptor)
|
|
170
|
+
_emit({"phase": "setup", "status": "prepared", "root": str(root),
|
|
171
|
+
"recommendation": f"Run netizen start --root {root}"}, json_output=json_output)
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
with root_maintenance_lock(root) as maintenance_descriptor:
|
|
175
|
+
assert_no_pending_restart(root, lock_descriptor=maintenance_descriptor)
|
|
176
|
+
status = manager.inspect(root)
|
|
177
|
+
report["instance"] = _status_value(status, root)
|
|
178
|
+
if command == "start":
|
|
179
|
+
if status is None:
|
|
180
|
+
report["phase"] = "register"
|
|
181
|
+
with instance_lifetime_lock(root) as descriptor:
|
|
182
|
+
validate_prepared_instance(root)
|
|
183
|
+
binding = _current_binding(root)
|
|
184
|
+
manager.register(binding, lifetime_descriptor=descriptor)
|
|
185
|
+
# Registration and process readiness are separate facts.
|
|
186
|
+
# Do not lose the known binding if the following start fails.
|
|
187
|
+
report["registered_binding"] = asdict(binding)
|
|
188
|
+
elif status is not None:
|
|
189
|
+
report["registered_binding"] = asdict(status.binding)
|
|
190
|
+
report.update(phase="start", completed=1, instance="unknown until ready is verified")
|
|
191
|
+
result = manager.start(root)
|
|
192
|
+
elif command == "stop":
|
|
193
|
+
report.update(phase="stop", instance="unknown until exit is verified")
|
|
194
|
+
if status is None:
|
|
195
|
+
with instance_lifetime_lock(root):
|
|
196
|
+
pass
|
|
197
|
+
result = None
|
|
198
|
+
else:
|
|
199
|
+
result = manager.stop(root)
|
|
200
|
+
elif command == "restart":
|
|
201
|
+
if status is None:
|
|
202
|
+
raise RuntimeError("instance has no service binding; use start explicitly to register it")
|
|
203
|
+
report.update(phase="stop", instance="unknown until exit is verified")
|
|
204
|
+
stopped = manager.stop(root)
|
|
205
|
+
report.update(phase="start", completed=1,
|
|
206
|
+
last_confirmed=_status_value(stopped, root),
|
|
207
|
+
instance="unknown until ready is verified")
|
|
208
|
+
result = manager.start(root)
|
|
209
|
+
elif command == "remove":
|
|
210
|
+
protected_codex_home = status.binding.codex_home if status is not None else None
|
|
211
|
+
inventory = purge_inventory(root, protected_codex_home=protected_codex_home) if args.purge else ()
|
|
212
|
+
_confirm_removal(root, status, inventory, purge=args.purge, yes=args.yes)
|
|
213
|
+
if status is not None:
|
|
214
|
+
report.update(phase="stop", instance="unknown until exit is verified")
|
|
215
|
+
stopped = manager.stop(root)
|
|
216
|
+
report.update(phase="unregister", completed=1,
|
|
217
|
+
last_confirmed=_status_value(stopped, root),
|
|
218
|
+
instance="unknown until unregistration is verified")
|
|
219
|
+
manager.remove(root)
|
|
220
|
+
report.update(phase="purge" if args.purge else "verify_removed", completed=2,
|
|
221
|
+
instance="service binding removed; verifying no running owner")
|
|
222
|
+
with instance_lifetime_lock(root) as descriptor:
|
|
223
|
+
if manager.inspect(root, lifetime_descriptor=descriptor) is not None:
|
|
224
|
+
raise RuntimeError("service binding still exists; no data was deleted")
|
|
225
|
+
if args.purge:
|
|
226
|
+
inventory = purge_instance_data(root, expected_inventory=inventory,
|
|
227
|
+
lifetime_descriptor=descriptor,
|
|
228
|
+
protected_codex_home=protected_codex_home)
|
|
229
|
+
_emit({"phase": "remove", "status": "removed", "root": str(root),
|
|
230
|
+
"deleted": [str(path) for path in inventory],
|
|
231
|
+
"data": "purged owned files" if args.purge else "preserved",
|
|
232
|
+
"recommendation": "Use setup for a new instance." if args.purge else
|
|
233
|
+
"Use start from the intended Python environment to register this retained instance."},
|
|
234
|
+
json_output=json_output)
|
|
235
|
+
return 0
|
|
236
|
+
else:
|
|
237
|
+
raise RuntimeError(f"unknown instance operation: {command}")
|
|
238
|
+
if command in {"start", "restart"}:
|
|
239
|
+
from .deployment.restart_worker import finish_manual_restart
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
operation = finish_manual_restart(root, lock_descriptor=maintenance_descriptor, status=result)
|
|
243
|
+
if operation is not None and operation.get("phase") not in {"recovered", "succeeded", "failed"}:
|
|
244
|
+
report["maintenance_warning"] = "Service is ready, but a prior Admin operation still needs reconciliation."
|
|
245
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
246
|
+
report["maintenance_warning"] = f"Service is ready; Admin operation reconciliation failed: {error}"
|
|
247
|
+
_emit({**report, "phase": command, "status": "succeeded", "completed": report["total"],
|
|
248
|
+
"instance": _status_value(result, root)},
|
|
249
|
+
json_output=json_output)
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
254
|
+
argument_parser = parser()
|
|
255
|
+
args = argument_parser.parse_args(argv)
|
|
256
|
+
if args.command is None:
|
|
257
|
+
argument_parser.print_help()
|
|
258
|
+
return 0
|
|
259
|
+
json_output = args.global_json or args.json
|
|
260
|
+
if args.command == "update" and args.global_root is not None:
|
|
261
|
+
argument_parser.error("update is environment-wide and does not accept --root")
|
|
262
|
+
report = {"command": args.command, "phase": "preflight", "completed": 0,
|
|
263
|
+
"status": "pending"}
|
|
264
|
+
try:
|
|
265
|
+
if args.command == "update":
|
|
266
|
+
from .cli_update import run_update
|
|
267
|
+
|
|
268
|
+
return run_update(json_output=json_output, backend=args.via)
|
|
269
|
+
selected_root = args.root if args.root is not None else args.global_root
|
|
270
|
+
root = resolve_instance_root(selected_root)
|
|
271
|
+
if args.command == "_serve":
|
|
272
|
+
from .service_launcher import launch
|
|
273
|
+
|
|
274
|
+
launch(root)
|
|
275
|
+
return 0
|
|
276
|
+
return _instance_command(args, root, json_output=json_output, report=report)
|
|
277
|
+
except KeyboardInterrupt:
|
|
278
|
+
_emit({**report, "status": "interrupted",
|
|
279
|
+
"reason": "operation interrupted; completed steps are not automatically rolled back",
|
|
280
|
+
"recommendation": "Inspect service and data state before retrying."}, json_output=json_output)
|
|
281
|
+
return 130
|
|
282
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
283
|
+
_emit({**report, "status": "failed", "reason": str(error),
|
|
284
|
+
"deleted": [str(path) for path in getattr(error, "deleted", ())],
|
|
285
|
+
"recommendation": "Inspect status and logs; fix the reported problem before retrying. "
|
|
286
|
+
"No automatic rollback is performed."}, json_output=json_output)
|
|
287
|
+
return 1
|