orchid-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.
- orchid_cli/__init__.py +1 -0
- orchid_cli/bootstrap.py +186 -0
- orchid_cli/commands/__init__.py +1 -0
- orchid_cli/commands/chat.py +428 -0
- orchid_cli/commands/config.py +38 -0
- orchid_cli/commands/index.py +45 -0
- orchid_cli/commands/skill.py +539 -0
- orchid_cli/main.py +30 -0
- orchid_cli-0.1.0.dist-info/METADATA +20 -0
- orchid_cli-0.1.0.dist-info/RECORD +13 -0
- orchid_cli-0.1.0.dist-info/WHEEL +4 -0
- orchid_cli-0.1.0.dist-info/entry_points.txt +2 -0
- orchid_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
orchid_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Orchid CLI
|
orchid_cli/bootstrap.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared bootstrapping — load config, build graph, initialize persistence.
|
|
3
|
+
|
|
4
|
+
Mirrors the lifespan logic from orchid-api but without FastAPI.
|
|
5
|
+
SQLite storage is always initialized by default (no external DB required).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from orchid_ai.config.loader import load_config
|
|
17
|
+
from orchid_ai.config.schema import AgentsConfig
|
|
18
|
+
from orchid_ai.core.repository import VectorReader, VectorStoreAdmin
|
|
19
|
+
from orchid_ai.graph.graph import build_graph
|
|
20
|
+
from orchid_ai.persistence.base import ChatStorage
|
|
21
|
+
from orchid_ai.persistence.factory import build_chat_storage
|
|
22
|
+
from orchid_ai.rag.factory import build_reader
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# ── YAML key → env var mapping (same as orchid-api/settings.py) ──────
|
|
27
|
+
_YAML_TO_ENV: dict[tuple[str, str], str] = {
|
|
28
|
+
("agents", "config_path"): "AGENTS_CONFIG_PATH",
|
|
29
|
+
("llm", "model"): "LITELLM_MODEL",
|
|
30
|
+
("llm", "ollama_api_base"): "OLLAMA_API_BASE",
|
|
31
|
+
("llm", "groq_api_key"): "GROQ_API_KEY",
|
|
32
|
+
("llm", "gemini_api_key"): "GEMINI_API_KEY",
|
|
33
|
+
("llm", "anthropic_api_key"): "ANTHROPIC_API_KEY",
|
|
34
|
+
("llm", "openai_api_key"): "OPENAI_API_KEY",
|
|
35
|
+
("auth", "dev_bypass"): "DEV_AUTH_BYPASS",
|
|
36
|
+
("auth", "identity_resolver_class"): "IDENTITY_RESOLVER_CLASS",
|
|
37
|
+
("auth", "domain"): "AUTH_DOMAIN",
|
|
38
|
+
("startup", "hook"): "STARTUP_HOOK",
|
|
39
|
+
("rag", "vector_backend"): "VECTOR_BACKEND",
|
|
40
|
+
("rag", "qdrant_url"): "QDRANT_URL",
|
|
41
|
+
("rag", "embedding_model"): "EMBEDDING_MODEL",
|
|
42
|
+
("rag", "openai_api_key"): "OPENAI_API_KEY",
|
|
43
|
+
("rag", "gemini_api_key"): "GEMINI_API_KEY",
|
|
44
|
+
("upload", "vision_model"): "VISION_MODEL",
|
|
45
|
+
("upload", "namespace"): "UPLOAD_NAMESPACE",
|
|
46
|
+
("upload", "max_size_mb"): "UPLOAD_MAX_SIZE_MB",
|
|
47
|
+
("upload", "chunk_size"): "CHUNK_SIZE",
|
|
48
|
+
("upload", "chunk_overlap"): "CHUNK_OVERLAP",
|
|
49
|
+
("storage", "class"): "CHAT_STORAGE_CLASS",
|
|
50
|
+
("storage", "dsn"): "CHAT_DB_DSN",
|
|
51
|
+
("mcp", "catalog_url"): "MCP_CATALOG_URL",
|
|
52
|
+
("mcp", "notifications_url"): "MCP_NOTIFICATIONS_URL",
|
|
53
|
+
("tracing", "langsmith_tracing"): "LANGSMITH_TRACING",
|
|
54
|
+
("tracing", "langsmith_api_key"): "LANGSMITH_API_KEY",
|
|
55
|
+
("tracing", "langsmith_project"): "LANGSMITH_PROJECT",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _apply_yaml_to_env(config_path: str) -> None:
|
|
60
|
+
"""Parse orchid.yml and export values as env vars (if not already set).
|
|
61
|
+
|
|
62
|
+
Storage settings (class, dsn) are skipped — the CLI has its own
|
|
63
|
+
defaults (SQLite at ~/.orchid/chats.db) that are more appropriate
|
|
64
|
+
than Docker-oriented paths often found in orchid.yml.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
import yaml
|
|
68
|
+
|
|
69
|
+
with open(config_path) as f:
|
|
70
|
+
data = yaml.safe_load(f) or {}
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
logger.warning("[CLI] Config file %s not found — ignoring", config_path)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Storage config in orchid.yml is typically Docker-oriented (e.g. /data/chats.db).
|
|
76
|
+
# The CLI defaults to ~/.orchid/chats.db which is always writable.
|
|
77
|
+
_SKIP_SECTIONS = {"storage"}
|
|
78
|
+
|
|
79
|
+
for section, body in data.items():
|
|
80
|
+
if not isinstance(body, dict) or section in _SKIP_SECTIONS:
|
|
81
|
+
continue
|
|
82
|
+
for key, value in body.items():
|
|
83
|
+
env_var = _YAML_TO_ENV.get((section, key))
|
|
84
|
+
if env_var and env_var not in os.environ:
|
|
85
|
+
os.environ[env_var] = str(value)
|
|
86
|
+
|
|
87
|
+
logger.debug("[CLI] Applied YAML config from %s", config_path)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Defaults — SQLite storage ships with orchid, no external DB needed
|
|
91
|
+
DEFAULT_STORAGE_CLASS = "orchid_ai.persistence.sqlite.SQLiteChatStorage"
|
|
92
|
+
DEFAULT_STORAGE_DSN = "~/.orchid/chats.db"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class OrchidContext:
|
|
97
|
+
"""Runtime context for CLI operations."""
|
|
98
|
+
|
|
99
|
+
graph: Any
|
|
100
|
+
reader: VectorReader
|
|
101
|
+
chat_repo: ChatStorage
|
|
102
|
+
config: AgentsConfig
|
|
103
|
+
model: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def bootstrap(
|
|
107
|
+
config_path: str,
|
|
108
|
+
*,
|
|
109
|
+
model: str = "",
|
|
110
|
+
vector_backend: str = "",
|
|
111
|
+
qdrant_url: str = "",
|
|
112
|
+
embedding_model: str = "",
|
|
113
|
+
chat_storage_class: str = "",
|
|
114
|
+
chat_db_dsn: str = "",
|
|
115
|
+
) -> OrchidContext:
|
|
116
|
+
"""
|
|
117
|
+
Load config, build reader, build graph — return a ready-to-use context.
|
|
118
|
+
|
|
119
|
+
Chat storage is always initialized (defaults to SQLite at ~/.orchid/chats.db).
|
|
120
|
+
"""
|
|
121
|
+
# Apply YAML config to environment — mirrors orchid-api/settings.py:_apply_yaml_config()
|
|
122
|
+
if config_path:
|
|
123
|
+
os.environ.setdefault("ORCHID_CONFIG", config_path)
|
|
124
|
+
_apply_yaml_to_env(config_path)
|
|
125
|
+
|
|
126
|
+
# Load YAML agent config
|
|
127
|
+
agents_config_path = os.environ.get("AGENTS_CONFIG_PATH", "agents.yaml")
|
|
128
|
+
agents_config = load_config(agents_config_path)
|
|
129
|
+
|
|
130
|
+
# Resolve defaults from env
|
|
131
|
+
resolved_model = model or os.environ.get("LITELLM_MODEL", "ollama/llama3.2")
|
|
132
|
+
resolved_backend = vector_backend or os.environ.get("VECTOR_BACKEND", "qdrant")
|
|
133
|
+
resolved_qdrant_url = qdrant_url or os.environ.get("QDRANT_URL", "http://qdrant:6333")
|
|
134
|
+
resolved_embedding = embedding_model or os.environ.get("EMBEDDING_MODEL", "text-embedding-3-small")
|
|
135
|
+
|
|
136
|
+
# Build reader
|
|
137
|
+
reader = build_reader(
|
|
138
|
+
vector_backend=resolved_backend,
|
|
139
|
+
qdrant_url=resolved_qdrant_url,
|
|
140
|
+
embedding_model=resolved_embedding,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Ensure collections
|
|
144
|
+
if isinstance(reader, VectorStoreAdmin):
|
|
145
|
+
namespaces = [a.rag.namespace for a in agents_config.agents.values() if a.rag.enabled and a.rag.namespace]
|
|
146
|
+
if namespaces:
|
|
147
|
+
await reader.ensure_collections([*namespaces, "uploads"])
|
|
148
|
+
|
|
149
|
+
# Chat persistence — always initialized, defaults to SQLite
|
|
150
|
+
resolved_storage_class = chat_storage_class or os.environ.get("CHAT_STORAGE_CLASS", "") or DEFAULT_STORAGE_CLASS
|
|
151
|
+
resolved_dsn = chat_db_dsn or os.environ.get("CHAT_DB_DSN", "") or DEFAULT_STORAGE_DSN
|
|
152
|
+
chat_repo = build_chat_storage(class_path=resolved_storage_class, dsn=resolved_dsn)
|
|
153
|
+
await chat_repo.init_db()
|
|
154
|
+
|
|
155
|
+
# Build graph
|
|
156
|
+
graph = build_graph(
|
|
157
|
+
config=agents_config,
|
|
158
|
+
default_model=resolved_model,
|
|
159
|
+
reader=reader,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
logger.info(
|
|
163
|
+
"[CLI] Ready — model=%s, backend=%s, storage=%s, agents=%s",
|
|
164
|
+
resolved_model,
|
|
165
|
+
resolved_backend,
|
|
166
|
+
resolved_storage_class.rsplit(".", 1)[-1],
|
|
167
|
+
list(agents_config.agents.keys()),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return OrchidContext(
|
|
171
|
+
graph=graph,
|
|
172
|
+
reader=reader,
|
|
173
|
+
chat_repo=chat_repo,
|
|
174
|
+
config=agents_config,
|
|
175
|
+
model=resolved_model,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@asynccontextmanager
|
|
180
|
+
async def cli_context(config_path: str, *, model: str = ""):
|
|
181
|
+
"""Bootstrap and ensure clean shutdown (closes aiosqlite before event loop exits)."""
|
|
182
|
+
ctx = await bootstrap(config_path, model=model)
|
|
183
|
+
try:
|
|
184
|
+
yield ctx
|
|
185
|
+
finally:
|
|
186
|
+
await ctx.chat_repo.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# CLI commands package
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chat commands — full CRUD + message send with persistence.
|
|
3
|
+
|
|
4
|
+
Mirrors orchid-api chat endpoints:
|
|
5
|
+
orchid chat create [--title "My Chat"]
|
|
6
|
+
orchid chat list
|
|
7
|
+
orchid chat delete <chat_id>
|
|
8
|
+
orchid chat history <chat_id>
|
|
9
|
+
orchid chat send <chat_id> "message"
|
|
10
|
+
orchid chat interactive [--chat <chat_id>]
|
|
11
|
+
orchid chat rename <chat_id> "new title"
|
|
12
|
+
orchid chat share <chat_id>
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
from orchid_ai.core.state import AuthContext
|
|
26
|
+
|
|
27
|
+
from ..bootstrap import cli_context
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(help="Chat management and messaging", no_args_is_help=True)
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
# CLI uses a fixed auth context — no OAuth needed
|
|
33
|
+
_CLI_AUTH = AuthContext(
|
|
34
|
+
access_token="cli-token",
|
|
35
|
+
tenant_key="cli",
|
|
36
|
+
user_id="cli-user",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── Chat CRUD ───────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def create(
|
|
45
|
+
title: str = typer.Option("New chat", "--title", "-t", help="Chat title"),
|
|
46
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
47
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
48
|
+
):
|
|
49
|
+
"""Create a new chat session."""
|
|
50
|
+
asyncio.run(_create(title, config, model))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _create(title: str, config_path: str, model: str) -> None:
|
|
54
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
55
|
+
session = await ctx.chat_repo.create_chat(
|
|
56
|
+
tenant_id=_CLI_AUTH.tenant_key,
|
|
57
|
+
user_id=_CLI_AUTH.user_id,
|
|
58
|
+
title=title,
|
|
59
|
+
)
|
|
60
|
+
console.print(f"[bold green]Created:[/bold green] {session.id}")
|
|
61
|
+
console.print(f" Title: {session.title}")
|
|
62
|
+
console.print(f" Created: {session.created_at.isoformat()}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("list")
|
|
66
|
+
def list_chats(
|
|
67
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
68
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
69
|
+
):
|
|
70
|
+
"""List all chat sessions."""
|
|
71
|
+
asyncio.run(_list_chats(config, model))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _list_chats(config_path: str, model: str) -> None:
|
|
75
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
76
|
+
sessions = await ctx.chat_repo.list_chats(
|
|
77
|
+
tenant_id=_CLI_AUTH.tenant_key,
|
|
78
|
+
user_id=_CLI_AUTH.user_id,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if not sessions:
|
|
82
|
+
console.print("[dim]No chats found. Use 'orchid chat create' to start one.[/dim]")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
table = Table(title="Chat Sessions")
|
|
86
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
87
|
+
table.add_column("Title", style="white")
|
|
88
|
+
table.add_column("Messages", justify="right")
|
|
89
|
+
table.add_column("Updated", style="dim")
|
|
90
|
+
table.add_column("Shared", justify="center")
|
|
91
|
+
|
|
92
|
+
for s in sessions:
|
|
93
|
+
messages = await ctx.chat_repo.get_messages(s.id, limit=1000)
|
|
94
|
+
table.add_row(
|
|
95
|
+
s.id[:12] + "…",
|
|
96
|
+
s.title[:40],
|
|
97
|
+
str(len(messages)),
|
|
98
|
+
s.updated_at.strftime("%Y-%m-%d %H:%M"),
|
|
99
|
+
"✓" if s.is_shared else "",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
console.print(table)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def delete(
|
|
107
|
+
chat_id: str = typer.Argument(..., help="Chat ID (or prefix)"),
|
|
108
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
109
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
110
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
111
|
+
):
|
|
112
|
+
"""Delete a chat session and all its messages."""
|
|
113
|
+
asyncio.run(_delete(chat_id, config, model, force))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _delete(chat_id: str, config_path: str, model: str, force: bool) -> None:
|
|
117
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
118
|
+
resolved_id = await _resolve_chat_id(ctx, chat_id)
|
|
119
|
+
if not resolved_id:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
chat = await ctx.chat_repo.get_chat(resolved_id)
|
|
123
|
+
if not force:
|
|
124
|
+
confirm = typer.confirm(f"Delete chat '{chat.title}' ({resolved_id[:12]}…)?")
|
|
125
|
+
if not confirm:
|
|
126
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
await ctx.chat_repo.delete_chat(resolved_id)
|
|
130
|
+
console.print(f"[bold red]Deleted:[/bold red] {resolved_id[:12]}…")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command()
|
|
134
|
+
def history(
|
|
135
|
+
chat_id: str = typer.Argument(..., help="Chat ID (or prefix)"),
|
|
136
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Max messages to show"),
|
|
137
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
138
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
139
|
+
):
|
|
140
|
+
"""Show message history for a chat."""
|
|
141
|
+
asyncio.run(_history(chat_id, limit, config, model))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def _history(chat_id: str, limit: int, config_path: str, model: str) -> None:
|
|
145
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
146
|
+
resolved_id = await _resolve_chat_id(ctx, chat_id)
|
|
147
|
+
if not resolved_id:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
chat = await ctx.chat_repo.get_chat(resolved_id)
|
|
151
|
+
messages = await ctx.chat_repo.get_messages(resolved_id, limit=limit)
|
|
152
|
+
|
|
153
|
+
console.print(f"[bold]{chat.title}[/bold] ({resolved_id[:12]}…)")
|
|
154
|
+
console.print()
|
|
155
|
+
|
|
156
|
+
if not messages:
|
|
157
|
+
console.print("[dim]No messages yet.[/dim]")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
for msg in messages:
|
|
161
|
+
if msg.role == "user":
|
|
162
|
+
console.print(f"[bold cyan]You:[/bold cyan] {msg.content}")
|
|
163
|
+
elif msg.role == "assistant":
|
|
164
|
+
console.print(f"[bold green]Assistant:[/bold green] {msg.content}")
|
|
165
|
+
if msg.agents_used:
|
|
166
|
+
console.print(f" [dim]Agents: {', '.join(msg.agents_used)}[/dim]")
|
|
167
|
+
else:
|
|
168
|
+
console.print(f"[dim]{msg.role}: {msg.content}[/dim]")
|
|
169
|
+
console.print()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def rename(
|
|
174
|
+
chat_id: str = typer.Argument(..., help="Chat ID (or prefix)"),
|
|
175
|
+
title: str = typer.Argument(..., help="New title"),
|
|
176
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
177
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
178
|
+
):
|
|
179
|
+
"""Rename a chat session."""
|
|
180
|
+
asyncio.run(_rename(chat_id, title, config, model))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def _rename(chat_id: str, title: str, config_path: str, model: str) -> None:
|
|
184
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
185
|
+
resolved_id = await _resolve_chat_id(ctx, chat_id)
|
|
186
|
+
if not resolved_id:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
await ctx.chat_repo.update_title(resolved_id, title)
|
|
190
|
+
console.print(f"[bold]Renamed:[/bold] {resolved_id[:12]}… → {title}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.command()
|
|
194
|
+
def share(
|
|
195
|
+
chat_id: str = typer.Argument(..., help="Chat ID (or prefix)"),
|
|
196
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
197
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
198
|
+
):
|
|
199
|
+
"""Mark a chat as shared."""
|
|
200
|
+
asyncio.run(_share(chat_id, config, model))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def _share(chat_id: str, config_path: str, model: str) -> None:
|
|
204
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
205
|
+
resolved_id = await _resolve_chat_id(ctx, chat_id)
|
|
206
|
+
if not resolved_id:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
await ctx.chat_repo.mark_shared(resolved_id)
|
|
210
|
+
console.print(f"[bold]Shared:[/bold] {resolved_id[:12]}…")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ── Messaging ───────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@app.command()
|
|
217
|
+
def send(
|
|
218
|
+
chat_id: str = typer.Argument(..., help="Chat ID (or prefix)"),
|
|
219
|
+
message: str = typer.Argument(..., help="The message to send"),
|
|
220
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
221
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
222
|
+
):
|
|
223
|
+
"""Send a message to a chat and print the response."""
|
|
224
|
+
asyncio.run(_send(chat_id, message, config, model))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def _send(chat_id: str, message: str, config_path: str, model: str) -> None:
|
|
228
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
229
|
+
resolved_id = await _resolve_chat_id(ctx, chat_id)
|
|
230
|
+
if not resolved_id:
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
response_text, agents_used = await _send_message(ctx, resolved_id, message)
|
|
234
|
+
|
|
235
|
+
console.print()
|
|
236
|
+
console.print(response_text)
|
|
237
|
+
if agents_used:
|
|
238
|
+
console.print(f"\n[dim]Agents used: {', '.join(agents_used)}[/dim]")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.command()
|
|
242
|
+
def interactive(
|
|
243
|
+
chat_id: Optional[str] = typer.Argument(None, help="Chat ID to resume (or prefix). Creates new if omitted."),
|
|
244
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
245
|
+
model: str = typer.Option("", "--model", "-m", help="Override LLM model"),
|
|
246
|
+
):
|
|
247
|
+
"""Start an interactive chat REPL with full persistence."""
|
|
248
|
+
asyncio.run(_interactive(chat_id, config, model))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def _interactive(chat_id: str | None, config_path: str, model: str) -> None:
|
|
252
|
+
async with cli_context(config_path, model=model) as ctx:
|
|
253
|
+
# Resolve or create a chat
|
|
254
|
+
if chat_id:
|
|
255
|
+
resolved_id = await _resolve_chat_id(ctx, chat_id)
|
|
256
|
+
if not resolved_id:
|
|
257
|
+
return
|
|
258
|
+
chat = await ctx.chat_repo.get_chat(resolved_id)
|
|
259
|
+
console.print(f"[bold]Resuming:[/bold] {chat.title} ({resolved_id[:12]}…)")
|
|
260
|
+
else:
|
|
261
|
+
chat = await ctx.chat_repo.create_chat(
|
|
262
|
+
tenant_id=_CLI_AUTH.tenant_key,
|
|
263
|
+
user_id=_CLI_AUTH.user_id,
|
|
264
|
+
title="Interactive session",
|
|
265
|
+
)
|
|
266
|
+
resolved_id = chat.id
|
|
267
|
+
console.print(f"[bold]New chat:[/bold] {resolved_id[:12]}…")
|
|
268
|
+
|
|
269
|
+
console.print()
|
|
270
|
+
console.print("[bold]Orchid Interactive Chat[/bold]")
|
|
271
|
+
console.print("Commands: /quit, /switch <id>, /list, /new [title], /history, /rename <title>")
|
|
272
|
+
console.print()
|
|
273
|
+
|
|
274
|
+
current_chat_id = resolved_id
|
|
275
|
+
|
|
276
|
+
while True:
|
|
277
|
+
try:
|
|
278
|
+
user_input = console.input("[bold cyan]You:[/bold cyan] ")
|
|
279
|
+
except (EOFError, KeyboardInterrupt):
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
stripped = user_input.strip()
|
|
283
|
+
if not stripped:
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
# Handle slash commands
|
|
287
|
+
if stripped.startswith("/"):
|
|
288
|
+
parts = stripped.split(maxsplit=1)
|
|
289
|
+
cmd = parts[0].lower()
|
|
290
|
+
arg = parts[1] if len(parts) > 1 else ""
|
|
291
|
+
|
|
292
|
+
if cmd in ("/quit", "/exit", "/q"):
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
elif cmd == "/list":
|
|
296
|
+
sessions = await ctx.chat_repo.list_chats(
|
|
297
|
+
tenant_id=_CLI_AUTH.tenant_key,
|
|
298
|
+
user_id=_CLI_AUTH.user_id,
|
|
299
|
+
)
|
|
300
|
+
if not sessions:
|
|
301
|
+
console.print("[dim]No chats.[/dim]")
|
|
302
|
+
else:
|
|
303
|
+
for s in sessions:
|
|
304
|
+
marker = " [bold]← current[/bold]" if s.id == current_chat_id else ""
|
|
305
|
+
console.print(f" {s.id[:12]}… {s.title}{marker}")
|
|
306
|
+
console.print()
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
elif cmd == "/switch":
|
|
310
|
+
if not arg:
|
|
311
|
+
console.print("[red]Usage: /switch <chat_id>[/red]")
|
|
312
|
+
continue
|
|
313
|
+
new_id = await _resolve_chat_id(ctx, arg)
|
|
314
|
+
if new_id:
|
|
315
|
+
current_chat_id = new_id
|
|
316
|
+
chat = await ctx.chat_repo.get_chat(current_chat_id)
|
|
317
|
+
console.print(f"[bold]Switched to:[/bold] {chat.title} ({current_chat_id[:12]}…)\n")
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
elif cmd == "/new":
|
|
321
|
+
title = arg or "Interactive session"
|
|
322
|
+
new_chat = await ctx.chat_repo.create_chat(
|
|
323
|
+
tenant_id=_CLI_AUTH.tenant_key,
|
|
324
|
+
user_id=_CLI_AUTH.user_id,
|
|
325
|
+
title=title,
|
|
326
|
+
)
|
|
327
|
+
current_chat_id = new_chat.id
|
|
328
|
+
console.print(f"[bold green]New chat:[/bold green] {current_chat_id[:12]}… — {title}\n")
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
elif cmd == "/history":
|
|
332
|
+
messages = await ctx.chat_repo.get_messages(current_chat_id, limit=20)
|
|
333
|
+
if not messages:
|
|
334
|
+
console.print("[dim]No messages yet.[/dim]\n")
|
|
335
|
+
else:
|
|
336
|
+
for msg in messages:
|
|
337
|
+
if msg.role == "user":
|
|
338
|
+
console.print(f" [cyan]You:[/cyan] {msg.content[:80]}")
|
|
339
|
+
elif msg.role == "assistant":
|
|
340
|
+
console.print(f" [green]Asst:[/green] {msg.content[:80]}")
|
|
341
|
+
console.print()
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
elif cmd == "/rename":
|
|
345
|
+
if not arg:
|
|
346
|
+
console.print("[red]Usage: /rename <new title>[/red]")
|
|
347
|
+
continue
|
|
348
|
+
await ctx.chat_repo.update_title(current_chat_id, arg)
|
|
349
|
+
console.print(f"[bold]Renamed:[/bold] {arg}\n")
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
else:
|
|
353
|
+
console.print(f"[red]Unknown command: {cmd}[/red]")
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# Send message
|
|
357
|
+
response_text, agents_used = await _send_message(ctx, current_chat_id, stripped)
|
|
358
|
+
console.print(f"\n[bold green]Assistant:[/bold green] {response_text}")
|
|
359
|
+
if agents_used:
|
|
360
|
+
console.print(f" [dim]Agents: {', '.join(agents_used)}[/dim]")
|
|
361
|
+
console.print()
|
|
362
|
+
|
|
363
|
+
console.print("\n[dim]Session ended.[/dim]")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── Helpers ─────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
async def _send_message(ctx, chat_id: str, message: str) -> tuple[str, list[str]]:
|
|
370
|
+
"""Send a message through the graph, persist to storage, return (response, agents_used)."""
|
|
371
|
+
# Load history
|
|
372
|
+
history_rows = await ctx.chat_repo.get_messages(chat_id, limit=50)
|
|
373
|
+
history_messages = []
|
|
374
|
+
for row in history_rows:
|
|
375
|
+
if row.role == "user":
|
|
376
|
+
history_messages.append(HumanMessage(content=row.content, id=row.id))
|
|
377
|
+
elif row.role == "assistant":
|
|
378
|
+
history_messages.append(AIMessage(content=row.content, id=row.id))
|
|
379
|
+
|
|
380
|
+
initial_state = {
|
|
381
|
+
"messages": history_messages + [HumanMessage(content=message)],
|
|
382
|
+
"auth_context": _CLI_AUTH,
|
|
383
|
+
"chat_id": chat_id,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
result = await ctx.graph.ainvoke(initial_state)
|
|
387
|
+
|
|
388
|
+
response_text = result.get("final_response", "No response generated.")
|
|
389
|
+
agents_used = result.get("active_agents", [])
|
|
390
|
+
|
|
391
|
+
# Persist original message + response
|
|
392
|
+
await ctx.chat_repo.add_message(chat_id, "user", message)
|
|
393
|
+
await ctx.chat_repo.add_message(chat_id, "assistant", response_text, agents_used=agents_used)
|
|
394
|
+
|
|
395
|
+
# Auto-title from first message
|
|
396
|
+
if not history_rows:
|
|
397
|
+
title = message[:50].strip()
|
|
398
|
+
if len(message) > 50:
|
|
399
|
+
title += "…"
|
|
400
|
+
await ctx.chat_repo.update_title(chat_id, title)
|
|
401
|
+
|
|
402
|
+
return response_text, agents_used
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
async def _resolve_chat_id(ctx, chat_id_prefix: str) -> str | None:
|
|
406
|
+
"""Resolve a chat ID prefix to a full ID. Prints error if not found."""
|
|
407
|
+
# Try exact match first
|
|
408
|
+
chat = await ctx.chat_repo.get_chat(chat_id_prefix)
|
|
409
|
+
if chat and chat.user_id == _CLI_AUTH.user_id:
|
|
410
|
+
return chat.id
|
|
411
|
+
|
|
412
|
+
# Try prefix match
|
|
413
|
+
sessions = await ctx.chat_repo.list_chats(
|
|
414
|
+
tenant_id=_CLI_AUTH.tenant_key,
|
|
415
|
+
user_id=_CLI_AUTH.user_id,
|
|
416
|
+
)
|
|
417
|
+
matches = [s for s in sessions if s.id.startswith(chat_id_prefix)]
|
|
418
|
+
|
|
419
|
+
if len(matches) == 1:
|
|
420
|
+
return matches[0].id
|
|
421
|
+
elif len(matches) > 1:
|
|
422
|
+
console.print(f"[red]Ambiguous prefix '{chat_id_prefix}' — matches {len(matches)} chats:[/red]")
|
|
423
|
+
for s in matches:
|
|
424
|
+
console.print(f" {s.id[:12]}… {s.title}")
|
|
425
|
+
return None
|
|
426
|
+
else:
|
|
427
|
+
console.print(f"[red]Chat not found: {chat_id_prefix}[/red]")
|
|
428
|
+
return None
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config command — validate YAML configuration files.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
orchid config validate examples/basketball/orchid.yml
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from orchid_ai.config.loader import load_config
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Configuration management", no_args_is_help=True)
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def validate(
|
|
21
|
+
config_path: str = typer.Argument(..., help="Path to agents.yaml config file"),
|
|
22
|
+
):
|
|
23
|
+
"""Validate an agents.yaml configuration file."""
|
|
24
|
+
try:
|
|
25
|
+
config = load_config(config_path)
|
|
26
|
+
console.print(f"[green]Valid[/green] — {len(config.agents)} agent(s) configured:")
|
|
27
|
+
for name, agent_cfg in config.agents.items():
|
|
28
|
+
desc = agent_cfg.description[:60] if agent_cfg.description else "(no description)"
|
|
29
|
+
cls = agent_cfg.class_path or "GenericAgent"
|
|
30
|
+
console.print(f" [bold]{name}[/bold] ({cls}) — {desc}")
|
|
31
|
+
|
|
32
|
+
if config.supervisor:
|
|
33
|
+
console.print(f"\n Supervisor: assistant_name={config.supervisor.assistant_name!r}")
|
|
34
|
+
if config.skills:
|
|
35
|
+
console.print(f" Orchestrator skills: {len(config.skills)}")
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
console.print(f"[red]Invalid[/red] — {exc}")
|
|
38
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Index command — seed the vector store with data.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
orchid index seed --config examples/basketball/orchid.yml --tenant default
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from orchid_ai.core.repository import VectorWriter
|
|
16
|
+
from orchid_ai.rag.indexer import StaticIndexer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Vector store indexing", no_args_is_help=True)
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def seed(
|
|
25
|
+
config: str = typer.Option("", "--config", "-c", help="Path to orchid.yml"),
|
|
26
|
+
tenant: str = typer.Option("default", "--tenant", "-t", help="Tenant ID for indexing"),
|
|
27
|
+
):
|
|
28
|
+
"""Seed the vector store with static data."""
|
|
29
|
+
asyncio.run(_seed(config, tenant))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _seed(config_path: str, tenant: str) -> None:
|
|
33
|
+
from ..bootstrap import cli_context
|
|
34
|
+
|
|
35
|
+
async with cli_context(config_path) as ctx:
|
|
36
|
+
if not isinstance(ctx.reader, VectorWriter):
|
|
37
|
+
console.print("[red]Error:[/red] Vector store does not support writing (backend may be 'null')")
|
|
38
|
+
raise typer.Exit(code=1)
|
|
39
|
+
|
|
40
|
+
indexer = StaticIndexer(writer=ctx.reader)
|
|
41
|
+
counts = await indexer.index_all(tenant_key=tenant)
|
|
42
|
+
|
|
43
|
+
console.print(f"[green]Indexed[/green] for tenant={tenant}:")
|
|
44
|
+
for namespace, count in counts.items():
|
|
45
|
+
console.print(f" {namespace}: {count} document(s)")
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill command — generate Claude Code skills from Orchid agent configuration.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
orchid skill generate examples/basketball/agents.yaml --output .claude/skills
|
|
6
|
+
orchid skill generate examples/helpdesk/config/agents.yaml -o ./skills --include basketball,psychologist
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib
|
|
12
|
+
import inspect
|
|
13
|
+
import shutil
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
from orchid_ai.config.loader import load_config
|
|
20
|
+
from orchid_ai.config.schema import (
|
|
21
|
+
AgentConfig,
|
|
22
|
+
AgentsConfig,
|
|
23
|
+
BuiltinToolConfig,
|
|
24
|
+
OrchestratorSkillConfig,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(help="Generate Claude Code skills from Orchid config", no_args_is_help=True)
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ── Public command ────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def generate(
|
|
36
|
+
config_path: str = typer.Argument(..., help="Path to agents.yaml config file"),
|
|
37
|
+
output: str = typer.Option(".claude/skills", "-o", "--output", help="Output directory for generated skills"),
|
|
38
|
+
include: str | None = typer.Option(
|
|
39
|
+
None, "--include", help="Comma-separated agent/skill names to include (default: all)"
|
|
40
|
+
),
|
|
41
|
+
overwrite: bool = typer.Option(False, "--overwrite", help="Overwrite existing skill directories"),
|
|
42
|
+
zip_archive: bool = typer.Option(False, "--zip", help="Create a zip archive of the generated skills"),
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Generate Claude Code skill folders from an Orchid agents.yaml configuration."""
|
|
45
|
+
try:
|
|
46
|
+
config = load_config(config_path)
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
console.print(f"[red]Error loading config:[/red] {exc}")
|
|
49
|
+
raise typer.Exit(code=1)
|
|
50
|
+
|
|
51
|
+
out_dir = Path(output)
|
|
52
|
+
include_set = {n.strip() for n in include.split(",")} if include else None
|
|
53
|
+
|
|
54
|
+
generated: list[str] = []
|
|
55
|
+
skipped: list[str] = []
|
|
56
|
+
|
|
57
|
+
# Generate agent skills
|
|
58
|
+
for agent_name, agent_cfg in config.agents.items():
|
|
59
|
+
if include_set and agent_name not in include_set:
|
|
60
|
+
continue
|
|
61
|
+
skill_dir = out_dir / agent_name
|
|
62
|
+
if skill_dir.exists() and not overwrite:
|
|
63
|
+
skipped.append(agent_name)
|
|
64
|
+
continue
|
|
65
|
+
_generate_agent_skill(skill_dir, agent_name, agent_cfg, config)
|
|
66
|
+
generated.append(agent_name)
|
|
67
|
+
|
|
68
|
+
# Generate orchestrator skills
|
|
69
|
+
for skill_name, skill_cfg in config.skills.items():
|
|
70
|
+
if include_set and skill_name not in include_set:
|
|
71
|
+
continue
|
|
72
|
+
skill_dir = out_dir / skill_name
|
|
73
|
+
if skill_dir.exists() and not overwrite:
|
|
74
|
+
skipped.append(skill_name)
|
|
75
|
+
continue
|
|
76
|
+
_generate_orchestrator_skill(skill_dir, skill_name, skill_cfg, config)
|
|
77
|
+
generated.append(skill_name)
|
|
78
|
+
|
|
79
|
+
# Summary
|
|
80
|
+
if generated:
|
|
81
|
+
console.print(f"\n[green]Generated {len(generated)} skill(s):[/green]")
|
|
82
|
+
for name in generated:
|
|
83
|
+
console.print(f" [bold]{out_dir / name}/SKILL.md[/bold]")
|
|
84
|
+
if skipped:
|
|
85
|
+
console.print(f"\n[yellow]Skipped {len(skipped)} (already exist, use --overwrite):[/yellow]")
|
|
86
|
+
for name in skipped:
|
|
87
|
+
console.print(f" {name}")
|
|
88
|
+
if not generated and not skipped:
|
|
89
|
+
console.print("[yellow]No agents or skills matched the filter.[/yellow]")
|
|
90
|
+
|
|
91
|
+
# Create zip archive if requested
|
|
92
|
+
if zip_archive and generated:
|
|
93
|
+
zip_path = Path(f"{output.rstrip('/')}.zip")
|
|
94
|
+
shutil.make_archive(str(zip_path.with_suffix("")), "zip", root_dir=out_dir.parent, base_dir=out_dir.name)
|
|
95
|
+
console.print(f"\n[green]Archive created:[/green] [bold]{zip_path}[/bold]")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Agent skill generation ────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _generate_agent_skill(
|
|
102
|
+
skill_dir: Path,
|
|
103
|
+
agent_name: str,
|
|
104
|
+
agent_cfg: AgentConfig,
|
|
105
|
+
config: AgentsConfig,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Generate a Claude Code skill directory for one Orchid agent."""
|
|
108
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
# Generate executable scripts for built-in tools
|
|
111
|
+
tool_scripts = _generate_tool_scripts(skill_dir, agent_cfg, config)
|
|
112
|
+
|
|
113
|
+
# Build SKILL.md
|
|
114
|
+
skill_md = _build_agent_skill_md(agent_name, agent_cfg, config, tool_scripts)
|
|
115
|
+
(skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _build_agent_skill_md(
|
|
119
|
+
agent_name: str,
|
|
120
|
+
agent_cfg: AgentConfig,
|
|
121
|
+
config: AgentsConfig,
|
|
122
|
+
tool_scripts: dict[str, _ToolScriptInfo],
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Build the SKILL.md content for an Orchid agent."""
|
|
125
|
+
parts: list[str] = []
|
|
126
|
+
|
|
127
|
+
# ── Frontmatter ──
|
|
128
|
+
description = _clean_description(agent_cfg.description)
|
|
129
|
+
allowed: list[str] = []
|
|
130
|
+
if tool_scripts:
|
|
131
|
+
allowed.append("Bash(python *)")
|
|
132
|
+
frontmatter_lines = [
|
|
133
|
+
"---",
|
|
134
|
+
f"name: {agent_name}",
|
|
135
|
+
f'description: "{_truncate(description, 240)}"',
|
|
136
|
+
]
|
|
137
|
+
if allowed:
|
|
138
|
+
frontmatter_lines.append(f'allowed-tools: "{" ".join(allowed)}"')
|
|
139
|
+
frontmatter_lines.append("---")
|
|
140
|
+
parts.append("\n".join(frontmatter_lines) + "\n")
|
|
141
|
+
|
|
142
|
+
# ── Title + origin ──
|
|
143
|
+
parts.append(f"# {agent_name}\n")
|
|
144
|
+
parts.append(
|
|
145
|
+
f"> Auto-generated from Orchid agent configuration. "
|
|
146
|
+
f"This skill replicates the knowledge and instructions of the `{agent_name}` agent.\n"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# ── Agent system prompt (the core value) ──
|
|
150
|
+
parts.append("## Instructions\n")
|
|
151
|
+
parts.append(agent_cfg.prompt.strip() + "\n")
|
|
152
|
+
|
|
153
|
+
# ── Built-in tools as executable scripts ──
|
|
154
|
+
if tool_scripts:
|
|
155
|
+
parts.append("## Available Tools\n")
|
|
156
|
+
parts.append(
|
|
157
|
+
"This skill includes executable Python scripts for each built-in tool. Run them to get real results.\n"
|
|
158
|
+
)
|
|
159
|
+
for tool_name, info in tool_scripts.items():
|
|
160
|
+
tool_cfg = config.tools.get(tool_name)
|
|
161
|
+
desc = tool_cfg.description if tool_cfg else ""
|
|
162
|
+
parts.append(f"### {tool_name}\n")
|
|
163
|
+
if desc:
|
|
164
|
+
parts.append(f"{desc}\n")
|
|
165
|
+
# Show usage with the actual parameters
|
|
166
|
+
parts.append(f"```bash\npython ${{CLAUDE_SKILL_DIR}}/scripts/{info.script_name} {info.usage_hint}\n```\n")
|
|
167
|
+
if info.parameters:
|
|
168
|
+
parts.append("**Parameters:**\n")
|
|
169
|
+
for param_name, param_desc in info.parameters.items():
|
|
170
|
+
parts.append(f"- `{param_name}`: {param_desc}")
|
|
171
|
+
parts.append("")
|
|
172
|
+
|
|
173
|
+
# ── MCP servers (informational, not portable) ──
|
|
174
|
+
if agent_cfg.mcp_servers:
|
|
175
|
+
parts.append("## External Integrations (Orchid Runtime Required)\n")
|
|
176
|
+
parts.append(
|
|
177
|
+
"The following MCP server integrations are available in the Orchid runtime "
|
|
178
|
+
"but cannot be used directly in Claude Code skills.\n"
|
|
179
|
+
)
|
|
180
|
+
for srv in agent_cfg.mcp_servers:
|
|
181
|
+
tool_list = ", ".join(t.name for t in srv.tools) if srv.tools else "(all)"
|
|
182
|
+
parts.append(f"- **{srv.name}** ({srv.transport}): tools = {tool_list}")
|
|
183
|
+
parts.append("")
|
|
184
|
+
|
|
185
|
+
# ── Agent-level skills (workflows) ──
|
|
186
|
+
if agent_cfg.skills:
|
|
187
|
+
parts.append("## Workflows\n")
|
|
188
|
+
parts.append(
|
|
189
|
+
"The original agent supports these multi-step workflows. "
|
|
190
|
+
"Follow these step sequences when the user's request matches.\n"
|
|
191
|
+
)
|
|
192
|
+
for skill_name, skill_cfg in agent_cfg.skills.items():
|
|
193
|
+
parts.append(f"### {skill_name}\n")
|
|
194
|
+
if skill_cfg.description:
|
|
195
|
+
parts.append(f"{skill_cfg.description.strip()}\n")
|
|
196
|
+
parts.append("**Steps:**\n")
|
|
197
|
+
for i, step in enumerate(skill_cfg.steps, 1):
|
|
198
|
+
if step.tool:
|
|
199
|
+
info = tool_scripts.get(step.tool)
|
|
200
|
+
if info:
|
|
201
|
+
parts.append(
|
|
202
|
+
f"{i}. Run `python ${{CLAUDE_SKILL_DIR}}/scripts/{info.script_name} {info.usage_hint}`"
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
src = f" (from {step.source})" if step.source else ""
|
|
206
|
+
parts.append(f"{i}. Call tool `{step.tool}`{src}")
|
|
207
|
+
elif step.agent:
|
|
208
|
+
parts.append(f"{i}. Delegate to agent `{step.agent}`: {step.instruction}")
|
|
209
|
+
parts.append("")
|
|
210
|
+
|
|
211
|
+
# ── RAG context note ──
|
|
212
|
+
if agent_cfg.rag.enabled:
|
|
213
|
+
ns = agent_cfg.rag.namespace or agent_name
|
|
214
|
+
parts.append("## RAG Context (Orchid Runtime Required)\n")
|
|
215
|
+
parts.append(
|
|
216
|
+
f"In the Orchid runtime, this agent retrieves contextual documents from "
|
|
217
|
+
f"the `{ns}` namespace (top-{agent_cfg.rag.k} results). "
|
|
218
|
+
f"This capability is not available in the Claude Code skill.\n"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return "\n".join(parts)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ── Orchestrator skill generation ─────────────────────────────────
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _generate_orchestrator_skill(
|
|
228
|
+
skill_dir: Path,
|
|
229
|
+
skill_name: str,
|
|
230
|
+
skill_cfg: OrchestratorSkillConfig,
|
|
231
|
+
config: AgentsConfig,
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Generate a Claude Code skill directory for an Orchid orchestrator skill."""
|
|
234
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
|
|
236
|
+
skill_md = _build_orchestrator_skill_md(skill_name, skill_cfg, config)
|
|
237
|
+
(skill_dir / "SKILL.md").write_text(skill_md, encoding="utf-8")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _build_orchestrator_skill_md(
|
|
241
|
+
skill_name: str,
|
|
242
|
+
skill_cfg: OrchestratorSkillConfig,
|
|
243
|
+
config: AgentsConfig,
|
|
244
|
+
) -> str:
|
|
245
|
+
"""Build the SKILL.md content for an Orchid orchestrator skill."""
|
|
246
|
+
parts: list[str] = []
|
|
247
|
+
|
|
248
|
+
description = _clean_description(skill_cfg.description)
|
|
249
|
+
parts.append(f'---\nname: {skill_name.replace("_", "-")}\ndescription: "{_truncate(description, 240)}"\n---\n')
|
|
250
|
+
|
|
251
|
+
parts.append(f"# {skill_name.replace('_', ' ').title()}\n")
|
|
252
|
+
parts.append(
|
|
253
|
+
"> Auto-generated from Orchid orchestrator skill. "
|
|
254
|
+
"This is a multi-agent workflow that coordinates several specialists.\n"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if skill_cfg.description:
|
|
258
|
+
parts.append("## Purpose\n")
|
|
259
|
+
parts.append(skill_cfg.description.strip() + "\n")
|
|
260
|
+
|
|
261
|
+
# ── Workflow steps ──
|
|
262
|
+
parts.append("## Workflow Steps\n")
|
|
263
|
+
parts.append("Execute these steps in order. Each step's output feeds into the next.\n")
|
|
264
|
+
for i, step in enumerate(skill_cfg.steps, 1):
|
|
265
|
+
agent_cfg = config.agents.get(step.agent)
|
|
266
|
+
agent_desc = _clean_description(agent_cfg.description) if agent_cfg else ""
|
|
267
|
+
parts.append(f"### Step {i}: {step.agent}\n")
|
|
268
|
+
if agent_desc:
|
|
269
|
+
parts.append(f"*Agent role: {agent_desc}*\n")
|
|
270
|
+
if step.instruction:
|
|
271
|
+
parts.append(f"**Instruction:** {step.instruction}\n")
|
|
272
|
+
# Include the agent's prompt as context
|
|
273
|
+
if agent_cfg:
|
|
274
|
+
parts.append("<details>\n<summary>Agent system prompt</summary>\n")
|
|
275
|
+
parts.append(f"```\n{agent_cfg.prompt.strip()}\n```\n")
|
|
276
|
+
parts.append("</details>\n")
|
|
277
|
+
|
|
278
|
+
# ── Participating agents summary ──
|
|
279
|
+
agent_names = [s.agent for s in skill_cfg.steps]
|
|
280
|
+
parts.append("## Participating Agents\n")
|
|
281
|
+
for name in dict.fromkeys(agent_names): # unique, preserving order
|
|
282
|
+
agent_cfg = config.agents.get(name)
|
|
283
|
+
if agent_cfg:
|
|
284
|
+
desc = _clean_description(agent_cfg.description)
|
|
285
|
+
parts.append(f"- **{name}**: {desc}")
|
|
286
|
+
parts.append("")
|
|
287
|
+
|
|
288
|
+
return "\n".join(parts)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ── Tool script generation ────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class _ToolScriptInfo:
|
|
295
|
+
"""Metadata about a generated tool script."""
|
|
296
|
+
|
|
297
|
+
__slots__ = ("script_name", "usage_hint", "parameters")
|
|
298
|
+
|
|
299
|
+
def __init__(self, script_name: str, usage_hint: str, parameters: dict[str, str]) -> None:
|
|
300
|
+
self.script_name = script_name
|
|
301
|
+
self.usage_hint = usage_hint
|
|
302
|
+
self.parameters = parameters
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _generate_tool_scripts(
|
|
306
|
+
skill_dir: Path,
|
|
307
|
+
agent_cfg: AgentConfig,
|
|
308
|
+
config: AgentsConfig,
|
|
309
|
+
) -> dict[str, _ToolScriptInfo]:
|
|
310
|
+
"""Generate executable Python scripts for each built-in tool.
|
|
311
|
+
|
|
312
|
+
Groups tools from the same source module into a single script file.
|
|
313
|
+
Returns a mapping of tool_name -> _ToolScriptInfo.
|
|
314
|
+
"""
|
|
315
|
+
tool_names = agent_cfg.tools
|
|
316
|
+
if not tool_names:
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
scripts_dir = skill_dir / "scripts"
|
|
320
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
|
|
322
|
+
# Group tools by source module path
|
|
323
|
+
module_tools: dict[str, list[tuple[str, BuiltinToolConfig]]] = {}
|
|
324
|
+
for tool_name in tool_names:
|
|
325
|
+
tool_cfg = config.tools.get(tool_name)
|
|
326
|
+
if not tool_cfg:
|
|
327
|
+
continue
|
|
328
|
+
module_path = tool_cfg.handler.rsplit(".", 1)[0]
|
|
329
|
+
module_tools.setdefault(module_path, []).append((tool_name, tool_cfg))
|
|
330
|
+
|
|
331
|
+
result: dict[str, _ToolScriptInfo] = {}
|
|
332
|
+
|
|
333
|
+
for module_path, tools_in_module in module_tools.items():
|
|
334
|
+
# Read the source file of the module
|
|
335
|
+
source = _read_module_source(module_path)
|
|
336
|
+
if source is None:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
# Determine script filename from the last part of the module path
|
|
340
|
+
module_short_name = module_path.rsplit(".", 1)[-1]
|
|
341
|
+
script_name = f"{module_short_name}.py"
|
|
342
|
+
|
|
343
|
+
# Build the __main__ CLI wrapper
|
|
344
|
+
cli_wrapper = _build_cli_wrapper(tools_in_module)
|
|
345
|
+
|
|
346
|
+
# Strip __future__ annotations from source if present (we re-add it)
|
|
347
|
+
clean_source = _strip_future_annotations(source)
|
|
348
|
+
|
|
349
|
+
script_content = (
|
|
350
|
+
'"""Auto-generated tool script from Orchid agent configuration."""\n'
|
|
351
|
+
"from __future__ import annotations\n\n"
|
|
352
|
+
f"{clean_source}\n\n"
|
|
353
|
+
f"{cli_wrapper}\n"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
(scripts_dir / script_name).write_text(script_content, encoding="utf-8")
|
|
357
|
+
|
|
358
|
+
# Build info for each tool in this module
|
|
359
|
+
for tool_name, tool_cfg in tools_in_module:
|
|
360
|
+
func_name = tool_cfg.handler.rsplit(".", 1)[1]
|
|
361
|
+
params = _extract_parameters(module_path, func_name)
|
|
362
|
+
usage_hint = _build_usage_hint(func_name, params)
|
|
363
|
+
result[tool_name] = _ToolScriptInfo(
|
|
364
|
+
script_name=script_name,
|
|
365
|
+
usage_hint=usage_hint,
|
|
366
|
+
parameters=params,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return result
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _read_module_source(module_path: str) -> str | None:
|
|
373
|
+
"""Read the source code of a Python module by its dotted import path."""
|
|
374
|
+
try:
|
|
375
|
+
module = importlib.import_module(module_path)
|
|
376
|
+
source_file = inspect.getfile(module)
|
|
377
|
+
return Path(source_file).read_text(encoding="utf-8")
|
|
378
|
+
except Exception:
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _strip_future_annotations(source: str) -> str:
|
|
383
|
+
"""Remove 'from __future__ import annotations' to avoid duplication."""
|
|
384
|
+
lines = source.splitlines(keepends=True)
|
|
385
|
+
filtered = []
|
|
386
|
+
for line in lines:
|
|
387
|
+
stripped = line.strip()
|
|
388
|
+
if stripped == "from __future__ import annotations":
|
|
389
|
+
continue
|
|
390
|
+
filtered.append(line)
|
|
391
|
+
return "".join(filtered)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _extract_parameters(module_path: str, func_name: str) -> dict[str, str]:
|
|
395
|
+
"""Extract parameter names and descriptions from a function's signature and docstring."""
|
|
396
|
+
try:
|
|
397
|
+
module = importlib.import_module(module_path)
|
|
398
|
+
func = getattr(module, func_name)
|
|
399
|
+
sig = inspect.signature(func)
|
|
400
|
+
docstring = inspect.getdoc(func) or ""
|
|
401
|
+
|
|
402
|
+
params: dict[str, str] = {}
|
|
403
|
+
for name, param in sig.parameters.items():
|
|
404
|
+
if name in ("kwargs", "self", "cls"):
|
|
405
|
+
continue
|
|
406
|
+
# Try to find description in docstring Parameters section
|
|
407
|
+
desc = _find_param_doc(docstring, name)
|
|
408
|
+
if not desc:
|
|
409
|
+
# Fall back to annotation
|
|
410
|
+
ann = param.annotation
|
|
411
|
+
if ann != inspect.Parameter.empty:
|
|
412
|
+
desc = str(ann).replace("'", "")
|
|
413
|
+
else:
|
|
414
|
+
desc = "string"
|
|
415
|
+
params[name] = desc
|
|
416
|
+
|
|
417
|
+
return params
|
|
418
|
+
except Exception:
|
|
419
|
+
return {}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _find_param_doc(docstring: str, param_name: str) -> str:
|
|
423
|
+
"""Extract a parameter description from a NumPy-style docstring."""
|
|
424
|
+
lines = docstring.splitlines()
|
|
425
|
+
in_params = False
|
|
426
|
+
found_param = False
|
|
427
|
+
for line in lines:
|
|
428
|
+
stripped = line.strip()
|
|
429
|
+
if stripped in ("Parameters", "Parameters:"):
|
|
430
|
+
in_params = True
|
|
431
|
+
continue
|
|
432
|
+
if in_params and stripped.startswith("---"):
|
|
433
|
+
continue
|
|
434
|
+
if in_params and stripped in ("Returns", "Returns:", "Raises", "Raises:"):
|
|
435
|
+
break
|
|
436
|
+
if in_params:
|
|
437
|
+
# Look for "param_name : type" line
|
|
438
|
+
if stripped.startswith(param_name) and ":" in stripped:
|
|
439
|
+
found_param = True
|
|
440
|
+
continue
|
|
441
|
+
if found_param:
|
|
442
|
+
if stripped and not stripped[0].isalpha() or not stripped:
|
|
443
|
+
# Description line (indented) or blank
|
|
444
|
+
if stripped:
|
|
445
|
+
return stripped
|
|
446
|
+
break
|
|
447
|
+
else:
|
|
448
|
+
# Next parameter
|
|
449
|
+
break
|
|
450
|
+
return ""
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _build_usage_hint(func_name: str, params: dict[str, str]) -> str:
|
|
454
|
+
"""Build a CLI usage hint like '--player_name "LeBron James"'."""
|
|
455
|
+
if not params:
|
|
456
|
+
return func_name
|
|
457
|
+
args = " ".join(f'--{name} "<{name}>"' for name in params)
|
|
458
|
+
return f"{func_name} {args}"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _build_cli_wrapper(tools_in_module: list[tuple[str, BuiltinToolConfig]]) -> str:
|
|
462
|
+
"""Build a __main__ CLI wrapper that dispatches to tool functions."""
|
|
463
|
+
func_entries: list[tuple[str, str]] = [] # (tool_name, func_name)
|
|
464
|
+
for tool_name, tool_cfg in tools_in_module:
|
|
465
|
+
func_name = tool_cfg.handler.rsplit(".", 1)[1]
|
|
466
|
+
func_entries.append((tool_name, func_name))
|
|
467
|
+
|
|
468
|
+
lines: list[str] = []
|
|
469
|
+
lines.append("# ── CLI wrapper (auto-generated) ──────────────────────────────")
|
|
470
|
+
lines.append("")
|
|
471
|
+
lines.append("")
|
|
472
|
+
lines.append("if __name__ == '__main__':")
|
|
473
|
+
lines.append(" import sys")
|
|
474
|
+
lines.append(" import json as _json")
|
|
475
|
+
lines.append("")
|
|
476
|
+
lines.append(" _TOOLS = {")
|
|
477
|
+
for tool_name, func_name in func_entries:
|
|
478
|
+
lines.append(f" '{func_name}': {func_name},")
|
|
479
|
+
lines.append(" }")
|
|
480
|
+
lines.append("")
|
|
481
|
+
tool_names_str = ", ".join(fn for _, fn in func_entries)
|
|
482
|
+
lines.append(" if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'):")
|
|
483
|
+
lines.append(" print(f'Usage: python {sys.argv[0]} <tool_name> [--arg value ...]')")
|
|
484
|
+
lines.append(f" print('Available tools: {tool_names_str}')")
|
|
485
|
+
lines.append(" sys.exit(0)")
|
|
486
|
+
lines.append("")
|
|
487
|
+
lines.append(" _tool_name = sys.argv[1]")
|
|
488
|
+
lines.append(" if _tool_name not in _TOOLS:")
|
|
489
|
+
lines.append(" print(f'Unknown tool: {_tool_name}')")
|
|
490
|
+
lines.append(f" print('Available tools: {tool_names_str}')")
|
|
491
|
+
lines.append(" sys.exit(1)")
|
|
492
|
+
lines.append("")
|
|
493
|
+
lines.append(" # Parse --key value arguments")
|
|
494
|
+
lines.append(" _kwargs = {}")
|
|
495
|
+
lines.append(" _args = sys.argv[2:]")
|
|
496
|
+
lines.append(" _i = 0")
|
|
497
|
+
lines.append(" while _i < len(_args):")
|
|
498
|
+
lines.append(" if _args[_i].startswith('--') and _i + 1 < len(_args):")
|
|
499
|
+
lines.append(" _kwargs[_args[_i][2:]] = _args[_i + 1]")
|
|
500
|
+
lines.append(" _i += 2")
|
|
501
|
+
lines.append(" else:")
|
|
502
|
+
lines.append(" _i += 1")
|
|
503
|
+
lines.append("")
|
|
504
|
+
lines.append(" # Coerce argument types using function annotations")
|
|
505
|
+
lines.append(" import inspect as _inspect")
|
|
506
|
+
lines.append(" _sig = _inspect.signature(_TOOLS[_tool_name])")
|
|
507
|
+
lines.append(" _coerced = {}")
|
|
508
|
+
lines.append(" for _k, _v in _kwargs.items():")
|
|
509
|
+
lines.append(" _param = _sig.parameters.get(_k)")
|
|
510
|
+
lines.append(" if _param and _param.annotation != _inspect.Parameter.empty:")
|
|
511
|
+
lines.append(" _ann = _param.annotation")
|
|
512
|
+
lines.append(" if _ann in (int, 'int'):")
|
|
513
|
+
lines.append(" _v = int(_v)")
|
|
514
|
+
lines.append(" elif _ann in (float, 'float'):")
|
|
515
|
+
lines.append(" _v = float(_v)")
|
|
516
|
+
lines.append(" elif _ann in (bool, 'bool'):")
|
|
517
|
+
lines.append(" _v = _v.lower() in ('true', '1', 'yes')")
|
|
518
|
+
lines.append(" _coerced[_k] = _v")
|
|
519
|
+
lines.append("")
|
|
520
|
+
lines.append(" _result = _TOOLS[_tool_name](**_coerced)")
|
|
521
|
+
lines.append(" print(_json.dumps(_result, indent=2, default=str))")
|
|
522
|
+
|
|
523
|
+
return "\n".join(lines)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ── Helpers ───────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _clean_description(text: str) -> str:
|
|
530
|
+
"""Collapse whitespace in a YAML multi-line description."""
|
|
531
|
+
return " ".join(text.split()).strip()
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _truncate(text: str, max_len: int) -> str:
|
|
535
|
+
"""Truncate text with ellipsis if needed, escaping quotes for YAML."""
|
|
536
|
+
text = text.replace('"', '\\"')
|
|
537
|
+
if len(text) <= max_len:
|
|
538
|
+
return text
|
|
539
|
+
return text[: max_len - 1] + "…"
|
orchid_cli/main.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orchid CLI — command-line interface for the Orchid agent framework.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
orchid chat "What are LeBron's stats?" --config examples/basketball/orchid.yml
|
|
6
|
+
orchid chat --interactive --config examples/basketball/orchid.yml
|
|
7
|
+
orchid config validate examples/basketball/orchid.yml
|
|
8
|
+
orchid index --config examples/basketball/orchid.yml
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from .commands import chat, config, index, skill
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="orchid",
|
|
19
|
+
help="Orchid — multi-agent AI framework CLI",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
app.add_typer(chat.app, name="chat")
|
|
24
|
+
app.add_typer(config.app, name="config")
|
|
25
|
+
app.add_typer(index.app, name="index")
|
|
26
|
+
app.add_typer(skill.app, name="skill")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
app()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orchid-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Orchid CLI — command-line interface for the Orchid agent framework
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.28.0
|
|
8
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
9
|
+
Requires-Dist: orchid-ai>=1.1.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.7.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0
|
|
12
|
+
Requires-Dist: rich>=13.0
|
|
13
|
+
Requires-Dist: typer>=0.12.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: gitlint>=0.19.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
orchid_cli/__init__.py,sha256=TR3LU0f00o8wk6UNhveP9HkWru6m0HE5lup8hSkJVc0,13
|
|
2
|
+
orchid_cli/bootstrap.py,sha256=d00k4b_TAwcxyxudbrdqweLdifj1KjwbcrEihsVqH7A,6743
|
|
3
|
+
orchid_cli/main.py,sha256=KaSBSoSGvsQuR2XDtDqNwoAFjmZifU8P7zrmSrUn_L4,769
|
|
4
|
+
orchid_cli/commands/__init__.py,sha256=ZgebDddFSsH7OC0ZjpvzOouWrwfLdfWilzAwu5de8AE,23
|
|
5
|
+
orchid_cli/commands/chat.py,sha256=klC7mz1BkulZQnZxEPD30ZL0JWzUhK5B_BHrbakwXJE,16363
|
|
6
|
+
orchid_cli/commands/config.py,sha256=M6mtK7nYXfS_EZU8Mq4gWlnLDRoxaYtAbeDxk6VSG9A,1293
|
|
7
|
+
orchid_cli/commands/index.py,sha256=M_uS1LLq6mqewvemxCw2c7pg7r5djO0CQfhM0cj4Irs,1371
|
|
8
|
+
orchid_cli/commands/skill.py,sha256=k3EYGr8P1IO53Osg0kAVCttRCTuPAedbLI_qEqWwxS0,21175
|
|
9
|
+
orchid_cli-0.1.0.dist-info/METADATA,sha256=X-v0bkRW5ZoHil6YNTK1bAo4UM6Um5-HdqdH_ukm88c,699
|
|
10
|
+
orchid_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
orchid_cli-0.1.0.dist-info/entry_points.txt,sha256=lQMig0J_0tiSlmBTME0s-Xt9TMxCaDfbDeOdKH2gyk8,47
|
|
12
|
+
orchid_cli-0.1.0.dist-info/licenses/LICENSE,sha256=Y7UF4tWj3YhozK8JcR7EqqEVYX1ZQSrNdII70UfMnVE,1076
|
|
13
|
+
orchid_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Orchid Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|