daita-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.
daita_cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ try:
2
+ import importlib.metadata
3
+ __version__ = importlib.metadata.version("daita-cli")
4
+ except Exception:
5
+ __version__ = "0.1.0"
@@ -0,0 +1,103 @@
1
+ """
2
+ DaitaAPIClient — async httpx client shared by CLI commands and MCP server.
3
+ """
4
+
5
+ import os
6
+ import httpx
7
+ from typing import Any
8
+
9
+ from daita_cli import __version__
10
+
11
+ _DEFAULT_BASE_URL = "https://api.daita-tech.io"
12
+
13
+
14
+ class APIError(Exception):
15
+ def __init__(self, status_code: int, detail: str, raw: Any = None):
16
+ super().__init__(detail)
17
+ self.status_code = status_code
18
+ self.detail = detail
19
+ self.raw = raw
20
+
21
+
22
+ class AuthError(APIError): ...
23
+ class NotFoundError(APIError): ...
24
+ class ValidationError(APIError): ...
25
+ class RateLimitError(APIError): ...
26
+ class ServerError(APIError): ...
27
+
28
+
29
+ def _raise_for(status: int, detail: str, raw: Any) -> None:
30
+ if status in (401, 403):
31
+ raise AuthError(status, detail, raw)
32
+ if status == 404:
33
+ raise NotFoundError(status, detail, raw)
34
+ if status in (400, 422):
35
+ raise ValidationError(status, detail, raw)
36
+ if status == 429:
37
+ raise RateLimitError(status, detail, raw)
38
+ if status >= 500:
39
+ raise ServerError(status, detail, raw)
40
+ raise APIError(status, detail, raw)
41
+
42
+
43
+ class DaitaAPIClient:
44
+ def __init__(self, api_key: str = None, base_url: str = None):
45
+ self.api_key = api_key or os.getenv("DAITA_API_KEY")
46
+ self.base_url = (base_url or os.getenv("DAITA_API_ENDPOINT") or _DEFAULT_BASE_URL).rstrip("/")
47
+ self._client: httpx.AsyncClient | None = None
48
+
49
+ def _headers(self) -> dict:
50
+ if not self.api_key:
51
+ raise AuthError(401, "DAITA_API_KEY is not set. Export it or pass --api-key.")
52
+ return {
53
+ "Authorization": f"Bearer {self.api_key}",
54
+ "User-Agent": f"Daita-CLI/{__version__}",
55
+ "Content-Type": "application/json",
56
+ }
57
+
58
+ async def __aenter__(self) -> "DaitaAPIClient":
59
+ self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
60
+ return self
61
+
62
+ async def __aexit__(self, *_) -> None:
63
+ if self._client:
64
+ await self._client.aclose()
65
+ self._client = None
66
+
67
+ def _check_client(self) -> httpx.AsyncClient:
68
+ if self._client is None:
69
+ raise RuntimeError("DaitaAPIClient must be used as an async context manager")
70
+ return self._client
71
+
72
+ async def _handle(self, resp: httpx.Response) -> Any:
73
+ if resp.is_success:
74
+ try:
75
+ return resp.json()
76
+ except Exception:
77
+ return resp.text
78
+ try:
79
+ body = resp.json()
80
+ detail = body.get("detail") or body.get("message") or str(body)
81
+ except Exception:
82
+ detail = resp.text or f"HTTP {resp.status_code}"
83
+ _raise_for(resp.status_code, detail, resp)
84
+
85
+ async def get(self, path: str, params: dict = None) -> Any:
86
+ resp = await self._check_client().get(path, headers=self._headers(), params=params)
87
+ return await self._handle(resp)
88
+
89
+ async def post(self, path: str, json: dict = None) -> Any:
90
+ resp = await self._check_client().post(path, headers=self._headers(), json=json)
91
+ return await self._handle(resp)
92
+
93
+ async def put(self, path: str, json: dict = None) -> Any:
94
+ resp = await self._check_client().put(path, headers=self._headers(), json=json)
95
+ return await self._handle(resp)
96
+
97
+ async def patch(self, path: str, json: dict = None) -> Any:
98
+ resp = await self._check_client().patch(path, headers=self._headers(), json=json)
99
+ return await self._handle(resp)
100
+
101
+ async def delete(self, path: str, params: dict = None) -> Any:
102
+ resp = await self._check_client().delete(path, headers=self._headers(), params=params)
103
+ return await self._handle(resp)
@@ -0,0 +1,58 @@
1
+ """
2
+ @api_command decorator — wraps async Click commands with client injection,
3
+ OutputFormatter injection, and standardised error/exit-code handling.
4
+
5
+ Usage:
6
+ @cli.command()
7
+ @click.argument("name")
8
+ @api_command
9
+ async def my_cmd(client, formatter, name): ...
10
+ """
11
+
12
+ import asyncio
13
+ import functools
14
+ import sys
15
+
16
+ import click
17
+
18
+ from daita_cli.api_client import AuthError, NotFoundError, APIError, DaitaAPIClient
19
+ from daita_cli.output import OutputFormatter
20
+
21
+
22
+ def api_command(f):
23
+ """
24
+ Decorator that:
25
+ 1. Runs the coroutine via asyncio.run()
26
+ 2. Injects `client` (DaitaAPIClient) and `formatter` (OutputFormatter) as first two args
27
+ 3. Maps exceptions to exit codes: 0=ok, 1=error, 2=auth, 130=interrupt
28
+ """
29
+ @functools.wraps(f)
30
+ @click.pass_context
31
+ def wrapper(ctx, *args, **kwargs):
32
+ obj = ctx.obj or {}
33
+ formatter: OutputFormatter = obj.get("formatter", OutputFormatter())
34
+
35
+ async def _run():
36
+ async with DaitaAPIClient() as client:
37
+ return await f(client, formatter, *args, **kwargs)
38
+
39
+ try:
40
+ asyncio.run(_run())
41
+ except AuthError as e:
42
+ formatter.error("AUTH_ERROR", str(e))
43
+ sys.exit(2)
44
+ except NotFoundError as e:
45
+ formatter.error("NOT_FOUND", str(e))
46
+ sys.exit(1)
47
+ except APIError as e:
48
+ formatter.error("API_ERROR", str(e), {"status_code": e.status_code})
49
+ sys.exit(1)
50
+ except KeyboardInterrupt:
51
+ sys.exit(130)
52
+ except click.ClickException:
53
+ raise
54
+ except Exception as e:
55
+ formatter.error("ERROR", str(e))
56
+ sys.exit(1)
57
+
58
+ return wrapper
File without changes
@@ -0,0 +1,52 @@
1
+ import click
2
+ from daita_cli.command_helpers import api_command
3
+
4
+
5
+ @click.group()
6
+ def agents():
7
+ """Manage agents."""
8
+ pass
9
+
10
+
11
+ @agents.command("list")
12
+ @click.option("--type", "agent_type", type=click.Choice(["agent", "workflow"]), help="Filter by type")
13
+ @click.option("--status", type=click.Choice(["active", "inactive"]), help="Filter by status")
14
+ @click.option("--page", default=1, show_default=True, help="Page number")
15
+ @click.option("--per-page", default=20, show_default=True, help="Items per page")
16
+ @api_command
17
+ async def list_agents(client, formatter, agent_type, status, page, per_page):
18
+ """List agents."""
19
+ params = {"page": page, "per_page": per_page}
20
+ if agent_type:
21
+ params["agent_type"] = agent_type
22
+ if status:
23
+ params["status_filter"] = status
24
+ data = await client.get("/api/v1/agents/agents", params=params)
25
+ items = data if isinstance(data, list) else data.get("agents", data.get("items", []))
26
+ formatter.list_items(
27
+ items,
28
+ columns=["id", "name", "type", "status", "created_at"],
29
+ title="Agents",
30
+ )
31
+
32
+
33
+ @agents.command("show")
34
+ @click.argument("agent_id")
35
+ @api_command
36
+ async def show_agent(client, formatter, agent_id):
37
+ """Show agent details."""
38
+ data = await client.get(f"/api/v1/agents/agents/{agent_id}")
39
+ formatter.item(data)
40
+
41
+
42
+ @agents.command("deployed")
43
+ @api_command
44
+ async def deployed_agents(client, formatter):
45
+ """List deployed agents with configuration."""
46
+ data = await client.get("/api/v1/agents/agents/deployed")
47
+ items = data if isinstance(data, list) else data.get("agents", data.get("items", []))
48
+ formatter.list_items(
49
+ items,
50
+ columns=["id", "name", "type", "status", "deployed_at"],
51
+ title="Deployed Agents",
52
+ )
@@ -0,0 +1,75 @@
1
+ """
2
+ Conversations — requires user_id for all requests.
3
+ The backend scopes conversations per user. From the CLI, user_id defaults to "cli"
4
+ (all CLI-created conversations share this scope). Override with --user-id.
5
+ """
6
+
7
+ import click
8
+ from daita_cli.command_helpers import api_command
9
+
10
+ _USER_ID_OPT = click.option(
11
+ "--user-id", default="cli", show_default=True,
12
+ help="User ID for scoping conversations (default: 'cli')",
13
+ )
14
+
15
+
16
+ @click.group()
17
+ def conversations():
18
+ """Manage conversations."""
19
+ pass
20
+
21
+
22
+ @conversations.command("list")
23
+ @_USER_ID_OPT
24
+ @click.option("--agent-name")
25
+ @click.option("--limit", default=20, show_default=True)
26
+ @api_command
27
+ async def list_conversations(client, formatter, user_id, agent_name, limit):
28
+ """List conversations."""
29
+ params = {"user_id": user_id, "limit": limit}
30
+ if agent_name:
31
+ params["agent_name"] = agent_name
32
+ data = await client.get("/api/v1/conversations", params=params)
33
+ items = data if isinstance(data, list) else data.get("conversations", data.get("items", []))
34
+ formatter.list_items(
35
+ items,
36
+ columns=["id", "title", "agent_name", "created_at"],
37
+ title="Conversations",
38
+ )
39
+
40
+
41
+ @conversations.command("show")
42
+ @click.argument("conversation_id")
43
+ @_USER_ID_OPT
44
+ @api_command
45
+ async def show_conversation(client, formatter, conversation_id, user_id):
46
+ """Show conversation details."""
47
+ data = await client.get(f"/api/v1/conversations/{conversation_id}", params={"user_id": user_id})
48
+ formatter.item(data)
49
+
50
+
51
+ @conversations.command("create")
52
+ @click.option("--agent-name", required=True)
53
+ @click.option("--title")
54
+ @_USER_ID_OPT
55
+ @api_command
56
+ async def create_conversation(client, formatter, agent_name, title, user_id):
57
+ """Create a new conversation."""
58
+ payload = {"agent_name": agent_name, "user_id": user_id}
59
+ if title:
60
+ payload["title"] = title
61
+ data = await client.post("/api/v1/conversations", json=payload)
62
+ formatter.success(data, message=f"Conversation created: {data.get('id', '')}")
63
+
64
+
65
+ @conversations.command("delete")
66
+ @click.argument("conversation_id")
67
+ @_USER_ID_OPT
68
+ @api_command
69
+ async def delete_conversation(client, formatter, conversation_id, user_id):
70
+ """Delete a conversation."""
71
+ data = await client.delete(
72
+ f"/api/v1/conversations/{conversation_id}",
73
+ params={"user_id": user_id},
74
+ )
75
+ formatter.success(data, message=f"Conversation {conversation_id} deleted.")
@@ -0,0 +1,182 @@
1
+ """
2
+ daita create agent|workflow <name> — add a component to the current project.
3
+ No daita-agents dependency required.
4
+ """
5
+
6
+ import sys
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import click
11
+ import yaml
12
+
13
+ from daita_cli.output import OutputFormatter
14
+ from daita_cli.project_utils import ensure_project_root
15
+
16
+
17
+ @click.group("create")
18
+ def create_group():
19
+ """Create agents, workflows, and other components."""
20
+ pass
21
+
22
+
23
+ @create_group.command("agent")
24
+ @click.argument("name")
25
+ @click.pass_context
26
+ def create_agent(ctx, name):
27
+ """Create a new agent."""
28
+ obj = ctx.obj or {}
29
+ formatter = obj.get("formatter", OutputFormatter())
30
+ try:
31
+ _create_component("agent", name, formatter)
32
+ except click.ClickException:
33
+ raise
34
+ except Exception as e:
35
+ formatter.error("ERROR", str(e))
36
+ sys.exit(1)
37
+
38
+
39
+ @create_group.command("workflow")
40
+ @click.argument("name")
41
+ @click.pass_context
42
+ def create_workflow(ctx, name):
43
+ """Create a new workflow."""
44
+ obj = ctx.obj or {}
45
+ formatter = obj.get("formatter", OutputFormatter())
46
+ try:
47
+ _create_component("workflow", name, formatter)
48
+ except click.ClickException:
49
+ raise
50
+ except Exception as e:
51
+ formatter.error("ERROR", str(e))
52
+ sys.exit(1)
53
+
54
+
55
+ def _to_class_name(name: str) -> str:
56
+ return "".join(w.capitalize() for w in name.replace("-", "_").split("_") if w)
57
+
58
+
59
+ def _clean_name(name: str) -> str:
60
+ return name.replace("-", "_").lower()
61
+
62
+
63
+ def _create_component(template: str, name: str, formatter: OutputFormatter):
64
+ project_root = ensure_project_root()
65
+ clean_name = _clean_name(name)
66
+ class_name = _to_class_name(clean_name)
67
+
68
+ if template == "agent":
69
+ dest_dir = project_root / "agents"
70
+ dest_file = dest_dir / f"{clean_name}.py"
71
+ if dest_file.exists():
72
+ raise click.ClickException(f"Agent '{clean_name}' already exists.")
73
+ dest_dir.mkdir(exist_ok=True)
74
+ dest_file.write_text(_agent_template(clean_name, class_name))
75
+ elif template == "workflow":
76
+ dest_dir = project_root / "workflows"
77
+ dest_file = dest_dir / f"{clean_name}.py"
78
+ if dest_file.exists():
79
+ raise click.ClickException(f"Workflow '{clean_name}' already exists.")
80
+ dest_dir.mkdir(exist_ok=True)
81
+ dest_file.write_text(_workflow_template(clean_name, class_name))
82
+ else:
83
+ raise click.ClickException(f"Unknown template: {template}")
84
+
85
+ # Prompt for display name (non-interactive: use default)
86
+ default_display = clean_name.replace("_", " ").title()
87
+ if sys.stdin.isatty():
88
+ display_name = click.prompt(
89
+ f" Display name for deployment",
90
+ default=default_display,
91
+ )
92
+ else:
93
+ display_name = default_display
94
+
95
+ _update_config(project_root, template + "s", clean_name, display_name)
96
+
97
+ formatter.success(
98
+ {"name": clean_name, "display_name": display_name, "file": str(dest_file)},
99
+ message=f" Created {template}: {clean_name} (display: '{display_name}')",
100
+ )
101
+
102
+
103
+ def _update_config(project_root: Path, component_key: str, name: str, display_name: str):
104
+ cfg_file = project_root / "daita-project.yaml"
105
+ if cfg_file.exists():
106
+ with open(cfg_file) as f:
107
+ config = yaml.safe_load(f) or {}
108
+ else:
109
+ config = {}
110
+
111
+ config.setdefault(component_key, [])
112
+ if not any(c["name"] == name for c in config[component_key]):
113
+ config[component_key].append({
114
+ "name": name,
115
+ "display_name": display_name,
116
+ "type": "standard" if component_key == "agents" else "basic",
117
+ "created_at": datetime.now().isoformat(),
118
+ })
119
+ with open(cfg_file, "w") as f:
120
+ yaml.dump(config, f, default_flow_style=False)
121
+
122
+
123
+ def _agent_template(name: str, class_name: str) -> str:
124
+ return f'''\
125
+ """
126
+ {class_name} Agent
127
+ """
128
+ from daita import Agent
129
+
130
+
131
+ def create_agent():
132
+ """Create the agent instance."""
133
+ return Agent(
134
+ name="{class_name}",
135
+ model="gpt-4o-mini",
136
+ prompt="You are a helpful AI assistant.",
137
+ )
138
+
139
+
140
+ if __name__ == "__main__":
141
+ import asyncio
142
+
143
+ async def main():
144
+ agent = create_agent()
145
+ await agent.start()
146
+ try:
147
+ answer = await agent.run("Hello!")
148
+ print(answer)
149
+ finally:
150
+ await agent.stop()
151
+
152
+ asyncio.run(main())
153
+ '''
154
+
155
+
156
+ def _workflow_template(name: str, class_name: str) -> str:
157
+ return f'''\
158
+ """
159
+ {class_name} Workflow
160
+ """
161
+ from daita import Agent, Workflow
162
+
163
+
164
+ def create_workflow():
165
+ """Create the workflow instance."""
166
+ workflow = Workflow("{class_name}")
167
+ agent = Agent(name="Agent", model="gpt-4o-mini", prompt="You are helpful.")
168
+ workflow.add_agent("agent", agent)
169
+ return workflow
170
+
171
+
172
+ if __name__ == "__main__":
173
+ import asyncio
174
+
175
+ async def main():
176
+ wf = create_workflow()
177
+ await wf.start()
178
+ await wf.stop()
179
+ print("Done")
180
+
181
+ asyncio.run(main())
182
+ '''
@@ -0,0 +1,73 @@
1
+ import click
2
+ from daita_cli.command_helpers import api_command
3
+
4
+
5
+ @click.group()
6
+ def deployments():
7
+ """Manage deployments."""
8
+ pass
9
+
10
+
11
+ @deployments.command("list")
12
+ @click.option("--limit", default=10, show_default=True)
13
+ @api_command
14
+ async def list_deployments(client, formatter, limit):
15
+ """List deployments."""
16
+ data = await client.get("/api/v1/deployments/api-key", params={"per_page": limit})
17
+ items = data if isinstance(data, list) else data.get("deployments", data.get("items", []))
18
+ formatter.list_items(
19
+ items,
20
+ columns=["deployment_id", "project_name", "environment", "status", "version", "deployed_at"],
21
+ title="Deployments",
22
+ )
23
+
24
+
25
+ @deployments.command("show")
26
+ @click.argument("deployment_id")
27
+ @api_command
28
+ async def show_deployment(client, formatter, deployment_id):
29
+ """Show deployment details."""
30
+ data = await client.get(f"/api/v1/deployments/{deployment_id}")
31
+ formatter.item(data)
32
+
33
+
34
+ @deployments.command("delete")
35
+ @click.argument("deployment_id")
36
+ @click.option("--force", is_flag=True, help="Skip confirmation")
37
+ @api_command
38
+ async def delete_deployment(client, formatter, deployment_id, force):
39
+ """Delete a deployment."""
40
+ if not force:
41
+ click.confirm(f"Delete deployment {deployment_id}?", abort=True)
42
+ data = await client.delete(f"/api/v1/deployments/{deployment_id}")
43
+ formatter.success(data, message=f"Deployment {deployment_id} deleted.")
44
+
45
+
46
+ @deployments.command("history")
47
+ @click.argument("project_name")
48
+ @click.option("--limit", default=10, show_default=True)
49
+ @api_command
50
+ async def deployment_history(client, formatter, project_name, limit):
51
+ """Show deployment history for a project."""
52
+ data = await client.get(
53
+ f"/api/v1/deployments/history/{project_name}",
54
+ params={"per_page": limit},
55
+ )
56
+ items = data if isinstance(data, list) else data.get("deployments", data.get("items", []))
57
+ formatter.list_items(
58
+ items,
59
+ columns=["deployment_id", "environment", "status", "version", "deployed_at"],
60
+ title=f"Deployment History: {project_name}",
61
+ )
62
+
63
+
64
+ @deployments.command("rollback")
65
+ @click.argument("deployment_id")
66
+ @click.option("--force", is_flag=True, help="Skip confirmation")
67
+ @api_command
68
+ async def rollback_deployment(client, formatter, deployment_id, force):
69
+ """Rollback to a previous deployment."""
70
+ if not force:
71
+ click.confirm(f"Rollback to deployment {deployment_id}?", abort=True)
72
+ data = await client.post(f"/api/v1/deployments/rollback/{deployment_id}")
73
+ formatter.success(data, message=f"Rolled back to deployment {deployment_id}.")
@@ -0,0 +1,76 @@
1
+ import click
2
+ from daita_cli.command_helpers import api_command
3
+
4
+
5
+ @click.group(invoke_without_command=True)
6
+ @click.option("--limit", default=10, show_default=True)
7
+ @click.option("--status", type=click.Choice(["queued", "running", "completed", "failed", "cancelled"]))
8
+ @click.option("--type", "target_type", type=click.Choice(["agent", "workflow"]))
9
+ @click.pass_context
10
+ def executions(ctx, limit, status, target_type):
11
+ """Manage executions."""
12
+ if ctx.invoked_subcommand is None:
13
+ # Backward compat: `daita executions` with no subcommand → list
14
+ ctx.invoke(list_executions, limit=limit, status=status, target_type=target_type)
15
+
16
+
17
+ @executions.command("list")
18
+ @click.option("--limit", default=10, show_default=True)
19
+ @click.option("--status", type=click.Choice(["queued", "running", "completed", "failed", "cancelled"]))
20
+ @click.option("--type", "target_type", type=click.Choice(["agent", "workflow"]))
21
+ @api_command
22
+ async def list_executions(client, formatter, limit, status, target_type):
23
+ """List executions."""
24
+ params = {"limit": limit}
25
+ if status:
26
+ params["status"] = status
27
+ if target_type:
28
+ params["target_type"] = target_type
29
+ data = await client.get("/api/v1/autonomous/executions", params=params)
30
+ items = data if isinstance(data, list) else data.get("executions", data.get("items", []))
31
+ formatter.list_items(
32
+ items,
33
+ columns=["execution_id", "target_name", "target_type", "status", "created_at", "duration_ms"],
34
+ title="Executions",
35
+ )
36
+
37
+
38
+ @executions.command("show")
39
+ @click.argument("execution_id")
40
+ @api_command
41
+ async def show_execution(client, formatter, execution_id):
42
+ """Show execution details."""
43
+ data = await client.get(f"/api/v1/autonomous/executions/{execution_id}")
44
+ formatter.item(data)
45
+
46
+
47
+ @executions.command("logs")
48
+ @click.argument("execution_id")
49
+ @click.option("--follow", "-f", is_flag=True, help="Poll until complete")
50
+ @api_command
51
+ async def execution_logs(client, formatter, execution_id, follow):
52
+ """Show logs for an execution."""
53
+ import asyncio
54
+ import sys
55
+
56
+ if follow:
57
+ while True:
58
+ data = await client.get(f"/api/v1/autonomous/executions/{execution_id}")
59
+ formatter.item(data, fields=["execution_id", "status", "created_at", "duration_ms", "error"])
60
+ if data.get("status") in ("completed", "success", "failed", "error", "cancelled"):
61
+ break
62
+ if not formatter.is_json:
63
+ print(" polling...")
64
+ await asyncio.sleep(2)
65
+ else:
66
+ data = await client.get(f"/api/v1/autonomous/executions/{execution_id}")
67
+ formatter.item(data)
68
+
69
+
70
+ @executions.command("cancel")
71
+ @click.argument("execution_id")
72
+ @api_command
73
+ async def cancel_execution(client, formatter, execution_id):
74
+ """Cancel a running execution."""
75
+ data = await client.delete(f"/api/v1/autonomous/executions/{execution_id}")
76
+ formatter.success(data, message=f"Execution {execution_id} cancelled.")