nonoka-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.
- nonoka_cli/__init__.py +5 -0
- nonoka_cli/__main__.py +7 -0
- nonoka_cli/cli.py +134 -0
- nonoka_cli/config/__init__.py +8 -0
- nonoka_cli/config/loader.py +130 -0
- nonoka_cli/config/models.py +51 -0
- nonoka_cli/core/__init__.py +9 -0
- nonoka_cli/core/agent_factory.py +78 -0
- nonoka_cli/core/context.py +17 -0
- nonoka_cli/core/orchestrator.py +138 -0
- nonoka_cli/shell/__init__.py +5 -0
- nonoka_cli/shell/repl.py +164 -0
- nonoka_cli/ui/__init__.py +5 -0
- nonoka_cli/ui/renderer.py +78 -0
- nonoka_cli/utils/__init__.py +1 -0
- nonoka_cli/utils/errors.py +28 -0
- nonoka_cli/utils/logging.py +75 -0
- nonoka_cli-0.1.0.dist-info/METADATA +69 -0
- nonoka_cli-0.1.0.dist-info/RECORD +21 -0
- nonoka_cli-0.1.0.dist-info/WHEEL +4 -0
- nonoka_cli-0.1.0.dist-info/entry_points.txt +2 -0
nonoka_cli/__init__.py
ADDED
nonoka_cli/__main__.py
ADDED
nonoka_cli/cli.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""CLI entry point for nonoka-cli.
|
|
2
|
+
|
|
3
|
+
Handles argument parsing, configuration loading, and REPL startup.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
from nonoka_cli.core.orchestrator import Orchestrator
|
|
18
|
+
from nonoka_cli.shell.repl import REPL
|
|
19
|
+
from nonoka_cli.ui.renderer import Renderer
|
|
20
|
+
from nonoka_cli.utils.logging import setup_logging
|
|
21
|
+
|
|
22
|
+
logger = structlog.get_logger("nonoka_cli.cli")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
"""Build the argument parser."""
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="nonoka-cli",
|
|
29
|
+
description="Terminal frontend for the Nonoka Agent framework",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--config",
|
|
33
|
+
type=Path,
|
|
34
|
+
default=None,
|
|
35
|
+
help="Path to configuration file (default: ~/.config/nonoka/config.yaml)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--model",
|
|
39
|
+
type=str,
|
|
40
|
+
default=None,
|
|
41
|
+
help="Override the model specified in config",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--verbose", "-v",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Output INFO level logs to console",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--debug",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Output DEBUG level logs to console",
|
|
52
|
+
)
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _run_repl(args: argparse.Namespace) -> int:
|
|
57
|
+
"""Initialize and run the REPL.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
args: Parsed command-line arguments.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Exit code (0 for success).
|
|
64
|
+
"""
|
|
65
|
+
orchestrator = Orchestrator()
|
|
66
|
+
renderer = Renderer()
|
|
67
|
+
repl = REPL(orchestrator, renderer)
|
|
68
|
+
|
|
69
|
+
# Handle Ctrl+C by interrupting the current execution
|
|
70
|
+
loop = asyncio.get_event_loop()
|
|
71
|
+
|
|
72
|
+
def _sigint_handler():
|
|
73
|
+
asyncio.create_task(repl.interrupt())
|
|
74
|
+
|
|
75
|
+
if sys.platform != "win32":
|
|
76
|
+
loop.add_signal_handler(signal.SIGINT, _sigint_handler)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
await orchestrator.initialize(config_path=args.config)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
print(f"Failed to initialize: {exc}", file=sys.stderr)
|
|
82
|
+
logger.error("initialization_failed", error=str(exc))
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
# Apply model override if provided
|
|
86
|
+
if args.model:
|
|
87
|
+
orchestrator.config.model = args.model
|
|
88
|
+
orchestrator._agent_factory.rebuild()
|
|
89
|
+
logger.info("model_overridden", model=args.model)
|
|
90
|
+
|
|
91
|
+
print(f"nonoka-cli initialized. Model: {orchestrator.config.model}")
|
|
92
|
+
print("Type /help for commands, /exit to quit.")
|
|
93
|
+
print()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
await repl.run()
|
|
97
|
+
finally:
|
|
98
|
+
await orchestrator.shutdown()
|
|
99
|
+
if sys.platform != "win32":
|
|
100
|
+
loop.remove_signal_handler(signal.SIGINT)
|
|
101
|
+
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main() -> int:
|
|
106
|
+
"""Main entry point.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Exit code (0 for success, 1 for error).
|
|
110
|
+
"""
|
|
111
|
+
parser = _build_parser()
|
|
112
|
+
args = parser.parse_args()
|
|
113
|
+
|
|
114
|
+
# Setup logging
|
|
115
|
+
log_level = (
|
|
116
|
+
logging.DEBUG if args.debug
|
|
117
|
+
else logging.INFO if args.verbose
|
|
118
|
+
else logging.WARNING
|
|
119
|
+
)
|
|
120
|
+
setup_logging(level=log_level, console=args.verbose or args.debug)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return asyncio.run(_run_repl(args))
|
|
124
|
+
except KeyboardInterrupt:
|
|
125
|
+
print("\nInterrupted.")
|
|
126
|
+
return 130
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
logger.error("fatal_error", error=str(exc))
|
|
129
|
+
print(f"Fatal error: {exc}", file=sys.stderr)
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Configuration loading and validation for nonoka-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from nonoka_cli.config.models import CLIConfig
|
|
6
|
+
from nonoka_cli.config.loader import ConfigLoader, load_config
|
|
7
|
+
|
|
8
|
+
__all__ = ["CLIConfig", "ConfigLoader", "load_config"]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Configuration loading with env-var substitution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
|
|
12
|
+
from nonoka_cli.config.models import CLIConfig
|
|
13
|
+
from nonoka_cli.utils.errors import ConfigError, ConfigNotFoundError
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger("nonoka_cli.config")
|
|
16
|
+
|
|
17
|
+
_ENV_PATTERN = re.compile(r"\$\{(?P<name>[A-Za-z_][A-Za-z0-9_]*)(?::-?(?P<default>[^}]*))?\}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _substitute_env_vars(value: Any) -> Any:
|
|
21
|
+
"""Recursively substitute ``${VAR}`` and ``${VAR:-default}`` in strings."""
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
def replacer(match: re.Match[str]) -> str:
|
|
24
|
+
var_name = match.group("name")
|
|
25
|
+
default = match.group("default")
|
|
26
|
+
result = os.getenv(var_name)
|
|
27
|
+
if result is None:
|
|
28
|
+
if default is not None:
|
|
29
|
+
return default
|
|
30
|
+
raise ConfigError(
|
|
31
|
+
f"Environment variable '{var_name}' is not set and no default provided"
|
|
32
|
+
)
|
|
33
|
+
return result
|
|
34
|
+
return _ENV_PATTERN.sub(replacer, value)
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
return {k: _substitute_env_vars(v) for k, v in value.items()}
|
|
37
|
+
if isinstance(value, list):
|
|
38
|
+
return [_substitute_env_vars(item) for item in value]
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConfigLoader:
|
|
43
|
+
"""Loads and validates CLI configuration from YAML files."""
|
|
44
|
+
|
|
45
|
+
DEFAULT_PATH = Path.home() / ".config" / "nonoka" / "config.yaml"
|
|
46
|
+
FALLBACK_PATH = Path.cwd() / "nonoka.yaml"
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def find_config_file(
|
|
50
|
+
cls,
|
|
51
|
+
explicit_path: Path | str | None = None,
|
|
52
|
+
) -> Path:
|
|
53
|
+
"""Search for a configuration file in priority order.
|
|
54
|
+
|
|
55
|
+
Priority: explicit_path > ~/.config/nonoka/config.yaml > ./nonoka.yaml
|
|
56
|
+
"""
|
|
57
|
+
if explicit_path is not None:
|
|
58
|
+
path = Path(explicit_path)
|
|
59
|
+
if path.exists():
|
|
60
|
+
return path
|
|
61
|
+
raise ConfigNotFoundError(f"Explicit config file not found: {path}")
|
|
62
|
+
|
|
63
|
+
if cls.DEFAULT_PATH.exists():
|
|
64
|
+
return cls.DEFAULT_PATH
|
|
65
|
+
|
|
66
|
+
if cls.FALLBACK_PATH.exists():
|
|
67
|
+
return cls.FALLBACK_PATH
|
|
68
|
+
|
|
69
|
+
raise ConfigNotFoundError(
|
|
70
|
+
f"No config file found. Searched: {cls.DEFAULT_PATH}, {cls.FALLBACK_PATH}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def load(
|
|
75
|
+
cls,
|
|
76
|
+
path: Path | str | None = None,
|
|
77
|
+
) -> CLIConfig:
|
|
78
|
+
"""Load configuration from a YAML file.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
path: Explicit config file path. If None, searches default locations.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Validated CLIConfig instance.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ConfigNotFoundError: If no config file is found.
|
|
88
|
+
ConfigError: If parsing or validation fails.
|
|
89
|
+
"""
|
|
90
|
+
config_path = cls.find_config_file(path)
|
|
91
|
+
logger.info("loading_config", path=str(config_path))
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
import yaml
|
|
95
|
+
except ImportError as exc:
|
|
96
|
+
raise ConfigError("PyYAML is required. Install: pip install pyyaml") from exc
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
raw = config_path.read_text(encoding="utf-8")
|
|
100
|
+
data = yaml.safe_load(raw)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
raise ConfigError(f"Failed to parse YAML: {exc}") from exc
|
|
103
|
+
|
|
104
|
+
if data is None:
|
|
105
|
+
data = {}
|
|
106
|
+
if not isinstance(data, dict):
|
|
107
|
+
raise ConfigError(
|
|
108
|
+
f"Config file must contain a top-level object, got {type(data).__name__}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Substitute env vars before validation
|
|
112
|
+
try:
|
|
113
|
+
data = _substitute_env_vars(data)
|
|
114
|
+
except ConfigError:
|
|
115
|
+
raise
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
raise ConfigError(f"Environment variable substitution failed: {exc}") from exc
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
config = CLIConfig.model_validate(data)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
raise ConfigError(f"Config validation failed: {exc}") from exc
|
|
123
|
+
|
|
124
|
+
logger.info("config_loaded", model=config.model)
|
|
125
|
+
return config
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def load_config(path: Path | str | None = None) -> CLIConfig:
|
|
129
|
+
"""Convenience function: load CLI configuration."""
|
|
130
|
+
return ConfigLoader.load(path)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Pydantic models for CLI configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CLIConfigModel(BaseModel):
|
|
12
|
+
"""CLI behavior configuration."""
|
|
13
|
+
theme: str = "dark"
|
|
14
|
+
auto_approve: bool = False
|
|
15
|
+
editor: str = "${EDITOR:-nano}"
|
|
16
|
+
max_history: int = 1000
|
|
17
|
+
multi_line_trigger: str = '"""'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HITLConfigModel(BaseModel):
|
|
21
|
+
"""HITL (Human-in-the-Loop) configuration."""
|
|
22
|
+
policy: str = "interactive"
|
|
23
|
+
dangerous_tools: list[str] = Field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MCPServerConfigModel(BaseModel):
|
|
27
|
+
"""Single MCP server configuration."""
|
|
28
|
+
transport: str
|
|
29
|
+
command: str
|
|
30
|
+
args: list[str] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CLIConfig(BaseModel):
|
|
34
|
+
"""Top-level configuration for nonoka-cli.
|
|
35
|
+
|
|
36
|
+
Matches the expected structure of ~/.config/nonoka/config.yaml.
|
|
37
|
+
"""
|
|
38
|
+
model: str = "gpt-4o"
|
|
39
|
+
system_prompt: str = "You are a helpful AI assistant."
|
|
40
|
+
mcp_servers: dict[str, MCPServerConfigModel] = Field(default_factory=dict)
|
|
41
|
+
tool_paths: list[Path] = Field(default_factory=list)
|
|
42
|
+
skills: list[str] = Field(default_factory=list)
|
|
43
|
+
cli: CLIConfigModel = Field(default_factory=CLIConfigModel)
|
|
44
|
+
hitl: HITLConfigModel = Field(default_factory=HITLConfigModel)
|
|
45
|
+
|
|
46
|
+
@field_validator("tool_paths", mode="before")
|
|
47
|
+
@classmethod
|
|
48
|
+
def _resolve_tool_paths(cls, v: Any) -> list[Path]:
|
|
49
|
+
if v is None:
|
|
50
|
+
return []
|
|
51
|
+
return [Path(p).expanduser() for p in v]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Core orchestration layer for nonoka-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from nonoka_cli.core.context import CLIContext
|
|
6
|
+
from nonoka_cli.core.agent_factory import AgentFactory
|
|
7
|
+
from nonoka_cli.core.orchestrator import Orchestrator
|
|
8
|
+
|
|
9
|
+
__all__ = ["CLIContext", "AgentFactory", "Orchestrator"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Agent factory — builds nonoka Agent from CLI configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
from nonoka import Agent, AgentBuilder
|
|
10
|
+
from nonoka_cli.config.models import CLIConfig
|
|
11
|
+
from nonoka_cli.utils.errors import AgentBuildError
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger("nonoka_cli.core")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentFactory:
|
|
17
|
+
"""Builds nonoka Agent instances from CLI configuration.
|
|
18
|
+
|
|
19
|
+
TODO: Integrate MCP tools, local tools, and skills.
|
|
20
|
+
Currently supports model and system_prompt only.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: CLIConfig):
|
|
24
|
+
self._config = config
|
|
25
|
+
self._agent: Agent | None = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def config(self) -> CLIConfig:
|
|
29
|
+
"""Current configuration."""
|
|
30
|
+
return self._config
|
|
31
|
+
|
|
32
|
+
def build(self) -> Agent:
|
|
33
|
+
"""Build (or rebuild) an Agent from the current configuration.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A nonoka Agent instance.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
AgentBuildError: If model is not configured.
|
|
40
|
+
"""
|
|
41
|
+
if not self._config.model:
|
|
42
|
+
raise AgentBuildError("No model configured. Set 'model' in config.yaml.")
|
|
43
|
+
|
|
44
|
+
logger.info(
|
|
45
|
+
"building_agent",
|
|
46
|
+
model=self._config.model,
|
|
47
|
+
system_prompt_length=len(self._config.system_prompt),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self._agent = (
|
|
51
|
+
AgentBuilder()
|
|
52
|
+
.model(self._config.model)
|
|
53
|
+
.system_prompt(self._config.system_prompt)
|
|
54
|
+
.max_turns(20)
|
|
55
|
+
.build()
|
|
56
|
+
)
|
|
57
|
+
return self._agent
|
|
58
|
+
|
|
59
|
+
def rebuild(self, config_patch: dict[str, Any] | None = None) -> Agent:
|
|
60
|
+
"""Rebuild Agent with an optional configuration patch.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
config_patch: Dict of config overrides (e.g. {"model": "gpt-4o"}).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The rebuilt Agent.
|
|
67
|
+
"""
|
|
68
|
+
if config_patch:
|
|
69
|
+
# Apply patch by creating a new config
|
|
70
|
+
data = self._config.model_dump()
|
|
71
|
+
data.update(config_patch)
|
|
72
|
+
self._config = self._config.__class__.model_validate(data)
|
|
73
|
+
|
|
74
|
+
return self.build()
|
|
75
|
+
|
|
76
|
+
def get_agent(self) -> Agent | None:
|
|
77
|
+
"""Return the currently built Agent, if any."""
|
|
78
|
+
return self._agent
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""CLI runtime context passed to tools via RunContext.deps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from nonoka_cli.config.models import CLIConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CLIContext:
|
|
13
|
+
"""CLI runtime context injected into tools."""
|
|
14
|
+
user: str
|
|
15
|
+
session_id: str
|
|
16
|
+
config: CLIConfig
|
|
17
|
+
working_dir: Path
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Orchestrator — coordinates config, agent, runner, and execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
from nonoka import Runner
|
|
11
|
+
from nonoka.core.runner import StreamEvent
|
|
12
|
+
|
|
13
|
+
from nonoka_cli.config.loader import ConfigLoader
|
|
14
|
+
from nonoka_cli.config.models import CLIConfig
|
|
15
|
+
from nonoka_cli.core.agent_factory import AgentFactory
|
|
16
|
+
from nonoka_cli.core.context import CLIContext
|
|
17
|
+
from nonoka_cli.utils.errors import ConfigError, OrchestratorError
|
|
18
|
+
|
|
19
|
+
logger = structlog.get_logger("nonoka_cli.core")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Orchestrator:
|
|
23
|
+
"""Orchestration layer — the bridge between Shell and nonoka kernel.
|
|
24
|
+
|
|
25
|
+
Responsibilities:
|
|
26
|
+
- Load configuration
|
|
27
|
+
- Build Agent and Runner
|
|
28
|
+
- Execute prompts via nonoka's streaming ReAct API
|
|
29
|
+
- Manage a single session_id
|
|
30
|
+
|
|
31
|
+
TODO: Add MCP, Skill, session management, HITL.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: CLIConfig | None = None):
|
|
35
|
+
"""Initialize the orchestrator.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Pre-loaded configuration. If None, will be loaded on initialize().
|
|
39
|
+
"""
|
|
40
|
+
self._config = config
|
|
41
|
+
self._agent_factory: AgentFactory | None = None
|
|
42
|
+
self._runner: Runner | None = None
|
|
43
|
+
self._session_id: str = str(uuid.uuid4())
|
|
44
|
+
self._initialized = False
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def config(self) -> CLIConfig:
|
|
48
|
+
"""Current configuration."""
|
|
49
|
+
if self._config is None:
|
|
50
|
+
raise OrchestratorError("Orchestrator not initialized. Call initialize() first.")
|
|
51
|
+
return self._config
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def session_id(self) -> str:
|
|
55
|
+
"""Current session identifier."""
|
|
56
|
+
return self._session_id
|
|
57
|
+
|
|
58
|
+
async def initialize(self, config_path: Path | str | None = None) -> None:
|
|
59
|
+
"""Load config, build agent, and create runner.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
config_path: Optional explicit path to config file.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ConfigError: If configuration cannot be loaded.
|
|
66
|
+
OrchestratorError: If agent or runner cannot be built.
|
|
67
|
+
"""
|
|
68
|
+
if self._config is None:
|
|
69
|
+
try:
|
|
70
|
+
self._config = ConfigLoader.load(config_path)
|
|
71
|
+
except ConfigError:
|
|
72
|
+
raise
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
raise ConfigError(f"Failed to load configuration: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
self._agent_factory = AgentFactory(self._config)
|
|
77
|
+
agent = self._agent_factory.build()
|
|
78
|
+
|
|
79
|
+
self._runner = Runner()
|
|
80
|
+
|
|
81
|
+
logger.info(
|
|
82
|
+
"orchestrator_initialized",
|
|
83
|
+
model=self._config.model,
|
|
84
|
+
session_id=self._session_id,
|
|
85
|
+
)
|
|
86
|
+
self._initialized = True
|
|
87
|
+
|
|
88
|
+
async def execute(self, prompt: str) -> AsyncIterator[StreamEvent]:
|
|
89
|
+
"""Execute a user prompt and yield streaming events.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
prompt: The user's input text.
|
|
93
|
+
|
|
94
|
+
Yields:
|
|
95
|
+
StreamEvent objects from nonoka's ReAct streaming API.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
OrchestratorError: If not initialized or execution fails.
|
|
99
|
+
"""
|
|
100
|
+
if not self._initialized or self._agent_factory is None or self._runner is None:
|
|
101
|
+
raise OrchestratorError("Orchestrator not initialized. Call initialize() first.")
|
|
102
|
+
|
|
103
|
+
agent = self._agent_factory.get_agent()
|
|
104
|
+
if agent is None:
|
|
105
|
+
raise OrchestratorError("No Agent available. Build failed during initialization.")
|
|
106
|
+
|
|
107
|
+
logger.info("executing_prompt", prompt_length=len(prompt), session_id=self._session_id)
|
|
108
|
+
|
|
109
|
+
deps = CLIContext(
|
|
110
|
+
user="local",
|
|
111
|
+
session_id=self._session_id,
|
|
112
|
+
config=self._config,
|
|
113
|
+
working_dir=Path.cwd(),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
async for event in self._runner.run_react_stream(
|
|
118
|
+
agent, prompt, deps=deps, session_id=self._session_id
|
|
119
|
+
):
|
|
120
|
+
yield event
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
logger.error("execution_failed", error=str(exc))
|
|
123
|
+
raise OrchestratorError(f"Execution failed: {exc}") from exc
|
|
124
|
+
|
|
125
|
+
def new_session(self) -> str:
|
|
126
|
+
"""Create a new session, discarding the old one.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The new session_id.
|
|
130
|
+
"""
|
|
131
|
+
self._session_id = str(uuid.uuid4())
|
|
132
|
+
logger.info("new_session_created", session_id=self._session_id)
|
|
133
|
+
return self._session_id
|
|
134
|
+
|
|
135
|
+
async def shutdown(self) -> None:
|
|
136
|
+
"""Graceful shutdown — clean up resources."""
|
|
137
|
+
logger.info("orchestrator_shutdown")
|
|
138
|
+
self._initialized = False
|
nonoka_cli/shell/repl.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""REPL — Read-Eval-Print Loop for nonoka-cli.
|
|
2
|
+
|
|
3
|
+
Provides interactive terminal session with command parsing,
|
|
4
|
+
interrupt handling, and prompt execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from nonoka_cli.core.orchestrator import Orchestrator
|
|
15
|
+
from nonoka_cli.ui.renderer import Renderer
|
|
16
|
+
from nonoka_cli.utils.errors import CLIError, OrchestratorError
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger("nonoka_cli.shell")
|
|
19
|
+
|
|
20
|
+
PROMPT = "nonoka> "
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class REPL:
|
|
24
|
+
"""Interactive REPL for nonoka-cli.
|
|
25
|
+
|
|
26
|
+
Features:
|
|
27
|
+
- Read user input line by line
|
|
28
|
+
- Route /-prefixed commands to handlers
|
|
29
|
+
- Pass plain text to orchestrator for Agent execution
|
|
30
|
+
- Handle Ctrl+C (interrupt) and Ctrl+D (exit)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
orchestrator: Orchestrator,
|
|
36
|
+
renderer: Renderer | None = None,
|
|
37
|
+
):
|
|
38
|
+
self._orchestrator = orchestrator
|
|
39
|
+
self._renderer = renderer or Renderer()
|
|
40
|
+
self._running = False
|
|
41
|
+
self._current_task: asyncio.Task | None = None
|
|
42
|
+
|
|
43
|
+
async def run(self) -> None:
|
|
44
|
+
"""Start the REPL main loop.
|
|
45
|
+
|
|
46
|
+
Reads from stdin, dispatches commands, and executes prompts
|
|
47
|
+
via the orchestrator until the user exits.
|
|
48
|
+
"""
|
|
49
|
+
self._running = True
|
|
50
|
+
logger.info("repl_started")
|
|
51
|
+
|
|
52
|
+
while self._running:
|
|
53
|
+
try:
|
|
54
|
+
user_input = await self._read_input()
|
|
55
|
+
except EOFError:
|
|
56
|
+
self._running = False
|
|
57
|
+
print("\nGoodbye!")
|
|
58
|
+
break
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
print("\n")
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if not user_input:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if user_input.startswith("/"):
|
|
67
|
+
await self._handle_command(user_input)
|
|
68
|
+
else:
|
|
69
|
+
await self._handle_prompt(user_input)
|
|
70
|
+
|
|
71
|
+
logger.info("repl_stopped")
|
|
72
|
+
|
|
73
|
+
async def _read_input(self) -> str:
|
|
74
|
+
"""Read a line of input from stdin asynchronously.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Stripped user input string.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
EOFError: On Ctrl+D (empty read).
|
|
81
|
+
"""
|
|
82
|
+
loop = asyncio.get_event_loop()
|
|
83
|
+
# Use loop.run_in_executor so input() does not block the event loop
|
|
84
|
+
raw = await loop.run_in_executor(None, input, PROMPT)
|
|
85
|
+
if raw is None:
|
|
86
|
+
raise EOFError()
|
|
87
|
+
return raw.strip()
|
|
88
|
+
|
|
89
|
+
async def _handle_command(self, raw: str) -> None:
|
|
90
|
+
"""Handle a /-prefixed CLI command.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
raw: The full command string (e.g. "/exit").
|
|
94
|
+
"""
|
|
95
|
+
parts = raw[1:].split()
|
|
96
|
+
if not parts:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
cmd = parts[0].lower()
|
|
100
|
+
args = parts[1:]
|
|
101
|
+
|
|
102
|
+
match cmd:
|
|
103
|
+
case "exit" | "quit":
|
|
104
|
+
self._running = False
|
|
105
|
+
print("Goodbye!")
|
|
106
|
+
|
|
107
|
+
case "new":
|
|
108
|
+
session_id = self._orchestrator.new_session()
|
|
109
|
+
print(f"New session: {session_id}")
|
|
110
|
+
|
|
111
|
+
case "help":
|
|
112
|
+
self._print_help()
|
|
113
|
+
|
|
114
|
+
case _:
|
|
115
|
+
print(f"Unknown command: /{cmd}. Type /help for available commands.")
|
|
116
|
+
|
|
117
|
+
async def _handle_prompt(self, prompt: str) -> None:
|
|
118
|
+
"""Execute a plain-text prompt through the orchestrator.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
prompt: User's natural language input.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
stream = self._orchestrator.execute(prompt)
|
|
125
|
+
self._current_task = asyncio.create_task(
|
|
126
|
+
self._renderer.render_stream(stream)
|
|
127
|
+
)
|
|
128
|
+
await self._current_task
|
|
129
|
+
except asyncio.CancelledError:
|
|
130
|
+
print("\n[Cancelled]")
|
|
131
|
+
except OrchestratorError as exc:
|
|
132
|
+
print(f"\n[Error] {exc}")
|
|
133
|
+
except CLIError as exc:
|
|
134
|
+
print(f"\n[Error] {exc}")
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
logger.error("unexpected_error", error=str(exc))
|
|
137
|
+
print(f"\n[Unexpected Error] {exc}")
|
|
138
|
+
finally:
|
|
139
|
+
self._current_task = None
|
|
140
|
+
|
|
141
|
+
def _print_help(self) -> None:
|
|
142
|
+
"""Print available commands."""
|
|
143
|
+
print("""
|
|
144
|
+
Available commands:
|
|
145
|
+
/exit Exit the CLI
|
|
146
|
+
/new Start a new session
|
|
147
|
+
/help Show this help message
|
|
148
|
+
|
|
149
|
+
Any other text is sent to the AI assistant.
|
|
150
|
+
""")
|
|
151
|
+
|
|
152
|
+
async def interrupt(self) -> None:
|
|
153
|
+
"""Interrupt the current execution (called on Ctrl+C).
|
|
154
|
+
|
|
155
|
+
Cancels the running render task and clears output.
|
|
156
|
+
"""
|
|
157
|
+
if self._current_task and not self._current_task.done():
|
|
158
|
+
self._current_task.cancel()
|
|
159
|
+
self._renderer.clear_current_output()
|
|
160
|
+
try:
|
|
161
|
+
await self._current_task
|
|
162
|
+
except asyncio.CancelledError:
|
|
163
|
+
pass
|
|
164
|
+
print("\n[Cancelled]")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Basic text renderer — streams nonoka StreamEvent to the terminal.
|
|
2
|
+
|
|
3
|
+
TODO: Add Markdown rendering, code highlighting, tool cards, stats panel.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import AsyncIterator
|
|
10
|
+
|
|
11
|
+
from nonoka.core.runner import StreamEvent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Renderer:
|
|
15
|
+
"""Renders nonoka StreamEvent stream to the terminal.
|
|
16
|
+
|
|
17
|
+
Implements minimal text streaming:
|
|
18
|
+
- content_delta: print incremental text
|
|
19
|
+
- tool_call_start / tool_call_result: print simple indicators
|
|
20
|
+
- error: print error message
|
|
21
|
+
- final: newline after response
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, file: object | None = None):
|
|
25
|
+
"""Args:
|
|
26
|
+
file: Output stream (default: sys.stdout).
|
|
27
|
+
"""
|
|
28
|
+
self._file = file or sys.stdout
|
|
29
|
+
self._in_tool_call = False
|
|
30
|
+
|
|
31
|
+
async def render_stream(self, stream: AsyncIterator[StreamEvent]) -> None:
|
|
32
|
+
"""Consume a StreamEvent async iterator and render to terminal.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
stream: AsyncIterator of StreamEvent from nonoka Runner.
|
|
36
|
+
"""
|
|
37
|
+
async for event in stream:
|
|
38
|
+
self._render_event(event)
|
|
39
|
+
|
|
40
|
+
def _render_event(self, event: StreamEvent) -> None:
|
|
41
|
+
"""Render a single StreamEvent."""
|
|
42
|
+
match event.type:
|
|
43
|
+
case "content_delta":
|
|
44
|
+
content = event.data.get("content", "")
|
|
45
|
+
if content:
|
|
46
|
+
self._file.write(content)
|
|
47
|
+
self._file.flush()
|
|
48
|
+
|
|
49
|
+
case "tool_call_start":
|
|
50
|
+
self._in_tool_call = True
|
|
51
|
+
tool_calls = event.data.get("tool_calls", [])
|
|
52
|
+
names = [tc.get("name", "unknown") for tc in tool_calls]
|
|
53
|
+
self._file.write(f"\n[Tool: {', '.join(names)}]\n")
|
|
54
|
+
self._file.flush()
|
|
55
|
+
|
|
56
|
+
case "tool_call_result":
|
|
57
|
+
self._in_tool_call = False
|
|
58
|
+
result = event.data.get("result")
|
|
59
|
+
if isinstance(result, dict) and "error" in result:
|
|
60
|
+
self._file.write(f"[Tool error: {result['error']}]\n")
|
|
61
|
+
else:
|
|
62
|
+
self._file.write("[Tool done]\n")
|
|
63
|
+
self._file.flush()
|
|
64
|
+
|
|
65
|
+
case "error":
|
|
66
|
+
error_msg = event.data.get("error", "Unknown error")
|
|
67
|
+
error_type = event.data.get("error_type", "error")
|
|
68
|
+
self._file.write(f"\n[{error_type.upper()}] {error_msg}\n")
|
|
69
|
+
self._file.flush()
|
|
70
|
+
|
|
71
|
+
case "final":
|
|
72
|
+
if not self._in_tool_call:
|
|
73
|
+
self._file.write("\n")
|
|
74
|
+
self._file.flush()
|
|
75
|
+
|
|
76
|
+
def clear_current_output(self) -> None:
|
|
77
|
+
"""Clear current output area (no-op for basic renderer)."""
|
|
78
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules for nonoka-cli."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Custom exceptions for nonoka-cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CLIError(Exception):
|
|
7
|
+
"""Base exception for all CLI errors."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigError(CLIError):
|
|
12
|
+
"""Configuration loading or validation failed."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigNotFoundError(ConfigError):
|
|
17
|
+
"""Configuration file not found."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentBuildError(CLIError):
|
|
22
|
+
"""Failed to build Agent from configuration."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OrchestratorError(CLIError):
|
|
27
|
+
"""Orchestrator execution error."""
|
|
28
|
+
pass
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Logging configuration for nonoka-cli using structlog."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup_logging(
|
|
13
|
+
level: int = logging.INFO,
|
|
14
|
+
log_file: Path | None = None,
|
|
15
|
+
console: bool = False,
|
|
16
|
+
) -> structlog.stdlib.BoundLogger:
|
|
17
|
+
"""Configure structlog for nonoka-cli.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
level: Logging level (default: INFO).
|
|
21
|
+
log_file: Path to log file. If None, defaults to
|
|
22
|
+
~/.local/share/nonoka/logs/nonoka-cli.log.
|
|
23
|
+
console: Whether to also output logs to stderr.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A configured structlog logger.
|
|
27
|
+
"""
|
|
28
|
+
# Default log file path
|
|
29
|
+
if log_file is None:
|
|
30
|
+
log_file = Path.home() / ".local" / "share" / "nonoka" / "logs" / "nonoka-cli.log"
|
|
31
|
+
|
|
32
|
+
# Ensure log directory exists
|
|
33
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
shared_processors = [
|
|
36
|
+
structlog.stdlib.filter_by_level,
|
|
37
|
+
structlog.stdlib.add_logger_name,
|
|
38
|
+
structlog.stdlib.add_log_level,
|
|
39
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
40
|
+
structlog.processors.StackInfoRenderer(),
|
|
41
|
+
structlog.processors.format_exc_info,
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
handlers: list[logging.Handler] = [
|
|
45
|
+
logging.FileHandler(log_file, encoding="utf-8"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
if console:
|
|
49
|
+
handlers.append(logging.StreamHandler(sys.stderr))
|
|
50
|
+
|
|
51
|
+
logging.basicConfig(
|
|
52
|
+
format="%(message)s",
|
|
53
|
+
level=level,
|
|
54
|
+
handlers=handlers,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
structlog.configure(
|
|
58
|
+
processors=shared_processors + [
|
|
59
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
60
|
+
],
|
|
61
|
+
context_class=dict,
|
|
62
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
63
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
64
|
+
cache_logger_on_first_use=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Configure formatter for stdlib handlers
|
|
68
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
69
|
+
processor=structlog.dev.ConsoleRenderer(colors=console),
|
|
70
|
+
foreign_pre_chain=shared_processors,
|
|
71
|
+
)
|
|
72
|
+
for handler in handlers:
|
|
73
|
+
handler.setFormatter(formatter)
|
|
74
|
+
|
|
75
|
+
return structlog.get_logger("nonoka_cli")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nonoka-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Terminal frontend for the Nonoka Agent framework
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: nonoka>=1.1.7
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Requires-Dist: structlog>=24.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# nonoka-cli
|
|
25
|
+
|
|
26
|
+
Terminal frontend for the [Nonoka](https://pypi.org/project/nonoka) Agent framework.
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Install
|
|
32
|
+
pip install nonoka-cli
|
|
33
|
+
|
|
34
|
+
# Start interactive REPL
|
|
35
|
+
nonoka-cli
|
|
36
|
+
|
|
37
|
+
# With custom config
|
|
38
|
+
nonoka-cli --config ~/.config/nonoka/config.yaml
|
|
39
|
+
|
|
40
|
+
# Override model
|
|
41
|
+
nonoka-cli --model gpt-4o
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Create `~/.config/nonoka/config.yaml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
model: "gpt-4o"
|
|
50
|
+
system_prompt: |
|
|
51
|
+
You are a helpful AI assistant.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Commands
|
|
55
|
+
|
|
56
|
+
- `/exit` — Quit the CLI
|
|
57
|
+
- `/new` — Start a new session
|
|
58
|
+
- `/help` — Show help
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Setup
|
|
64
|
+
uv venv
|
|
65
|
+
uv pip install -e ".[dev]"
|
|
66
|
+
|
|
67
|
+
# Run tests
|
|
68
|
+
pytest
|
|
69
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
nonoka_cli/__init__.py,sha256=bWtxzZxy-h9XeGsGBQ4-CMXhbjiMwQkotjDpnaGnzyU,130
|
|
2
|
+
nonoka_cli/__main__.py,sha256=Xa46NPUKiCdUSR0GUnJY9HtEt5RXXheOwam96pX3_N4,159
|
|
3
|
+
nonoka_cli/cli.py,sha256=oxKAopJt9tzxUJ2qJ2l163OYrm9CyRBH2xB2FwXhPuw,3163
|
|
4
|
+
nonoka_cli/config/__init__.py,sha256=CQ1ti5x1HKsIG00lyx7KedmAGiibbzirAL_YbSpOP3k,262
|
|
5
|
+
nonoka_cli/config/loader.py,sha256=3OSW3OIOP5l7SbXYKFLarzrRNuhJcu32O3buMudZeHk,3747
|
|
6
|
+
nonoka_cli/config/models.py,sha256=Q-tcG94zFgWIk0vJ5ikSVnEhcILMVS-pDa-AJa5NrpU,1472
|
|
7
|
+
nonoka_cli/core/__init__.py,sha256=cHCJXJHqQ12GV2P3kVhSXvgIjcTbQ2pDsH-G1dsGy7M,298
|
|
8
|
+
nonoka_cli/core/agent_factory.py,sha256=TjsnNEEySegVQ17eKzHgREaXqRzhetpjXdo1XNe_epI,1996
|
|
9
|
+
nonoka_cli/core/context.py,sha256=Ti81oHE0FjaUp32X_3h7OX1Z8iM64Iy7fi4Gidz052Q,357
|
|
10
|
+
nonoka_cli/core/orchestrator.py,sha256=CV7sppeuqR-f24v4CZlF-gjBXX5LJMsIZlwzJEW0FE4,4105
|
|
11
|
+
nonoka_cli/shell/__init__.py,sha256=8ytQyy-XtRw6-45WFMVpCevjADUDBmXVmDkAn5Qn2BQ,101
|
|
12
|
+
nonoka_cli/shell/repl.py,sha256=3CceBY2arH9J5i6SdINmJeOUVlQWBFq40o-fothv-L0,4173
|
|
13
|
+
nonoka_cli/ui/__init__.py,sha256=fcxR0JTtkc0LBIMrN9kAchfw7UJ4zYaAfLTE_3gMYG8,112
|
|
14
|
+
nonoka_cli/ui/renderer.py,sha256=5FEQaG75JjNtUSNVwkDx7ELxX2b_d3du8RmzMzRBooo,2389
|
|
15
|
+
nonoka_cli/utils/__init__.py,sha256=mGxMnE0iPLieZWXRJAv1hJEoSOkA51lzv6M3tZtel6Q,38
|
|
16
|
+
nonoka_cli/utils/errors.py,sha256=zuVmKqbyf5jVnOJ0map-AGUMfOX4fZWM8xzsVGC16L0,506
|
|
17
|
+
nonoka_cli/utils/logging.py,sha256=dSSaOpnuWt3SMBeao69z6wMin6wGvFCC83CfuGD85_U,1972
|
|
18
|
+
nonoka_cli-0.1.0.dist-info/METADATA,sha256=ch_1NxyysF109_K9R36Apontxmu4o73g_KH_xYKrjvY,1480
|
|
19
|
+
nonoka_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
20
|
+
nonoka_cli-0.1.0.dist-info/entry_points.txt,sha256=laiysCgrJzjJNv7s_-puse-hfAng0lD9skaT-YPG8Ag,51
|
|
21
|
+
nonoka_cli-0.1.0.dist-info/RECORD,,
|