reviso-cli 0.1.0__tar.gz

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 (34) hide show
  1. reviso_cli-0.1.0/LICENSE +25 -0
  2. reviso_cli-0.1.0/PKG-INFO +146 -0
  3. reviso_cli-0.1.0/README.md +100 -0
  4. reviso_cli-0.1.0/pyproject.toml +39 -0
  5. reviso_cli-0.1.0/setup.cfg +4 -0
  6. reviso_cli-0.1.0/src/reviso_cli/__init__.py +8 -0
  7. reviso_cli-0.1.0/src/reviso_cli/__main__.py +6 -0
  8. reviso_cli-0.1.0/src/reviso_cli/access.py +49 -0
  9. reviso_cli-0.1.0/src/reviso_cli/cli/__init__.py +276 -0
  10. reviso_cli-0.1.0/src/reviso_cli/cli/doctor.py +88 -0
  11. reviso_cli-0.1.0/src/reviso_cli/cli/guest_comments.py +61 -0
  12. reviso_cli-0.1.0/src/reviso_cli/cli/invites.py +72 -0
  13. reviso_cli-0.1.0/src/reviso_cli/cli/versions.py +86 -0
  14. reviso_cli-0.1.0/src/reviso_cli/client/__init__.py +231 -0
  15. reviso_cli-0.1.0/src/reviso_cli/client/collab.py +39 -0
  16. reviso_cli-0.1.0/src/reviso_cli/client/errors.py +28 -0
  17. reviso_cli-0.1.0/src/reviso_cli/client/http.py +59 -0
  18. reviso_cli-0.1.0/src/reviso_cli/client/participation.py +120 -0
  19. reviso_cli-0.1.0/src/reviso_cli/command_journal.py +142 -0
  20. reviso_cli-0.1.0/src/reviso_cli/document_route_keys.py +35 -0
  21. reviso_cli-0.1.0/src/reviso_cli/document_search.py +82 -0
  22. reviso_cli-0.1.0/src/reviso_cli/errors.py +6 -0
  23. reviso_cli-0.1.0/src/reviso_cli/ids.py +17 -0
  24. reviso_cli-0.1.0/src/reviso_cli/local.py +53 -0
  25. reviso_cli-0.1.0/src/reviso_cli/origin.py +35 -0
  26. reviso_cli-0.1.0/src/reviso_cli/paths.py +67 -0
  27. reviso_cli-0.1.0/src/reviso_cli/scope.py +65 -0
  28. reviso_cli-0.1.0/src/reviso_cli.egg-info/PKG-INFO +146 -0
  29. reviso_cli-0.1.0/src/reviso_cli.egg-info/SOURCES.txt +32 -0
  30. reviso_cli-0.1.0/src/reviso_cli.egg-info/dependency_links.txt +1 -0
  31. reviso_cli-0.1.0/src/reviso_cli.egg-info/entry_points.txt +2 -0
  32. reviso_cli-0.1.0/src/reviso_cli.egg-info/requires.txt +3 -0
  33. reviso_cli-0.1.0/src/reviso_cli.egg-info/top_level.txt +1 -0
  34. reviso_cli-0.1.0/tests/test_client.py +98 -0
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Reviso
4
+
5
+ This license applies to the contents of this repository: the Reviso command line
6
+ client. It does not apply to the Reviso product, its service, or its server
7
+ software, none of which are open source and none of which are licensed here.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: reviso-cli
3
+ Version: 0.1.0
4
+ Summary: Command line client for Reviso: publish drafts for human review, read reviewer comments back, and publish revisions safely.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 Reviso
8
+
9
+ This license applies to the contents of this repository: the Reviso command line
10
+ client. It does not apply to the Reviso product, its service, or its server
11
+ software, none of which are open source and none of which are licensed here.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+
31
+ Keywords: reviso,review,collaboration,documents,cli,agent
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Topic :: Text Processing :: Markup
40
+ Requires-Python: >=3.11
41
+ Description-Content-Type: text/markdown
42
+ License-File: LICENSE
43
+ Provides-Extra: test
44
+ Requires-Dist: pytest>=8.0; extra == "test"
45
+ Dynamic: license-file
46
+
47
+ # reviso-cli
48
+
49
+ Command line client for [Reviso](https://reviso.work), a content collaboration
50
+ system where agents publish Markdown and HTML drafts and a human team reviews
51
+ them.
52
+
53
+ This is the client half of Reviso. It talks to a Reviso server over HTTP and
54
+ contains no server code. It has **no runtime dependencies**: the transport is
55
+ `urllib`, so publishing a Markdown file does not pull a dependency graph into an
56
+ agent's environment.
57
+
58
+ ## Install
59
+
60
+ Not on PyPI yet, so install from source:
61
+
62
+ ```bash
63
+ pip install git+https://github.com/reviso-si/reviso-cli
64
+ # or run it without installing
65
+ uvx --from git+https://github.com/reviso-si/reviso-cli reviso --help
66
+ ```
67
+
68
+ Installs the `reviso` command. Once the package is published this becomes
69
+ `pip install reviso-cli`.
70
+
71
+ ## Quick start
72
+
73
+ ```bash
74
+ reviso auth login --server https://reviso.work
75
+ # set REVISO_AUTOMATION_KEY in the environment first, or pass --key
76
+ reviso doctor --json
77
+
78
+ reviso publish draft.md --title "Q3 launch plan" --description "Initial draft"
79
+ # prints the document URL
80
+
81
+ reviso documents --json # find the document id
82
+ reviso packet doc_abc123 --format json # reviewer comments and version metadata
83
+ reviso versions doc_abc123 # read the current version id
84
+
85
+ reviso update doc_abc123 revised.md \
86
+ --description "Address reviewer feedback" \
87
+ --base-version-id ver_xyz
88
+ ```
89
+
90
+ ## The rule that matters
91
+
92
+ **Always pass `--base-version-id` when you revise a document.**
93
+
94
+ Omitting it is not a safe default. Without it the command re-bases onto whatever
95
+ version is newest and lets your write win, which silently discards a human's
96
+ concurrent edit. The safe shape is always two steps:
97
+
98
+ ```bash
99
+ reviso versions <document_id> # take the newest version_id
100
+ reviso update <document_id> file --description "..." --base-version-id <ver_...>
101
+ ```
102
+
103
+ If the write is rejected as a conflict, re-read and re-apply your change on top of
104
+ the human's edit. Do not retry with the flag removed.
105
+
106
+ ## Command groups
107
+
108
+ | Group | Commands |
109
+ | --- | --- |
110
+ | Connection | `auth`, `doctor`, `health`, `ready`, `version` |
111
+ | Documents | `publish`, `open`, `status`, `documents`, `search`, `packet` |
112
+ | Revisions | `versions`, `diff`, `update`, `rollback` |
113
+ | Reviewers | `invite`, `guest`, `comment`, `reply`, `resolve` |
114
+ | Data | `export`, `import`, `workspaces` |
115
+
116
+ ## Deliberately not in this client
117
+
118
+ These are boundaries, not gaps to be filled later:
119
+
120
+ - **No server commands.** Starting a Reviso server is not part of the client.
121
+ - **No share-link creation.** A share link carries a bearer token in its URL, and
122
+ creating one is a browser action where the person handing it out can see who it
123
+ is going to. Invites by email are here instead.
124
+ - **No owner or operator surfaces.** Metrics, debug replay, and internal replay
125
+ endpoints are not exposed.
126
+
127
+ If you need MCP tools rather than a shell, the hosted connector is the better
128
+ path in most hosts. See [reviso-agent](https://github.com/reviso-si/reviso-agent)
129
+ for the agent skill.
130
+
131
+ ## Known limitations
132
+
133
+ - `publish` prints only the document URL. It does not return the document id, so
134
+ use `reviso documents --json` to continue from the id.
135
+ - `update` and `rollback` accept an omitted base version and fall back to
136
+ last-writer-wins. Always pass the flag. A future release should make the safe
137
+ behaviour the default.
138
+
139
+ ## License
140
+
141
+ MIT. See [LICENSE](LICENSE).
142
+
143
+ The Reviso product itself is **not** open source. This repository covers the
144
+ client only; it grants no rights to the service or its server software. See
145
+ [VISIBILITY.md](VISIBILITY.md) for the boundary this repository holds, and
146
+ [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
@@ -0,0 +1,100 @@
1
+ # reviso-cli
2
+
3
+ Command line client for [Reviso](https://reviso.work), a content collaboration
4
+ system where agents publish Markdown and HTML drafts and a human team reviews
5
+ them.
6
+
7
+ This is the client half of Reviso. It talks to a Reviso server over HTTP and
8
+ contains no server code. It has **no runtime dependencies**: the transport is
9
+ `urllib`, so publishing a Markdown file does not pull a dependency graph into an
10
+ agent's environment.
11
+
12
+ ## Install
13
+
14
+ Not on PyPI yet, so install from source:
15
+
16
+ ```bash
17
+ pip install git+https://github.com/reviso-si/reviso-cli
18
+ # or run it without installing
19
+ uvx --from git+https://github.com/reviso-si/reviso-cli reviso --help
20
+ ```
21
+
22
+ Installs the `reviso` command. Once the package is published this becomes
23
+ `pip install reviso-cli`.
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ reviso auth login --server https://reviso.work
29
+ # set REVISO_AUTOMATION_KEY in the environment first, or pass --key
30
+ reviso doctor --json
31
+
32
+ reviso publish draft.md --title "Q3 launch plan" --description "Initial draft"
33
+ # prints the document URL
34
+
35
+ reviso documents --json # find the document id
36
+ reviso packet doc_abc123 --format json # reviewer comments and version metadata
37
+ reviso versions doc_abc123 # read the current version id
38
+
39
+ reviso update doc_abc123 revised.md \
40
+ --description "Address reviewer feedback" \
41
+ --base-version-id ver_xyz
42
+ ```
43
+
44
+ ## The rule that matters
45
+
46
+ **Always pass `--base-version-id` when you revise a document.**
47
+
48
+ Omitting it is not a safe default. Without it the command re-bases onto whatever
49
+ version is newest and lets your write win, which silently discards a human's
50
+ concurrent edit. The safe shape is always two steps:
51
+
52
+ ```bash
53
+ reviso versions <document_id> # take the newest version_id
54
+ reviso update <document_id> file --description "..." --base-version-id <ver_...>
55
+ ```
56
+
57
+ If the write is rejected as a conflict, re-read and re-apply your change on top of
58
+ the human's edit. Do not retry with the flag removed.
59
+
60
+ ## Command groups
61
+
62
+ | Group | Commands |
63
+ | --- | --- |
64
+ | Connection | `auth`, `doctor`, `health`, `ready`, `version` |
65
+ | Documents | `publish`, `open`, `status`, `documents`, `search`, `packet` |
66
+ | Revisions | `versions`, `diff`, `update`, `rollback` |
67
+ | Reviewers | `invite`, `guest`, `comment`, `reply`, `resolve` |
68
+ | Data | `export`, `import`, `workspaces` |
69
+
70
+ ## Deliberately not in this client
71
+
72
+ These are boundaries, not gaps to be filled later:
73
+
74
+ - **No server commands.** Starting a Reviso server is not part of the client.
75
+ - **No share-link creation.** A share link carries a bearer token in its URL, and
76
+ creating one is a browser action where the person handing it out can see who it
77
+ is going to. Invites by email are here instead.
78
+ - **No owner or operator surfaces.** Metrics, debug replay, and internal replay
79
+ endpoints are not exposed.
80
+
81
+ If you need MCP tools rather than a shell, the hosted connector is the better
82
+ path in most hosts. See [reviso-agent](https://github.com/reviso-si/reviso-agent)
83
+ for the agent skill.
84
+
85
+ ## Known limitations
86
+
87
+ - `publish` prints only the document URL. It does not return the document id, so
88
+ use `reviso documents --json` to continue from the id.
89
+ - `update` and `rollback` accept an omitted base version and fall back to
90
+ last-writer-wins. Always pass the flag. A future release should make the safe
91
+ behaviour the default.
92
+
93
+ ## License
94
+
95
+ MIT. See [LICENSE](LICENSE).
96
+
97
+ The Reviso product itself is **not** open source. This repository covers the
98
+ client only; it grants no rights to the service or its server software. See
99
+ [VISIBILITY.md](VISIBILITY.md) for the boundary this repository holds, and
100
+ [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "reviso-cli"
7
+ version = "0.1.0"
8
+ description = "Command line client for Reviso: publish drafts for human review, read reviewer comments back, and publish revisions safely."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ keywords = ["reviso", "review", "collaboration", "documents", "cli", "agent"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Text Processing :: Markup",
22
+ ]
23
+
24
+ # The client is stdlib-only on purpose. Installing it must not pull a dependency
25
+ # graph into an agent's environment to publish a Markdown file.
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ test = ["pytest>=8.0"]
30
+
31
+ [project.scripts]
32
+ reviso = "reviso_cli.cli:main"
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """Reviso command line client."""
2
+ from __future__ import annotations
3
+
4
+ from .client import RevisoClient
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["RevisoClient", "__version__"]
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,49 @@
1
+ """Access levels used by invites and share links.
2
+
3
+ The three levels below are the vocabulary a user types and sees. What each level
4
+ resolves to on the server is deliberately not represented here: the server owns
5
+ that mapping, and the client sends the level, not an expanded result.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ ACCESS_TO_ROLE = {
10
+ "view": "viewer",
11
+ "comment": "commenter",
12
+ "edit": "editor",
13
+ }
14
+
15
+ VALID_ACCESS = tuple(ACCESS_TO_ROLE)
16
+
17
+
18
+ def normalize_access(access: str) -> str:
19
+ value = str(access or "comment").strip()
20
+ if value not in ACCESS_TO_ROLE:
21
+ raise ValueError("access must be view, comment, or edit")
22
+ return value
23
+
24
+
25
+ def role_for_access(access: str) -> str:
26
+ return ACCESS_TO_ROLE[normalize_access(access)]
27
+
28
+
29
+ def access_from_body(body: dict) -> str:
30
+ if str(body.get("role", "")).strip():
31
+ raise ValueError("role is no longer accepted; pass access")
32
+ return normalize_access(str(body.get("access") or "comment"))
33
+
34
+
35
+ def invite_access_from_body(body: dict) -> str:
36
+ return access_from_body(body)
37
+
38
+
39
+ def invite_for_public(item: dict) -> dict:
40
+ result = {key: value for key, value in item.items() if key != "role"}
41
+ result["access"] = normalize_access(str(item.get("access") or "comment"))
42
+ return result
43
+
44
+
45
+ def invite_list_for_public(result: dict) -> dict:
46
+ return {
47
+ **result,
48
+ "invites": [invite_for_public(item) for item in result.get("invites", [])],
49
+ }
@@ -0,0 +1,276 @@
1
+ """Command line dispatcher. Every server call goes through RevisoClient."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from .. import command_journal
10
+ from ..client import RevisoClient
11
+ from ..document_route_keys import public_document_path
12
+ from ..document_search import search_documents
13
+ from ..errors import ValidationError
14
+ from ..local import read_mapping, write_mapping # noqa: F401
15
+ from ..origin import DEFAULT_SERVER_URL
16
+ from ..paths import config_path, packet_path, read_json, write_json
17
+ from ..scope import resolve_agent_create_scope
18
+ from .doctor import doctor_report, print_doctor
19
+ from .guest_comments import add_guest_comment_commands
20
+ from .invites import add_invite_commands
21
+ from .versions import add_version_commands, format_file_arg
22
+
23
+ # Response blocks that are browser UI state. The human status line drops them;
24
+ # --json keeps them.
25
+ ACCESS_KEYS = ("effective_access", "security_policy")
26
+
27
+
28
+ def _load_config() -> dict:
29
+ path = config_path()
30
+ if path.exists():
31
+ return read_json(path)
32
+ return {}
33
+
34
+
35
+ def _server_url() -> str:
36
+ cfg = _load_config()
37
+ return os.environ.get("REVISO_SERVER",
38
+ cfg.get("server", DEFAULT_SERVER_URL))
39
+
40
+
41
+ def _automation_key(cfg: dict) -> str:
42
+ return (os.environ.get("REVISO_AUTOMATION_KEY")
43
+ or cfg.get("automation_key", ""))
44
+
45
+
46
+ def _client() -> RevisoClient:
47
+ return RevisoClient(_server_url(), _automation_key(_load_config()) or None)
48
+
49
+
50
+ def cmd_auth(args: argparse.Namespace) -> None:
51
+ if args.action == "login":
52
+ key = args.key or os.environ.get("REVISO_AUTOMATION_KEY", "")
53
+ server = args.server or os.environ.get("REVISO_SERVER",
54
+ DEFAULT_SERVER_URL)
55
+ if not key:
56
+ print("No automation key. Create one in Reviso under Settings -> "
57
+ "Integrations, then set REVISO_AUTOMATION_KEY or pass --key.")
58
+ return
59
+ write_json(config_path(), {"server": server, "automation_key": key})
60
+ print(f"Saved to {config_path()}")
61
+ elif args.action == "status":
62
+ cfg = _load_config()
63
+ print(f"server: {cfg.get('server', '(not set)')}")
64
+ print(f"key: {'set' if cfg.get('automation_key') else '(not set)'}")
65
+ print(f"config: {config_path()}")
66
+ else:
67
+ print(f"unknown auth action: {args.action}")
68
+
69
+
70
+ def cmd_publish(args: argparse.Namespace) -> None:
71
+ path = Path(args.file)
72
+ scope = _workspace_scope(args.workspace_id or "", args.parent_id or "")
73
+ content = path.read_text()
74
+ fmt = format_file_arg(args, path)
75
+ title = args.title or path.name
76
+ # A journaled operation_id is reused across reruns of THIS command, so a
77
+ # crashed-then-rerun publish replays instead of double-creating. Cleared
78
+ # only after every local success step has completed.
79
+ parts = [title, content, fmt, args.description or "",
80
+ scope.workspace_id, scope.parent_id]
81
+ op = command_journal.operation_id_for("publish", parts)
82
+ result = _client().publish(title, content, fmt, args.description,
83
+ scope.workspace_id, scope.parent_id,
84
+ operation_id=op)
85
+ mapping = write_mapping(result, path)
86
+ print(mapping["url"])
87
+ command_journal.forget("publish", parts)
88
+
89
+
90
+ def cmd_open(args: argparse.Namespace) -> None:
91
+ print(f"{_server_url()}{public_document_path(args.document_id)}")
92
+
93
+
94
+ def cmd_status(args: argparse.Namespace) -> None:
95
+ payload = _client().status(args.document_id)
96
+ if args.json:
97
+ print(json.dumps(payload, indent=2, sort_keys=True))
98
+ return
99
+ for key, value in payload.items():
100
+ if key in ACCESS_KEYS:
101
+ continue
102
+ print(f"{key}: {value}")
103
+
104
+
105
+ def cmd_server_info(args: argparse.Namespace) -> None:
106
+ client = _client()
107
+ payload = {"health": client.health,
108
+ "ready": client.ready,
109
+ "version": client.version_info}[args.kind]()
110
+ print(json.dumps(payload, indent=2, sort_keys=True))
111
+
112
+
113
+ def cmd_workspaces(args: argparse.Namespace) -> None:
114
+ payload = _client().workspaces()
115
+ if args.json:
116
+ print(json.dumps(payload, indent=2, sort_keys=True))
117
+ return
118
+ for ws in payload.get("workspaces", []):
119
+ print(f"{ws.get('workspace_id')}\t{ws.get('workspace_name')}\t"
120
+ f"[{ws.get('workspace_type')}]\t{ws.get('role')}")
121
+
122
+
123
+ def cmd_documents(args: argparse.Namespace) -> None:
124
+ payload = _client().dashboard(args.workspace_id or "")
125
+ if args.json:
126
+ print(json.dumps(payload, indent=2, sort_keys=True))
127
+ return
128
+ for doc in payload.get("reviews", []):
129
+ status = doc.get("lifecycle_status") or ""
130
+ print(f"{doc.get('document_id')}\t{doc.get('title')}\t"
131
+ f"[{status}]\t{doc.get('unresolved_count')} open")
132
+
133
+
134
+ def cmd_search(args: argparse.Namespace) -> None:
135
+ try:
136
+ payload = search_documents(
137
+ _client(), args.query, workspace_id=args.workspace_id or "",
138
+ limit=args.limit, archived=args.archived,
139
+ include_content=args.include_content)
140
+ except ValidationError as exc:
141
+ raise SystemExit(str(exc)) from exc
142
+ if args.json:
143
+ print(json.dumps(payload, indent=2, sort_keys=True))
144
+ return
145
+ for doc in payload.get("matches", []):
146
+ print(f"{doc.get('document_id')}\t{doc.get('title')}\t"
147
+ f"[{doc.get('match_field')}]\t{doc.get('snippet')}")
148
+
149
+
150
+ def cmd_doctor(args: argparse.Namespace) -> None:
151
+ cfg = _load_config()
152
+ report = doctor_report(
153
+ _client(), _server_url(), _automation_key(cfg),
154
+ os.environ.get("REVISO_WORKSPACE_ID", cfg.get("workspace_id", "")))
155
+ if args.json:
156
+ print(json.dumps(report, indent=2, sort_keys=True))
157
+ return
158
+ print_doctor(report)
159
+
160
+
161
+ def cmd_packet(args: argparse.Namespace) -> None:
162
+ v = "2" if args.v2 else "3"
163
+ packet = _client().packet(args.document_id, args.thread_id, version=v)
164
+ pid = packet.get("document_id", args.document_id)
165
+ path = packet_path(f"pkt_{pid}", "json")
166
+ write_json(path, packet)
167
+ print(str(path))
168
+
169
+
170
+ def _workspace_scope(explicit: str = "", parent_id: str = ""):
171
+ cfg = _load_config()
172
+ key = _automation_key(cfg)
173
+ return resolve_agent_create_scope(
174
+ explicit_workspace_id=explicit, explicit_parent_id=parent_id,
175
+ config=cfg, has_automation_key=key.startswith("rak_"))
176
+
177
+
178
+ def _add_basic(sub: argparse._SubParsersAction) -> None:
179
+ auth = sub.add_parser("auth")
180
+ auth.add_argument("action", nargs="?", default="login")
181
+ auth.add_argument("--server")
182
+ auth.add_argument("--key")
183
+ auth.set_defaults(func=cmd_auth)
184
+ pub = sub.add_parser("publish")
185
+ pub.add_argument("file")
186
+ pub.add_argument("--title")
187
+ pub.add_argument("--format", choices=["markdown", "md", "html"])
188
+ pub.add_argument("--description", default="Initial draft")
189
+ pub.add_argument("--workspace-id", default="")
190
+ pub.add_argument("--parent-id", default="")
191
+ pub.set_defaults(func=cmd_publish)
192
+ opn = sub.add_parser("open")
193
+ opn.add_argument("document_id")
194
+ opn.set_defaults(func=cmd_open)
195
+ st = sub.add_parser("status")
196
+ st.add_argument("document_id")
197
+ st.add_argument("--json", action="store_true")
198
+ st.set_defaults(func=cmd_status)
199
+ wss = sub.add_parser("workspaces")
200
+ wss.add_argument("--json", action="store_true")
201
+ wss.set_defaults(func=cmd_workspaces)
202
+ docs = sub.add_parser("documents")
203
+ docs.add_argument("--workspace-id", default="")
204
+ docs.add_argument("--json", action="store_true")
205
+ docs.set_defaults(func=cmd_documents)
206
+ search = sub.add_parser("search")
207
+ search.add_argument("query")
208
+ search.add_argument("--workspace-id", default="")
209
+ search.add_argument("--limit", type=int, default=10)
210
+ search.add_argument("--archived", action="store_true")
211
+ search.add_argument("--include-content", action="store_true")
212
+ search.add_argument("--json", action="store_true")
213
+ search.set_defaults(func=cmd_search)
214
+ doctor = sub.add_parser("doctor")
215
+ doctor.add_argument("--json", action="store_true")
216
+ doctor.set_defaults(func=cmd_doctor)
217
+ for name in ("health", "ready", "version"):
218
+ srv = sub.add_parser(name)
219
+ srv.set_defaults(func=cmd_server_info, kind=name)
220
+
221
+
222
+ def _add_packet(sub: argparse._SubParsersAction) -> None:
223
+ pkt = sub.add_parser("packet")
224
+ pkt.add_argument("document_id")
225
+ pkt.add_argument("--format", choices=["json"], default="json")
226
+ pkt.add_argument("--thread-id", action="append")
227
+ pkt.add_argument("--v2", action="store_true",
228
+ help="Use legacy packet v2 with sections")
229
+ pkt.set_defaults(func=cmd_packet)
230
+
231
+
232
+ def _add_review_io(sub: argparse._SubParsersAction) -> None:
233
+ exp = sub.add_parser("export")
234
+ exp.add_argument("document_id")
235
+ exp.add_argument("--output", default="")
236
+ exp.set_defaults(func=cmd_export)
237
+ imp = sub.add_parser("import")
238
+ imp.add_argument("file")
239
+ imp.add_argument("--workspace-id", default="")
240
+ imp.add_argument("--parent-id", default="")
241
+ imp.set_defaults(func=cmd_import)
242
+
243
+
244
+ def cmd_export(args: argparse.Namespace) -> None:
245
+ bundle = _client().export_review(args.document_id)
246
+ if args.output:
247
+ write_json(Path(args.output), bundle)
248
+ print(args.output)
249
+ return
250
+ print(json.dumps(bundle, indent=2, sort_keys=True))
251
+
252
+
253
+ def cmd_import(args: argparse.Namespace) -> None:
254
+ bundle = read_json(Path(args.file))
255
+ scope = _workspace_scope(args.workspace_id or "", args.parent_id or "")
256
+ result = _client().import_review(
257
+ bundle, scope.workspace_id, scope.parent_id)
258
+ print(json.dumps(result, indent=2, sort_keys=True))
259
+
260
+
261
+ def build_parser() -> argparse.ArgumentParser:
262
+ parser = argparse.ArgumentParser(prog="reviso")
263
+ sub = parser.add_subparsers(dest="cmd", required=True)
264
+ _add_basic(sub)
265
+ _add_packet(sub)
266
+ _add_review_io(sub)
267
+ add_guest_comment_commands(sub, _client)
268
+ add_invite_commands(sub, _client)
269
+ add_version_commands(sub, _client)
270
+ return parser
271
+
272
+
273
+ def main(argv: list[str] | None = None) -> None:
274
+ parser = build_parser()
275
+ args = parser.parse_args(argv)
276
+ args.func(args)