nonoka-cli 0.2.11__tar.gz → 0.2.13__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.
Files changed (125) hide show
  1. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/PKG-INFO +60 -26
  2. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/README.md +59 -25
  3. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/pyproject.toml +1 -1
  4. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/__init__.py +1 -1
  5. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/cli.py +42 -20
  6. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/config_cmd.py +24 -13
  7. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/doctor_cmd.py +24 -8
  8. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/opencode_cmd.py +52 -24
  9. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/run_cmd.py +18 -5
  10. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/config/loader.py +6 -2
  11. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/config/models.py +2 -1
  12. nonoka_cli-0.2.13/src/nonoka_cli/safety/__init__.py +20 -0
  13. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/safety/preflight.py +13 -0
  14. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/tools/builtins/file_tools.py +12 -2
  15. nonoka_cli-0.2.13/tests/unit/commands/test_cli_entrypoint.py +56 -0
  16. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_config_cmd.py +7 -1
  17. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_doctor_cmd.py +16 -0
  18. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_opencode_cmd.py +28 -2
  19. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_run_cmd.py +21 -0
  20. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/config/test_loader.py +4 -0
  21. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/config/test_models.py +11 -0
  22. nonoka_cli-0.2.13/tests/unit/safety/test_preflight.py +27 -0
  23. nonoka_cli-0.2.13/tests/unit/test_install_script.py +200 -0
  24. nonoka_cli-0.2.13/tests/unit/tools/test_file_tools.py +120 -0
  25. nonoka_cli-0.2.11/src/nonoka_cli/safety/__init__.py +0 -6
  26. nonoka_cli-0.2.11/tests/unit/safety/test_preflight.py +0 -14
  27. nonoka_cli-0.2.11/tests/unit/tools/test_file_tools.py +0 -60
  28. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/.gitignore +0 -0
  29. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/LICENSE +0 -0
  30. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/NOTICE +0 -0
  31. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/__main__.py +0 -0
  32. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/benchmark/__init__.py +0 -0
  33. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/benchmark/harbor.py +0 -0
  34. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/benchmark/swe_bench.py +0 -0
  35. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/benchmark/swe_environment.py +0 -0
  36. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/benchmark/swe_runner.py +0 -0
  37. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/benchmark/watchdog.py +0 -0
  38. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/bridge/__init__.py +0 -0
  39. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/bridge/events.py +0 -0
  40. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/bridge/handler.py +0 -0
  41. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/bridge/nonoka_tools.py +0 -0
  42. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/bridge/protocol.py +0 -0
  43. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/bridge/server.py +0 -0
  44. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/__init__.py +0 -0
  45. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/benchmark_cmd.py +0 -0
  46. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/eval_cmd.py +0 -0
  47. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/logs_cmd.py +0 -0
  48. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/plugin_cmd.py +0 -0
  49. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/commands/sessions_cmd.py +0 -0
  50. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/config/__init__.py +0 -0
  51. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/config/manager.py +0 -0
  52. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/__init__.py +0 -0
  53. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/agent_factory.py +0 -0
  54. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/context.py +0 -0
  55. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/context_trimmer.py +0 -0
  56. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/git_service.py +0 -0
  57. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/mcp_service.py +0 -0
  58. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/namespaces.py +0 -0
  59. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/operational_signals.py +0 -0
  60. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/orchestrator.py +0 -0
  61. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/plugin_manifest.py +0 -0
  62. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/plugin_manifest_converter.py +0 -0
  63. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/project_agents.py +0 -0
  64. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/prompt_builder.py +0 -0
  65. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/repo_map_service.py +0 -0
  66. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/run_evidence.py +0 -0
  67. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/runner_service.py +0 -0
  68. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/scorecard.py +0 -0
  69. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/semantic_cache.py +0 -0
  70. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/session_service.py +0 -0
  71. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/task_state.py +0 -0
  72. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/tool_output_policy.py +0 -0
  73. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/core/tool_service.py +0 -0
  74. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/mcp/__init__.py +0 -0
  75. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/mcp/manager.py +0 -0
  76. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/mcp/models.py +0 -0
  77. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/safety/sandbox.py +0 -0
  78. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/sessions/__init__.py +0 -0
  79. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/sessions/manager.py +0 -0
  80. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/sessions/models.py +0 -0
  81. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/skills/__init__.py +0 -0
  82. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/tools/__init__.py +0 -0
  83. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/tools/builtins/__init__.py +0 -0
  84. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/tools/builtins/agent_tools.py +0 -0
  85. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/tools/loader.py +0 -0
  86. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/utils/__init__.py +0 -0
  87. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/utils/errors.py +0 -0
  88. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/utils/logging.py +0 -0
  89. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/src/nonoka_cli/utils/trace_logger.py +0 -0
  90. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/conftest.py +0 -0
  91. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/fixtures/bridge_external_capabilities.json +0 -0
  92. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/integration/test_server_external_capabilities.py +0 -0
  93. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/integration/test_watchdog_process_group.py +0 -0
  94. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/benchmark/test_harbor.py +0 -0
  95. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/benchmark/test_swe_environment.py +0 -0
  96. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/benchmark/test_watchdog.py +0 -0
  97. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/bridge/test_events.py +0 -0
  98. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/bridge/test_handler.py +0 -0
  99. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/bridge/test_nonoka_tools.py +0 -0
  100. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/bridge/test_package.py +0 -0
  101. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/bridge/test_protocol.py +0 -0
  102. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/bridge/test_server.py +0 -0
  103. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_benchmark_cmd.py +0 -0
  104. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_eval_cmd.py +0 -0
  105. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_logs_cmd.py +0 -0
  106. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_plugin_cmd.py +0 -0
  107. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/commands/test_swe_bench_cmd.py +0 -0
  108. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_agent_factory.py +0 -0
  109. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_context_trimmer.py +0 -0
  110. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_git_service.py +0 -0
  111. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_operational_signals.py +0 -0
  112. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_orchestrator.py +0 -0
  113. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_plugin_manifest.py +0 -0
  114. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_plugin_manifest_converter.py +0 -0
  115. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_project_agents.py +0 -0
  116. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_prompt_builder.py +0 -0
  117. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_repo_map_service.py +0 -0
  118. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_safety_policy.py +0 -0
  119. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_scorecard.py +0 -0
  120. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_semantic_cache.py +0 -0
  121. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_task_state.py +0 -0
  122. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/core/test_tool_output_policy.py +0 -0
  123. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/sessions/test_manager.py +0 -0
  124. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/utils/test_logging.py +0 -0
  125. {nonoka_cli-0.2.11 → nonoka_cli-0.2.13}/tests/unit/utils/test_trace_logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nonoka-cli
