nock-cli 0.1.0__tar.gz → 0.3.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.
- nock_cli-0.3.0/PKG-INFO +137 -0
- nock_cli-0.3.0/README.md +114 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/pyproject.toml +2 -2
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli/__init__.py +1 -1
- nock_cli-0.3.0/src/nock_cli/agent.py +193 -0
- nock_cli-0.3.0/src/nock_cli/chat.py +19 -0
- nock_cli-0.3.0/src/nock_cli/code.py +62 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli/download.py +16 -3
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli/engine.py +227 -180
- nock_cli-0.3.0/src/nock_cli/main.py +251 -0
- nock_cli-0.3.0/src/nock_cli/models.py +81 -0
- nock_cli-0.3.0/src/nock_cli/serve.py +181 -0
- nock_cli-0.3.0/src/nock_cli.egg-info/PKG-INFO +137 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli.egg-info/SOURCES.txt +4 -0
- nock_cli-0.3.0/tests/test_cli.py +301 -0
- nock_cli-0.1.0/PKG-INFO +0 -90
- nock_cli-0.1.0/README.md +0 -67
- nock_cli-0.1.0/src/nock_cli/main.py +0 -124
- nock_cli-0.1.0/src/nock_cli/models.py +0 -42
- nock_cli-0.1.0/src/nock_cli.egg-info/PKG-INFO +0 -90
- nock_cli-0.1.0/tests/test_cli.py +0 -101
- {nock_cli-0.1.0 → nock_cli-0.3.0}/LICENSE +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/setup.cfg +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli/__main__.py +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli/paths.py +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli/video.py +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli.egg-info/dependency_links.txt +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli.egg-info/entry_points.txt +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli.egg-info/requires.txt +0 -0
- {nock_cli-0.1.0 → nock_cli-0.3.0}/src/nock_cli.egg-info/top_level.txt +0 -0
nock_cli-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nock-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Open models that live on your machine: code, video and a coding agent, offline.
|
|
5
|
+
Author: NockAI
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://nockai.dev
|
|
8
|
+
Project-URL: Model, https://huggingface.co/Nock-AI/nock-video-4b-GGUF
|
|
9
|
+
Keywords: local,offline,llm,video,llama.cpp,gguf
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Multimedia :: Video
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: links
|
|
21
|
+
Requires-Dist: yt-dlp; extra == "links"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Nock CLI
|
|
25
|
+
|
|
26
|
+
Open models that live on your machine. One tool for the NockAI family: code, video, and a coding agent, offline.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
nock add coder video
|
|
30
|
+
nock code "regex for an EVM address"
|
|
31
|
+
nock video find meeting.mp4 "when do they talk about the budget"
|
|
32
|
+
nock fix ./my-project "the total is off by one" # experimental, big GPU
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Everything runs on your computer. The models run in a local llama.cpp server on `127.0.0.1`. Nothing is uploaded.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
You need Python 3.9 or newer.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install nock-cli
|
|
43
|
+
nock add coder # or: video, agent
|
|
44
|
+
nock doctor
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`nock add` downloads, once:
|
|
48
|
+
- the engine: an official [llama.cpp](https://github.com/ggml-org/llama.cpp) build (about 35 MB; Vulkan on Windows
|
|
49
|
+
and Linux, Metal on Apple Silicon). Already have `llama-server`? Nock uses it.
|
|
50
|
+
- the skill's model from Hugging Face.
|
|
51
|
+
|
|
52
|
+
| Skill | Model | Download | Needs |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| `coder` | [Nock Coder 1.5B](https://huggingface.co/Nock-AI/nock-coder-1.5b-GGUF) | 1 GB | any laptop |
|
|
55
|
+
| `video` | [Nock Video 4B](https://huggingface.co/Nock-AI/nock-video-4b-GGUF) (preview) | 3.4 GB | a GPU with 6 GB for 64 frames; [ffmpeg](https://ffmpeg.org/download.html) |
|
|
56
|
+
| `agent` | [Nock Agent 31B](https://huggingface.co/Nock-AI/nock-agent-31b-GGUF) (experimental) | 18.7 GB | a GPU with about 20 GB (a Mac with 32 GB or more may work; not tested) |
|
|
57
|
+
|
|
58
|
+
`nock list` shows what is installed, `nock remove <skill>` deletes a skill's files. Files go to `~/.nock`
|
|
59
|
+
(set `NOCK_HOME` to use another folder).
|
|
60
|
+
|
|
61
|
+
## Nock Coder: `nock code`
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
nock code "write a Solidity ERC-20 with a cap"
|
|
65
|
+
cat error.log | nock code "why does this fail?"
|
|
66
|
+
nock code # a chat; type exit to quit
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Code first, one short line of explanation. The same model and prompt as `ollama run nockai/nock-coder`. It is a
|
|
70
|
+
1.5B model, so review code before you run it. On HumanEval it passes 118 of 164.
|
|
71
|
+
|
|
72
|
+
## Nock Video: `nock video`
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
nock video summarize meeting.mp4
|
|
76
|
+
nock video ask clip.mp4 "what error message shows on screen?"
|
|
77
|
+
nock video find clip.mp4 "when the cake comes out"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Files or links. Links are downloaded to your own machine with [yt-dlp](https://github.com/yt-dlp/yt-dlp), which
|
|
81
|
+
you install yourself (`pipx inject nock-cli yt-dlp`). Only use videos you have the right to use.
|
|
82
|
+
|
|
83
|
+
Nock Video 4B is a **preview**. On our test of 20 videos, through llama.cpp at 64 frames, it found the moment in
|
|
84
|
+
43 of 56 questions, answered 59 of 60 questions about what is on screen, and gave 6 of 7 good summaries. Full
|
|
85
|
+
results, including the misses, are on the [model card](https://huggingface.co/Nock-AI/nock-video-4b-GGUF).
|
|
86
|
+
|
|
87
|
+
- It reads frames only, not sound.
|
|
88
|
+
- A long video is read as at most 64 frames, so short events in long videos can be missed.
|
|
89
|
+
- 64 frames used about 5.5 GB of GPU memory in our test. On GPUs under 6 GB, Nock uses 32 frames (the first
|
|
90
|
+
preview dropped from 42 to 35 of 56 moments at 32 frames; the current model was not measured at 32).
|
|
91
|
+
`--frames` overrides it.
|
|
92
|
+
|
|
93
|
+
## Nock Agent: `nock fix` (experimental)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
nock fix ./my-project "the total is off by one"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Nock Agent reads the code, makes the smallest edit and runs the tests. On our agent test it solved 145 of 150 runs
|
|
100
|
+
and edited a test file 0 times. It works on your own files, so **every edit is shown as a diff and every command
|
|
101
|
+
before it runs, and each one needs your yes**. `--yes` skips the questions; use it only in a copy or a git branch.
|
|
102
|
+
It refuses to start on GPUs under about 20 GB (`--force` to try anyway, very slowly).
|
|
103
|
+
|
|
104
|
+
## Use it from your apps: `nock serve`
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
nock serve
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
One local, OpenAI-compatible address for every skill you have installed. In your editor or tool, set the base URL to
|
|
111
|
+
`http://127.0.0.1:8080/v1`, any API key, and the model to `nock-coder`, `nock-video` or `nock-agent`.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
curl http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d '{"model": "nock-coder", "messages": [{"role": "user", "content": "regex for an EVM address"}]}'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The model starts on first use, and one model is in memory at a time, so asking for another swaps them (it can take
|
|
118
|
+
a while). Streaming works. It listens on this machine only, refuses requests from websites, and logs no questions or
|
|
119
|
+
answers. `--port` changes the port.
|
|
120
|
+
|
|
121
|
+
## Speed and GPUs
|
|
122
|
+
|
|
123
|
+
`nock doctor` shows which device the engine uses. If it says **software device** or **no GPU device**, the
|
|
124
|
+
engine runs on the CPU and answers take many minutes. This happens inside most containers and VMs. On a machine
|
|
125
|
+
with an NVIDIA GPU, `nock setup --backend cuda` installs the CUDA build instead (a bigger download: about 630 MB on Windows, 740 MB on Linux).
|
|
126
|
+
|
|
127
|
+
| Option | Meaning |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `--backend vulkan\|cuda\|cpu\|metal` | which llama.cpp build `nock add` / `nock setup` installs |
|
|
130
|
+
| `NOCK_LLAMA_SERVER=/path/llama-server` | use your own llama.cpp build |
|
|
131
|
+
| `NOCK_HOME=/path` | where models and the engine are stored |
|
|
132
|
+
|
|
133
|
+
Server logs are in `~/.nock/logs/` if something goes wrong.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
Apache-2.0. Each model has its own license on its model card (all Apache-2.0).
|
nock_cli-0.3.0/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Nock CLI
|
|
2
|
+
|
|
3
|
+
Open models that live on your machine. One tool for the NockAI family: code, video, and a coding agent, offline.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
nock add coder video
|
|
7
|
+
nock code "regex for an EVM address"
|
|
8
|
+
nock video find meeting.mp4 "when do they talk about the budget"
|
|
9
|
+
nock fix ./my-project "the total is off by one" # experimental, big GPU
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Everything runs on your computer. The models run in a local llama.cpp server on `127.0.0.1`. Nothing is uploaded.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
You need Python 3.9 or newer.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pipx install nock-cli
|
|
20
|
+
nock add coder # or: video, agent
|
|
21
|
+
nock doctor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`nock add` downloads, once:
|
|
25
|
+
- the engine: an official [llama.cpp](https://github.com/ggml-org/llama.cpp) build (about 35 MB; Vulkan on Windows
|
|
26
|
+
and Linux, Metal on Apple Silicon). Already have `llama-server`? Nock uses it.
|
|
27
|
+
- the skill's model from Hugging Face.
|
|
28
|
+
|
|
29
|
+
| Skill | Model | Download | Needs |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `coder` | [Nock Coder 1.5B](https://huggingface.co/Nock-AI/nock-coder-1.5b-GGUF) | 1 GB | any laptop |
|
|
32
|
+
| `video` | [Nock Video 4B](https://huggingface.co/Nock-AI/nock-video-4b-GGUF) (preview) | 3.4 GB | a GPU with 6 GB for 64 frames; [ffmpeg](https://ffmpeg.org/download.html) |
|
|
33
|
+
| `agent` | [Nock Agent 31B](https://huggingface.co/Nock-AI/nock-agent-31b-GGUF) (experimental) | 18.7 GB | a GPU with about 20 GB (a Mac with 32 GB or more may work; not tested) |
|
|
34
|
+
|
|
35
|
+
`nock list` shows what is installed, `nock remove <skill>` deletes a skill's files. Files go to `~/.nock`
|
|
36
|
+
(set `NOCK_HOME` to use another folder).
|
|
37
|
+
|
|
38
|
+
## Nock Coder: `nock code`
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
nock code "write a Solidity ERC-20 with a cap"
|
|
42
|
+
cat error.log | nock code "why does this fail?"
|
|
43
|
+
nock code # a chat; type exit to quit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Code first, one short line of explanation. The same model and prompt as `ollama run nockai/nock-coder`. It is a
|
|
47
|
+
1.5B model, so review code before you run it. On HumanEval it passes 118 of 164.
|
|
48
|
+
|
|
49
|
+
## Nock Video: `nock video`
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
nock video summarize meeting.mp4
|
|
53
|
+
nock video ask clip.mp4 "what error message shows on screen?"
|
|
54
|
+
nock video find clip.mp4 "when the cake comes out"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Files or links. Links are downloaded to your own machine with [yt-dlp](https://github.com/yt-dlp/yt-dlp), which
|
|
58
|
+
you install yourself (`pipx inject nock-cli yt-dlp`). Only use videos you have the right to use.
|
|
59
|
+
|
|
60
|
+
Nock Video 4B is a **preview**. On our test of 20 videos, through llama.cpp at 64 frames, it found the moment in
|
|
61
|
+
43 of 56 questions, answered 59 of 60 questions about what is on screen, and gave 6 of 7 good summaries. Full
|
|
62
|
+
results, including the misses, are on the [model card](https://huggingface.co/Nock-AI/nock-video-4b-GGUF).
|
|
63
|
+
|
|
64
|
+
- It reads frames only, not sound.
|
|
65
|
+
- A long video is read as at most 64 frames, so short events in long videos can be missed.
|
|
66
|
+
- 64 frames used about 5.5 GB of GPU memory in our test. On GPUs under 6 GB, Nock uses 32 frames (the first
|
|
67
|
+
preview dropped from 42 to 35 of 56 moments at 32 frames; the current model was not measured at 32).
|
|
68
|
+
`--frames` overrides it.
|
|
69
|
+
|
|
70
|
+
## Nock Agent: `nock fix` (experimental)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
nock fix ./my-project "the total is off by one"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Nock Agent reads the code, makes the smallest edit and runs the tests. On our agent test it solved 145 of 150 runs
|
|
77
|
+
and edited a test file 0 times. It works on your own files, so **every edit is shown as a diff and every command
|
|
78
|
+
before it runs, and each one needs your yes**. `--yes` skips the questions; use it only in a copy or a git branch.
|
|
79
|
+
It refuses to start on GPUs under about 20 GB (`--force` to try anyway, very slowly).
|
|
80
|
+
|
|
81
|
+
## Use it from your apps: `nock serve`
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
nock serve
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
One local, OpenAI-compatible address for every skill you have installed. In your editor or tool, set the base URL to
|
|
88
|
+
`http://127.0.0.1:8080/v1`, any API key, and the model to `nock-coder`, `nock-video` or `nock-agent`.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
curl http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d '{"model": "nock-coder", "messages": [{"role": "user", "content": "regex for an EVM address"}]}'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The model starts on first use, and one model is in memory at a time, so asking for another swaps them (it can take
|
|
95
|
+
a while). Streaming works. It listens on this machine only, refuses requests from websites, and logs no questions or
|
|
96
|
+
answers. `--port` changes the port.
|
|
97
|
+
|
|
98
|
+
## Speed and GPUs
|
|
99
|
+
|
|
100
|
+
`nock doctor` shows which device the engine uses. If it says **software device** or **no GPU device**, the
|
|
101
|
+
engine runs on the CPU and answers take many minutes. This happens inside most containers and VMs. On a machine
|
|
102
|
+
with an NVIDIA GPU, `nock setup --backend cuda` installs the CUDA build instead (a bigger download: about 630 MB on Windows, 740 MB on Linux).
|
|
103
|
+
|
|
104
|
+
| Option | Meaning |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `--backend vulkan\|cuda\|cpu\|metal` | which llama.cpp build `nock add` / `nock setup` installs |
|
|
107
|
+
| `NOCK_LLAMA_SERVER=/path/llama-server` | use your own llama.cpp build |
|
|
108
|
+
| `NOCK_HOME=/path` | where models and the engine are stored |
|
|
109
|
+
|
|
110
|
+
Server logs are in `~/.nock/logs/` if something goes wrong.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
Apache-2.0. Each model has its own license on its model card (all Apache-2.0).
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nock-cli"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Open models that live on your machine
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Open models that live on your machine: code, video and a coding agent, offline."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Nock Agent (experimental): fixes an issue in a project folder on your machine.
|
|
2
|
+
|
|
3
|
+
The loop and the five tools are the ones Nock Agent was tested with (runpod/harness.py). Because it works on your
|
|
4
|
+
own files, every edit is shown as a diff and every command is shown before it runs, and each needs a yes
|
|
5
|
+
(`--yes` skips the questions). Reading and searching need no approval. Paths outside the project are refused.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import difflib
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from nock_cli.chat import complete
|
|
15
|
+
|
|
16
|
+
MAX_STEPS = 30
|
|
17
|
+
COMMAND_TIMEOUT = 120
|
|
18
|
+
MAX_TOKENS = 2048
|
|
19
|
+
SEARCH_SUFFIXES = (".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".h", ".cpp",
|
|
20
|
+
".cs", ".sol", ".sh", ".txt", ".md", ".cfg", ".toml", ".json", ".yaml", ".yml", ".html", ".css")
|
|
21
|
+
SKIPPED_FOLDERS = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build", ".next", "target"}
|
|
22
|
+
|
|
23
|
+
AGENT_PROMPT = (
|
|
24
|
+
"You are Nock Agent, a coding agent working in a project folder. Use the tools to read the code, make the "
|
|
25
|
+
"smallest correct edit, and run the tests. Never edit test files. When the tests pass, reply with a one line "
|
|
26
|
+
"summary and stop calling tools."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
TOOLS = [
|
|
30
|
+
{"type": "function", "function": {
|
|
31
|
+
"name": "read_file", "description": "Read a text file from the project. Optionally only a line range.",
|
|
32
|
+
"parameters": {"type": "object", "required": ["path"], "properties": {
|
|
33
|
+
"path": {"type": "string", "description": "File path relative to the project root"},
|
|
34
|
+
"start_line": {"type": "integer", "description": "First line to read (1 based)"},
|
|
35
|
+
"end_line": {"type": "integer", "description": "Last line to read"},
|
|
36
|
+
}}}},
|
|
37
|
+
{"type": "function", "function": {
|
|
38
|
+
"name": "edit_file", "description": "Replace one exact piece of text in a file with new text.",
|
|
39
|
+
"parameters": {"type": "object", "required": ["path", "old_text", "new_text"], "properties": {
|
|
40
|
+
"path": {"type": "string"},
|
|
41
|
+
"old_text": {"type": "string", "description": "Exact text to find, copied from the file"},
|
|
42
|
+
"new_text": {"type": "string", "description": "Text to put in its place"},
|
|
43
|
+
}}}},
|
|
44
|
+
{"type": "function", "function": {
|
|
45
|
+
"name": "write_file", "description": "Create a file or overwrite it with new content.",
|
|
46
|
+
"parameters": {"type": "object", "required": ["path", "content"], "properties": {
|
|
47
|
+
"path": {"type": "string"}, "content": {"type": "string"},
|
|
48
|
+
}}}},
|
|
49
|
+
{"type": "function", "function": {
|
|
50
|
+
"name": "search_code", "description": "Search all project files for a piece of text. Returns file:line matches.",
|
|
51
|
+
"parameters": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string"}}}}},
|
|
52
|
+
{"type": "function", "function": {
|
|
53
|
+
"name": "run_command", "description": "Run a shell command in the project folder and return its output.",
|
|
54
|
+
"parameters": {"type": "object", "required": ["command"], "properties": {"command": {"type": "string"}}}}},
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Approver:
|
|
59
|
+
"""Asks before each change. `always` (from --yes) approves everything without asking."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, always=False, ask=input):
|
|
62
|
+
self.always = always
|
|
63
|
+
self.ask = ask
|
|
64
|
+
|
|
65
|
+
def __call__(self, title, detail):
|
|
66
|
+
print(f"\n{title}\n{detail}", file=sys.stderr)
|
|
67
|
+
if self.always:
|
|
68
|
+
return True
|
|
69
|
+
try:
|
|
70
|
+
return self.ask("Allow? [y/N] ").strip().lower() in ("y", "yes")
|
|
71
|
+
except EOFError:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def inside(root, path):
|
|
76
|
+
target = (root / path).resolve()
|
|
77
|
+
if target != root and root not in target.parents:
|
|
78
|
+
raise ValueError("path outside the project")
|
|
79
|
+
return target
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def diff_text(path, before, after):
|
|
83
|
+
lines = difflib.unified_diff(before.splitlines(), after.splitlines(), f"a/{path}", f"b/{path}", lineterm="")
|
|
84
|
+
return "\n".join(lines) or "(no change)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def project_files(root):
|
|
88
|
+
for path in sorted(root.rglob("*")):
|
|
89
|
+
if path.is_file() and path.suffix in SEARCH_SUFFIXES and not SKIPPED_FOLDERS.intersection(path.relative_to(root).parts):
|
|
90
|
+
yield path
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_tool(root, name, arguments, approve):
|
|
94
|
+
"""Runs one tool call. Errors and refusals go back to the model as text."""
|
|
95
|
+
try:
|
|
96
|
+
if name == "read_file":
|
|
97
|
+
text = inside(root, arguments["path"]).read_text(encoding="utf-8")
|
|
98
|
+
if not arguments.get("start_line") and not arguments.get("end_line"):
|
|
99
|
+
return text or "(empty file)"
|
|
100
|
+
lines = text.splitlines()
|
|
101
|
+
start = max(1, int(arguments.get("start_line") or 1))
|
|
102
|
+
end = min(len(lines), int(arguments.get("end_line") or len(lines)))
|
|
103
|
+
return f"Lines {start} to {end} of {len(lines)}:\n" + "\n".join(lines[start - 1:end])
|
|
104
|
+
if name == "search_code":
|
|
105
|
+
matches = []
|
|
106
|
+
for file in project_files(root):
|
|
107
|
+
try:
|
|
108
|
+
lines = file.read_text(encoding="utf-8").splitlines()
|
|
109
|
+
except UnicodeDecodeError:
|
|
110
|
+
continue
|
|
111
|
+
for number, line in enumerate(lines, 1):
|
|
112
|
+
if arguments["query"] in line:
|
|
113
|
+
matches.append(f"{file.relative_to(root).as_posix()}:{number}: {line.strip()}")
|
|
114
|
+
return "\n".join(matches[:30]) or "No matches."
|
|
115
|
+
if name == "edit_file":
|
|
116
|
+
target = inside(root, arguments["path"])
|
|
117
|
+
text = target.read_text(encoding="utf-8")
|
|
118
|
+
if arguments["old_text"] not in text:
|
|
119
|
+
return "Error: old_text not found in the file. Read the file again and copy the text exactly."
|
|
120
|
+
updated = text.replace(arguments["old_text"], arguments["new_text"], 1)
|
|
121
|
+
if not approve(f"Edit {arguments['path']}:", diff_text(arguments["path"], text, updated)):
|
|
122
|
+
return "The user declined this edit."
|
|
123
|
+
target.write_text(updated, encoding="utf-8")
|
|
124
|
+
return f"Edited {arguments['path']}"
|
|
125
|
+
if name == "write_file":
|
|
126
|
+
target = inside(root, arguments["path"])
|
|
127
|
+
before = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
128
|
+
title = f"{'Overwrite' if target.exists() else 'Create'} {arguments['path']}:"
|
|
129
|
+
if not approve(title, diff_text(arguments["path"], before, arguments["content"])):
|
|
130
|
+
return "The user declined writing this file."
|
|
131
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
target.write_text(arguments["content"], encoding="utf-8")
|
|
133
|
+
return f"Wrote {arguments['path']}"
|
|
134
|
+
if name == "run_command":
|
|
135
|
+
if not approve("Run in the project folder:", f" $ {arguments['command']}"):
|
|
136
|
+
return "The user declined running this command."
|
|
137
|
+
completed = subprocess.run(arguments["command"], shell=True, cwd=root, capture_output=True, text=True,
|
|
138
|
+
timeout=COMMAND_TIMEOUT)
|
|
139
|
+
return ((completed.stdout + completed.stderr)[-3000:]).strip() or f"(no output, exit code {completed.returncode})"
|
|
140
|
+
return f"Error: unknown tool {name}"
|
|
141
|
+
except subprocess.TimeoutExpired:
|
|
142
|
+
return f"Error: the command took longer than {COMMAND_TIMEOUT} seconds and was stopped."
|
|
143
|
+
except Exception as error: # reported to the model, like an editor would
|
|
144
|
+
return f"Error: {type(error).__name__}: {error}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def parse_arguments(raw):
|
|
148
|
+
if isinstance(raw, dict):
|
|
149
|
+
return raw
|
|
150
|
+
try:
|
|
151
|
+
value = json.loads(raw or "{}")
|
|
152
|
+
return value if isinstance(value, dict) else {}
|
|
153
|
+
except json.JSONDecodeError:
|
|
154
|
+
return {}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def describe(name, arguments):
|
|
158
|
+
if name == "read_file":
|
|
159
|
+
return f"reads {arguments.get('path')}"
|
|
160
|
+
if name == "search_code":
|
|
161
|
+
return f"searches for {arguments.get('query')!r}"
|
|
162
|
+
return name.replace("_", " ")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run(project, task, approve, start_server, max_steps=MAX_STEPS):
|
|
166
|
+
root = Path(project).resolve()
|
|
167
|
+
if not root.is_dir():
|
|
168
|
+
raise ValueError(f"Not a folder: {project}")
|
|
169
|
+
messages = [
|
|
170
|
+
{"role": "system", "content": AGENT_PROMPT},
|
|
171
|
+
{"role": "user", "content": f"{task} Find and run the project's tests to check your fix."},
|
|
172
|
+
]
|
|
173
|
+
with start_server() as server:
|
|
174
|
+
for step in range(max_steps):
|
|
175
|
+
message = complete(server.url, messages, MAX_TOKENS, tools=TOOLS)
|
|
176
|
+
calls = message.get("tool_calls") or []
|
|
177
|
+
record = {"role": "assistant", "content": message.get("content") or ""}
|
|
178
|
+
if calls:
|
|
179
|
+
record["tool_calls"] = [
|
|
180
|
+
{"id": call.get("id") or f"call_{step}_{index}", "type": "function",
|
|
181
|
+
"function": {"name": call["function"]["name"], "arguments": json.dumps(parse_arguments(call["function"].get("arguments")))}}
|
|
182
|
+
for index, call in enumerate(calls)
|
|
183
|
+
]
|
|
184
|
+
messages.append(record)
|
|
185
|
+
if not calls:
|
|
186
|
+
return record["content"].strip() or "Done."
|
|
187
|
+
for call in record["tool_calls"]:
|
|
188
|
+
name, arguments = call["function"]["name"], parse_arguments(call["function"]["arguments"])
|
|
189
|
+
if name in ("read_file", "search_code"):
|
|
190
|
+
print(f" Nock Agent {describe(name, arguments)}", file=sys.stderr)
|
|
191
|
+
output = run_tool(root, name, arguments, approve)
|
|
192
|
+
messages.append({"role": "tool", "tool_call_id": call["id"], "name": name, "content": output})
|
|
193
|
+
return f"Stopped after {max_steps} steps without finishing. Check the changes it made."
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""One call to the local llama-server's OpenAI-style chat endpoint (127.0.0.1 only)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
|
|
6
|
+
REQUEST_TIMEOUT = 1800 # CPU-only machines can take many minutes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def complete(server_url, messages, max_tokens, temperature=0.0, tools=None, extra=None):
|
|
10
|
+
"""Returns the assistant message (a dict with `content` and maybe `tool_calls`)."""
|
|
11
|
+
body = {"messages": messages, "temperature": temperature, "max_tokens": max_tokens}
|
|
12
|
+
if tools:
|
|
13
|
+
body["tools"] = tools
|
|
14
|
+
body["tool_choice"] = "auto"
|
|
15
|
+
body.update(extra or {})
|
|
16
|
+
request = urllib.request.Request(f"{server_url}/v1/chat/completions", data=json.dumps(body).encode(),
|
|
17
|
+
headers={"Content-Type": "application/json"})
|
|
18
|
+
with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as response:
|
|
19
|
+
return json.load(response)["choices"][0]["message"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Nock Coder in the terminal: one question, piped input, or a chat until you quit.
|
|
2
|
+
|
|
3
|
+
Same system prompt and temperature as the Ollama model (ollama/Modelfile), so answers match `ollama run nockai/nock-coder`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from nock_cli.chat import complete
|
|
9
|
+
|
|
10
|
+
SYSTEM_PROMPT = ("You are Nock Coder, an open coding model from NockAI. Reply with code first. Keep any explanation to "
|
|
11
|
+
"one short sentence. For Solidity, use pragma solidity ^0.8.0 unless the user asks for another version.")
|
|
12
|
+
TEMPERATURE = 0.2
|
|
13
|
+
MAX_TOKENS = 1024
|
|
14
|
+
MAX_PIPED_CHARS = 20000 # about 5K tokens: leaves room in the 8K context for the question and the answer
|
|
15
|
+
EXIT_WORDS = ("exit", "quit", ":q")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def first_message(prompt, piped):
|
|
19
|
+
"""The question with piped text (a file or an error log). The text goes first and the question last: with the
|
|
20
|
+
question first, Nock Coder repeated the code instead of answering (checked 2026-10-26 on a ZeroDivisionError log)."""
|
|
21
|
+
if piped and len(piped) > MAX_PIPED_CHARS:
|
|
22
|
+
print(f"Input is long: only the last {MAX_PIPED_CHARS:,} characters are sent.", file=sys.stderr)
|
|
23
|
+
piped = piped[-MAX_PIPED_CHARS:]
|
|
24
|
+
if piped and prompt:
|
|
25
|
+
return f"```\n{piped.rstrip()}\n```\n\n{prompt}"
|
|
26
|
+
return prompt or piped
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_piped():
|
|
30
|
+
"""Text piped into nock, or None. A closed or detached stdin (scripts, nohup, cron) counts as nothing piped."""
|
|
31
|
+
try:
|
|
32
|
+
if sys.stdin is None or sys.stdin.isatty():
|
|
33
|
+
return None
|
|
34
|
+
return sys.stdin.read() or None
|
|
35
|
+
except (OSError, ValueError):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run(prompt, start_server):
|
|
40
|
+
piped = read_piped()
|
|
41
|
+
message = first_message(prompt, piped)
|
|
42
|
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
43
|
+
with start_server() as server:
|
|
44
|
+
if message:
|
|
45
|
+
messages.append({"role": "user", "content": message})
|
|
46
|
+
print(complete(server.url, messages, MAX_TOKENS, TEMPERATURE)["content"].strip())
|
|
47
|
+
return
|
|
48
|
+
# No question and nothing piped: a chat. The conversation is kept so follow-ups have context.
|
|
49
|
+
print("Nock Coder, offline. Type a question, or exit to quit.", file=sys.stderr)
|
|
50
|
+
while True:
|
|
51
|
+
try:
|
|
52
|
+
question = input("> ").strip()
|
|
53
|
+
except EOFError:
|
|
54
|
+
return
|
|
55
|
+
if question.lower() in EXIT_WORDS:
|
|
56
|
+
return
|
|
57
|
+
if not question:
|
|
58
|
+
continue
|
|
59
|
+
messages.append({"role": "user", "content": question})
|
|
60
|
+
reply = complete(server.url, messages, MAX_TOKENS, TEMPERATURE)["content"].strip()
|
|
61
|
+
messages.append({"role": "assistant", "content": reply})
|
|
62
|
+
print(reply + "\n")
|
|
@@ -40,15 +40,28 @@ def download(url, target, label):
|
|
|
40
40
|
handle.write(chunk)
|
|
41
41
|
done += len(chunk)
|
|
42
42
|
show_progress(label, done, total)
|
|
43
|
-
sys.stderr.
|
|
43
|
+
if sys.stderr.isatty():
|
|
44
|
+
sys.stderr.write("\n")
|
|
44
45
|
partial.replace(target)
|
|
45
46
|
return target
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
_last_logged_step = {}
|
|
50
|
+
|
|
51
|
+
|
|
48
52
|
def show_progress(label, done, total):
|
|
53
|
+
"""One updating line on a terminal; in a log file (no terminal) a line every 10%, so logs stay readable."""
|
|
49
54
|
megabytes = done / 1048576
|
|
50
55
|
if total:
|
|
51
|
-
|
|
56
|
+
line = f" {label}: {megabytes:,.0f} / {total / 1048576:,.0f} MB ({done * 100 // total}%)"
|
|
57
|
+
else:
|
|
58
|
+
line = f" {label}: {megabytes:,.0f} MB"
|
|
59
|
+
if sys.stderr.isatty():
|
|
60
|
+
sys.stderr.write("\r" + line)
|
|
52
61
|
else:
|
|
53
|
-
|
|
62
|
+
step = done * 10 // total if total else int(megabytes // 500)
|
|
63
|
+
if _last_logged_step.get(label) == step:
|
|
64
|
+
return
|
|
65
|
+
_last_logged_step[label] = step
|
|
66
|
+
sys.stderr.write(line + "\n")
|
|
54
67
|
sys.stderr.flush()
|