nbclaw 0.1.0__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.
- nbclaw-0.1.0/.claude/scheduled_tasks.lock +1 -0
- nbclaw-0.1.0/.gitignore +10 -0
- nbclaw-0.1.0/LOG.md +17 -0
- nbclaw-0.1.0/Makefile +25 -0
- nbclaw-0.1.0/PKG-INFO +166 -0
- nbclaw-0.1.0/README.md +156 -0
- nbclaw-0.1.0/deploy/com.nbclaw.daemon.plist +51 -0
- nbclaw-0.1.0/nbclaw/__init__.py +10 -0
- nbclaw-0.1.0/nbclaw/__main__.py +28 -0
- nbclaw-0.1.0/nbclaw/agent_runner.py +83 -0
- nbclaw-0.1.0/nbclaw/commands.py +56 -0
- nbclaw-0.1.0/nbclaw/config.py +277 -0
- nbclaw-0.1.0/nbclaw/daemon.py +375 -0
- nbclaw-0.1.0/nbclaw/nl_schedule.py +135 -0
- nbclaw-0.1.0/nbclaw/scheduler.py +253 -0
- nbclaw-0.1.0/nbclaw/signal_client.py +199 -0
- nbclaw-0.1.0/nbclaw.toml.example +46 -0
- nbclaw-0.1.0/pyproject.toml +27 -0
- nbclaw-0.1.0/tests/test_integration.py +194 -0
- nbclaw-0.1.0/tests/test_sse_live.py +74 -0
- nbclaw-0.1.0/tests/test_units.py +453 -0
- nbclaw-0.1.0/uv.lock +2302 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"1e776140-e63d-4779-bc4e-98c7f710c1ff","pid":77930,"procStart":"Thu Jun 25 23:11:53 2026","acquiredAt":1782429113867}
|
nbclaw-0.1.0/.gitignore
ADDED
nbclaw-0.1.0/LOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Log
|
|
2
|
+
|
|
3
|
+
## Switch swival to the PyPI release
|
|
4
|
+
|
|
5
|
+
The user asked to depend on the published swival release rather than the relative
|
|
6
|
+
local path. Removed the `[tool.uv.sources]` override in `pyproject.toml` that pointed
|
|
7
|
+
swival at `../swival` as an editable install, then re-ran `uv lock` so the lockfile
|
|
8
|
+
resolves swival 1.0.33 from PyPI. Confirmed with `uv sync` that the editable install
|
|
9
|
+
was replaced by the registry wheel and that `import swival` still works.
|
|
10
|
+
|
|
11
|
+
## Add a Makefile
|
|
12
|
+
|
|
13
|
+
The user asked for a convenient Makefile modeled on the one in `~/src/swival`. Adapted
|
|
14
|
+
that file to nbclaw: kept the `install`, `test`, `lint`, `format`, `check`, `clean`, and
|
|
15
|
+
`dist` targets but pointed them at the `nbclaw/` package, and dropped swival's `website`
|
|
16
|
+
and Homebrew-formula steps since this project has no `build.py` or `scripts/`. Verified
|
|
17
|
+
`make check` and `make test` both pass.
|
nbclaw-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.PHONY: all install test lint format check clean dist
|
|
2
|
+
|
|
3
|
+
all: check
|
|
4
|
+
|
|
5
|
+
install:
|
|
6
|
+
uv sync
|
|
7
|
+
|
|
8
|
+
test:
|
|
9
|
+
uv run python -m pytest tests/ -v --durations=25
|
|
10
|
+
|
|
11
|
+
lint:
|
|
12
|
+
uv run ruff check nbclaw/ tests/
|
|
13
|
+
|
|
14
|
+
format:
|
|
15
|
+
uv run ruff format nbclaw/ tests/
|
|
16
|
+
|
|
17
|
+
check: lint
|
|
18
|
+
uv run ruff format --check nbclaw/ tests/
|
|
19
|
+
|
|
20
|
+
clean:
|
|
21
|
+
rm -rf dist/ build/ __pycache__ nbclaw/__pycache__ tests/__pycache__ .pytest_cache .ruff_cache
|
|
22
|
+
find . -name '*.pyc' -delete
|
|
23
|
+
|
|
24
|
+
dist: clean
|
|
25
|
+
uv build
|
nbclaw-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nbclaw
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: No Bullshit Claw — a 24/7 Signal-driven Swival agent daemon
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: croniter>=3.0.0
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: swival
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# nbclaw
|
|
12
|
+
|
|
13
|
+
No Bullshit Claw: a small daemon that puts a [Swival](https://swival.dev/) agent n the other end of a Signal conversation.
|
|
14
|
+
|
|
15
|
+
You text it. It does the work and texts back. You can also tell it to do things on a schedule ("every weekday at 9, summarize the git log") and cancel those later.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- Python 3.13+ and [uv](https://docs.astral.sh/uv/).
|
|
20
|
+
- A running model. Anything Swival supports works; the quickest is to use [LM Studio](https://lmstudio.ai/) with a tool-calling model loaded.
|
|
21
|
+
- `signal-cli` registered to a phone number and running as an HTTP daemon:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
signal-cli --account XXXXXXXXX daemon --http 127.0.0.1:3080
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
cd nbclaw
|
|
31
|
+
uv sync
|
|
32
|
+
|
|
33
|
+
uv run nbclaw \
|
|
34
|
+
--allow YOURPHONE \
|
|
35
|
+
--model ornith-1.0-9b \
|
|
36
|
+
--notify YOURPHONE
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`--allow` is the number that's permitted to drive the agent (your phone).
|
|
40
|
+
`--notify` is optional and just sends a "nbclaw is online" message on start.
|
|
41
|
+
|
|
42
|
+
Now message the signal-cli number from your phone:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
you : list the python files in ~/src and count them
|
|
46
|
+
nbclaw: There are 42 .py files under ~/src. ...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Authorization (read this)
|
|
50
|
+
|
|
51
|
+
By default the agent runs in **autonomous** mode: it can run shell commands and read or edit files anywhere the account it runs as can reach. The workspace is just where it starts, not a fence. But it means **anyone on the allowlist effectively has a shell on this machine**.
|
|
52
|
+
|
|
53
|
+
- Always set `--allow` to the specific numbers you trust. With no allowlist set, every incoming message is ignored (fail closed).
|
|
54
|
+
- `--allow-all` exists for testing only. Don't use it on a machine you care about.
|
|
55
|
+
- `--safe` makes the agent read-only: no shell commands, no file edits. Good for a "just answer questions" bot.
|
|
56
|
+
- In a group chat, authorization is checked against the **sender**, but the reply goes to the **whole group**. So one allowed member can make the bot post agent output to everyone in that group. Keep the allowlist to people you trust with that, or only message the bot in 1:1 chats / Note to Self.
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
Send these as Signal messages. Anything not starting with `/` goes to the agent.
|
|
61
|
+
|
|
62
|
+
| Command | What it does |
|
|
63
|
+
| ----------------------- | ------------------------------------- |
|
|
64
|
+
| `/help` | List the commands. |
|
|
65
|
+
| `/status` | Model, mode, uptime, number of crons. |
|
|
66
|
+
| `/reset` | Forget this conversation's context. |
|
|
67
|
+
| `/cron <plain English>` | Schedule a task, described naturally. |
|
|
68
|
+
| `/cron list` | Show scheduled tasks. |
|
|
69
|
+
| `/cron del <name>` | Cancel a scheduled task. |
|
|
70
|
+
| `/cron run <name>` | Run a scheduled task right now. |
|
|
71
|
+
|
|
72
|
+
### Scheduling
|
|
73
|
+
|
|
74
|
+
Just say it after `/cron` in plain English. The model works out the timing and gives the task a short name:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
/cron every weekday at 9am summarize my git log in ~/src/app
|
|
78
|
+
/cron remind me to stretch every 2 hours
|
|
79
|
+
/cron tomorrow at 8am say good morning
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Both recurring schedules and one-time reminders ("in 10 minutes…", "tomorrow at 8am…") are understood; one-time jobs delete themselves after they fire.
|
|
83
|
+
|
|
84
|
+
Results are delivered to the conversation that created the cron, prefixed with its name. Crons run as independent one-shots, so they never pollute your chat's context.
|
|
85
|
+
|
|
86
|
+
Use `/cron list` to see names, then `/cron del <name>` to cancel.
|
|
87
|
+
|
|
88
|
+
If you'd rather be exact, the power-user form takes a literal schedule:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
/cron add standup 0 9 * * 1-5 | summarize today's commits in ~/src/myrepo
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
where the schedule is a 5-field cron expression, `@every 30m` (`30s`/`5m`/`2h`/`1d`), or `@hourly` / `@daily` / `@weekly` / `@monthly`.
|
|
95
|
+
|
|
96
|
+
## Configuration file
|
|
97
|
+
|
|
98
|
+
Flags cover the common cases. For anything else, point `--config` at a TOML file.
|
|
99
|
+
|
|
100
|
+
Top-level keys mirror the settings; a `[swival]` table is passed straight through to `swival.Session`, so the full agent is configurable.
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
# nbclaw.toml
|
|
104
|
+
signal_url = "http://127.0.0.1:3080"
|
|
105
|
+
allow = ["YOURPHONE"]
|
|
106
|
+
notify = "YOURPHONE"
|
|
107
|
+
|
|
108
|
+
provider = "lmstudio"
|
|
109
|
+
model = "ornith-1.0-9b"
|
|
110
|
+
# base_url = "http://127.0.0.1:1234" # only if not the provider default
|
|
111
|
+
max_turns = 60
|
|
112
|
+
|
|
113
|
+
state_dir = "~/.nbclaw"
|
|
114
|
+
|
|
115
|
+
# MCP servers, in swival's format.
|
|
116
|
+
[mcp_servers.fetch]
|
|
117
|
+
command = "uvx"
|
|
118
|
+
args = ["mcp-server-fetch"]
|
|
119
|
+
|
|
120
|
+
# Anything here is forwarded verbatim to swival.Session.
|
|
121
|
+
[swival]
|
|
122
|
+
temperature = 0.2
|
|
123
|
+
reasoning_effort = "medium"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Run it:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
uv run nbclaw --config nbclaw.toml
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
CLI flags override the file.
|
|
133
|
+
|
|
134
|
+
## Running 24/7
|
|
135
|
+
|
|
136
|
+
The agent is meant to stay up.
|
|
137
|
+
|
|
138
|
+
On macOS, a launchd job keeps it alive across logouts and reboots. A template is in `deploy/com.nbclaw.daemon.plist`; edit the paths and numbers, then:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
cp deploy/com.nbclaw.daemon.plist ~/Library/LaunchAgents/
|
|
142
|
+
launchctl load ~/Library/LaunchAgents/com.nbclaw.daemon.plist
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
On Linux, a user systemd unit does the same job; the plist documents the same command line.
|
|
146
|
+
|
|
147
|
+
## State
|
|
148
|
+
|
|
149
|
+
Everything lives under `state_dir` (default `~/.nbclaw`):
|
|
150
|
+
|
|
151
|
+
- `crons.json` — scheduled tasks, written atomically.
|
|
152
|
+
- `workspace/` — the agent's working directory (its `base_dir`).
|
|
153
|
+
|
|
154
|
+
## Environment variables
|
|
155
|
+
|
|
156
|
+
- `NBCLAW_LOG` sets the log level (default `INFO`; try `DEBUG`).
|
|
157
|
+
|
|
158
|
+
## But why this since there's already XYZ?
|
|
159
|
+
|
|
160
|
+
NBClaw uses Swival as a Python library, so the CLI isn't required. This lets it work well even with small, local models and short context windows. No large models needed.
|
|
161
|
+
|
|
162
|
+
More importantly, NBClaw is ridiculously lightweight and incredibly easy to install and use.
|
|
163
|
+
|
|
164
|
+
No bloat. It intentionally ships with a minimal set of tools, but it gets the job done. And if you need more, you can extend it with skills, MCP servers, and whatever else fits your workflow.
|
|
165
|
+
|
|
166
|
+
It may not be for you, but this is the minimal claw-style agent I always wanted.
|
nbclaw-0.1.0/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# nbclaw
|
|
2
|
+
|
|
3
|
+
No Bullshit Claw: a small daemon that puts a [Swival](https://swival.dev/) agent n the other end of a Signal conversation.
|
|
4
|
+
|
|
5
|
+
You text it. It does the work and texts back. You can also tell it to do things on a schedule ("every weekday at 9, summarize the git log") and cancel those later.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Python 3.13+ and [uv](https://docs.astral.sh/uv/).
|
|
10
|
+
- A running model. Anything Swival supports works; the quickest is to use [LM Studio](https://lmstudio.ai/) with a tool-calling model loaded.
|
|
11
|
+
- `signal-cli` registered to a phone number and running as an HTTP daemon:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
signal-cli --account XXXXXXXXX daemon --http 127.0.0.1:3080
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
cd nbclaw
|
|
21
|
+
uv sync
|
|
22
|
+
|
|
23
|
+
uv run nbclaw \
|
|
24
|
+
--allow YOURPHONE \
|
|
25
|
+
--model ornith-1.0-9b \
|
|
26
|
+
--notify YOURPHONE
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`--allow` is the number that's permitted to drive the agent (your phone).
|
|
30
|
+
`--notify` is optional and just sends a "nbclaw is online" message on start.
|
|
31
|
+
|
|
32
|
+
Now message the signal-cli number from your phone:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
you : list the python files in ~/src and count them
|
|
36
|
+
nbclaw: There are 42 .py files under ~/src. ...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authorization (read this)
|
|
40
|
+
|
|
41
|
+
By default the agent runs in **autonomous** mode: it can run shell commands and read or edit files anywhere the account it runs as can reach. The workspace is just where it starts, not a fence. But it means **anyone on the allowlist effectively has a shell on this machine**.
|
|
42
|
+
|
|
43
|
+
- Always set `--allow` to the specific numbers you trust. With no allowlist set, every incoming message is ignored (fail closed).
|
|
44
|
+
- `--allow-all` exists for testing only. Don't use it on a machine you care about.
|
|
45
|
+
- `--safe` makes the agent read-only: no shell commands, no file edits. Good for a "just answer questions" bot.
|
|
46
|
+
- In a group chat, authorization is checked against the **sender**, but the reply goes to the **whole group**. So one allowed member can make the bot post agent output to everyone in that group. Keep the allowlist to people you trust with that, or only message the bot in 1:1 chats / Note to Self.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
Send these as Signal messages. Anything not starting with `/` goes to the agent.
|
|
51
|
+
|
|
52
|
+
| Command | What it does |
|
|
53
|
+
| ----------------------- | ------------------------------------- |
|
|
54
|
+
| `/help` | List the commands. |
|
|
55
|
+
| `/status` | Model, mode, uptime, number of crons. |
|
|
56
|
+
| `/reset` | Forget this conversation's context. |
|
|
57
|
+
| `/cron <plain English>` | Schedule a task, described naturally. |
|
|
58
|
+
| `/cron list` | Show scheduled tasks. |
|
|
59
|
+
| `/cron del <name>` | Cancel a scheduled task. |
|
|
60
|
+
| `/cron run <name>` | Run a scheduled task right now. |
|
|
61
|
+
|
|
62
|
+
### Scheduling
|
|
63
|
+
|
|
64
|
+
Just say it after `/cron` in plain English. The model works out the timing and gives the task a short name:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
/cron every weekday at 9am summarize my git log in ~/src/app
|
|
68
|
+
/cron remind me to stretch every 2 hours
|
|
69
|
+
/cron tomorrow at 8am say good morning
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Both recurring schedules and one-time reminders ("in 10 minutes…", "tomorrow at 8am…") are understood; one-time jobs delete themselves after they fire.
|
|
73
|
+
|
|
74
|
+
Results are delivered to the conversation that created the cron, prefixed with its name. Crons run as independent one-shots, so they never pollute your chat's context.
|
|
75
|
+
|
|
76
|
+
Use `/cron list` to see names, then `/cron del <name>` to cancel.
|
|
77
|
+
|
|
78
|
+
If you'd rather be exact, the power-user form takes a literal schedule:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
/cron add standup 0 9 * * 1-5 | summarize today's commits in ~/src/myrepo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
where the schedule is a 5-field cron expression, `@every 30m` (`30s`/`5m`/`2h`/`1d`), or `@hourly` / `@daily` / `@weekly` / `@monthly`.
|
|
85
|
+
|
|
86
|
+
## Configuration file
|
|
87
|
+
|
|
88
|
+
Flags cover the common cases. For anything else, point `--config` at a TOML file.
|
|
89
|
+
|
|
90
|
+
Top-level keys mirror the settings; a `[swival]` table is passed straight through to `swival.Session`, so the full agent is configurable.
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
# nbclaw.toml
|
|
94
|
+
signal_url = "http://127.0.0.1:3080"
|
|
95
|
+
allow = ["YOURPHONE"]
|
|
96
|
+
notify = "YOURPHONE"
|
|
97
|
+
|
|
98
|
+
provider = "lmstudio"
|
|
99
|
+
model = "ornith-1.0-9b"
|
|
100
|
+
# base_url = "http://127.0.0.1:1234" # only if not the provider default
|
|
101
|
+
max_turns = 60
|
|
102
|
+
|
|
103
|
+
state_dir = "~/.nbclaw"
|
|
104
|
+
|
|
105
|
+
# MCP servers, in swival's format.
|
|
106
|
+
[mcp_servers.fetch]
|
|
107
|
+
command = "uvx"
|
|
108
|
+
args = ["mcp-server-fetch"]
|
|
109
|
+
|
|
110
|
+
# Anything here is forwarded verbatim to swival.Session.
|
|
111
|
+
[swival]
|
|
112
|
+
temperature = 0.2
|
|
113
|
+
reasoning_effort = "medium"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run it:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
uv run nbclaw --config nbclaw.toml
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
CLI flags override the file.
|
|
123
|
+
|
|
124
|
+
## Running 24/7
|
|
125
|
+
|
|
126
|
+
The agent is meant to stay up.
|
|
127
|
+
|
|
128
|
+
On macOS, a launchd job keeps it alive across logouts and reboots. A template is in `deploy/com.nbclaw.daemon.plist`; edit the paths and numbers, then:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
cp deploy/com.nbclaw.daemon.plist ~/Library/LaunchAgents/
|
|
132
|
+
launchctl load ~/Library/LaunchAgents/com.nbclaw.daemon.plist
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
On Linux, a user systemd unit does the same job; the plist documents the same command line.
|
|
136
|
+
|
|
137
|
+
## State
|
|
138
|
+
|
|
139
|
+
Everything lives under `state_dir` (default `~/.nbclaw`):
|
|
140
|
+
|
|
141
|
+
- `crons.json` — scheduled tasks, written atomically.
|
|
142
|
+
- `workspace/` — the agent's working directory (its `base_dir`).
|
|
143
|
+
|
|
144
|
+
## Environment variables
|
|
145
|
+
|
|
146
|
+
- `NBCLAW_LOG` sets the log level (default `INFO`; try `DEBUG`).
|
|
147
|
+
|
|
148
|
+
## But why this since there's already XYZ?
|
|
149
|
+
|
|
150
|
+
NBClaw uses Swival as a Python library, so the CLI isn't required. This lets it work well even with small, local models and short context windows. No large models needed.
|
|
151
|
+
|
|
152
|
+
More importantly, NBClaw is ridiculously lightweight and incredibly easy to install and use.
|
|
153
|
+
|
|
154
|
+
No bloat. It intentionally ships with a minimal set of tools, but it gets the job done. And if you need more, you can extend it with skills, MCP servers, and whatever else fits your workflow.
|
|
155
|
+
|
|
156
|
+
It may not be for you, but this is the minimal claw-style agent I always wanted.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
launchd template for running nbclaw 24/7 on macOS.
|
|
4
|
+
|
|
5
|
+
Edit the paths and phone numbers below, then:
|
|
6
|
+
cp deploy/com.nbclaw.daemon.plist ~/Library/LaunchAgents/
|
|
7
|
+
launchctl load ~/Library/LaunchAgents/com.nbclaw.daemon.plist
|
|
8
|
+
|
|
9
|
+
KeepAlive restarts nbclaw if it ever exits. It assumes signal-cli and the
|
|
10
|
+
model server are already running (run those as their own launchd jobs).
|
|
11
|
+
|
|
12
|
+
The equivalent command line for a Linux systemd user unit is:
|
|
13
|
+
ExecStart=/opt/homebrew/bin/uv run --project %h/src/nbclaw nbclaw --config %h/.nbclaw/nbclaw.toml
|
|
14
|
+
-->
|
|
15
|
+
<plist version="1.0">
|
|
16
|
+
<dict>
|
|
17
|
+
<key>Label</key>
|
|
18
|
+
<string>com.nbclaw.daemon</string>
|
|
19
|
+
|
|
20
|
+
<key>ProgramArguments</key>
|
|
21
|
+
<array>
|
|
22
|
+
<string>/opt/homebrew/bin/uv</string>
|
|
23
|
+
<string>run</string>
|
|
24
|
+
<string>--project</string>
|
|
25
|
+
<string>/Users/YOU/src/nbclaw</string>
|
|
26
|
+
<string>nbclaw</string>
|
|
27
|
+
<string>--config</string>
|
|
28
|
+
<string>/Users/YOU/.nbclaw/nbclaw.toml</string>
|
|
29
|
+
</array>
|
|
30
|
+
|
|
31
|
+
<key>WorkingDirectory</key>
|
|
32
|
+
<string>/Users/YOU/src/nbclaw</string>
|
|
33
|
+
|
|
34
|
+
<key>EnvironmentVariables</key>
|
|
35
|
+
<dict>
|
|
36
|
+
<key>NBCLAW_LOG</key>
|
|
37
|
+
<string>INFO</string>
|
|
38
|
+
</dict>
|
|
39
|
+
|
|
40
|
+
<key>RunAtLoad</key>
|
|
41
|
+
<true/>
|
|
42
|
+
|
|
43
|
+
<key>KeepAlive</key>
|
|
44
|
+
<true/>
|
|
45
|
+
|
|
46
|
+
<key>StandardOutPath</key>
|
|
47
|
+
<string>/Users/YOU/.nbclaw/nbclaw.log</string>
|
|
48
|
+
<key>StandardErrorPath</key>
|
|
49
|
+
<string>/Users/YOU/.nbclaw/nbclaw.log</string>
|
|
50
|
+
</dict>
|
|
51
|
+
</plist>
|
|
@@ -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()
|
|
@@ -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")
|
|
@@ -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
|