3
- Version: 0.2.11
3
+ Version: 0.2.13
4
4
  Summary: OpenCode backend for the Nonoka Agent framework
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -33,7 +33,7 @@ English | [简体中文](README.zh-CN.md)
33
33
 
34
34
  OpenCode backend for the [Nonoka](https://pypi.org/project/nonoka/) Agent framework.
35
35
 
36
- `nonoka-cli` runs as a stdio NDJSON bridge server (`nonoka-cli --server`) that
36
+ Nonoka runs as a stdio NDJSON bridge server (`python -m nonoka_cli --server`) that
37
37
  talks to the `nonoka-opencode-provider` TypeScript package. When used inside
38
38
  OpenCode, Nonoka acts as the conversation/decision backend while OpenCode owns
39
39
  tool execution and human-in-the-loop (HITL) approval using its native tools.
@@ -51,24 +51,40 @@ The installer will:
51
51
  1. Check Python 3.10+ and Node/npm.
52
52
  2. Install or update OpenCode.
53
53
  3. Install `nonoka-cli` and the OpenCode provider.
54
- 4. Generate `~/.config/nonoka/config.yaml` and `~/.config/opencode/opencode.json`.
54
+ 4. Ask where to keep the installation, configuration, and npm packages.
55
+ 5. Generate the Nonoka and project-level OpenCode configuration.
56
+
57
+ The prompts show what each directory contains and provide these defaults:
58
+
59
+ ```text
60
+ Installation directory (Python environment, launchers, and npm tools; e.g. ~/nonoka)
61
+ [~/.local/share/nonoka]
62
+ Configuration directory (config.yaml and .env; e.g. ~/.config/nonoka)
63
+ [~/.config/nonoka]
64
+ npm prefix (OpenCode/provider global packages)
65
+ [~/.local/share/nonoka/npm]
66
+ ```
67
+
68
+ Press Enter to accept a default, or type a path such as `~/tools/nonoka`.
69
+ The generated `INSTALL_DIR/bin/nonoka` launcher remembers all three paths, so
70
+ using its absolute path does not require any environment exports.
55
71
 
56
72
  After installing, configure your API key and run `nonoka`:
57
73
 
58
74
  ```bash
59
75
  # Interactive: it will ask for your key and save it to ~/.config/nonoka/.env
60
- nonoka-cli config init
76
+ nonoka config init
61
77
 
62
78
  # Or set it manually
63
79
  export DEEPSEEK_API_KEY=<your-key>
64
80
 
65
- nonoka-cli doctor
81
+ nonoka doctor
66
82
  nonoka
67
83
  ```
68
84
 
69
- `nonoka` is a shortcut for `nonoka-cli run` and starts the OpenCode TUI with the
70
- Nonoka backend. You can also use `nonoka run --message "<task>"` for one-shot
71
- CLI usage.
85
+ `nonoka` is the primary command. It starts the OpenCode TUI when invoked without
86
+ a subcommand, while `nonoka run --message "<task>"` provides one-shot CLI usage.
87
+ The legacy `nonoka-cli` executable remains available for compatibility.
72
88
 
73
89
  `nonoka-cli` automatically loads `~/.config/nonoka/.env` and `./.env` on startup,
74
90
  so you don't need to `export` every time if you save the key in `.env`.
@@ -76,6 +92,21 @@ so you don't need to `export` every time if you save the key in `.env`.
76
92
  > To use `uv` instead of `pip`, or to run non-interactively, pass flags:
77
93
  > `curl -fsSL https://nonoka.dev/install.sh | bash -s -- --uv --yes`.
78
94
 
95
+ For a non-interactive custom location, use flags (environment variables with
96
+ the same names are also supported):
97
+
98
+ ```bash
99
+ bash install.sh --yes --uv --npm-opencode \
100
+ --install-dir ~/tools/nonoka \
101
+ --config-dir ~/.config/nonoka \
102
+ --npm-prefix ~/tools/nonoka/npm
103
+
104
+ ~/tools/nonoka/bin/nonoka doctor
105
+ ```
106
+
107
+ The corresponding variables are `NONOKA_INSTALL_DIR`, `NONOKA_CONFIG_DIR`,
108
+ and `NONOKA_NPM_PREFIX`. CLI flags take precedence over environment variables.
109
+
79
110
  ## Manual installation
80
111
 
81
112
  ```bash
@@ -94,24 +125,24 @@ npm install -g nonoka-opencode-provider
94
125
  `~/.config/nonoka/.env`):
95
126
 
96
127
  ```bash
97
- nonoka-cli config init
128
+ nonoka config init
98
129
  ```
99
130
 
100
131
  For scripted setups, use the non-interactive mode (you'll still need to set the
101
132
  API key via `.env` or `export`):
102
133
 
103
134
  ```bash
104
- nonoka-cli config init --yes --model deepseek/deepseek-v4-pro
135
+ nonoka config init --yes --model deepseek/deepseek-v4-pro
105
136
  ```
106
137
 
107
138
  2. Generate an OpenCode config in the current project or globally:
108
139
 
109
140
  ```bash
110
141
  # Project-level
111
- nonoka-cli opencode init
142
+ nonoka init
112
143
 
113
144
  # User-level
114
- nonoka-cli opencode init --global
145
+ nonoka init --global
115
146
  ```
116
147
 
117
148
  3. Make sure your model API key is exported, then run:
@@ -132,7 +163,7 @@ Example output:
132
163
 
133
164
  ```
134
165
  nonoka-cli doctor
135
- ✓ nonoka-cli 0.2.11
166
+ ✓ nonoka-cli 0.2.13
136
167
  ✓ Python 3.11
137
168
  ✓ opencode 1.18.2
138
169
  ✓ provider nonoka-opencode-provider@0.2.17
@@ -403,7 +434,7 @@ similarity score without storing raw cache queries. `max_total_tokens` and
403
434
  is unavailable, `fail_on_unknown_cost: true` terminates the task rather than
404
435
  silently exceeding the cost budget.
405
436
 
406
- ### `nonoka-cli opencode init`
437
+ ### `nonoka init`
407
438
 
408
439
  Generate or merge an `opencode.json` in the current directory and create
409
440
  `.opencode/agents/build.md` from your nonoka `system_prompt`. The generated
@@ -412,7 +443,8 @@ nonoka config path to the backend.
412
443
 
413
444
  ## OpenCode configuration
414
445
 
415
- `nonoka-cli opencode init` generates two things:
446
+ `nonoka init` generates two things. `nonoka opencode init` remains as a
447
+ backward-compatible, explicit spelling:
416
448
 
417
449
  1. `opencode.json` in the current directory, which wires OpenCode to the
418
450
  `nonoka-opencode-provider` package and sets HITL permissions.
@@ -429,7 +461,7 @@ A typical generated `opencode.json` looks like:
429
461
  "npm": "nonoka-opencode-provider",
430
462
  "name": "Nonoka",
431
463
  "options": {
432
- "serverCommand": ["bash", "-c", "nonoka-cli --server 2>/tmp/nonoka-server.log"],
464
+ "serverCommand": ["/path/to/python", "-m", "nonoka_cli", "--server"],
433
465
  "cwd": ".",
434
466
  "configPath": "~/.config/nonoka/config.yaml"
435
467
  },
@@ -463,12 +495,12 @@ A typical generated `opencode.json` looks like:
463
495
 
464
496
  The `"tools": {"skill": false}` line disables OpenCode's native `skill:<name>`
465
497
  tool so it does not collide with nonoka's `load_skill` / `skill__<name>__<tool>`
466
- workflow. `nonoka-cli opencode init` writes this automatically.
498
+ workflow. `nonoka init` writes this automatically.
467
499
 
468
500
  ## Prompt ownership
469
501
 
470
502
  Nonoka owns the canonical system prompt via `system_prompt` in
471
- `~/.config/nonoka/config.yaml`. When you run `nonoka-cli opencode init`, the
503
+ `~/.config/nonoka/config.yaml`. When you run `nonoka init`, the
472
504
  command adapts that prompt and writes it to `.opencode/agents/build.md` so
473
505
  OpenCode uses it for its primary agent. OpenCode-specific guidelines (tool
474
506
  names, approval behavior, path conventions) are appended automatically; they are
@@ -482,10 +514,9 @@ When running inside OpenCode, HITL is handled by OpenCode itself. The generated
482
514
  Nonoka forwards OpenCode's native tool definitions to the model, approval
483
515
  dialogs render natively for `bash`, `read`, `write`, and `edit` operations.
484
516
 
485
- `nonoka-cli`'s own `cli.auto_approve` and `hitl.policy` settings only apply to
486
- standalone server / CLI mode. In OpenCode mode, permissions are governed by the
487
- `permission` block in `opencode.json`. To keep `nonoka.yaml` as the single
488
- source of truth, add a `permissions` block and re-run `nonoka-cli opencode init`:
517
+ In OpenCode mode, `nonoka init` derives both generated permission blocks from
518
+ `cli.auto_approve` and optional `permissions` overrides in `nonoka.yaml`. Add a
519
+ `permissions` block and re-run `nonoka init` to keep YAML as the source of truth:
489
520
 
490
521
  ```yaml
491
522
  permissions:
@@ -495,8 +526,9 @@ permissions:
495
526
  edit: ask
496
527
  ```
497
528
 
498
- `cli.auto_approve: true` still auto-allows the core coding tools when no
499
- `permissions` block is present. For standalone mode, use `hitl.policy: auto`.
529
+ `cli.auto_approve: true` auto-allows the core coding tools, including read-only
530
+ `glob` and `grep`, before explicit overrides are applied. For standalone mode,
531
+ `hitl.policy` still controls Nonoka-owned tool approval.
500
532
 
501
533
  ## External-tools mode
502
534
 
@@ -511,7 +543,8 @@ default. OpenCode sends its native tool list (e.g. `bash`, `read`, `write`,
511
543
  `Runner.resume_external_tools()`.
512
544
 
513
545
  To start external-tools mode, run OpenCode with the generated `opencode.json`;
514
- the provider spawns `nonoka-cli --server` automatically.
546
+ the provider automatically spawns `python -m nonoka_cli --server` with the
547
+ interpreter that generated the project config.
515
548
 
516
549
  ## MCP and Skill support
517
550
 
@@ -710,7 +743,8 @@ here because they affect the TUI/HITL experience but cannot be fixed inside
710
743
 
711
744
  ## Server logs and request traces
712
745
 
713
- When running inside OpenCode, the provider spawns `nonoka-cli --server` as a
746
+ When running inside OpenCode, the provider spawns the interpreter-pinned
747
+ `python -m nonoka_cli --server` command as a
714
748
  long-lived NDJSON bridge. Server stderr is redirected by the provider to a
715
749
  per-working-directory log file so it does not pollute OpenCode's TUI:
716
750
 
@@ -4,7 +4,7 @@ English | [简体中文](README.zh-CN.md)
4
4
 
5
5
  OpenCode backend for the [Nonoka](https://pypi.org/project/nonoka/) Agent framework.
6
6
 
7
- `nonoka-cli` runs as a stdio NDJSON bridge server (`nonoka-cli --server`) that
7
+ Nonoka runs as a stdio NDJSON bridge server (`python -m nonoka_cli --server`) that
8
8
  talks to the `nonoka-opencode-provider` TypeScript package. When used inside
9
9
  OpenCode, Nonoka acts as the conversation/decision backend while OpenCode owns
10
10
  tool execution and human-in-the-loop (HITL) approval using its native tools.
@@ -22,24 +22,40 @@ The installer will:
22
22
  1. Check Python 3.10+ and Node/npm.
23
23
  2. Install or update OpenCode.
24
24
  3. Install `nonoka-cli` and the OpenCode provider.
25
- 4. Generate `~/.config/nonoka/config.yaml` and `~/.config/opencode/opencode.json`.
25
+ 4. Ask where to keep the installation, configuration, and npm packages.
26
+ 5. Generate the Nonoka and project-level OpenCode configuration.
27
+
28
+ The prompts show what each directory contains and provide these defaults:
29
+
30
+ ```text
31
+ Installation directory (Python environment, launchers, and npm tools; e.g. ~/nonoka)
32
+ [~/.local/share/nonoka]
33
+ Configuration directory (config.yaml and .env; e.g. ~/.config/nonoka)
34
+ [~/.config/nonoka]
35
+ npm prefix (OpenCode/provider global packages)
36
+ [~/.local/share/nonoka/npm]
37
+ ```
38
+
39
+ Press Enter to accept a default, or type a path such as `~/tools/nonoka`.
40
+ The generated `INSTALL_DIR/bin/nonoka` launcher remembers all three paths, so
41
+ using its absolute path does not require any environment exports.
26
42
 
27
43
  After installing, configure your API key and run `nonoka`:
28
44
 
29
45
  ```bash
30
46
  # Interactive: it will ask for your key and save it to ~/.config/nonoka/.env
31
- nonoka-cli config init
47
+ nonoka config init
32
48
 
33
49
  # Or set it manually
34
50
  export DEEPSEEK_API_KEY=<your-key>
35
51
 
36
- nonoka-cli doctor
52
+ nonoka doctor
37
53
  nonoka
38
54
  ```
39
55
 
40
- `nonoka` is a shortcut for `nonoka-cli run` and starts the OpenCode TUI with the
41
- Nonoka backend. You can also use `nonoka run --message "<task>"` for one-shot
42
- CLI usage.
56
+ `nonoka` is the primary command. It starts the OpenCode TUI when invoked without
57
+ a subcommand, while `nonoka run --message "<task>"` provides one-shot CLI usage.
58
+ The legacy `nonoka-cli` executable remains available for compatibility.
43
59
 
44
60
  `nonoka-cli` automatically loads `~/.config/nonoka/.env` and `./.env` on startup,
45
61
  so you don't need to `export` every time if you save the key in `.env`.
@@ -47,6 +63,21 @@ so you don't need to `export` every time if you save the key in `.env`.
47
63
  > To use `uv` instead of `pip`, or to run non-interactively, pass flags:
48
64
  > `curl -fsSL https://nonoka.dev/install.sh | bash -s -- --uv --yes`.
49
65
 
66
+ For a non-interactive custom location, use flags (environment variables with
67
+ the same names are also supported):
68
+
69
+ ```bash
70
+ bash install.sh --yes --uv --npm-opencode \
71
+ --install-dir ~/tools/nonoka \
72
+ --config-dir ~/.config/nonoka \
73
+ --npm-prefix ~/tools/nonoka/npm
74
+
75
+ ~/tools/nonoka/bin/nonoka doctor
76
+ ```
77
+
78
+ The corresponding variables are `NONOKA_INSTALL_DIR`, `NONOKA_CONFIG_DIR`,
79
+ and `NONOKA_NPM_PREFIX`. CLI flags take precedence over environment variables.
80
+
50
81
  ## Manual installation
51
82
 
52
83
  ```bash
@@ -65,24 +96,24 @@ npm install -g nonoka-opencode-provider
65
96
  `~/.config/nonoka/.env`):
66
97
 
67
98
  ```bash
68
- nonoka-cli config init
99
+ nonoka config init
69
100
  ```
70
101
 
71
102
  For scripted setups, use the non-interactive mode (you'll still need to set the
72
103
  API key via `.env` or `export`):
73
104
 
74
105
  ```bash
75
- nonoka-cli config init --yes --model deepseek/deepseek-v4-pro
106
+ nonoka config init --yes --model deepseek/deepseek-v4-pro
76
107
  ```
77
108
 
78
109
  2. Generate an OpenCode config in the current project or globally:
79
110
 
80
111
  ```bash
81
112
  # Project-level
82
- nonoka-cli opencode init
113
+ nonoka init
83
114
 
84
115
  # User-level
85
- nonoka-cli opencode init --global
116
+ nonoka init --global
86
117
  ```
87
118
 
88
119
  3. Make sure your model API key is exported, then run:
@@ -103,7 +134,7 @@ Example output:
103
134
 
104
135
  ```
105
136
  nonoka-cli doctor
106
- ✓ nonoka-cli 0.2.11
137
+ ✓ nonoka-cli 0.2.13
107
138
  ✓ Python 3.11
108
139
  ✓ opencode 1.18.2
109
140
  ✓ provider nonoka-opencode-provider@0.2.17
@@ -374,7 +405,7 @@ similarity score without storing raw cache queries. `max_total_tokens` and
374
405
  is unavailable, `fail_on_unknown_cost: true` terminates the task rather than
375
406
  silently exceeding the cost budget.
376
407
 
377
- ### `nonoka-cli opencode init`
408
+ ### `nonoka init`
378
409
 
379
410
  Generate or merge an `opencode.json` in the current directory and create
380
411
  `.opencode/agents/build.md` from your nonoka `system_prompt`. The generated
@@ -383,7 +414,8 @@ nonoka config path to the backend.
383
414
 
384
415
  ## OpenCode configuration
385
416
 
386
- `nonoka-cli opencode init` generates two things:
417
+ `nonoka init` generates two things. `nonoka opencode init` remains as a
418
+ backward-compatible, explicit spelling:
387
419
 
388
420
  1. `opencode.json` in the current directory, which wires OpenCode to the
389
421
  `nonoka-opencode-provider` package and sets HITL permissions.
@@ -400,7 +432,7 @@ A typical generated `opencode.json` looks like:
400
432
  "npm": "nonoka-opencode-provider",
401
433
  "name": "Nonoka",
402
434
  "options": {
403
- "serverCommand": ["bash", "-c", "nonoka-cli --server 2>/tmp/nonoka-server.log"],
435
+ "serverCommand": ["/path/to/python", "-m", "nonoka_cli", "--server"],
404
436
  "cwd": ".",
405
437
  "configPath": "~/.config/nonoka/config.yaml"
406
438
  },
@@ -434,12 +466,12 @@ A typical generated `opencode.json` looks like:
434
466
 
435
467
  The `"tools": {"skill": false}` line disables OpenCode's native `skill:<name>`
436
468
  tool so it does not collide with nonoka's `load_skill` / `skill__<name>__<tool>`
437
- workflow. `nonoka-cli opencode init` writes this automatically.
469
+ workflow. `nonoka init` writes this automatically.
438
470
 
439
471
  ## Prompt ownership
440
472
 
441
473
  Nonoka owns the canonical system prompt via `system_prompt` in
442
- `~/.config/nonoka/config.yaml`. When you run `nonoka-cli opencode init`, the
474
+ `~/.config/nonoka/config.yaml`. When you run `nonoka init`, the
443
475
  command adapts that prompt and writes it to `.opencode/agents/build.md` so
444
476
  OpenCode uses it for its primary agent. OpenCode-specific guidelines (tool
445
477
  names, approval behavior, path conventions) are appended automatically; they are
@@ -453,10 +485,9 @@ When running inside OpenCode, HITL is handled by OpenCode itself. The generated
453
485
  Nonoka forwards OpenCode's native tool definitions to the model, approval
454
486
  dialogs render natively for `bash`, `read`, `write`, and `edit` operations.
455
487
 
456
- `nonoka-cli`'s own `cli.auto_approve` and `hitl.policy` settings only apply to
457
- standalone server / CLI mode. In OpenCode mode, permissions are governed by the
458
- `permission` block in `opencode.json`. To keep `nonoka.yaml` as the single
459
- source of truth, add a `permissions` block and re-run `nonoka-cli opencode init`:
488
+ In OpenCode mode, `nonoka init` derives both generated permission blocks from
489
+ `cli.auto_approve` and optional `permissions` overrides in `nonoka.yaml`. Add a
490
+ `permissions` block and re-run `nonoka init` to keep YAML as the source of truth:
460
491
 
461
492
  ```yaml
462
493
  permissions:
@@ -466,8 +497,9 @@ permissions:
466
497
  edit: ask
467
498
  ```
468
499
 
469
- `cli.auto_approve: true` still auto-allows the core coding tools when no
470
- `permissions` block is present. For standalone mode, use `hitl.policy: auto`.
500
+ `cli.auto_approve: true` auto-allows the core coding tools, including read-only
501
+ `glob` and `grep`, before explicit overrides are applied. For standalone mode,
502
+ `hitl.policy` still controls Nonoka-owned tool approval.
471
503
 
472
504
  ## External-tools mode
473
505
 
@@ -482,7 +514,8 @@ default. OpenCode sends its native tool list (e.g. `bash`, `read`, `write`,
482
514
  `Runner.resume_external_tools()`.
483
515
 
484
516
  To start external-tools mode, run OpenCode with the generated `opencode.json`;
485
- the provider spawns `nonoka-cli --server` automatically.
517
+ the provider automatically spawns `python -m nonoka_cli --server` with the
518
+ interpreter that generated the project config.
486
519
 
487
520
  ## MCP and Skill support
488
521
 
@@ -681,7 +714,8 @@ here because they affect the TUI/HITL experience but cannot be fixed inside
681
714
 
682
715
  ## Server logs and request traces
683
716
 
684
- When running inside OpenCode, the provider spawns `nonoka-cli --server` as a
717
+ When running inside OpenCode, the provider spawns the interpreter-pinned
718
+ `python -m nonoka_cli --server` command as a
685
719
  long-lived NDJSON bridge. Server stderr is redirected by the provider to a
686
720
  per-working-directory log file so it does not pollute OpenCode's TUI:
687
721
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nonoka-cli"
7
- version = "0.2.11"
7
+ version = "0.2.13"
8
8
  description = "OpenCode backend for the Nonoka Agent framework"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.11"
5
+ __version__ = "0.2.13"
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import importlib.metadata
6
7
  import logging
7
8
  import os
8
9
  import sys
@@ -42,17 +43,37 @@ from nonoka_cli.utils.logging import setup_logging
42
43
  logger = structlog.get_logger("nonoka_cli.cli")
43
44
 
44
45
 
45
- def _load_env_files() -> None:
46
+ def _version() -> str:
47
+ """Return the installed CLI version, including editable installs."""
48
+ try:
49
+ return importlib.metadata.version("nonoka-cli")
50
+ except importlib.metadata.PackageNotFoundError:
51
+ return "unknown"
52
+
53
+
54
+ def _load_env_files(config_path: Path | str | None = None) -> None:
46
55
  """Load .env files so config env-var substitution works transparently.
47
56
 
48
57
  Priority (lowest to highest):
49
- 1. ~/.config/nonoka/.env
50
- 2. ./.env
58
+ 1. $NONOKA_CONFIG_DIR/.env or ~/.config/nonoka/.env
59
+ 2. the explicit config file's sibling .env
60
+ 3. ./.env
51
61
  Existing environment variables always win.
52
62
  """
53
- global_env = Path.home() / ".config" / "nonoka" / ".env"
63
+ configured_dir = Path(
64
+ os.getenv("NONOKA_CONFIG_DIR", str(Path.home() / ".config" / "nonoka"))
65
+ ).expanduser()
66
+ candidates = [configured_dir / ".env"]
67
+ if config_path is not None:
68
+ candidates.append(Path(config_path).expanduser().resolve().parent / ".env")
54
69
  local_env = Path.cwd() / ".env"
55
- for path in (global_env, local_env):
70
+ candidates.append(local_env)
71
+ seen: set[Path] = set()
72
+ for path in candidates:
73
+ path = path.resolve()
74
+ if path in seen:
75
+ continue
76
+ seen.add(path)
56
77
  if path.exists():
57
78
  load_dotenv(dotenv_path=path, override=False)
58
79
  logger.debug("loaded_env_file", path=str(path))
@@ -61,9 +82,14 @@ def _load_env_files() -> None:
61
82
  def _build_parser() -> argparse.ArgumentParser:
62
83
  """Build the argument parser."""
63
84
  parser = argparse.ArgumentParser(
64
- prog="nonoka-cli",
85
+ prog="nonoka",
65
86
  description="Terminal frontend for the Nonoka Agent framework",
66
87
  )
88
+ parser.add_argument(
89
+ "--version",
90
+ action="version",
91
+ version=f"%(prog)s {_version()}",
92
+ )
67
93
  parser.add_argument(
68
94
  "--config",
69
95
  type=Path,
@@ -96,6 +122,7 @@ def _build_parser() -> argparse.ArgumentParser:
96
122
  subparsers = parser.add_subparsers(dest="command")
97
123
  config_cmd.add_subparser(subparsers)
98
124
  doctor_cmd.add_subparser(subparsers)
125
+ opencode_cmd.add_init_subparser(subparsers)
99
126
  opencode_cmd.add_subparser(subparsers)
100
127
  plugin_cmd.add_subparser(subparsers)
101
128
  run_cmd.add_subparser(subparsers)
@@ -116,28 +143,23 @@ def main() -> int:
116
143
  parser = _build_parser()
117
144
  args = parser.parse_args()
118
145
 
119
- # Load .env files early so configuration and LLM providers see API keys.
120
- _load_env_files()
146
+ log_level = (
147
+ logging.DEBUG if args.debug
148
+ else logging.INFO if args.verbose
149
+ else logging.WARNING
150
+ )
151
+ setup_logging(level=log_level, console=args.verbose or args.debug)
121
152
 
153
+ # Load .env files after logging is configured so the debug-only path message
154
+ # does not leak into normal command output.
155
+ _load_env_files(args.config)
122
156
  if args.server:
123
157
  return server_main(config_path=args.config, model=args.model)
124
158
 
125
159
  if args.command and getattr(args, "func", None):
126
- log_level = (
127
- logging.DEBUG if args.debug
128
- else logging.INFO if args.verbose
129
- else logging.WARNING
130
- )
131
- setup_logging(level=log_level, console=args.verbose or args.debug)
132
160
  return args.func(args)
133
161
 
134
162
  # No subcommand given: launch the OpenCode TUI by default.
135
- log_level = (
136
- logging.DEBUG if args.debug
137
- else logging.INFO if args.verbose
138
- else logging.WARNING
139
- )
140
- setup_logging(level=log_level, console=args.verbose or args.debug)
141
163
  return run_cmd.launch_tui(args)
142
164
 
143
165
 
@@ -23,6 +23,16 @@ logger = structlog.get_logger("nonoka_cli.commands.config")
23
23
  _GLOBAL_ENV_PATH = Path.home() / ".config" / "nonoka" / ".env"
24
24
 
25
25
 
26
+ def _env_path_for_config(config_path: Path | None) -> Path:
27
+ """Keep credentials beside an explicitly selected configuration file."""
28
+ if config_path is not None:
29
+ return config_path.expanduser().resolve().parent / ".env"
30
+ configured_dir = os.getenv("NONOKA_CONFIG_DIR")
31
+ if configured_dir:
32
+ return Path(configured_dir).expanduser().resolve() / ".env"
33
+ return _GLOBAL_ENV_PATH
34
+
35
+
26
36
  def _load_manager(args: argparse.Namespace) -> ConfigManager:
27
37
  """Load or create a ConfigManager from the requested path."""
28
38
  path = getattr(args, "config", None)
@@ -162,23 +172,23 @@ def _api_key_env_for_model(model: str) -> str:
162
172
  return "OPENAI_API_KEY"
163
173
 
164
174
 
165
- def _api_key_source_summary(env_var: str) -> str:
175
+ def _api_key_source_summary(env_var: str, env_path: Path = _GLOBAL_ENV_PATH) -> str:
166
176
  """Return a short description of where the API key is currently sourced."""
167
177
  if os.getenv(env_var):
168
178
  return f"from environment (${env_var})"
169
- if _load_env_file(_GLOBAL_ENV_PATH).get(env_var):
170
- return f"from {_GLOBAL_ENV_PATH}"
179
+ if _load_env_file(env_path).get(env_var):
180
+ return f"from {env_path}"
171
181
  return ""
172
182
 
173
183
 
174
- def _collect_api_key(model: str) -> tuple[str, str, str]:
184
+ def _collect_api_key(model: str, env_path: Path = _GLOBAL_ENV_PATH) -> tuple[str, str, str]:
175
185
  """Collect API key info and optionally persist it.
176
186
 
177
187
  Returns:
178
188
  (env_var_name, api_key_for_config, summary_for_user)
179
189
  """
180
190
  env_var = _api_key_env_for_model(model)
181
- existing = os.getenv(env_var) or _load_env_file(_GLOBAL_ENV_PATH).get(env_var)
191
+ existing = os.getenv(env_var) or _load_env_file(env_path).get(env_var)
182
192
  if existing:
183
193
  print(f"Found {env_var} already set.")
184
194
  return env_var, "", f"using existing ${env_var}"
@@ -203,16 +213,17 @@ def _collect_api_key(model: str) -> tuple[str, str, str]:
203
213
  return env_var, "", "not saved"
204
214
 
205
215
  # Default: save to .env
206
- _write_env_file(_GLOBAL_ENV_PATH, env_var, key_input)
216
+ _write_env_file(env_path, env_var, key_input)
207
217
  # Make it available for the rest of this process too.
208
218
  os.environ[env_var] = key_input
209
- print(f"Saved {env_var} to {_GLOBAL_ENV_PATH}")
210
- return env_var, "", f"saved to {_GLOBAL_ENV_PATH}"
219
+ print(f"Saved {env_var} to {env_path}")
220
+ return env_var, "", f"saved to {env_path}"
211
221
 
212
222
 
213
223
  def cmd_init(args: argparse.Namespace) -> int:
214
224
  """Create an initial nonoka config, interactively or with --yes defaults."""
215
- path = Path(args.config) if args.config else ConfigLoader.DEFAULT_PATH
225
+ path = Path(args.config).expanduser() if args.config else ConfigLoader.DEFAULT_PATH
226
+ env_path = _env_path_for_config(path if args.config else None)
216
227
 
217
228
  print(f"Creating nonoka configuration at: {path}")
218
229
 
@@ -220,7 +231,7 @@ def cmd_init(args: argparse.Namespace) -> int:
220
231
  model = getattr(args, "model", None) or "deepseek/deepseek-v4-pro"
221
232
  auto_approve = getattr(args, "auto_approve", False)
222
233
  env_var = _api_key_env_for_model(model)
223
- key_summary = _api_key_source_summary(env_var) or "not configured"
234
+ key_summary = _api_key_source_summary(env_var, env_path) or "not configured"
224
235
 
225
236
  config = CLIConfig(
226
237
  model=model,
@@ -240,9 +251,9 @@ def cmd_init(args: argparse.Namespace) -> int:
240
251
  print(f"Configuration saved to {path}")
241
252
  print(f"Model: {model}")
242
253
  print(f"API key ({env_var}): {key_summary}")
243
- if not os.getenv(env_var) and not _load_env_file(_GLOBAL_ENV_PATH).get(env_var):
254
+ if not os.getenv(env_var) and not _load_env_file(env_path).get(env_var):
244
255
  print(
245
- f"Set your API key with: nonoka-cli config init (interactive) "
256
+ f"Set your API key with: nonoka config init (interactive) "
246
257
  f"or export {env_var}=<your-key>"
247
258
  )
248
259
  return 0
@@ -254,7 +265,7 @@ def cmd_init(args: argparse.Namespace) -> int:
254
265
  )
255
266
 
256
267
  model = _read_input("Model identifier", "deepseek/deepseek-v4-pro")
257
- env_var, api_key_value, key_summary = _collect_api_key(model)
268
+ env_var, api_key_value, key_summary = _collect_api_key(model, env_path)
258
269
 
259
270
  system_prompt = _read_input(
260
271
  "Optional system prompt",