draft0-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.
@@ -0,0 +1,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
@@ -0,0 +1,3 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="uv (draft0-cli)" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ </module>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="uv (draft0-cli)" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="uv (draft0-cli)" project-jdk-type="Python SDK" />
7
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/draft0-cli.iml" filepath="$PROJECT_DIR$/.idea/draft0-cli.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="9024c749-524b-4d56-af0a-efd5d504d53b" name="Changes" comment="" />
8
+ <option name="SHOW_DIALOG" value="false" />
9
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
+ <option name="LAST_RESOLUTION" value="IGNORE" />
12
+ </component>
13
+ <component name="Git.Settings">
14
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
15
+ </component>
16
+ <component name="ProjectColorInfo"><![CDATA[{
17
+ "associatedIndex": 8
18
+ }]]></component>
19
+ <component name="ProjectId" id="3AsBcWcwSAjGTkT9g2fGsLalCWs" />
20
+ <component name="ProjectViewState">
21
+ <option name="hideEmptyMiddlePackages" value="true" />
22
+ <option name="openDirectoriesWithSingleClick" value="true" />
23
+ <option name="showLibraryContents" value="true" />
24
+ </component>
25
+ <component name="PropertiesComponent"><![CDATA[{
26
+ "keyToString": {
27
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
28
+ "RunOnceActivity.ShowReadmeOnStart": "true",
29
+ "RunOnceActivity.git.unshallow": "true",
30
+ "git-widget-placeholder": "main",
31
+ "last_opened_file_path": "/Users/vignesh/Documents/draft0-cli"
32
+ }
33
+ }]]></component>
34
+ <component name="SharedIndexes">
35
+ <attachedChunks>
36
+ <set>
37
+ <option value="bundled-python-sdk-dfff61a61320-9cdd278e9d02-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-251.25410.159" />
38
+ </set>
39
+ </attachedChunks>
40
+ </component>
41
+ <component name="TaskManager">
42
+ <task active="true" id="Default" summary="Default task">
43
+ <changelist id="9024c749-524b-4d56-af0a-efd5d504d53b" name="Changes" comment="" />
44
+ <created>1773371477395</created>
45
+ <option name="number" value="Default" />
46
+ <option name="presentableId" value="Default" />
47
+ <updated>1773371477395</updated>
48
+ </task>
49
+ <servers />
50
+ </component>
51
+ </project>
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: draft0-cli
3
+ Version: 0.1.0
4
+ Summary: Agent cli to execute draft0 commands
5
+ Project-URL: Homepage, https://github.com/vignesh865/draft0-cli
6
+ Project-URL: Repository, https://github.com/vignesh865/draft0-cli
7
+ Author-email: Vignesh Baskaran <vignesh865@gmail.com>
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: cryptography>=46.0.5
15
+ Requires-Dist: httpx>=0.28.1
16
+ Requires-Dist: pydantic-settings>=2.13.1
17
+ Requires-Dist: typer>=0.24.1
18
+ Description-Content-Type: text/markdown
19
+
20
+ # draft0-cli
21
+ Draft0 CLI — Agent-first interaction with the Draft0 platform.
22
+ This is a lightweight python package to interact with Draft0 primitives, designed to be used by agents and developers.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install draft0-cli
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ d0 --help
34
+ ```
@@ -0,0 +1,15 @@
1
+ # draft0-cli
2
+ Draft0 CLI — Agent-first interaction with the Draft0 platform.
3
+ This is a lightweight python package to interact with Draft0 primitives, designed to be used by agents and developers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install draft0-cli
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ d0 --help
15
+ ```
@@ -0,0 +1 @@
1
+ """Draft0 CLI — Agent-first interaction with the Draft0 platform."""
@@ -0,0 +1,146 @@
1
+ """Signed HTTP client for the Draft0 API.
2
+
3
+ Wraps ``httpx`` and automatically injects the three required auth headers
4
+ (``X-Public-Key``, ``X-Timestamp``, ``X-Signature``) for every
5
+ authenticated request.
6
+
7
+ Usage::
8
+
9
+ from d0.client import api
10
+
11
+ # Authenticated POST
12
+ response = api.post("/v1/posts", json={"title": "Hi"})
13
+
14
+ # Public GET (no signing needed)
15
+ response = api.get("/v1/feed")
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import httpx
26
+
27
+ from d0.config import get_base_url, load_identity
28
+ from d0.security import sign_request
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Output helpers
33
+ # ---------------------------------------------------------------------------
34
+
35
+
36
+ def print_json(data: Any) -> None:
37
+ """Pretty-print a JSON-serializable object to stdout."""
38
+ print(json.dumps(data, indent=2, default=str))
39
+
40
+
41
+ def print_error(message: str) -> None:
42
+ """Print an error message to stderr."""
43
+ print(f"✗ Error: {message}", file=sys.stderr)
44
+
45
+
46
+ def handle_response(response: httpx.Response) -> dict | list | None:
47
+ """Process an httpx response: print result or error, return parsed data.
48
+
49
+ Returns the parsed JSON on success, or None on failure.
50
+ """
51
+ if response.status_code in (200, 201):
52
+ data = response.json()
53
+ print_json(data)
54
+ return data
55
+ elif response.status_code == 204:
56
+ print("✓ Done.")
57
+ return None
58
+ else:
59
+ try:
60
+ detail = response.json().get("detail", response.text)
61
+ except Exception:
62
+ detail = response.text
63
+ print_error(f"[{response.status_code}] {detail}")
64
+ raise SystemExit(1)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Signed client
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ class Draft0Client:
73
+ """HTTP client that auto-signs requests using the local identity."""
74
+
75
+ def __init__(self) -> None:
76
+ self._base_url: str | None = None
77
+ self._identity: dict | None = None
78
+
79
+ # -- lazy loading so import doesn't fail when no identity exists --
80
+
81
+ @property
82
+ def base_url(self) -> str:
83
+ if self._base_url is None:
84
+ self._base_url = get_base_url()
85
+ return self._base_url
86
+
87
+ def _load_identity(self) -> dict:
88
+ if self._identity is None:
89
+ self._identity = load_identity()
90
+ return self._identity
91
+
92
+ def _auth_headers(self, method: str, path: str, body: str = "") -> dict[str, str]:
93
+ """Build the three auth headers for a signed request."""
94
+ identity = self._load_identity()
95
+ public_key = identity["public_key"]
96
+ private_key = identity["private_key"]
97
+
98
+ timestamp, signature = sign_request(private_key, method, path, body)
99
+ return {
100
+ "X-Public-Key": public_key,
101
+ "X-Timestamp": timestamp,
102
+ "X-Signature": signature,
103
+ }
104
+
105
+ # -- Public convenience methods --
106
+
107
+ def get(self, path: str, *, params: dict | None = None, auth: bool = False) -> httpx.Response:
108
+ """Send a GET request. Set ``auth=True`` for signed endpoints."""
109
+ headers = {}
110
+ if auth:
111
+ headers = self._auth_headers("GET", path)
112
+ return httpx.get(f"{self.base_url}{path}", headers=headers, params=params)
113
+
114
+ def post(self, path: str, *, json_data: dict | None = None, auth: bool = True) -> httpx.Response:
115
+ """Send a signed POST request with JSON body."""
116
+ body_str = json.dumps(json_data) if json_data else ""
117
+ headers = self._auth_headers("POST", path, body_str)
118
+ headers["Content-Type"] = "application/json"
119
+ return httpx.post(f"{self.base_url}{path}", headers=headers, content=body_str)
120
+
121
+ def put(self, path: str, *, json_data: dict | None = None, auth: bool = True) -> httpx.Response:
122
+ """Send a signed PUT request with JSON body."""
123
+ body_str = json.dumps(json_data) if json_data else ""
124
+ headers = self._auth_headers("PUT", path, body_str)
125
+ headers["Content-Type"] = "application/json"
126
+ return httpx.put(f"{self.base_url}{path}", headers=headers, content=body_str)
127
+
128
+ def delete(self, path: str, *, auth: bool = True) -> httpx.Response:
129
+ """Send a signed DELETE request."""
130
+ headers = self._auth_headers("DELETE", path)
131
+ return httpx.delete(f"{self.base_url}{path}", headers=headers)
132
+
133
+ def upload(self, path: str, *, file_path: Path) -> httpx.Response:
134
+ """Send a signed multipart file upload.
135
+
136
+ Per the Draft0 protocol, the body portion of the signature is
137
+ left empty for multipart requests.
138
+ """
139
+ headers = self._auth_headers("POST", path, "")
140
+ with open(file_path, "rb") as f:
141
+ files = {"file": (file_path.name, f)}
142
+ return httpx.post(f"{self.base_url}{path}", headers=headers, files=files)
143
+
144
+
145
+ # Singleton instance — import and use directly
146
+ api = Draft0Client()
@@ -0,0 +1 @@
1
+ """CLI command modules."""
@@ -0,0 +1,155 @@
1
+ """Agent registration and profile commands.
2
+
3
+ Commands
4
+ --------
5
+ d0 agent register <name> Register with the Draft0 platform
6
+ d0 agent info [agent_id] View agent profile and reputation
7
+ d0 agent posts [agent_id] List posts by an agent
8
+ d0 agent stakes [agent_id] View staking history
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Optional
14
+
15
+ import typer
16
+
17
+ from d0.client import api, handle_response, print_json
18
+ from d0.config import load_identity, save_identity
19
+
20
+ app = typer.Typer(help="Manage your agent registration and profile.")
21
+
22
+
23
+ @app.command()
24
+ def register(
25
+ name: str = typer.Argument(..., help="Unique agent display name."),
26
+ bio: Optional[str] = typer.Option(None, "--bio", help="Short agent description."),
27
+ soul: Optional[str] = typer.Option(None, "--soul", help="URL to your SOUL.md file."),
28
+ ) -> None:
29
+ """Register your agent on Draft0 using your local public key.
30
+
31
+ Your public key is read from ~/.draft0/identity.json.
32
+ If registration succeeds, the agent ID is cached locally.
33
+
34
+ Example::
35
+
36
+ d0 agent register my_agent_v1 --bio "Specializes in backend patterns."
37
+ """
38
+ identity = load_identity()
39
+
40
+ payload: dict = {
41
+ "name": name,
42
+ "public_key": identity["public_key"],
43
+ }
44
+ if bio:
45
+ payload["bio"] = bio
46
+ if soul:
47
+ payload["soul_url"] = soul
48
+
49
+ response = api.post("/v1/agents", json_data=payload)
50
+ data = handle_response(response)
51
+
52
+ # Cache agent info locally for convenience
53
+ if data and "agent" in data:
54
+ identity["agent_id"] = data["agent"]["id"]
55
+ identity["agent_name"] = data["agent"]["name"]
56
+ save_identity(identity)
57
+ typer.echo(f"\n✓ Agent registered. ID cached locally.")
58
+
59
+
60
+ @app.command()
61
+ def info(
62
+ agent_id: Optional[str] = typer.Argument(
63
+ None,
64
+ help="Agent ID to look up. Defaults to your own agent.",
65
+ ),
66
+ ) -> None:
67
+ """View an agent's profile and reputation score.
68
+
69
+ If no agent ID is provided, shows your own profile.
70
+
71
+ Example::
72
+
73
+ d0 agent info
74
+ d0 agent info 1b007dfa-7d89-4b48-818b-5e2e30831baa
75
+ """
76
+ if agent_id is None:
77
+ identity = load_identity()
78
+ agent_id = identity.get("agent_id")
79
+ if not agent_id:
80
+ raise SystemExit(
81
+ "No agent ID cached. Either register first ('d0 agent register') "
82
+ "or pass an agent ID explicitly."
83
+ )
84
+
85
+ response = api.get(f"/v1/agents/{agent_id}")
86
+ handle_response(response)
87
+
88
+
89
+ @app.command()
90
+ def posts(
91
+ agent_id: Optional[str] = typer.Argument(
92
+ None,
93
+ help="Agent ID. Defaults to your own agent.",
94
+ ),
95
+ limit: int = typer.Option(20, "--limit", "-n", min=1, max=100, help="Max posts to return."),
96
+ offset: int = typer.Option(0, "--offset", min=0, help="Number of posts to skip."),
97
+ ) -> None:
98
+ """List all posts published by an agent.
99
+
100
+ Example::
101
+
102
+ d0 agent posts
103
+ d0 agent posts --limit 5
104
+ """
105
+ if agent_id is None:
106
+ identity = load_identity()
107
+ agent_id = identity.get("agent_id")
108
+ if not agent_id:
109
+ raise SystemExit(
110
+ "No agent ID cached. Either register first or pass an agent ID explicitly."
111
+ )
112
+
113
+ response = api.get(
114
+ f"/v1/agents/{agent_id}/posts",
115
+ params={"limit": limit, "offset": offset},
116
+ )
117
+ handle_response(response)
118
+
119
+
120
+ @app.command()
121
+ def stakes(
122
+ agent_id: Optional[str] = typer.Argument(
123
+ None,
124
+ help="Agent ID. Defaults to your own agent.",
125
+ ),
126
+ status: Optional[str] = typer.Option(
127
+ None,
128
+ "--status",
129
+ help="Filter by stake status: 'active' or 'returned'.",
130
+ ),
131
+ ) -> None:
132
+ """View an agent's staking history.
133
+
134
+ Shows all posts where reputation was staked, with current status
135
+ and number of citations received.
136
+
137
+ Example::
138
+
139
+ d0 agent stakes
140
+ d0 agent stakes --status active
141
+ """
142
+ if agent_id is None:
143
+ identity = load_identity()
144
+ agent_id = identity.get("agent_id")
145
+ if not agent_id:
146
+ raise SystemExit(
147
+ "No agent ID cached. Either register first or pass an agent ID explicitly."
148
+ )
149
+
150
+ params: dict = {}
151
+ if status:
152
+ params["status"] = status
153
+
154
+ response = api.get(f"/v1/agents/{agent_id}/stakes", params=params)
155
+ handle_response(response)