nbclaw 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nbclaw/__init__.py +10 -0
- nbclaw/__main__.py +28 -0
- nbclaw/agent_runner.py +83 -0
- nbclaw/commands.py +56 -0
- nbclaw/config.py +277 -0
- nbclaw/daemon.py +375 -0
- nbclaw/nl_schedule.py +135 -0
- nbclaw/scheduler.py +253 -0
- nbclaw/signal_client.py +199 -0
- nbclaw-0.1.0.dist-info/METADATA +166 -0
- nbclaw-0.1.0.dist-info/RECORD +13 -0
- nbclaw-0.1.0.dist-info/WHEEL +4 -0
- nbclaw-0.1.0.dist-info/entry_points.txt +2 -0
nbclaw/__init__.py
ADDED
nbclaw/__main__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Entry point: ``python -m nbclaw`` / ``nbclaw``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from .config import build_config
|
|
10
|
+
from .daemon import Daemon
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=os.environ.get("NBCLAW_LOG", "INFO").upper(),
|
|
16
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
17
|
+
datefmt="%H:%M:%S",
|
|
18
|
+
)
|
|
19
|
+
config = build_config()
|
|
20
|
+
daemon = Daemon(config)
|
|
21
|
+
try:
|
|
22
|
+
asyncio.run(daemon.run())
|
|
23
|
+
except KeyboardInterrupt:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
nbclaw/agent_runner.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Bridges Signal conversations to the swival agent.
|
|
2
|
+
|
|
3
|
+
``swival.Session`` is synchronous and CPU/IO heavy, so every call is run in a
|
|
4
|
+
thread pool. Because the target here is a single local model, the daemon funnels
|
|
5
|
+
all agent work through one worker (see daemon.py); this class is therefore not
|
|
6
|
+
trying to be concurrency-safe across many simultaneous runs.
|
|
7
|
+
|
|
8
|
+
Each conversation keeps its own long-lived ``Session`` so chat context carries
|
|
9
|
+
across messages. Cron jobs run as independent one-shots and never touch chat
|
|
10
|
+
context.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from swival import AgentError, Result, Session
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("nbclaw.agent")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_close(session: Session) -> None:
|
|
25
|
+
try:
|
|
26
|
+
session.close()
|
|
27
|
+
except Exception as exc: # pragma: no cover - cleanup best effort
|
|
28
|
+
log.debug("session close error: %s", exc)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AgentRunner:
|
|
32
|
+
def __init__(self, session_kwargs: dict[str, Any]) -> None:
|
|
33
|
+
self._kwargs = session_kwargs
|
|
34
|
+
self._sessions: dict[str, Session] = {}
|
|
35
|
+
|
|
36
|
+
def _session_for(self, key: str) -> Session:
|
|
37
|
+
session = self._sessions.get(key)
|
|
38
|
+
if session is None:
|
|
39
|
+
log.info("creating session for %s", key)
|
|
40
|
+
session = Session(**self._kwargs)
|
|
41
|
+
self._sessions[key] = session
|
|
42
|
+
return session
|
|
43
|
+
|
|
44
|
+
def reset(self, key: str) -> bool:
|
|
45
|
+
"""Drop a conversation's context. Returns True if there was one."""
|
|
46
|
+
session = self._sessions.pop(key, None)
|
|
47
|
+
if session is None:
|
|
48
|
+
return False
|
|
49
|
+
_safe_close(session)
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
# --- blocking primitives (run inside the executor) -----------------
|
|
53
|
+
def _ask_blocking(self, key: str, prompt: str) -> str:
|
|
54
|
+
session = self._session_for(key)
|
|
55
|
+
result = session.ask(prompt)
|
|
56
|
+
return _answer_text(result)
|
|
57
|
+
|
|
58
|
+
def _once_blocking(self, prompt: str) -> str:
|
|
59
|
+
session = Session(**self._kwargs)
|
|
60
|
+
try:
|
|
61
|
+
return _answer_text(session.run(prompt))
|
|
62
|
+
finally:
|
|
63
|
+
_safe_close(session)
|
|
64
|
+
|
|
65
|
+
# --- async wrappers ------------------------------------------------
|
|
66
|
+
async def chat(self, key: str, prompt: str) -> str:
|
|
67
|
+
return await asyncio.to_thread(self._ask_blocking, key, prompt)
|
|
68
|
+
|
|
69
|
+
async def once(self, prompt: str) -> str:
|
|
70
|
+
return await asyncio.to_thread(self._once_blocking, prompt)
|
|
71
|
+
|
|
72
|
+
def close_all(self) -> None:
|
|
73
|
+
for session in self._sessions.values():
|
|
74
|
+
_safe_close(session)
|
|
75
|
+
self._sessions.clear()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _answer_text(result: Result) -> str:
|
|
79
|
+
if result.answer:
|
|
80
|
+
return result.answer
|
|
81
|
+
if result.exhausted:
|
|
82
|
+
raise AgentError("agent ran out of turns without an answer")
|
|
83
|
+
raise AgentError("agent returned no answer")
|
nbclaw/commands.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Parsing helpers and help text for the slash-command interface.
|
|
2
|
+
|
|
3
|
+
Anything that doesn't start with ``/`` is treated as a prompt for the agent.
|
|
4
|
+
The command dispatch itself lives in :mod:`nbclaw.daemon` because it needs the
|
|
5
|
+
daemon's live state (scheduler, sessions, start time).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
HELP_TEXT = """nbclaw — commands
|
|
11
|
+
|
|
12
|
+
Plain text is sent to the agent. Slash commands:
|
|
13
|
+
|
|
14
|
+
/help show this help
|
|
15
|
+
/status model, uptime, active crons
|
|
16
|
+
/reset forget this conversation's context
|
|
17
|
+
|
|
18
|
+
Scheduling — just say it in plain English after /cron:
|
|
19
|
+
/cron every weekday at 9am summarize my git log in ~/src/app
|
|
20
|
+
/cron remind me to stretch every 2 hours
|
|
21
|
+
/cron tomorrow at 8am say good morning
|
|
22
|
+
|
|
23
|
+
/cron list list scheduled tasks
|
|
24
|
+
/cron del <name> cancel a scheduled task
|
|
25
|
+
/cron run <name> run a scheduled task right now
|
|
26
|
+
|
|
27
|
+
Power-user form (exact cron expression):
|
|
28
|
+
/cron add <name> <schedule> | <prompt>
|
|
29
|
+
schedules: 0 9 * * 1-5 · @every 30m · @hourly @daily @weekly @monthly
|
|
30
|
+
""".strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CronAddError(ValueError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_cron_add(args: str) -> tuple[str, str, str]:
|
|
38
|
+
"""Parse the body of ``/cron add`` into (name, schedule, prompt).
|
|
39
|
+
|
|
40
|
+
Grammar: ``<name> <schedule...> | <prompt>``
|
|
41
|
+
The ``|`` separates the (space-containing) schedule from the prompt.
|
|
42
|
+
"""
|
|
43
|
+
if "|" not in args:
|
|
44
|
+
raise CronAddError("missing '|' separating the schedule from the prompt")
|
|
45
|
+
head, prompt = args.split("|", 1)
|
|
46
|
+
prompt = prompt.strip()
|
|
47
|
+
head_parts = head.split()
|
|
48
|
+
if len(head_parts) < 2:
|
|
49
|
+
raise CronAddError("expected: <name> <schedule> | <prompt>")
|
|
50
|
+
name = head_parts[0]
|
|
51
|
+
schedule = " ".join(head_parts[1:])
|
|
52
|
+
if not prompt:
|
|
53
|
+
raise CronAddError("the prompt is empty")
|
|
54
|
+
if not schedule:
|
|
55
|
+
raise CronAddError("the schedule is empty")
|
|
56
|
+
return name, schedule, prompt
|
nbclaw/config.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Configuration for the nbclaw daemon.
|
|
2
|
+
|
|
3
|
+
Settings come from three layers, lowest precedence first:
|
|
4
|
+
|
|
5
|
+
1. Built-in defaults (this file).
|
|
6
|
+
2. A TOML config file (``--config nbclaw.toml``).
|
|
7
|
+
3. Command-line flags.
|
|
8
|
+
|
|
9
|
+
The TOML file mirrors the dataclass field names. A ``[swival]`` table is
|
|
10
|
+
forwarded verbatim as keyword arguments to ``swival.Session`` so the full
|
|
11
|
+
agent can be configured even for options nbclaw doesn't expose directly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import tomllib
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Literal
|
|
21
|
+
|
|
22
|
+
# First line of every AGENTS.md we write. Lets us recognise (and safely
|
|
23
|
+
# overwrite) our own file without clobbering one a user put in their workspace.
|
|
24
|
+
MANAGED_MARKER = "<!-- managed by nbclaw — edits will be overwritten -->"
|
|
25
|
+
|
|
26
|
+
# Injected into the agent (as AGENTS.md in the workspace) so it behaves like a
|
|
27
|
+
# chat assistant, not a coding agent that "keeps going until the task is done".
|
|
28
|
+
# Without this, small local models tool-loop on trivial messages like "Hi".
|
|
29
|
+
DEFAULT_INSTRUCTIONS = """\
|
|
30
|
+
# nbclaw assistant
|
|
31
|
+
|
|
32
|
+
You are a personal assistant reachable over Signal text messages. Replies are
|
|
33
|
+
read on a phone, so keep them short and conversational — no headings or long
|
|
34
|
+
lists unless asked.
|
|
35
|
+
|
|
36
|
+
## Conversation
|
|
37
|
+
|
|
38
|
+
Every message is part of one ongoing conversation. Read each new message in the
|
|
39
|
+
context of everything said before it, and resolve follow-ups against earlier
|
|
40
|
+
turns instead of asking what they refer to. For example, if you just answered
|
|
41
|
+
"add 2 + 2" with "4" and the next message is "add 10", that means 4 + 10 = 14.
|
|
42
|
+
Only ask for clarification when the conversation genuinely gives you nothing to
|
|
43
|
+
go on.
|
|
44
|
+
|
|
45
|
+
## Style
|
|
46
|
+
|
|
47
|
+
- For greetings, small talk, and general questions, just answer in a single
|
|
48
|
+
reply. Do NOT read files, run commands, or use any tools for these.
|
|
49
|
+
- Use tools only when the user clearly asks you to do something on this computer
|
|
50
|
+
(e.g. "list my files", "what's in ~/notes.txt", "run the tests"). Do exactly
|
|
51
|
+
that, then report the result briefly.
|
|
52
|
+
- Never go exploring on your own or keep working past what was asked. One
|
|
53
|
+
message in, one helpful reply out.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class InstructionsWrite:
|
|
59
|
+
"""Outcome of :meth:`Config.write_instructions`."""
|
|
60
|
+
|
|
61
|
+
path: Path | None
|
|
62
|
+
action: Literal["written", "skipped", "disabled"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Config:
|
|
67
|
+
# --- Signal / signal-cli ---
|
|
68
|
+
signal_url: str = "http://127.0.0.1:3080"
|
|
69
|
+
# Senders allowed to drive the agent (E.164 numbers and/or UUIDs).
|
|
70
|
+
allow: list[str] = field(default_factory=list)
|
|
71
|
+
allow_all: bool = False
|
|
72
|
+
# Number to notify when the daemon comes online (optional). Note-to-Self works.
|
|
73
|
+
notify: str | None = None
|
|
74
|
+
|
|
75
|
+
# --- Model / provider ---
|
|
76
|
+
provider: str = "lmstudio"
|
|
77
|
+
model: str | None = None
|
|
78
|
+
base_url: str | None = None
|
|
79
|
+
api_key: str | None = None
|
|
80
|
+
|
|
81
|
+
# --- Agent behaviour ---
|
|
82
|
+
# When safe=True the agent is read-only (no shell commands, no edits).
|
|
83
|
+
safe: bool = False
|
|
84
|
+
# Backstop on the agent loop. Kept low: a chat reply rarely needs many turns,
|
|
85
|
+
# and a low cap means even a confused model returns *something* promptly.
|
|
86
|
+
max_turns: int = 20
|
|
87
|
+
# Agent persona/instructions, injected as the workspace AGENTS.md. None uses
|
|
88
|
+
# the built-in chat-assistant framing; set "" to inject nothing.
|
|
89
|
+
instructions: str | None = None
|
|
90
|
+
# MCP servers, in swival's format: {name: {command|url, args?, env?, headers?}}.
|
|
91
|
+
mcp_servers: dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
# Extra kwargs forwarded verbatim to swival.Session (the [swival] TOML table).
|
|
93
|
+
swival: dict[str, Any] = field(default_factory=dict)
|
|
94
|
+
|
|
95
|
+
# --- State / workspace ---
|
|
96
|
+
state_dir: Path = field(default_factory=lambda: Path.home() / ".nbclaw")
|
|
97
|
+
workspace_dir: Path | None = None # defaults to state_dir/workspace
|
|
98
|
+
|
|
99
|
+
# Grace period (seconds) for an in-flight job to finish on shutdown before
|
|
100
|
+
# it's abandoned. TOML-configurable; rarely needs changing.
|
|
101
|
+
shutdown_timeout: float = 20.0
|
|
102
|
+
|
|
103
|
+
def resolved_workspace(self) -> Path:
|
|
104
|
+
return self.workspace_dir or (self.state_dir / "workspace")
|
|
105
|
+
|
|
106
|
+
def crons_path(self) -> Path:
|
|
107
|
+
return self.state_dir / "crons.json"
|
|
108
|
+
|
|
109
|
+
def effective_instructions(self) -> str:
|
|
110
|
+
"""The AGENTS.md text to inject, or "" to inject nothing."""
|
|
111
|
+
return DEFAULT_INSTRUCTIONS if self.instructions is None else self.instructions
|
|
112
|
+
|
|
113
|
+
def _owns_agents_md(self, path: Path) -> bool:
|
|
114
|
+
"""True if we may overwrite ``path``.
|
|
115
|
+
|
|
116
|
+
The default managed workspace is entirely ours. A custom workspace_dir
|
|
117
|
+
may be a real project, so there we only touch an AGENTS.md that doesn't
|
|
118
|
+
exist yet or that carries our marker (i.e. a previous run wrote it).
|
|
119
|
+
"""
|
|
120
|
+
if self.workspace_dir is None:
|
|
121
|
+
return True
|
|
122
|
+
if not path.exists():
|
|
123
|
+
return True
|
|
124
|
+
try:
|
|
125
|
+
first_line = path.read_text().split("\n", 1)[0]
|
|
126
|
+
except OSError:
|
|
127
|
+
return False
|
|
128
|
+
return first_line.strip() == MANAGED_MARKER
|
|
129
|
+
|
|
130
|
+
def write_instructions(self) -> InstructionsWrite:
|
|
131
|
+
"""Materialize the agent instructions as the workspace AGENTS.md.
|
|
132
|
+
|
|
133
|
+
swival appends AGENTS.md to its system prompt, so this is how we give the
|
|
134
|
+
agent its chat-assistant persona. To avoid clobbering a user's own
|
|
135
|
+
AGENTS.md when workspace_dir points at a real project, an existing file
|
|
136
|
+
we didn't write is left untouched.
|
|
137
|
+
"""
|
|
138
|
+
text = self.effective_instructions().strip()
|
|
139
|
+
if not text:
|
|
140
|
+
return InstructionsWrite(None, "disabled")
|
|
141
|
+
path = self.resolved_workspace() / "AGENTS.md"
|
|
142
|
+
if not self._owns_agents_md(path):
|
|
143
|
+
return InstructionsWrite(path, "skipped")
|
|
144
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
path.write_text(f"{MANAGED_MARKER}\n{text}\n")
|
|
146
|
+
return InstructionsWrite(path, "written")
|
|
147
|
+
|
|
148
|
+
def session_kwargs(self) -> dict[str, Any]:
|
|
149
|
+
"""Build the keyword arguments passed to ``swival.Session``."""
|
|
150
|
+
kwargs: dict[str, Any] = {
|
|
151
|
+
"base_dir": str(self.resolved_workspace()),
|
|
152
|
+
"provider": self.provider,
|
|
153
|
+
"max_turns": self.max_turns,
|
|
154
|
+
# We manage multi-turn context ourselves via long-lived Session objects;
|
|
155
|
+
# swival's own on-disk history/continue isn't what we want for a chat bot.
|
|
156
|
+
"history": False,
|
|
157
|
+
"continue_here": False,
|
|
158
|
+
}
|
|
159
|
+
if self.model:
|
|
160
|
+
kwargs["model"] = self.model
|
|
161
|
+
if self.base_url:
|
|
162
|
+
kwargs["base_url"] = self.base_url
|
|
163
|
+
if self.api_key:
|
|
164
|
+
kwargs["api_key"] = self.api_key
|
|
165
|
+
if self.mcp_servers:
|
|
166
|
+
kwargs["mcp_servers"] = self.mcp_servers
|
|
167
|
+
if self.safe:
|
|
168
|
+
kwargs.update(commands="none", files="none", read_guard=True)
|
|
169
|
+
else:
|
|
170
|
+
# Capable assistant: it can act when asked. Access is gated by the
|
|
171
|
+
# sender allowlist. We deliberately avoid yolo — its autonomy push
|
|
172
|
+
# makes small models tool-loop on plain chat; the AGENTS.md persona
|
|
173
|
+
# (see Config.write_instructions) keeps replies tool-free by default.
|
|
174
|
+
kwargs.update(commands="all", files="all")
|
|
175
|
+
# The [swival] table overrides anything above.
|
|
176
|
+
kwargs.update(self.swival)
|
|
177
|
+
return kwargs
|
|
178
|
+
|
|
179
|
+
def parser_session_kwargs(self) -> dict[str, Any]:
|
|
180
|
+
"""Minimal, tool-free Session kwargs for a single JSON parse call."""
|
|
181
|
+
kwargs: dict[str, Any] = {
|
|
182
|
+
"provider": self.provider,
|
|
183
|
+
"commands": "none",
|
|
184
|
+
"files": "none",
|
|
185
|
+
"memory": False,
|
|
186
|
+
"history": False,
|
|
187
|
+
"no_skills": True,
|
|
188
|
+
"continue_here": False,
|
|
189
|
+
"max_turns": 2,
|
|
190
|
+
}
|
|
191
|
+
for key in ("model", "base_url", "api_key"):
|
|
192
|
+
value = getattr(self, key)
|
|
193
|
+
if value:
|
|
194
|
+
kwargs[key] = value
|
|
195
|
+
return kwargs
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def build_config(argv: list[str] | None = None) -> Config:
|
|
199
|
+
parser = argparse.ArgumentParser(
|
|
200
|
+
prog="nbclaw",
|
|
201
|
+
description="No Bullshit Claw — a 24/7 Signal-driven Swival agent daemon.",
|
|
202
|
+
)
|
|
203
|
+
parser.add_argument("--config", type=Path, help="Path to a TOML config file.")
|
|
204
|
+
parser.add_argument("--signal-url", help="signal-cli HTTP daemon base URL.")
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--allow",
|
|
207
|
+
action="append",
|
|
208
|
+
default=None,
|
|
209
|
+
metavar="NUMBER|UUID",
|
|
210
|
+
help="Allow this sender to drive the agent (repeatable).",
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--allow-all",
|
|
214
|
+
action="store_true",
|
|
215
|
+
default=None,
|
|
216
|
+
help="Allow ALL senders (dangerous — the agent can run shell commands).",
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument("--notify", help="Number to message when the daemon starts.")
|
|
219
|
+
parser.add_argument("--provider", help="swival provider (lmstudio, generic, ...).")
|
|
220
|
+
parser.add_argument("--model", help="Model id (e.g. ornith-1.0-9b).")
|
|
221
|
+
parser.add_argument("--base-url", help="Override the provider endpoint URL.")
|
|
222
|
+
parser.add_argument("--api-key", help="API key for the provider, if needed.")
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"--safe",
|
|
225
|
+
action="store_true",
|
|
226
|
+
default=None,
|
|
227
|
+
help="Read-only agent: no shell commands, no file edits.",
|
|
228
|
+
)
|
|
229
|
+
parser.add_argument("--max-turns", type=int, help="Max agent loop iterations.")
|
|
230
|
+
parser.add_argument(
|
|
231
|
+
"--state-dir", type=Path, help="Directory for crons + workspace."
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument("--workspace-dir", type=Path, help="Agent working directory.")
|
|
234
|
+
|
|
235
|
+
args = parser.parse_args(argv)
|
|
236
|
+
|
|
237
|
+
cfg = Config()
|
|
238
|
+
if args.config:
|
|
239
|
+
with args.config.open("rb") as f:
|
|
240
|
+
data = tomllib.load(f)
|
|
241
|
+
swival_table = data.pop("swival", None)
|
|
242
|
+
for key, value in data.items():
|
|
243
|
+
if not hasattr(cfg, key):
|
|
244
|
+
raise SystemExit(f"Unknown config key: {key!r} in {args.config}")
|
|
245
|
+
if key in ("state_dir", "workspace_dir") and value is not None:
|
|
246
|
+
value = Path(value).expanduser()
|
|
247
|
+
setattr(cfg, key, value)
|
|
248
|
+
if isinstance(swival_table, dict):
|
|
249
|
+
cfg.swival = swival_table
|
|
250
|
+
|
|
251
|
+
# CLI overrides (only when explicitly provided).
|
|
252
|
+
if args.signal_url is not None:
|
|
253
|
+
cfg.signal_url = args.signal_url
|
|
254
|
+
if args.allow is not None:
|
|
255
|
+
cfg.allow = args.allow
|
|
256
|
+
if args.allow_all is not None:
|
|
257
|
+
cfg.allow_all = args.allow_all
|
|
258
|
+
if args.notify is not None:
|
|
259
|
+
cfg.notify = args.notify
|
|
260
|
+
if args.provider is not None:
|
|
261
|
+
cfg.provider = args.provider
|
|
262
|
+
if args.model is not None:
|
|
263
|
+
cfg.model = args.model
|
|
264
|
+
if args.base_url is not None:
|
|
265
|
+
cfg.base_url = args.base_url
|
|
266
|
+
if args.api_key is not None:
|
|
267
|
+
cfg.api_key = args.api_key
|
|
268
|
+
if args.safe is not None:
|
|
269
|
+
cfg.safe = args.safe
|
|
270
|
+
if args.max_turns is not None:
|
|
271
|
+
cfg.max_turns = args.max_turns
|
|
272
|
+
if args.state_dir is not None:
|
|
273
|
+
cfg.state_dir = args.state_dir.expanduser()
|
|
274
|
+
if args.workspace_dir is not None:
|
|
275
|
+
cfg.workspace_dir = args.workspace_dir.expanduser()
|
|
276
|
+
|
|
277
|
+
return cfg
|