nonoka-cli 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/PKG-INFO +20 -8
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/README.md +17 -6
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/nonoka.yaml.example +6 -0
- nonoka_cli-0.2.3/packages/nonoka-opencode-provider/nonoka-opencode-provider-0.2.0.tgz +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/pyproject.toml +3 -2
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/cli.py +20 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/commands/config_cmd.py +109 -29
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/config/models.py +2 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/commands/test_config_cmd.py +39 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/commands/test_doctor_cmd.py +10 -7
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/uv.lock +7 -5
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/.gitignore +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/LICENSE +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/NOTICE +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/TODO.md +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/install.sh +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/nonoka.yaml +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/.gitignore +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/LICENSE +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/README.md +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/bun.lock +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/package.json +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/src/index.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/src/nonoka-language-model.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/src/protocol.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/src/stream.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tests/language-model.test.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tests/protocol.test.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tests/stream.test.ts +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tsconfig.json +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/__main__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/bridge/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/bridge/events.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/bridge/handler.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/bridge/protocol.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/bridge/server.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/commands/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/commands/doctor_cmd.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/commands/opencode_cmd.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/config/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/config/loader.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/config/manager.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/agent_factory.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/context.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/mcp_service.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/orchestrator.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/runner_service.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/session_service.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/core/tool_service.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/mcp/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/mcp/manager.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/mcp/models.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/sessions/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/sessions/manager.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/sessions/models.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/skills/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/skills/manager.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/tools/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/tools/builtins/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/tools/builtins/file_tools.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/tools/loader.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/utils/__init__.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/utils/errors.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/src/nonoka_cli/utils/logging.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/conftest.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/bridge/test_events.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/bridge/test_handler.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/bridge/test_protocol.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/commands/test_opencode_cmd.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/config/test_loader.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/config/test_models.py +0 -0
- {nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/tests/unit/sessions/test_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonoka-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: OpenCode backend for the Nonoka Agent framework
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -14,8 +14,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
|
-
Requires-Dist: nonoka>=1.3.
|
|
17
|
+
Requires-Dist: nonoka>=1.3.2
|
|
18
18
|
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0
|
|
19
20
|
Requires-Dist: pyyaml>=6.0
|
|
20
21
|
Requires-Dist: structlog>=24.0.0
|
|
21
22
|
Provides-Extra: dev
|
|
@@ -48,14 +49,22 @@ The installer will:
|
|
|
48
49
|
3. Install `nonoka-cli` and the OpenCode provider.
|
|
49
50
|
4. Generate `~/.config/nonoka/config.yaml` and `~/.config/opencode/opencode.json`.
|
|
50
51
|
|
|
51
|
-
After installing,
|
|
52
|
+
After installing, configure your API key and run `opencode`:
|
|
52
53
|
|
|
53
54
|
```bash
|
|
55
|
+
# Interactive: it will ask for your key and save it to ~/.config/nonoka/.env
|
|
56
|
+
nonoka-cli config init
|
|
57
|
+
|
|
58
|
+
# Or set it manually
|
|
54
59
|
export DEEPSEEK_API_KEY=<your-key>
|
|
60
|
+
|
|
55
61
|
nonoka-cli doctor
|
|
56
62
|
opencode
|
|
57
63
|
```
|
|
58
64
|
|
|
65
|
+
`nonoka-cli` automatically loads `~/.config/nonoka/.env` and `./.env` on startup,
|
|
66
|
+
so you don't need to `export` every time if you save the key in `.env`.
|
|
67
|
+
|
|
59
68
|
> To use `uv` instead of `pip`, or to run non-interactively, pass flags:
|
|
60
69
|
> `curl -fsSL https://nonoka.dev/install.sh | bash -s -- --uv --yes`.
|
|
61
70
|
|
|
@@ -73,13 +82,15 @@ npm install -g nonoka-opencode-provider
|
|
|
73
82
|
|
|
74
83
|
## Quick start
|
|
75
84
|
|
|
76
|
-
1. Create your nonoka config
|
|
85
|
+
1. Create your nonoka config (it will ask for your API key and save it to
|
|
86
|
+
`~/.config/nonoka/.env`):
|
|
77
87
|
|
|
78
88
|
```bash
|
|
79
89
|
nonoka-cli config init
|
|
80
90
|
```
|
|
81
91
|
|
|
82
|
-
For scripted setups, use the non-interactive mode
|
|
92
|
+
For scripted setups, use the non-interactive mode (you'll still need to set the
|
|
93
|
+
API key via `.env` or `export`):
|
|
83
94
|
|
|
84
95
|
```bash
|
|
85
96
|
nonoka-cli config init --yes --model deepseek-chat
|
|
@@ -134,9 +145,10 @@ nonoka-cli doctor --check-llm
|
|
|
134
145
|
### `nonoka-cli config init`
|
|
135
146
|
|
|
136
147
|
Interactive wizard that writes `~/.config/nonoka/config.yaml`. It asks for a
|
|
137
|
-
model identifier (e.g. `deepseek-chat`, `openai/gpt-4o`, `ollama/llama3.3`),
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
model identifier (e.g. `deepseek-chat`, `openai/gpt-4o`, `ollama/llama3.3`), a
|
|
149
|
+
masked API key, and whether to save it to `~/.config/nonoka/.env` (recommended),
|
|
150
|
+
directly in `config.yaml`, or skip saving. It also asks for a system prompt and
|
|
151
|
+
whether to auto-approve all tool calls.
|
|
140
152
|
|
|
141
153
|
Non-interactive example:
|
|
142
154
|
|
|
@@ -22,14 +22,22 @@ The installer will:
|
|
|
22
22
|
3. Install `nonoka-cli` and the OpenCode provider.
|
|
23
23
|
4. Generate `~/.config/nonoka/config.yaml` and `~/.config/opencode/opencode.json`.
|
|
24
24
|
|
|
25
|
-
After installing,
|
|
25
|
+
After installing, configure your API key and run `opencode`:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
+
# Interactive: it will ask for your key and save it to ~/.config/nonoka/.env
|
|
29
|
+
nonoka-cli config init
|
|
30
|
+
|
|
31
|
+
# Or set it manually
|
|
28
32
|
export DEEPSEEK_API_KEY=<your-key>
|
|
33
|
+
|
|
29
34
|
nonoka-cli doctor
|
|
30
35
|
opencode
|
|
31
36
|
```
|
|
32
37
|
|
|
38
|
+
`nonoka-cli` automatically loads `~/.config/nonoka/.env` and `./.env` on startup,
|
|
39
|
+
so you don't need to `export` every time if you save the key in `.env`.
|
|
40
|
+
|
|
33
41
|
> To use `uv` instead of `pip`, or to run non-interactively, pass flags:
|
|
34
42
|
> `curl -fsSL https://nonoka.dev/install.sh | bash -s -- --uv --yes`.
|
|
35
43
|
|
|
@@ -47,13 +55,15 @@ npm install -g nonoka-opencode-provider
|
|
|
47
55
|
|
|
48
56
|
## Quick start
|
|
49
57
|
|
|
50
|
-
1. Create your nonoka config
|
|
58
|
+
1. Create your nonoka config (it will ask for your API key and save it to
|
|
59
|
+
`~/.config/nonoka/.env`):
|
|
51
60
|
|
|
52
61
|
```bash
|
|
53
62
|
nonoka-cli config init
|
|
54
63
|
```
|
|
55
64
|
|
|
56
|
-
For scripted setups, use the non-interactive mode
|
|
65
|
+
For scripted setups, use the non-interactive mode (you'll still need to set the
|
|
66
|
+
API key via `.env` or `export`):
|
|
57
67
|
|
|
58
68
|
```bash
|
|
59
69
|
nonoka-cli config init --yes --model deepseek-chat
|
|
@@ -108,9 +118,10 @@ nonoka-cli doctor --check-llm
|
|
|
108
118
|
### `nonoka-cli config init`
|
|
109
119
|
|
|
110
120
|
Interactive wizard that writes `~/.config/nonoka/config.yaml`. It asks for a
|
|
111
|
-
model identifier (e.g. `deepseek-chat`, `openai/gpt-4o`, `ollama/llama3.3`),
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
model identifier (e.g. `deepseek-chat`, `openai/gpt-4o`, `ollama/llama3.3`), a
|
|
122
|
+
masked API key, and whether to save it to `~/.config/nonoka/.env` (recommended),
|
|
123
|
+
directly in `config.yaml`, or skip saving. It also asks for a system prompt and
|
|
124
|
+
whether to auto-approve all tool calls.
|
|
114
125
|
|
|
115
126
|
Non-interactive example:
|
|
116
127
|
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
model: "deepseek-chat"
|
|
5
5
|
|
|
6
|
+
# API key / base URL are normally loaded from environment variables or a .env
|
|
7
|
+
# file. nonoka-cli automatically loads ~/.config/nonoka/.env and ./.env.
|
|
8
|
+
# You can also set them here (not recommended for api_key).
|
|
9
|
+
# api_key: "${DEEPSEEK_API_KEY}"
|
|
10
|
+
# base_url: "https://api.deepseek.com"
|
|
11
|
+
|
|
6
12
|
# Optional: override the default system prompt.
|
|
7
13
|
# If omitted, nonoka-cli uses a built-in coding-assistant prompt that
|
|
8
14
|
# explains the available tools and encourages autonomous tool use.
|
|
Binary file
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nonoka-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "OpenCode backend for the Nonoka Agent framework"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -20,10 +20,11 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.13",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
"nonoka>=1.3.
|
|
23
|
+
"nonoka>=1.3.2",
|
|
24
24
|
"structlog>=24.0.0",
|
|
25
25
|
"pydantic>=2.0",
|
|
26
26
|
"pyyaml>=6.0",
|
|
27
|
+
"python-dotenv>=1.0",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.scripts]
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
|
15
15
|
|
|
16
16
|
import structlog
|
|
17
|
+
from dotenv import load_dotenv
|
|
17
18
|
|
|
18
19
|
from nonoka_cli.bridge.server import main as server_main
|
|
19
20
|
from nonoka_cli.commands import config_cmd, doctor_cmd, opencode_cmd
|
|
@@ -22,6 +23,22 @@ from nonoka_cli.utils.logging import setup_logging
|
|
|
22
23
|
logger = structlog.get_logger("nonoka_cli.cli")
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _load_env_files() -> None:
|
|
27
|
+
"""Load .env files so config env-var substitution works transparently.
|
|
28
|
+
|
|
29
|
+
Priority (lowest to highest):
|
|
30
|
+
1. ~/.config/nonoka/.env
|
|
31
|
+
2. ./.env
|
|
32
|
+
Existing environment variables always win.
|
|
33
|
+
"""
|
|
34
|
+
global_env = Path.home() / ".config" / "nonoka" / ".env"
|
|
35
|
+
local_env = Path.cwd() / ".env"
|
|
36
|
+
for path in (global_env, local_env):
|
|
37
|
+
if path.exists():
|
|
38
|
+
load_dotenv(dotenv_path=path, override=False)
|
|
39
|
+
logger.debug("loaded_env_file", path=str(path))
|
|
40
|
+
|
|
41
|
+
|
|
25
42
|
def _build_parser() -> argparse.ArgumentParser:
|
|
26
43
|
"""Build the argument parser."""
|
|
27
44
|
parser = argparse.ArgumentParser(
|
|
@@ -74,6 +91,9 @@ def main() -> int:
|
|
|
74
91
|
parser = _build_parser()
|
|
75
92
|
args = parser.parse_args()
|
|
76
93
|
|
|
94
|
+
# Load .env files early so configuration and LLM providers see API keys.
|
|
95
|
+
_load_env_files()
|
|
96
|
+
|
|
77
97
|
if args.server:
|
|
78
98
|
return server_main(config_path=args.config, model=args.model)
|
|
79
99
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import getpass
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
8
9
|
from pathlib import Path
|
|
@@ -19,6 +20,9 @@ from nonoka_cli.utils.errors import ConfigError
|
|
|
19
20
|
logger = structlog.get_logger("nonoka_cli.commands.config")
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
_GLOBAL_ENV_PATH = Path.home() / ".config" / "nonoka" / ".env"
|
|
24
|
+
|
|
25
|
+
|
|
22
26
|
def _load_manager(args: argparse.Namespace) -> ConfigManager:
|
|
23
27
|
"""Load or create a ConfigManager from the requested path."""
|
|
24
28
|
path = getattr(args, "config", None)
|
|
@@ -49,6 +53,52 @@ def _confirm(prompt: str, default: bool = False) -> bool:
|
|
|
49
53
|
return answer in ("y", "yes")
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
def _read_secret(prompt: str) -> str:
|
|
57
|
+
"""Read a secret from stdin, masking input when possible."""
|
|
58
|
+
try:
|
|
59
|
+
return getpass.getpass(f"{prompt}: ").strip()
|
|
60
|
+
except (EOFError, KeyboardInterrupt):
|
|
61
|
+
return ""
|
|
62
|
+
except Exception:
|
|
63
|
+
# Fallback for non-TTY environments where getpass may fail.
|
|
64
|
+
value = _read_input(f"{prompt} (input will be visible)")
|
|
65
|
+
if value:
|
|
66
|
+
print("Warning: input was visible because this terminal does not support secure input.")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _load_env_file(path: Path) -> dict[str, str]:
|
|
71
|
+
"""Parse a simple KEY=VALUE .env file, ignoring comments and blank lines."""
|
|
72
|
+
values: dict[str, str] = {}
|
|
73
|
+
if not path.exists():
|
|
74
|
+
return values
|
|
75
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
76
|
+
line = line.strip()
|
|
77
|
+
if not line or line.startswith("#"):
|
|
78
|
+
continue
|
|
79
|
+
if "=" in line:
|
|
80
|
+
key, value = line.split("=", 1)
|
|
81
|
+
values[key.strip()] = value.strip()
|
|
82
|
+
return values
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_env_file(path: Path, env_var: str, value: str) -> None:
|
|
86
|
+
"""Write or update a key in a .env file."""
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
values = _load_env_file(path)
|
|
89
|
+
values[env_var] = value
|
|
90
|
+
|
|
91
|
+
lines: list[str] = ["# nonoka API keys and environment overrides", ""]
|
|
92
|
+
for key, val in values.items():
|
|
93
|
+
if " " in val or "#" in val:
|
|
94
|
+
val = f'"{val}"'
|
|
95
|
+
lines.append(f"{key}={val}")
|
|
96
|
+
lines.append("")
|
|
97
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
98
|
+
# Restrict permissions on the .env file.
|
|
99
|
+
path.chmod(0o600)
|
|
100
|
+
|
|
101
|
+
|
|
52
102
|
def _set_dotted(data: dict[str, Any], key: str, value: Any) -> None:
|
|
53
103
|
"""Set a possibly dotted key in a nested dict, creating parents as needed."""
|
|
54
104
|
parts = key.split(".")
|
|
@@ -107,6 +157,54 @@ def _api_key_env_for_model(model: str) -> str:
|
|
|
107
157
|
return "OPENAI_API_KEY"
|
|
108
158
|
|
|
109
159
|
|
|
160
|
+
def _api_key_source_summary(env_var: str) -> str:
|
|
161
|
+
"""Return a short description of where the API key is currently sourced."""
|
|
162
|
+
if os.getenv(env_var):
|
|
163
|
+
return f"from environment (${env_var})"
|
|
164
|
+
if _load_env_file(_GLOBAL_ENV_PATH).get(env_var):
|
|
165
|
+
return f"from {_GLOBAL_ENV_PATH}"
|
|
166
|
+
return ""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _collect_api_key(model: str) -> tuple[str, str, str]:
|
|
170
|
+
"""Collect API key info and optionally persist it.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
(env_var_name, api_key_for_config, summary_for_user)
|
|
174
|
+
"""
|
|
175
|
+
env_var = _api_key_env_for_model(model)
|
|
176
|
+
existing = os.getenv(env_var) or _load_env_file(_GLOBAL_ENV_PATH).get(env_var)
|
|
177
|
+
if existing:
|
|
178
|
+
print(f"Found {env_var} already set.")
|
|
179
|
+
return env_var, "", f"using existing ${env_var}"
|
|
180
|
+
|
|
181
|
+
key_input = _read_secret(f"{env_var} value (press Enter to skip)")
|
|
182
|
+
if not key_input:
|
|
183
|
+
return env_var, "", "no key provided"
|
|
184
|
+
|
|
185
|
+
print("")
|
|
186
|
+
print("Where would you like to save the API key?")
|
|
187
|
+
print(" [d] ~/.config/nonoka/.env (recommended, auto-loaded, file permission 600)")
|
|
188
|
+
print(" [c] directly in config.yaml (not recommended)")
|
|
189
|
+
print(" [s] skip saving, set it manually later")
|
|
190
|
+
choice = _read_input("Choice", "d").lower().strip()
|
|
191
|
+
|
|
192
|
+
if choice in ("c", "config"):
|
|
193
|
+
print("Warning: the API key will be stored in plain text in config.yaml.")
|
|
194
|
+
return env_var, key_input, "stored in config.yaml"
|
|
195
|
+
|
|
196
|
+
if choice in ("s", "skip"):
|
|
197
|
+
print(f"Remember to set the key later: export {env_var}=<your-key>")
|
|
198
|
+
return env_var, "", "not saved"
|
|
199
|
+
|
|
200
|
+
# Default: save to .env
|
|
201
|
+
_write_env_file(_GLOBAL_ENV_PATH, env_var, key_input)
|
|
202
|
+
# Make it available for the rest of this process too.
|
|
203
|
+
os.environ[env_var] = key_input
|
|
204
|
+
print(f"Saved {env_var} to {_GLOBAL_ENV_PATH}")
|
|
205
|
+
return env_var, "", f"saved to {_GLOBAL_ENV_PATH}"
|
|
206
|
+
|
|
207
|
+
|
|
110
208
|
def cmd_init(args: argparse.Namespace) -> int:
|
|
111
209
|
"""Create an initial nonoka config, interactively or with --yes defaults."""
|
|
112
210
|
path = Path(args.config) if args.config else ConfigLoader.DEFAULT_PATH
|
|
@@ -117,13 +215,12 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
117
215
|
model = getattr(args, "model", None) or "deepseek-chat"
|
|
118
216
|
auto_approve = getattr(args, "auto_approve", False)
|
|
119
217
|
env_var = _api_key_env_for_model(model)
|
|
120
|
-
|
|
121
|
-
if os.getenv(env_var):
|
|
122
|
-
api_key_display = "${" + env_var + "}"
|
|
218
|
+
key_summary = _api_key_source_summary(env_var) or "not configured"
|
|
123
219
|
|
|
124
220
|
config = CLIConfig(
|
|
125
221
|
model=model,
|
|
126
222
|
system_prompt=_DEFAULT_SYSTEM_PROMPT,
|
|
223
|
+
api_key="",
|
|
127
224
|
cli=CLIBehaviorConfig(auto_approve=auto_approve),
|
|
128
225
|
hitl=HITLConfigModel(
|
|
129
226
|
policy="auto" if auto_approve else "interactive",
|
|
@@ -136,10 +233,12 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
136
233
|
print("")
|
|
137
234
|
print(f"Configuration saved to {path}")
|
|
138
235
|
print(f"Model: {model}")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
236
|
+
print(f"API key ({env_var}): {key_summary}")
|
|
237
|
+
if not os.getenv(env_var) and not _load_env_file(_GLOBAL_ENV_PATH).get(env_var):
|
|
238
|
+
print(
|
|
239
|
+
f"Set your API key with: nonoka-cli config init (interactive) "
|
|
240
|
+
f"or export {env_var}=<your-key>"
|
|
241
|
+
)
|
|
143
242
|
return 0
|
|
144
243
|
|
|
145
244
|
print("")
|
|
@@ -149,26 +248,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
149
248
|
)
|
|
150
249
|
|
|
151
250
|
model = _read_input("Model identifier", "deepseek-chat")
|
|
152
|
-
env_var =
|
|
153
|
-
"Environment variable for the API key (optional, e.g. DEEPSEEK_API_KEY)",
|
|
154
|
-
"",
|
|
155
|
-
) or _api_key_env_for_model(model)
|
|
156
|
-
|
|
157
|
-
api_key_display = ""
|
|
158
|
-
if env_var:
|
|
159
|
-
existing = os.getenv(env_var, "")
|
|
160
|
-
if existing:
|
|
161
|
-
print(f"Found {env_var} in environment.")
|
|
162
|
-
api_key_display = "${" + env_var + "}"
|
|
163
|
-
else:
|
|
164
|
-
key_input = _read_input(f"{env_var} value (press Enter to skip)", "")
|
|
165
|
-
if key_input:
|
|
166
|
-
prompt = "Save the API key directly in the config file? (Not recommended)"
|
|
167
|
-
if _confirm(prompt, default=False):
|
|
168
|
-
api_key_display = key_input
|
|
169
|
-
else:
|
|
170
|
-
api_key_display = "${" + env_var + "}"
|
|
171
|
-
print(f"Please export {env_var} in your environment before running nonoka.")
|
|
251
|
+
env_var, api_key_value, key_summary = _collect_api_key(model)
|
|
172
252
|
|
|
173
253
|
system_prompt = _read_input(
|
|
174
254
|
"Optional system prompt",
|
|
@@ -180,6 +260,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
180
260
|
config = CLIConfig(
|
|
181
261
|
model=model,
|
|
182
262
|
system_prompt=system_prompt,
|
|
263
|
+
api_key=api_key_value,
|
|
183
264
|
cli=CLIBehaviorConfig(auto_approve=auto_approve),
|
|
184
265
|
hitl=HITLConfigModel(
|
|
185
266
|
policy="auto" if auto_approve else "interactive",
|
|
@@ -192,8 +273,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
192
273
|
|
|
193
274
|
print("")
|
|
194
275
|
print(f"Configuration saved to {path}")
|
|
195
|
-
|
|
196
|
-
print(f"Remember to export your API key: export {env_var}=<your-key>")
|
|
276
|
+
print(f"API key ({env_var}): {key_summary}")
|
|
197
277
|
return 0
|
|
198
278
|
|
|
199
279
|
|
|
@@ -37,6 +37,8 @@ class CLIConfig(BaseModel):
|
|
|
37
37
|
"""
|
|
38
38
|
model: str = "gpt-4o"
|
|
39
39
|
system_prompt: str = ""
|
|
40
|
+
api_key: str = ""
|
|
41
|
+
base_url: str = ""
|
|
40
42
|
mcp_servers: dict[str, MCPServerConfigModel] = Field(default_factory=dict)
|
|
41
43
|
tool_paths: list[Path] = Field(default_factory=list)
|
|
42
44
|
skills: list[str] = Field(default_factory=list)
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import os
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
from nonoka_cli.commands import config_cmd
|
|
8
10
|
from nonoka_cli.commands.config_cmd import _coerce_value, _set_dotted, cmd_init, cmd_set
|
|
9
11
|
from nonoka_cli.config.loader import ConfigLoader
|
|
10
12
|
from nonoka_cli.config.models import CLIConfig
|
|
@@ -58,3 +60,40 @@ def test_config_init_yes(tmp_path: Path):
|
|
|
58
60
|
assert cfg.model == "deepseek-chat"
|
|
59
61
|
assert cfg.cli.auto_approve is False
|
|
60
62
|
assert cfg.hitl.policy == "interactive"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_write_env_file(tmp_path: Path):
|
|
66
|
+
env_path = tmp_path / ".env"
|
|
67
|
+
config_cmd._write_env_file(env_path, "DEEPSEEK_API_KEY", "sk-secret")
|
|
68
|
+
assert env_path.exists()
|
|
69
|
+
assert env_path.stat().st_mode & 0o777 == 0o600
|
|
70
|
+
values = config_cmd._load_env_file(env_path)
|
|
71
|
+
assert values.get("DEEPSEEK_API_KEY") == "sk-secret"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_config_init_saves_api_key_to_env(tmp_path: Path, monkeypatch):
|
|
75
|
+
config_path = tmp_path / "config.yaml"
|
|
76
|
+
env_path = tmp_path / ".env"
|
|
77
|
+
monkeypatch.setattr(config_cmd, "_GLOBAL_ENV_PATH", env_path)
|
|
78
|
+
|
|
79
|
+
inputs = iter(["deepseek-chat", "", "d", ""])
|
|
80
|
+
def fake_read_input(prompt: str, default: str = "") -> str:
|
|
81
|
+
return next(inputs) if default == "" else default
|
|
82
|
+
monkeypatch.setattr(config_cmd, "_read_input", fake_read_input)
|
|
83
|
+
monkeypatch.setattr(config_cmd, "_read_secret", lambda prompt: "sk-test-key")
|
|
84
|
+
monkeypatch.setattr(config_cmd, "_confirm", lambda prompt, default=False: False)
|
|
85
|
+
|
|
86
|
+
args = argparse.Namespace(config=str(config_path), yes=False)
|
|
87
|
+
assert cmd_init(args) == 0
|
|
88
|
+
|
|
89
|
+
assert config_path.exists()
|
|
90
|
+
cfg = ConfigLoader.load(config_path)
|
|
91
|
+
assert cfg.model == "deepseek-chat"
|
|
92
|
+
assert cfg.api_key == ""
|
|
93
|
+
|
|
94
|
+
values = config_cmd._load_env_file(env_path)
|
|
95
|
+
assert values.get("DEEPSEEK_API_KEY") == "sk-test-key"
|
|
96
|
+
assert os.getenv("DEEPSEEK_API_KEY") == "sk-test-key"
|
|
97
|
+
|
|
98
|
+
# Clean up so the key does not leak into other tests.
|
|
99
|
+
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from unittest import mock
|
|
9
8
|
|
|
@@ -52,8 +51,9 @@ class TestCheckNonokaCliVersion:
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
class TestCheckConfig:
|
|
55
|
-
def test_missing_config(self, tmp_path: Path):
|
|
56
|
-
|
|
54
|
+
def test_missing_config(self, tmp_path: Path, monkeypatch):
|
|
55
|
+
monkeypatch.setattr(ConfigLoader, "DEFAULT_PATH", tmp_path / "config.yaml")
|
|
56
|
+
monkeypatch.chdir(tmp_path)
|
|
57
57
|
result, cfg = check_config(None)
|
|
58
58
|
assert result.status == "error"
|
|
59
59
|
assert cfg is None
|
|
@@ -75,7 +75,8 @@ class TestCheckApiKey:
|
|
|
75
75
|
assert result.status == "ok"
|
|
76
76
|
assert "DEEPSEEK_API_KEY" in result.message
|
|
77
77
|
|
|
78
|
-
def test_key_missing(self):
|
|
78
|
+
def test_key_missing(self, monkeypatch):
|
|
79
|
+
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
|
79
80
|
cfg = CLIConfig(model="deepseek-chat")
|
|
80
81
|
result = check_api_key(cfg)
|
|
81
82
|
assert result.status == "error"
|
|
@@ -125,8 +126,9 @@ class TestCheckProvider:
|
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
class TestCheckOpencodeConfig:
|
|
128
|
-
def test_missing_config(self, tmp_path: Path):
|
|
129
|
-
|
|
129
|
+
def test_missing_config(self, tmp_path: Path, monkeypatch):
|
|
130
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
131
|
+
monkeypatch.chdir(tmp_path)
|
|
130
132
|
result = check_opencode_config()
|
|
131
133
|
assert result.status == "error"
|
|
132
134
|
|
|
@@ -144,7 +146,8 @@ class TestCheckOpencodeConfig:
|
|
|
144
146
|
result = check_opencode_config()
|
|
145
147
|
assert result.status == "ok"
|
|
146
148
|
|
|
147
|
-
def test_config_without_nonoka_provider(self, tmp_path: Path):
|
|
149
|
+
def test_config_without_nonoka_provider(self, tmp_path: Path, monkeypatch):
|
|
150
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
148
151
|
config_path = tmp_path / "opencode.json"
|
|
149
152
|
config_path.write_text(json.dumps({"model": "gpt-4o"}))
|
|
150
153
|
result = check_opencode_config()
|
|
@@ -1609,7 +1609,7 @@ wheels = [
|
|
|
1609
1609
|
|
|
1610
1610
|
[[package]]
|
|
1611
1611
|
name = "nonoka"
|
|
1612
|
-
version = "1.3.
|
|
1612
|
+
version = "1.3.2"
|
|
1613
1613
|
source = { registry = "https://pypi.org/simple" }
|
|
1614
1614
|
dependencies = [
|
|
1615
1615
|
{ name = "anyio" },
|
|
@@ -1623,18 +1623,19 @@ dependencies = [
|
|
|
1623
1623
|
{ name = "structlog" },
|
|
1624
1624
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
1625
1625
|
]
|
|
1626
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1626
|
+
sdist = { url = "https://files.pythonhosted.org/packages/49/36/37a0f7f82c3ee199f7502973c746d647654a90538c540f9cff8c43bae911/nonoka-1.3.2.tar.gz", hash = "sha256:fb00ab1feb7be0791664694b9216ae7f8214435f3b97be12a6ac596f6ff174a2", size = 140261, upload-time = "2026-07-02T15:26:01.99Z" }
|
|
1627
1627
|
wheels = [
|
|
1628
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1628
|
+
{ url = "https://files.pythonhosted.org/packages/87/99/1f9e7a0fbdc4c31b8b41e16f6c73f44e0daa5c2d16a1ccc0dc11b94a1aba/nonoka-1.3.2-py3-none-any.whl", hash = "sha256:795c6b1c14aaa37d73a0855f999c3d031013d3a0eafcc69295e4b67afc310575", size = 103557, upload-time = "2026-07-02T15:25:55.658Z" },
|
|
1629
1629
|
]
|
|
1630
1630
|
|
|
1631
1631
|
[[package]]
|
|
1632
1632
|
name = "nonoka-cli"
|
|
1633
|
-
version = "0.2.
|
|
1633
|
+
version = "0.2.3"
|
|
1634
1634
|
source = { editable = "." }
|
|
1635
1635
|
dependencies = [
|
|
1636
1636
|
{ name = "nonoka" },
|
|
1637
1637
|
{ name = "pydantic" },
|
|
1638
|
+
{ name = "python-dotenv" },
|
|
1638
1639
|
{ name = "pyyaml" },
|
|
1639
1640
|
{ name = "structlog" },
|
|
1640
1641
|
]
|
|
@@ -1648,11 +1649,12 @@ dev = [
|
|
|
1648
1649
|
|
|
1649
1650
|
[package.metadata]
|
|
1650
1651
|
requires-dist = [
|
|
1651
|
-
{ name = "nonoka", specifier = ">=1.3.
|
|
1652
|
+
{ name = "nonoka", specifier = ">=1.3.2" },
|
|
1652
1653
|
{ name = "pydantic", specifier = ">=2.0" },
|
|
1653
1654
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
|
1654
1655
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
|
|
1655
1656
|
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" },
|
|
1657
|
+
{ name = "python-dotenv", specifier = ">=1.0" },
|
|
1656
1658
|
{ name = "pyyaml", specifier = ">=6.0" },
|
|
1657
1659
|
{ name = "structlog", specifier = ">=24.0.0" },
|
|
1658
1660
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/src/nonoka-language-model.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tests/language-model.test.ts
RENAMED
|
File without changes
|
{nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tests/protocol.test.ts
RENAMED
|
File without changes
|
{nonoka_cli-0.2.1 → nonoka_cli-0.2.3}/packages/nonoka-opencode-provider/tests/stream.test.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|