feat(cli): add takopi config subcommand (#153)
This commit is contained in:
@@ -51,10 +51,18 @@ Rules:
|
|||||||
|
|
||||||
Plugin visibility can be restricted via:
|
Plugin visibility can be restricted via:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[plugins]
|
|
||||||
enabled = ["takopi-engine-acme", "takopi-transport-slack"]
|
```sh
|
||||||
```
|
takopi config set plugins.enabled '["takopi-engine-acme", "takopi-transport-slack"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugins]
|
||||||
|
enabled = ["takopi-engine-acme", "takopi-transport-slack"]
|
||||||
|
```
|
||||||
|
|
||||||
When set, Takopi filters by **distribution name** (package metadata), not by entrypoint name.
|
When set, Takopi filters by **distribution name** (package metadata), not by entrypoint name.
|
||||||
This lets you:
|
This lets you:
|
||||||
|
|||||||
@@ -9,10 +9,18 @@ Chat sessions store one resume token per engine per chat (per sender in group ch
|
|||||||
|
|
||||||
If you chose **handoff** during onboarding and want to switch to chat mode:
|
If you chose **handoff** during onboarding and want to switch to chat mode:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
session_mode = "chat" # stateless | chat
|
```sh
|
||||||
```
|
takopi config set transports.telegram.session_mode "chat"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
session_mode = "chat" # stateless | chat
|
||||||
|
```
|
||||||
|
|
||||||
With `session_mode = "chat"`, new messages in the chat continue the current thread automatically.
|
With `session_mode = "chat"`, new messages in the chat continue the current thread automatically.
|
||||||
|
|
||||||
@@ -32,10 +40,18 @@ Chat sessions do not remove reply-to-continue. If resume lines are visible, you
|
|||||||
|
|
||||||
If you prefer a cleaner chat, hide resume lines:
|
If you prefer a cleaner chat, hide resume lines:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
show_resume_line = false
|
```sh
|
||||||
```
|
takopi config set transports.telegram.show_resume_line false
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
show_resume_line = false
|
||||||
|
```
|
||||||
|
|
||||||
## How it behaves in groups
|
## How it behaves in groups
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,28 @@ Upload files into the active repo/worktree or fetch files back into Telegram.
|
|||||||
|
|
||||||
## Enable file transfer
|
## Enable file transfer
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram.files]
|
|
||||||
enabled = true
|
```sh
|
||||||
auto_put = true
|
takopi config set transports.telegram.files.enabled true
|
||||||
auto_put_mode = "upload" # upload | prompt
|
takopi config set transports.telegram.files.auto_put true
|
||||||
uploads_dir = "incoming"
|
takopi config set transports.telegram.files.auto_put_mode "upload"
|
||||||
allowed_user_ids = [123456789]
|
takopi config set transports.telegram.files.uploads_dir "incoming"
|
||||||
deny_globs = [".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]
|
takopi config set transports.telegram.files.allowed_user_ids "[123456789]"
|
||||||
```
|
takopi config set transports.telegram.files.deny_globs '[".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram.files]
|
||||||
|
enabled = true
|
||||||
|
auto_put = true
|
||||||
|
auto_put_mode = "upload" # upload | prompt
|
||||||
|
uploads_dir = "incoming"
|
||||||
|
allowed_user_ids = [123456789]
|
||||||
|
deny_globs = [".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]
|
||||||
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
@@ -56,4 +69,3 @@ Directories are zipped automatically.
|
|||||||
|
|
||||||
- [Commands & directives](../reference/commands-and-directives.md)
|
- [Commands & directives](../reference/commands-and-directives.md)
|
||||||
- [Config reference](../reference/config.md)
|
- [Config reference](../reference/config.md)
|
||||||
|
|
||||||
|
|||||||
+52
-18
@@ -11,10 +11,18 @@ takopi init happy-gadgets
|
|||||||
|
|
||||||
This adds a project to your config:
|
This adds a project to your config:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
```
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
## Target a project from chat
|
## Target a project from chat
|
||||||
|
|
||||||
@@ -28,30 +36,56 @@ Send:
|
|||||||
|
|
||||||
Projects can override global defaults:
|
Projects can override global defaults:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
default_engine = "claude"
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
worktrees_dir = ".worktrees"
|
takopi config set projects.happy-gadgets.default_engine "claude"
|
||||||
worktree_base = "master"
|
takopi config set projects.happy-gadgets.worktrees_dir ".worktrees"
|
||||||
```
|
takopi config set projects.happy-gadgets.worktree_base "master"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
default_engine = "claude"
|
||||||
|
worktrees_dir = ".worktrees"
|
||||||
|
worktree_base = "master"
|
||||||
|
```
|
||||||
|
|
||||||
If you expect to edit config while Takopi is running, enable hot reload:
|
If you expect to edit config while Takopi is running, enable hot reload:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
watch_config = true
|
|
||||||
```
|
```sh
|
||||||
|
takopi config set watch_config true
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
watch_config = true
|
||||||
|
```
|
||||||
|
|
||||||
## Set a default project
|
## Set a default project
|
||||||
|
|
||||||
If you mostly work in one repo:
|
If you mostly work in one repo:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
default_project = "happy-gadgets"
|
|
||||||
```
|
```sh
|
||||||
|
takopi config set default_project "happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
default_project = "happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Context resolution](../reference/context-resolution.md)
|
- [Context resolution](../reference/context-resolution.md)
|
||||||
- [Worktrees](worktrees.md)
|
- [Worktrees](worktrees.md)
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,20 @@ takopi chat-id --project happy-gadgets
|
|||||||
|
|
||||||
Then send any message in the target chat. Takopi captures the `chat_id` and updates your config:
|
Then send any message in the target chat. Takopi captures the `chat_id` and updates your config:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
chat_id = -1001234567890
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
```
|
takopi config set projects.happy-gadgets.chat_id -1001234567890
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
chat_id = -1001234567890
|
||||||
|
```
|
||||||
|
|
||||||
Messages from that chat now default to the project.
|
Messages from that chat now default to the project.
|
||||||
|
|
||||||
@@ -36,4 +45,3 @@ takopi chat-id
|
|||||||
|
|
||||||
- [Topics](topics.md)
|
- [Topics](topics.md)
|
||||||
- [Context resolution](../reference/context-resolution.md)
|
- [Context resolution](../reference/context-resolution.md)
|
||||||
|
|
||||||
|
|||||||
+14
-5
@@ -27,11 +27,20 @@ Topics bind Telegram **forum threads** to a project/branch context. Each topic k
|
|||||||
|
|
||||||
## Enable topics
|
## Enable topics
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram.topics]
|
|
||||||
enabled = true
|
```sh
|
||||||
scope = "auto" # auto | main | projects | all
|
takopi config set transports.telegram.topics.enabled true
|
||||||
```
|
takopi config set transports.telegram.topics.scope "auto"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram.topics]
|
||||||
|
enabled = true
|
||||||
|
scope = "auto" # auto | main | projects | all
|
||||||
|
```
|
||||||
|
|
||||||
### Scope explained
|
### Scope explained
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ Enable transcription so voice notes become normal text runs.
|
|||||||
|
|
||||||
## Enable transcription
|
## Enable transcription
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
voice_transcription = true
|
```sh
|
||||||
voice_transcription_model = "gpt-4o-mini-transcribe" # optional
|
takopi config set transports.telegram.voice_transcription true
|
||||||
```
|
takopi config set transports.telegram.voice_transcription_model "gpt-4o-mini-transcribe"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
voice_transcription = true
|
||||||
|
voice_transcription_model = "gpt-4o-mini-transcribe" # optional
|
||||||
|
```
|
||||||
|
|
||||||
Set `OPENAI_API_KEY` in your environment (uses OpenAI’s transcription API).
|
Set `OPENAI_API_KEY` in your environment (uses OpenAI’s transcription API).
|
||||||
|
|
||||||
@@ -24,4 +33,3 @@ If transcription fails, you’ll get an error message and the run is skipped.
|
|||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Config reference](../reference/config.md)
|
- [Config reference](../reference/config.md)
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ Use `@branch` to run tasks in a dedicated git worktree for that branch.
|
|||||||
|
|
||||||
Add a `worktrees_dir` (and optionally a base branch) to the project:
|
Add a `worktrees_dir` (and optionally a base branch) to the project:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
worktrees_dir = ".worktrees" # relative to project path
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
worktree_base = "master" # base branch for new worktrees
|
takopi config set projects.happy-gadgets.worktrees_dir ".worktrees"
|
||||||
```
|
takopi config set projects.happy-gadgets.worktree_base "master"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
worktrees_dir = ".worktrees" # relative to project path
|
||||||
|
worktree_base = "master" # base branch for new worktrees
|
||||||
|
```
|
||||||
|
|
||||||
## Run in a branch worktree
|
## Run in a branch worktree
|
||||||
|
|
||||||
@@ -39,4 +49,3 @@ When you reply, this context carries forward (you usually don’t need to repeat
|
|||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Context resolution](../reference/context-resolution.md)
|
- [Context resolution](../reference/context-resolution.md)
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,18 @@ BACKEND = EngineBackend(
|
|||||||
|
|
||||||
Engine config is a raw table in `takopi.toml`:
|
Engine config is a raw table in `takopi.toml`:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[myengine]
|
|
||||||
model = "..."
|
```sh
|
||||||
```
|
takopi config set myengine.model "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[myengine]
|
||||||
|
model = "..."
|
||||||
|
```
|
||||||
|
|
||||||
## Transport backend plugin
|
## Transport backend plugin
|
||||||
|
|
||||||
@@ -92,19 +100,35 @@ BACKEND = MyCommand()
|
|||||||
|
|
||||||
Configure under `[plugins.<id>]`:
|
Configure under `[plugins.<id>]`:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[plugins.hello]
|
|
||||||
greeting = "hello"
|
```sh
|
||||||
```
|
takopi config set plugins.hello.greeting "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugins.hello]
|
||||||
|
greeting = "hello"
|
||||||
|
```
|
||||||
|
|
||||||
The parsed dict is available as `ctx.plugin_config` in `handle()`.
|
The parsed dict is available as `ctx.plugin_config` in `handle()`.
|
||||||
|
|
||||||
## Enable/disable installed plugins
|
## Enable/disable installed plugins
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[plugins]
|
|
||||||
enabled = ["takopi-transport-slack", "takopi-engine-acme"]
|
```sh
|
||||||
```
|
takopi config set plugins.enabled '["takopi-transport-slack", "takopi-engine-acme"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugins]
|
||||||
|
enabled = ["takopi-transport-slack", "takopi-engine-acme"]
|
||||||
|
```
|
||||||
|
|
||||||
- `enabled = []` (default) means “load all installed plugins”.
|
- `enabled = []` (default) means “load all installed plugins”.
|
||||||
- If non-empty, only distributions with matching names are visible.
|
- If non-empty, only distributions with matching names are visible.
|
||||||
|
|||||||
+151
-26
@@ -4,9 +4,17 @@ Takopi reads configuration from `~/.takopi/takopi.toml`.
|
|||||||
|
|
||||||
If you expect to edit config while Takopi is running, set:
|
If you expect to edit config while Takopi is running, set:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
watch_config = true
|
|
||||||
```
|
```sh
|
||||||
|
takopi config set watch_config true
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
watch_config = true
|
||||||
|
```
|
||||||
|
|
||||||
## Top-level keys
|
## Top-level keys
|
||||||
|
|
||||||
@@ -19,11 +27,20 @@ watch_config = true
|
|||||||
|
|
||||||
## `transports.telegram`
|
## `transports.telegram`
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
bot_token = "..."
|
```sh
|
||||||
chat_id = 123
|
takopi config set transports.telegram.bot_token "..."
|
||||||
```
|
takopi config set transports.telegram.chat_id 123
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
bot_token = "..."
|
||||||
|
chat_id = 123
|
||||||
|
```
|
||||||
|
|
||||||
| Key | Type | Default | Notes |
|
| Key | Type | Default | Notes |
|
||||||
|-----|------|---------|-------|
|
|-----|------|---------|-------|
|
||||||
@@ -62,14 +79,26 @@ File size limits (not configurable):
|
|||||||
|
|
||||||
## `projects.<alias>`
|
## `projects.<alias>`
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
worktrees_dir = ".worktrees"
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
default_engine = "claude"
|
takopi config set projects.happy-gadgets.worktrees_dir ".worktrees"
|
||||||
worktree_base = "master"
|
takopi config set projects.happy-gadgets.default_engine "claude"
|
||||||
chat_id = -1001234567890
|
takopi config set projects.happy-gadgets.worktree_base "master"
|
||||||
```
|
takopi config set projects.happy-gadgets.chat_id -1001234567890
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
worktrees_dir = ".worktrees"
|
||||||
|
default_engine = "claude"
|
||||||
|
worktree_base = "master"
|
||||||
|
chat_id = -1001234567890
|
||||||
|
```
|
||||||
|
|
||||||
| Key | Type | Default | Notes |
|
| Key | Type | Default | Notes |
|
||||||
|-----|------|---------|-------|
|
|-----|------|---------|-------|
|
||||||
@@ -85,10 +114,18 @@ Legacy config note: top-level `bot_token` / `chat_id` are auto-migrated into `[t
|
|||||||
|
|
||||||
### `plugins.enabled`
|
### `plugins.enabled`
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[plugins]
|
|
||||||
enabled = ["takopi-transport-slack", "takopi-engine-acme"]
|
```sh
|
||||||
```
|
takopi config set plugins.enabled '["takopi-transport-slack", "takopi-engine-acme"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[plugins]
|
||||||
|
enabled = ["takopi-transport-slack", "takopi-engine-acme"]
|
||||||
|
```
|
||||||
|
|
||||||
- `enabled = []` (default) means “load all installed plugins”.
|
- `enabled = []` (default) means “load all installed plugins”.
|
||||||
- If non-empty, only distributions with matching names are visible (case-insensitive).
|
- If non-empty, only distributions with matching names are visible (case-insensitive).
|
||||||
@@ -99,11 +136,99 @@ Plugin-specific configuration lives under `[plugins.<id>]` and is passed to comm
|
|||||||
|
|
||||||
## Engine-specific config tables
|
## Engine-specific config tables
|
||||||
|
|
||||||
Engines can have top-level config tables keyed by engine id, for example:
|
Engines use **top-level tables** keyed by engine id. Built-in engines are listed
|
||||||
|
here; plugin engines should document their own keys.
|
||||||
|
|
||||||
```toml
|
### `codex`
|
||||||
[codex]
|
|
||||||
model = "..."
|
|
||||||
```
|
|
||||||
|
|
||||||
The shape is engine-defined.
|
| Key | Type | Default | Notes |
|
||||||
|
|-----|------|---------|-------|
|
||||||
|
| `extra_args` | string[] | `["-c", "notify=[]"]` | Extra CLI args for `codex` (exec-only flags are rejected). |
|
||||||
|
| `profile` | string | (unset) | Passed as `--profile <name>` and used as the session title. |
|
||||||
|
|
||||||
|
=== "takopi config"
|
||||||
|
|
||||||
|
```sh
|
||||||
|
takopi config set codex.extra_args '["-c", "notify=[]"]'
|
||||||
|
takopi config set codex.profile "work"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[codex]
|
||||||
|
extra_args = ["-c", "notify=[]"]
|
||||||
|
profile = "work"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `claude`
|
||||||
|
|
||||||
|
| Key | Type | Default | Notes |
|
||||||
|
|-----|------|---------|-------|
|
||||||
|
| `model` | string | (unset) | Optional model override. |
|
||||||
|
| `allowed_tools` | string[] | `["Bash", "Read", "Edit", "Write"]` | Auto-approve tool rules. |
|
||||||
|
| `dangerously_skip_permissions` | bool | `false` | Skip Claude permissions prompts. |
|
||||||
|
| `use_api_billing` | bool | `false` | Keep `ANTHROPIC_API_KEY` for API billing. |
|
||||||
|
|
||||||
|
=== "takopi config"
|
||||||
|
|
||||||
|
```sh
|
||||||
|
takopi config set claude.model "claude-sonnet-4-5-20250929"
|
||||||
|
takopi config set claude.allowed_tools '["Bash", "Read", "Edit", "Write"]'
|
||||||
|
takopi config set claude.dangerously_skip_permissions false
|
||||||
|
takopi config set claude.use_api_billing false
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[claude]
|
||||||
|
model = "claude-sonnet-4-5-20250929"
|
||||||
|
allowed_tools = ["Bash", "Read", "Edit", "Write"]
|
||||||
|
dangerously_skip_permissions = false
|
||||||
|
use_api_billing = false
|
||||||
|
```
|
||||||
|
|
||||||
|
### `pi`
|
||||||
|
|
||||||
|
| Key | Type | Default | Notes |
|
||||||
|
|-----|------|---------|-------|
|
||||||
|
| `model` | string | (unset) | Passed as `--model`. |
|
||||||
|
| `provider` | string | (unset) | Passed as `--provider`. |
|
||||||
|
| `extra_args` | string[] | `[]` | Extra CLI args for `pi`. |
|
||||||
|
|
||||||
|
=== "takopi config"
|
||||||
|
|
||||||
|
```sh
|
||||||
|
takopi config set pi.model "..."
|
||||||
|
takopi config set pi.provider "..."
|
||||||
|
takopi config set pi.extra_args "[]"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[pi]
|
||||||
|
model = "..."
|
||||||
|
provider = "..."
|
||||||
|
extra_args = []
|
||||||
|
```
|
||||||
|
|
||||||
|
### `opencode`
|
||||||
|
|
||||||
|
| Key | Type | Default | Notes |
|
||||||
|
|-----|------|---------|-------|
|
||||||
|
| `model` | string | (unset) | Optional model override. |
|
||||||
|
|
||||||
|
=== "takopi config"
|
||||||
|
|
||||||
|
```sh
|
||||||
|
takopi config set opencode.model "claude-sonnet"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[opencode]
|
||||||
|
model = "claude-sonnet"
|
||||||
|
```
|
||||||
|
|||||||
@@ -18,22 +18,39 @@ worktree-based runs via `@branch`.
|
|||||||
All config lives in `~/.takopi/takopi.toml`.
|
All config lives in `~/.takopi/takopi.toml`.
|
||||||
See [Config](config.md) for the full reference.
|
See [Config](config.md) for the full reference.
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
default_engine = "codex" # optional
|
|
||||||
default_project = "z80" # optional
|
|
||||||
transport = "telegram" # optional, defaults to "telegram"
|
|
||||||
|
|
||||||
[transports.telegram]
|
```sh
|
||||||
bot_token = "..." # required
|
takopi config set default_engine "codex"
|
||||||
chat_id = 123 # required
|
takopi config set default_project "z80"
|
||||||
|
takopi config set transport "telegram"
|
||||||
|
takopi config set transports.telegram.bot_token "..."
|
||||||
|
takopi config set transports.telegram.chat_id 123
|
||||||
|
takopi config set projects.z80.path "~/dev/z80"
|
||||||
|
takopi config set projects.z80.worktrees_dir ".worktrees"
|
||||||
|
takopi config set projects.z80.default_engine "codex"
|
||||||
|
takopi config set projects.z80.worktree_base "master"
|
||||||
|
takopi config set projects.z80.chat_id -123
|
||||||
|
```
|
||||||
|
|
||||||
[projects.z80]
|
=== "toml"
|
||||||
path = "~/dev/z80" # required (repo root)
|
|
||||||
worktrees_dir = ".worktrees" # optional, default ".worktrees"
|
```toml
|
||||||
default_engine = "codex" # optional, per-project override
|
default_engine = "codex" # optional
|
||||||
worktree_base = "master" # optional, base for new branches
|
default_project = "z80" # optional
|
||||||
chat_id = -123 # optional, project chat id
|
transport = "telegram" # optional, defaults to "telegram"
|
||||||
```
|
|
||||||
|
[transports.telegram]
|
||||||
|
bot_token = "..." # required
|
||||||
|
chat_id = 123 # required
|
||||||
|
|
||||||
|
[projects.z80]
|
||||||
|
path = "~/dev/z80" # required (repo root)
|
||||||
|
worktrees_dir = ".worktrees" # optional, default ".worktrees"
|
||||||
|
default_engine = "codex" # optional, per-project override
|
||||||
|
worktree_base = "master" # optional, base for new branches
|
||||||
|
chat_id = -123 # optional, project chat id
|
||||||
|
```
|
||||||
|
|
||||||
Legacy config note: top-level `bot_token` / `chat_id` are auto-migrated into
|
Legacy config note: top-level `bot_token` / `chat_id` are auto-migrated into
|
||||||
`[transports.telegram]` on startup.
|
`[transports.telegram]` on startup.
|
||||||
|
|||||||
@@ -68,17 +68,29 @@ Add a new optional `[claude]` section.
|
|||||||
|
|
||||||
Recommended v1 schema:
|
Recommended v1 schema:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
# ~/.takopi/takopi.toml
|
|
||||||
|
|
||||||
default_engine = "claude"
|
```sh
|
||||||
|
takopi config set default_engine "claude"
|
||||||
|
takopi config set claude.model "claude-sonnet-4-5-20250929"
|
||||||
|
takopi config set claude.allowed_tools '["Bash", "Read", "Edit", "Write"]'
|
||||||
|
takopi config set claude.dangerously_skip_permissions false
|
||||||
|
takopi config set claude.use_api_billing false
|
||||||
|
```
|
||||||
|
|
||||||
[claude]
|
=== "toml"
|
||||||
model = "claude-sonnet-4-5-20250929" # optional (Claude Code supports model override in settings too)
|
|
||||||
allowed_tools = ["Bash", "Read", "Edit", "Write"] # optional but strongly recommended for automation
|
```toml
|
||||||
dangerously_skip_permissions = false # optional (high risk; prefer sandbox use only)
|
# ~/.takopi/takopi.toml
|
||||||
use_api_billing = false # optional (keep ANTHROPIC_API_KEY for API billing)
|
|
||||||
```
|
default_engine = "claude"
|
||||||
|
|
||||||
|
[claude]
|
||||||
|
model = "claude-sonnet-4-5-20250929" # optional (Claude Code supports model override in settings too)
|
||||||
|
allowed_tools = ["Bash", "Read", "Edit", "Write"] # optional but strongly recommended for automation
|
||||||
|
dangerously_skip_permissions = false # optional (high risk; prefer sandbox use only)
|
||||||
|
use_api_billing = false # optional (keep ANTHROPIC_API_KEY for API billing)
|
||||||
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
|||||||
@@ -210,15 +210,26 @@ Claude runner implementation summary (no Takopi domain model changes):
|
|||||||
|
|
||||||
A minimal TOML config for Claude:
|
A minimal TOML config for Claude:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[claude]
|
|
||||||
# model: opus | sonnet | haiku
|
|
||||||
model = "sonnet"
|
|
||||||
|
|
||||||
allowed_tools = ["Bash", "Read", "Edit", "Write", "WebSearch"]
|
```sh
|
||||||
dangerously_skip_permissions = false
|
takopi config set claude.model "sonnet"
|
||||||
use_api_billing = false
|
takopi config set claude.allowed_tools '["Bash", "Read", "Edit", "Write", "WebSearch"]'
|
||||||
```
|
takopi config set claude.dangerously_skip_permissions false
|
||||||
|
takopi config set claude.use_api_billing false
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[claude]
|
||||||
|
# model: opus | sonnet | haiku
|
||||||
|
model = "sonnet"
|
||||||
|
|
||||||
|
allowed_tools = ["Bash", "Read", "Edit", "Write", "WebSearch"]
|
||||||
|
dangerously_skip_permissions = false
|
||||||
|
use_api_billing = false
|
||||||
|
```
|
||||||
|
|
||||||
Takopi only maps these keys to Claude CLI flags; other options should be configured in Claude Code settings.
|
Takopi only maps these keys to Claude CLI flags; other options should be configured in Claude Code settings.
|
||||||
If `allowed_tools` is omitted, Takopi defaults to `["Bash", "Read", "Edit", "Write"]`.
|
If `allowed_tools` is omitted, Takopi defaults to `["Bash", "Read", "Edit", "Write"]`.
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ npm i -g opencode-ai@latest
|
|||||||
|
|
||||||
Add to your `takopi.toml`:
|
Add to your `takopi.toml`:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[opencode]
|
|
||||||
model = "claude-sonnet" # optional
|
```sh
|
||||||
```
|
takopi config set opencode.model "claude-sonnet"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[opencode]
|
||||||
|
model = "claude-sonnet" # optional
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -58,16 +58,27 @@ Add a new optional `[pi]` section.
|
|||||||
|
|
||||||
Recommended schema:
|
Recommended schema:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
# ~/.takopi/takopi.toml
|
|
||||||
|
|
||||||
default_engine = "pi"
|
```sh
|
||||||
|
takopi config set default_engine "pi"
|
||||||
|
takopi config set pi.model "..."
|
||||||
|
takopi config set pi.provider "..."
|
||||||
|
takopi config set pi.extra_args "[]"
|
||||||
|
```
|
||||||
|
|
||||||
[pi]
|
=== "toml"
|
||||||
model = "..." # optional; passed as --model
|
|
||||||
provider = "..." # optional; passed as --provider
|
```toml
|
||||||
extra_args = [] # optional list of strings, appended verbatim
|
# ~/.takopi/takopi.toml
|
||||||
```
|
|
||||||
|
default_engine = "pi"
|
||||||
|
|
||||||
|
[pi]
|
||||||
|
model = "..." # optional; passed as --model
|
||||||
|
provider = "..." # optional; passed as --provider
|
||||||
|
extra_args = [] # optional list of strings, appended verbatim
|
||||||
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
|||||||
@@ -144,11 +144,21 @@ transformation.
|
|||||||
|
|
||||||
A minimal TOML config for Pi:
|
A minimal TOML config for Pi:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[pi]
|
|
||||||
model = "..."
|
```sh
|
||||||
provider = "..."
|
takopi config set pi.model "..."
|
||||||
extra_args = []
|
takopi config set pi.provider "..."
|
||||||
```
|
takopi config set pi.extra_args "[]"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[pi]
|
||||||
|
model = "..."
|
||||||
|
provider = "..."
|
||||||
|
extra_args = []
|
||||||
|
```
|
||||||
|
|
||||||
Use `extra_args` for any Pi CLI flags not explicitly mapped.
|
Use `extra_args` for any Pi CLI flags not explicitly mapped.
|
||||||
|
|||||||
@@ -28,10 +28,19 @@ directive pipeline as typed text.
|
|||||||
|
|
||||||
Configuration (under `[transports.telegram]`):
|
Configuration (under `[transports.telegram]`):
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
voice_transcription = true
|
|
||||||
voice_transcription_model = "gpt-4o-mini-transcribe" # optional
|
```sh
|
||||||
```
|
takopi config set transports.telegram.voice_transcription true
|
||||||
|
takopi config set transports.telegram.voice_transcription_model "gpt-4o-mini-transcribe"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
voice_transcription = true
|
||||||
|
voice_transcription_model = "gpt-4o-mini-transcribe" # optional
|
||||||
|
```
|
||||||
|
|
||||||
Set `OPENAI_API_KEY` in the environment. If transcription is enabled but the API key
|
Set `OPENAI_API_KEY` in the environment. If transcription is enabled but the API key
|
||||||
is missing or the audio download fails, takopi replies with a short error and skips
|
is missing or the audio download fails, takopi replies with a short error and skips
|
||||||
@@ -88,9 +97,17 @@ Behavior:
|
|||||||
|
|
||||||
Configuration (under `[transports.telegram]`):
|
Configuration (under `[transports.telegram]`):
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
forward_coalesce_s = 1.0 # set 0 to disable the delay
|
|
||||||
```
|
```sh
|
||||||
|
takopi config set transports.telegram.forward_coalesce_s 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
forward_coalesce_s = 1.0 # set 0 to disable the delay
|
||||||
|
```
|
||||||
|
|
||||||
## Chat sessions (optional)
|
## Chat sessions (optional)
|
||||||
|
|
||||||
@@ -100,10 +117,19 @@ use chat mode with auto-resume enabled.
|
|||||||
|
|
||||||
Configuration (under `[transports.telegram]`):
|
Configuration (under `[transports.telegram]`):
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
show_resume_line = true # set false to hide resume lines
|
|
||||||
session_mode = "chat" # or "stateless"
|
```sh
|
||||||
```
|
takopi config set transports.telegram.show_resume_line true
|
||||||
|
takopi config set transports.telegram.session_mode "chat"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
show_resume_line = true # set false to hide resume lines
|
||||||
|
session_mode = "chat" # or "stateless"
|
||||||
|
```
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
@@ -124,10 +150,18 @@ By default, takopi trims long final responses to ~3500 characters to stay under
|
|||||||
Telegram's 4096 character limit after entity parsing. You can opt into splitting
|
Telegram's 4096 character limit after entity parsing. You can opt into splitting
|
||||||
instead:
|
instead:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
message_overflow = "split" # trim | split
|
```sh
|
||||||
```
|
takopi config set transports.telegram.message_overflow "split"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
message_overflow = "split" # trim | split
|
||||||
|
```
|
||||||
|
|
||||||
Split mode sends multiple messages. Each chunk includes the footer; follow-up
|
Split mode sends multiple messages. Each chunk includes the footer; follow-up
|
||||||
chunks add a "continued (N/M)" header.
|
chunks add a "continued (N/M)" header.
|
||||||
@@ -140,11 +174,20 @@ topic, so replies keep the right context even after restarts.
|
|||||||
|
|
||||||
Configuration (under `[transports.telegram]`):
|
Configuration (under `[transports.telegram]`):
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram.topics]
|
|
||||||
enabled = true
|
```sh
|
||||||
scope = "auto" # auto | main | projects | all
|
takopi config set transports.telegram.topics.enabled true
|
||||||
```
|
takopi config set transports.telegram.topics.scope "auto"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram.topics]
|
||||||
|
enabled = true
|
||||||
|
scope = "auto" # auto | main | projects | all
|
||||||
|
```
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
|
|||||||
@@ -57,11 +57,20 @@ To continue the same session, **reply** to a message with a resume line:
|
|||||||
|
|
||||||
You can manually change these settings in your config file:
|
You can manually change these settings in your config file:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
session_mode = "chat" # "chat" or "stateless"
|
```sh
|
||||||
show_resume_line = false # true or false
|
takopi config set transports.telegram.session_mode "chat"
|
||||||
```
|
takopi config set transports.telegram.show_resume_line false
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
session_mode = "chat" # "chat" or "stateless"
|
||||||
|
show_resume_line = false # true or false
|
||||||
|
```
|
||||||
|
|
||||||
Or re-run onboarding to pick a different workflow:
|
Or re-run onboarding to pick a different workflow:
|
||||||
|
|
||||||
@@ -76,10 +85,18 @@ Resume lines are still shown when no project context is set, so replies can bran
|
|||||||
|
|
||||||
If you prefer always-visible resume lines, set:
|
If you prefer always-visible resume lines, set:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[transports.telegram]
|
|
||||||
show_resume_line = true
|
```sh
|
||||||
```
|
takopi config set transports.telegram.show_resume_line true
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[transports.telegram]
|
||||||
|
show_resume_line = true
|
||||||
|
```
|
||||||
|
|
||||||
## Reply-to-continue still works
|
## Reply-to-continue still works
|
||||||
|
|
||||||
|
|||||||
+81
-36
@@ -268,54 +268,99 @@ Your config file lives at `~/.takopi/takopi.toml`. The exact contents depend on
|
|||||||
|
|
||||||
=== "assistant"
|
=== "assistant"
|
||||||
|
|
||||||
```toml title="~/.takopi/takopi.toml"
|
=== "takopi config"
|
||||||
default_engine = "codex"
|
|
||||||
transport = "telegram"
|
|
||||||
|
|
||||||
[transports.telegram]
|
```sh
|
||||||
bot_token = "..."
|
takopi config set default_engine "codex"
|
||||||
chat_id = 123456789
|
takopi config set transport "telegram"
|
||||||
session_mode = "chat" # auto-resume
|
takopi config set transports.telegram.bot_token "..."
|
||||||
show_resume_line = false # cleaner chat
|
takopi config set transports.telegram.chat_id 123456789
|
||||||
|
takopi config set transports.telegram.session_mode "chat"
|
||||||
|
takopi config set transports.telegram.show_resume_line false
|
||||||
|
takopi config set transports.telegram.topics.enabled false
|
||||||
|
takopi config set transports.telegram.topics.scope "auto"
|
||||||
|
```
|
||||||
|
|
||||||
[transports.telegram.topics]
|
=== "toml"
|
||||||
enabled = false
|
|
||||||
scope = "auto"
|
```toml title="~/.takopi/takopi.toml"
|
||||||
```
|
default_engine = "codex"
|
||||||
|
transport = "telegram"
|
||||||
|
|
||||||
|
[transports.telegram]
|
||||||
|
bot_token = "..."
|
||||||
|
chat_id = 123456789
|
||||||
|
session_mode = "chat" # auto-resume
|
||||||
|
show_resume_line = false # cleaner chat
|
||||||
|
|
||||||
|
[transports.telegram.topics]
|
||||||
|
enabled = false
|
||||||
|
scope = "auto"
|
||||||
|
```
|
||||||
|
|
||||||
=== "workspace"
|
=== "workspace"
|
||||||
|
|
||||||
```toml title="~/.takopi/takopi.toml"
|
=== "takopi config"
|
||||||
default_engine = "codex"
|
|
||||||
transport = "telegram"
|
|
||||||
|
|
||||||
[transports.telegram]
|
```sh
|
||||||
bot_token = "..."
|
takopi config set default_engine "codex"
|
||||||
chat_id = -1001234567890 # forum group
|
takopi config set transport "telegram"
|
||||||
session_mode = "chat"
|
takopi config set transports.telegram.bot_token "..."
|
||||||
show_resume_line = false
|
takopi config set transports.telegram.chat_id -1001234567890
|
||||||
|
takopi config set transports.telegram.session_mode "chat"
|
||||||
|
takopi config set transports.telegram.show_resume_line false
|
||||||
|
takopi config set transports.telegram.topics.enabled true
|
||||||
|
takopi config set transports.telegram.topics.scope "auto"
|
||||||
|
```
|
||||||
|
|
||||||
[transports.telegram.topics]
|
=== "toml"
|
||||||
enabled = true # topics on
|
|
||||||
scope = "auto"
|
```toml title="~/.takopi/takopi.toml"
|
||||||
```
|
default_engine = "codex"
|
||||||
|
transport = "telegram"
|
||||||
|
|
||||||
|
[transports.telegram]
|
||||||
|
bot_token = "..."
|
||||||
|
chat_id = -1001234567890 # forum group
|
||||||
|
session_mode = "chat"
|
||||||
|
show_resume_line = false
|
||||||
|
|
||||||
|
[transports.telegram.topics]
|
||||||
|
enabled = true # topics on
|
||||||
|
scope = "auto"
|
||||||
|
```
|
||||||
|
|
||||||
=== "handoff"
|
=== "handoff"
|
||||||
|
|
||||||
```toml title="~/.takopi/takopi.toml"
|
=== "takopi config"
|
||||||
default_engine = "codex"
|
|
||||||
transport = "telegram"
|
|
||||||
|
|
||||||
[transports.telegram]
|
```sh
|
||||||
bot_token = "..."
|
takopi config set default_engine "codex"
|
||||||
chat_id = 123456789
|
takopi config set transport "telegram"
|
||||||
session_mode = "stateless" # reply-to-continue
|
takopi config set transports.telegram.bot_token "..."
|
||||||
show_resume_line = true # always show resume lines
|
takopi config set transports.telegram.chat_id 123456789
|
||||||
|
takopi config set transports.telegram.session_mode "stateless"
|
||||||
|
takopi config set transports.telegram.show_resume_line true
|
||||||
|
takopi config set transports.telegram.topics.enabled false
|
||||||
|
takopi config set transports.telegram.topics.scope "auto"
|
||||||
|
```
|
||||||
|
|
||||||
[transports.telegram.topics]
|
=== "toml"
|
||||||
enabled = false
|
|
||||||
scope = "auto"
|
```toml title="~/.takopi/takopi.toml"
|
||||||
```
|
default_engine = "codex"
|
||||||
|
transport = "telegram"
|
||||||
|
|
||||||
|
[transports.telegram]
|
||||||
|
bot_token = "..."
|
||||||
|
chat_id = 123456789
|
||||||
|
session_mode = "stateless" # reply-to-continue
|
||||||
|
show_resume_line = true # always show resume lines
|
||||||
|
|
||||||
|
[transports.telegram.topics]
|
||||||
|
enabled = false
|
||||||
|
scope = "auto"
|
||||||
|
```
|
||||||
|
|
||||||
This config file controls all of Takopi's behavior. You can edit it directly to change settings or add advanced features.
|
This config file controls all of Takopi's behavior. You can edit it directly to change settings or add advanced features.
|
||||||
|
|
||||||
|
|||||||
@@ -109,11 +109,20 @@ Each topic remembers its own default.
|
|||||||
|
|
||||||
Set a default engine in your project config:
|
Set a default engine in your project config:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
default_engine = "claude"
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
```
|
takopi config set projects.happy-gadgets.default_engine "claude"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
default_engine = "claude"
|
||||||
|
```
|
||||||
|
|
||||||
Now `/happy-gadgets` tasks default to Claude, even if your global default is Codex.
|
Now `/happy-gadgets` tasks default to Claude, even if your global default is Codex.
|
||||||
|
|
||||||
@@ -144,15 +153,28 @@ This means: resume lines always win, then explicit directives, then the most spe
|
|||||||
|
|
||||||
**Pattern: Quick questions vs. deep work**
|
**Pattern: Quick questions vs. deep work**
|
||||||
|
|
||||||
```
|
=== "takopi config"
|
||||||
# Global default for quick stuff
|
|
||||||
default_engine = "codex"
|
|
||||||
|
|
||||||
# Project default for complex codebase
|
```sh
|
||||||
[projects.backend]
|
# Global default for quick stuff
|
||||||
path = "~/dev/backend"
|
takopi config set default_engine "codex"
|
||||||
default_engine = "claude"
|
|
||||||
```
|
# Project default for complex codebase
|
||||||
|
takopi config set projects.backend.path "~/dev/backend"
|
||||||
|
takopi config set projects.backend.default_engine "claude"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Global default for quick stuff
|
||||||
|
default_engine = "codex"
|
||||||
|
|
||||||
|
# Project default for complex codebase
|
||||||
|
[projects.backend]
|
||||||
|
path = "~/dev/backend"
|
||||||
|
default_engine = "claude"
|
||||||
|
```
|
||||||
|
|
||||||
Simple messages go to Codex. `/backend` messages go to Claude.
|
Simple messages go to Codex. `/backend` messages go to Claude.
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,18 @@ saved project 'happy-gadgets' to ~/.takopi/takopi.toml
|
|||||||
|
|
||||||
This adds an entry to your config (Takopi also fills in defaults like `worktrees_dir`, `default_engine`, and sometimes `worktree_base`):
|
This adds an entry to your config (Takopi also fills in defaults like `worktrees_dir`, `default_engine`, and sometimes `worktree_base`):
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
```
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
!!! tip "Project aliases are also Telegram commands"
|
!!! tip "Project aliases are also Telegram commands"
|
||||||
The alias becomes a `/command` you can use in chat. Keep them short and lowercase: `myapp`, `backend`, `docs`.
|
The alias becomes a `/command` you can use in chat. Keep them short and lowercase: `myapp`, `backend`, `docs`.
|
||||||
@@ -69,12 +77,22 @@ Worktrees let you run tasks on feature branches without touching your main check
|
|||||||
|
|
||||||
Add worktree config to your project:
|
Add worktree config to your project:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
[projects.happy-gadgets]
|
|
||||||
path = "~/dev/happy-gadgets"
|
```sh
|
||||||
worktrees_dir = ".worktrees" # where branches go
|
takopi config set projects.happy-gadgets.path "~/dev/happy-gadgets"
|
||||||
worktree_base = "main" # base for new branches
|
takopi config set projects.happy-gadgets.worktrees_dir ".worktrees"
|
||||||
```
|
takopi config set projects.happy-gadgets.worktree_base "main"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[projects.happy-gadgets]
|
||||||
|
path = "~/dev/happy-gadgets"
|
||||||
|
worktrees_dir = ".worktrees" # where branches go
|
||||||
|
worktree_base = "main" # base for new branches
|
||||||
|
```
|
||||||
|
|
||||||
!!! note "Ignore the worktrees directory"
|
!!! note "Ignore the worktrees directory"
|
||||||
Add `.worktrees/` to your global gitignore so it doesn't clutter `git status`:
|
Add `.worktrees/` to your global gitignore so it doesn't clutter `git status`:
|
||||||
@@ -125,9 +143,17 @@ The `ctx:` line in each message carries the context forward.
|
|||||||
|
|
||||||
If you mostly work in one repo, set it as the default:
|
If you mostly work in one repo, set it as the default:
|
||||||
|
|
||||||
```toml
|
=== "takopi config"
|
||||||
default_project = "happy-gadgets"
|
|
||||||
```
|
```sh
|
||||||
|
takopi config set default_project "happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
default_project = "happy-gadgets"
|
||||||
|
```
|
||||||
|
|
||||||
Now messages without a `/project` prefix go to that repo:
|
Now messages without a `/project` prefix go to that repo:
|
||||||
|
|
||||||
|
|||||||
+297
-2
@@ -1,19 +1,29 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import tomllib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from importlib.metadata import EntryPoint
|
from importlib.metadata import EntryPoint
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from pydantic import BaseModel
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .config import ConfigError, load_or_init_config, write_config
|
from .config import (
|
||||||
|
ConfigError,
|
||||||
|
HOME_CONFIG_PATH,
|
||||||
|
dump_toml,
|
||||||
|
load_or_init_config,
|
||||||
|
read_config,
|
||||||
|
write_config,
|
||||||
|
)
|
||||||
from .config_migrations import migrate_config
|
from .config_migrations import migrate_config
|
||||||
from .commands import get_command
|
from .commands import get_command
|
||||||
from .backends import EngineBackend
|
from .backends import EngineBackend
|
||||||
@@ -47,6 +57,14 @@ from .telegram.topics import _validate_topics_setup_for
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
_KEY_SEGMENT_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||||
|
_MISSING = object()
|
||||||
|
_CONFIG_PATH_OPTION = typer.Option(
|
||||||
|
None,
|
||||||
|
"--config-path",
|
||||||
|
help="Override the default config path.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_settings_optional() -> tuple[TakopiSettings | None, Path | None]:
|
def _load_settings_optional() -> tuple[TakopiSettings | None, Path | None]:
|
||||||
try:
|
try:
|
||||||
@@ -671,6 +689,276 @@ def plugins_cmd(
|
|||||||
typer.echo(f" {group} {err.name} ({dist}): {err.error}")
|
typer.echo(f" {group} {err.name} ({dist}): {err.error}")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_config_path_override(value: Path | None) -> Path:
|
||||||
|
if value is None:
|
||||||
|
return HOME_CONFIG_PATH
|
||||||
|
return value.expanduser()
|
||||||
|
|
||||||
|
|
||||||
|
def _exit_config_error(exc: ConfigError, *, code: int = 2) -> None:
|
||||||
|
typer.echo(f"error: {exc}", err=True)
|
||||||
|
raise typer.Exit(code=code) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_key_path(raw: str) -> list[str]:
|
||||||
|
value = raw.strip()
|
||||||
|
if not value:
|
||||||
|
raise ConfigError("Invalid key path; expected a non-empty value.")
|
||||||
|
segments = value.split(".")
|
||||||
|
for segment in segments:
|
||||||
|
if not segment:
|
||||||
|
raise ConfigError(f"Invalid key path {raw!r}; empty segment.")
|
||||||
|
if not _KEY_SEGMENT_RE.fullmatch(segment):
|
||||||
|
raise ConfigError(
|
||||||
|
f"Invalid key segment {segment!r} in {raw!r}; "
|
||||||
|
"use only letters, numbers, '_' or '-'."
|
||||||
|
)
|
||||||
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_value(raw: str) -> Any:
|
||||||
|
value = raw.strip()
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return tomllib.loads(f"__v__ = {value}")["__v__"]
|
||||||
|
except tomllib.TOMLDecodeError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _toml_literal(value: Any) -> str:
|
||||||
|
dumped = dump_toml({"__v__": value})
|
||||||
|
prefix = "__v__ = "
|
||||||
|
if dumped.startswith(prefix):
|
||||||
|
return dumped[len(prefix) :].rstrip("\n")
|
||||||
|
raise ConfigError("Unsupported config value; unable to render TOML literal.")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_value_from_settings(
|
||||||
|
settings: TakopiSettings, segments: list[str]
|
||||||
|
) -> Any:
|
||||||
|
node: Any = settings
|
||||||
|
for segment in segments:
|
||||||
|
if isinstance(node, BaseModel):
|
||||||
|
if segment in node.__class__.model_fields:
|
||||||
|
node = getattr(node, segment)
|
||||||
|
else:
|
||||||
|
extra = node.model_extra or {}
|
||||||
|
node = extra.get(segment, _MISSING)
|
||||||
|
elif isinstance(node, dict):
|
||||||
|
node = node.get(segment, _MISSING)
|
||||||
|
else:
|
||||||
|
return _MISSING
|
||||||
|
if node is _MISSING:
|
||||||
|
return _MISSING
|
||||||
|
if isinstance(node, BaseModel):
|
||||||
|
return node.model_dump(exclude_unset=True)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_config(config: dict[str, Any]) -> list[tuple[str, Any]]:
|
||||||
|
items: list[tuple[str, Any]] = []
|
||||||
|
|
||||||
|
def _walk(node: Any, prefix: str) -> None:
|
||||||
|
if isinstance(node, dict):
|
||||||
|
for key in sorted(node):
|
||||||
|
value = node[key]
|
||||||
|
path = f"{prefix}.{key}" if prefix else key
|
||||||
|
if isinstance(value, dict):
|
||||||
|
_walk(value, path)
|
||||||
|
else:
|
||||||
|
items.append((path, value))
|
||||||
|
elif prefix:
|
||||||
|
items.append((prefix, node))
|
||||||
|
|
||||||
|
_walk(config, "")
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config_or_exit(path: Path, *, missing_code: int) -> dict[str, Any]:
|
||||||
|
if not path.exists():
|
||||||
|
_fail_missing_config(path)
|
||||||
|
raise typer.Exit(code=missing_code)
|
||||||
|
try:
|
||||||
|
return read_config(path)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def config_path_cmd(
|
||||||
|
config_path: Path | None = _CONFIG_PATH_OPTION,
|
||||||
|
) -> None:
|
||||||
|
"""Print the resolved config path."""
|
||||||
|
path = _resolve_config_path_override(config_path)
|
||||||
|
typer.echo(_config_path_display(path))
|
||||||
|
|
||||||
|
|
||||||
|
def config_list(
|
||||||
|
config_path: Path | None = _CONFIG_PATH_OPTION,
|
||||||
|
) -> None:
|
||||||
|
"""List config keys as flattened dot-paths."""
|
||||||
|
path = _resolve_config_path_override(config_path)
|
||||||
|
config = _load_config_or_exit(path, missing_code=1)
|
||||||
|
try:
|
||||||
|
for key, value in _flatten_config(config):
|
||||||
|
literal = _toml_literal(value)
|
||||||
|
typer.echo(f"{key} = {literal}")
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def config_get(
|
||||||
|
key: str = typer.Argument(..., help="Dot-path key to fetch."),
|
||||||
|
config_path: Path | None = _CONFIG_PATH_OPTION,
|
||||||
|
) -> None:
|
||||||
|
"""Fetch a single config key."""
|
||||||
|
path = _resolve_config_path_override(config_path)
|
||||||
|
config = _load_config_or_exit(path, missing_code=2)
|
||||||
|
try:
|
||||||
|
segments = _parse_key_path(key)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
node: Any = config
|
||||||
|
for index, segment in enumerate(segments):
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
prefix = ".".join(segments[:index])
|
||||||
|
_exit_config_error(
|
||||||
|
ConfigError(f"Invalid `{prefix}` in {path}; expected a table.")
|
||||||
|
)
|
||||||
|
if segment not in node:
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
node = node[segment]
|
||||||
|
|
||||||
|
if isinstance(node, dict):
|
||||||
|
typer.echo(
|
||||||
|
f"error: {'.'.join(segments)!r} is a table; pick a leaf node.",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
raise typer.Exit(code=2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
typer.echo(_toml_literal(node))
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def config_set(
|
||||||
|
key: str = typer.Argument(..., help="Dot-path key to set."),
|
||||||
|
value: str = typer.Argument(..., help="Value to assign (auto-parsed)."),
|
||||||
|
config_path: Path | None = _CONFIG_PATH_OPTION,
|
||||||
|
) -> None:
|
||||||
|
"""Set a config value."""
|
||||||
|
path = _resolve_config_path_override(config_path)
|
||||||
|
config = _load_config_or_exit(path, missing_code=2)
|
||||||
|
try:
|
||||||
|
segments = _parse_key_path(key)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
migrate_config(config, config_path=path)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
parsed = _parse_value(value)
|
||||||
|
node: Any = config
|
||||||
|
for index, segment in enumerate(segments[:-1]):
|
||||||
|
next_node = node.get(segment)
|
||||||
|
if next_node is None:
|
||||||
|
created: dict[str, Any] = {}
|
||||||
|
node[segment] = created
|
||||||
|
node = created
|
||||||
|
continue
|
||||||
|
if not isinstance(next_node, dict):
|
||||||
|
prefix = ".".join(segments[: index + 1])
|
||||||
|
_exit_config_error(
|
||||||
|
ConfigError(f"Invalid `{prefix}` in {path}; expected a table.")
|
||||||
|
)
|
||||||
|
node = next_node
|
||||||
|
node[segments[-1]] = parsed
|
||||||
|
|
||||||
|
try:
|
||||||
|
settings = validate_settings_data(config, config_path=path)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
normalized = _normalized_value_from_settings(settings, segments)
|
||||||
|
if normalized is not _MISSING:
|
||||||
|
node[segments[-1]] = normalized
|
||||||
|
parsed = normalized
|
||||||
|
|
||||||
|
try:
|
||||||
|
write_config(config, path)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rendered = _toml_literal(parsed)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
typer.echo(f"updated {'.'.join(segments)} = {rendered}")
|
||||||
|
|
||||||
|
|
||||||
|
def config_unset(
|
||||||
|
key: str = typer.Argument(..., help="Dot-path key to remove."),
|
||||||
|
config_path: Path | None = _CONFIG_PATH_OPTION,
|
||||||
|
) -> None:
|
||||||
|
"""Remove a config key."""
|
||||||
|
path = _resolve_config_path_override(config_path)
|
||||||
|
config = _load_config_or_exit(path, missing_code=2)
|
||||||
|
try:
|
||||||
|
segments = _parse_key_path(key)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
migrate_config(config, config_path=path)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
node: Any = config
|
||||||
|
stack: list[tuple[dict[str, Any], str]] = []
|
||||||
|
for index, segment in enumerate(segments[:-1]):
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
prefix = ".".join(segments[:index])
|
||||||
|
_exit_config_error(
|
||||||
|
ConfigError(f"Invalid `{prefix}` in {path}; expected a table.")
|
||||||
|
)
|
||||||
|
next_node = node.get(segment)
|
||||||
|
if next_node is None:
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
if not isinstance(next_node, dict):
|
||||||
|
prefix = ".".join(segments[: index + 1])
|
||||||
|
_exit_config_error(
|
||||||
|
ConfigError(f"Invalid `{prefix}` in {path}; expected a table.")
|
||||||
|
)
|
||||||
|
stack.append((node, segment))
|
||||||
|
node = next_node
|
||||||
|
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
prefix = ".".join(segments[:-1])
|
||||||
|
_exit_config_error(
|
||||||
|
ConfigError(f"Invalid `{prefix}` in {path}; expected a table.")
|
||||||
|
)
|
||||||
|
leaf = segments[-1]
|
||||||
|
if leaf not in node:
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
node.pop(leaf, None)
|
||||||
|
|
||||||
|
while stack and not node:
|
||||||
|
parent, key_name = stack.pop()
|
||||||
|
parent.pop(key_name, None)
|
||||||
|
node = parent
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_settings_data(config, config_path=path)
|
||||||
|
write_config(config, path)
|
||||||
|
except ConfigError as exc:
|
||||||
|
_exit_config_error(exc)
|
||||||
|
|
||||||
|
|
||||||
def app_main(
|
def app_main(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
version: bool = typer.Option(
|
version: bool = typer.Option(
|
||||||
@@ -774,11 +1062,18 @@ def create_app() -> typer.Typer:
|
|||||||
invoke_without_command=True,
|
invoke_without_command=True,
|
||||||
help="Telegram bridge for coding agents. Docs: https://takopi.dev/",
|
help="Telegram bridge for coding agents. Docs: https://takopi.dev/",
|
||||||
)
|
)
|
||||||
|
config_app = typer.Typer(help="Read and modify takopi config.")
|
||||||
|
config_app.command(name="path")(config_path_cmd)
|
||||||
|
config_app.command(name="list")(config_list)
|
||||||
|
config_app.command(name="get")(config_get)
|
||||||
|
config_app.command(name="set")(config_set)
|
||||||
|
config_app.command(name="unset")(config_unset)
|
||||||
app.command(name="init")(init)
|
app.command(name="init")(init)
|
||||||
app.command(name="chat-id")(chat_id)
|
app.command(name="chat-id")(chat_id)
|
||||||
app.command(name="doctor")(doctor)
|
app.command(name="doctor")(doctor)
|
||||||
app.command(name="onboarding-paths")(onboarding_paths)
|
app.command(name="onboarding-paths")(onboarding_paths)
|
||||||
app.command(name="plugins")(plugins_cmd)
|
app.command(name="plugins")(plugins_cmd)
|
||||||
|
app.add_typer(config_app, name="config")
|
||||||
app.callback()(app_main)
|
app.callback()(app_main)
|
||||||
for engine_id in _engine_ids_for_cli():
|
for engine_id in _engine_ids_for_cli():
|
||||||
help_text = f"Run with the {engine_id} engine."
|
help_text = f"Run with the {engine_id} engine."
|
||||||
|
|||||||
+28
-1
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import tomllib
|
import tomllib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import tomli_w
|
import tomli_w
|
||||||
@@ -104,4 +106,29 @@ def dump_toml(config: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
def write_config(config: dict[str, Any], path: Path) -> None:
|
def write_config(config: dict[str, Any], path: Path) -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(dump_toml(config), encoding="utf-8")
|
payload = dump_toml(config)
|
||||||
|
tmp_path: Path | None = None
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
dir=path.parent,
|
||||||
|
prefix=f".{path.name}.",
|
||||||
|
suffix=".tmp",
|
||||||
|
delete=False,
|
||||||
|
) as tmp:
|
||||||
|
tmp.write(payload)
|
||||||
|
tmp.flush()
|
||||||
|
os.fsync(tmp.fileno())
|
||||||
|
tmp_path = Path(tmp.name)
|
||||||
|
os.replace(tmp_path, path)
|
||||||
|
except OSError as e:
|
||||||
|
raise ConfigError(f"Failed to write config file {path}: {e}") from e
|
||||||
|
finally:
|
||||||
|
if tmp_path is not None:
|
||||||
|
try:
|
||||||
|
tmp_path.unlink()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import re
|
|||||||
ID_PATTERN = r"^[a-z0-9_]{1,32}$"
|
ID_PATTERN = r"^[a-z0-9_]{1,32}$"
|
||||||
_ID_RE = re.compile(ID_PATTERN)
|
_ID_RE = re.compile(ID_PATTERN)
|
||||||
|
|
||||||
RESERVED_CLI_COMMANDS = frozenset({"init", "plugins", "doctor"})
|
RESERVED_CLI_COMMANDS = frozenset({"config", "doctor", "init", "plugins"})
|
||||||
RESERVED_CHAT_COMMANDS = frozenset(
|
RESERVED_CHAT_COMMANDS = frozenset(
|
||||||
{"cancel", "file", "new", "agent", "model", "reasoning", "trigger", "topic", "ctx"}
|
{"cancel", "file", "new", "agent", "model", "reasoning", "trigger", "topic", "ctx"}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
from takopi import cli
|
||||||
|
|
||||||
|
|
||||||
|
def _write_min_config(path: Path) -> None:
|
||||||
|
path.write_text(
|
||||||
|
'transport = "telegram"\n'
|
||||||
|
"\n"
|
||||||
|
"[transports.telegram]\n"
|
||||||
|
'bot_token = "token"\n'
|
||||||
|
"chat_id = 123\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_list_outputs_flattened(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "takopi.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
'transport = "telegram"\n'
|
||||||
|
"watch_config = true\n"
|
||||||
|
"\n"
|
||||||
|
"[transports.telegram]\n"
|
||||||
|
'bot_token = "token"\n'
|
||||||
|
"chat_id = 123\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
["config", "list", "--config-path", str(config_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
lines = [line.strip() for line in result.output.splitlines() if line.strip()]
|
||||||
|
assert 'transport = "telegram"' in lines
|
||||||
|
assert "watch_config = true" in lines
|
||||||
|
assert 'transports.telegram.bot_token = "token"' in lines
|
||||||
|
assert "transports.telegram.chat_id = 123" in lines
|
||||||
|
assert not any(line.startswith("default_engine") for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_get_outputs_literal_and_table_error(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "takopi.toml"
|
||||||
|
_write_min_config(config_path)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
[
|
||||||
|
"config",
|
||||||
|
"get",
|
||||||
|
"transports.telegram.chat_id",
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.strip() == "123"
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
[
|
||||||
|
"config",
|
||||||
|
"get",
|
||||||
|
"transports.telegram",
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert "table" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_get_missing_key(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "takopi.toml"
|
||||||
|
_write_min_config(config_path)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
["config", "get", "nope", "--config-path", str(config_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert result.output == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_parses_and_writes(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "takopi.toml"
|
||||||
|
_write_min_config(config_path)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
["config", "set", "watch_config", "true", "--config-path", str(config_path)],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
[
|
||||||
|
"config",
|
||||||
|
"set",
|
||||||
|
"default_engine",
|
||||||
|
"openai",
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
[
|
||||||
|
"config",
|
||||||
|
"set",
|
||||||
|
"watch_config",
|
||||||
|
"False",
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
[
|
||||||
|
"config",
|
||||||
|
"set",
|
||||||
|
"transports.telegram.chat_id",
|
||||||
|
"456",
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert data["watch_config"] is False
|
||||||
|
assert data["default_engine"] == "openai"
|
||||||
|
assert data["transports"]["telegram"]["chat_id"] == 456
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_unset_prunes_tables(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "takopi.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
'transport = "telegram"\n'
|
||||||
|
"\n"
|
||||||
|
"[transports.telegram]\n"
|
||||||
|
'bot_token = "token"\n'
|
||||||
|
"chat_id = 123\n"
|
||||||
|
"\n"
|
||||||
|
"[projects.foo]\n"
|
||||||
|
'path = "/tmp/repo"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
["config", "unset", "projects.foo", "--config-path", str(config_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert "projects" not in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_set_schema_validation_error(tmp_path: Path) -> None:
|
||||||
|
config_path = tmp_path / "takopi.toml"
|
||||||
|
config_path.write_text(
|
||||||
|
'transport = "telegram"\n'
|
||||||
|
"\n"
|
||||||
|
"[transports.telegram]\n"
|
||||||
|
'bot_token = "token"\n'
|
||||||
|
"chat_id = 123\n"
|
||||||
|
"\n"
|
||||||
|
"[projects.foo]\n"
|
||||||
|
'path = "/tmp/repo"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.create_app(),
|
||||||
|
[
|
||||||
|
"config",
|
||||||
|
"set",
|
||||||
|
"projects.foo.extra",
|
||||||
|
"nope",
|
||||||
|
"--config-path",
|
||||||
|
str(config_path),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 2
|
||||||
|
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
assert "extra" not in data.get("projects", {}).get("foo", {})
|
||||||
Reference in New Issue
Block a user