toolfuncs 0.1.4__tar.gz → 0.3.1__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.
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/.github/workflows/release.yml +2 -2
- toolfuncs-0.3.1/PKG-INFO +198 -0
- toolfuncs-0.3.1/README.md +180 -0
- toolfuncs-0.3.1/docs/contract.md +84 -0
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/pyproject.toml +11 -6
- toolfuncs-0.3.1/src/toolfuncs/__init__.py +79 -0
- toolfuncs-0.3.1/src/toolfuncs/__main__.py +88 -0
- toolfuncs-0.3.1/src/toolfuncs/_errors.py +40 -0
- toolfuncs-0.3.1/src/toolfuncs/_setup.py +159 -0
- toolfuncs-0.3.1/src/toolfuncs/_tools.py +377 -0
- toolfuncs-0.3.1/src/toolfuncs/agent_hooks.py +75 -0
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/src/toolfuncs/sdk.py +5 -13
- toolfuncs-0.3.1/tests/test_agent_hooks.py +133 -0
- toolfuncs-0.3.1/tests/test_sdk_cli.py +393 -0
- toolfuncs-0.3.1/tests/test_tools.py +294 -0
- toolfuncs-0.3.1/uv.lock +694 -0
- toolfuncs-0.1.4/PKG-INFO +0 -217
- toolfuncs-0.1.4/README.md +0 -201
- toolfuncs-0.1.4/docs/contract.md +0 -70
- toolfuncs-0.1.4/src/toolfuncs/__init__.py +0 -91
- toolfuncs-0.1.4/src/toolfuncs/__main__.py +0 -84
- toolfuncs-0.1.4/src/toolfuncs/_discovery.py +0 -152
- toolfuncs-0.1.4/src/toolfuncs/_errors.py +0 -50
- toolfuncs-0.1.4/src/toolfuncs/_importer.py +0 -609
- toolfuncs-0.1.4/src/toolfuncs/_metadata.py +0 -241
- toolfuncs-0.1.4/src/toolfuncs/_redaction.py +0 -18
- toolfuncs-0.1.4/src/toolfuncs/_shims.py +0 -139
- toolfuncs-0.1.4/tests/test_discovery.py +0 -192
- toolfuncs-0.1.4/tests/test_importer_direct.py +0 -160
- toolfuncs-0.1.4/tests/test_importer_projects.py +0 -233
- toolfuncs-0.1.4/tests/test_sdk_cli.py +0 -294
- toolfuncs-0.1.4/uv.lock +0 -1382
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/.gitignore +0 -0
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/.python-version +0 -0
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/src/toolfuncs/py.typed +0 -0
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/tests/__init__.py +0 -0
- {toolfuncs-0.1.4 → toolfuncs-0.3.1}/tests/conftest.py +0 -0
|
@@ -7,7 +7,7 @@ on:
|
|
|
7
7
|
jobs:
|
|
8
8
|
build:
|
|
9
9
|
name: Build distributions
|
|
10
|
-
runs-on:
|
|
10
|
+
runs-on: [self-hosted, Linux, X64]
|
|
11
11
|
permissions:
|
|
12
12
|
contents: read
|
|
13
13
|
|
|
@@ -55,7 +55,7 @@ jobs:
|
|
|
55
55
|
publish:
|
|
56
56
|
name: Publish to PyPI
|
|
57
57
|
needs: build
|
|
58
|
-
runs-on:
|
|
58
|
+
runs-on: [self-hosted, Linux, X64]
|
|
59
59
|
environment:
|
|
60
60
|
name: pypi
|
|
61
61
|
url: https://pypi.org/project/toolfuncs/
|
toolfuncs-0.3.1/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: toolfuncs
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Executable typed Python function tools discovered through PATH
|
|
5
|
+
Project-URL: Repository, https://github.com/nimashoghi/toolfuncs
|
|
6
|
+
Project-URL: Issues, https://github.com/nimashoghi/toolfuncs/issues
|
|
7
|
+
Author-email: Nima Shoghi <nima@boltz.bio>
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: cyclopts<5,>=4.23.2
|
|
10
|
+
Requires-Dist: packaging>=24
|
|
11
|
+
Requires-Dist: pip>=25
|
|
12
|
+
Requires-Dist: pydantic-core<3,>=2
|
|
13
|
+
Requires-Dist: pydantic<3,>=2
|
|
14
|
+
Requires-Dist: tomli<3,>=2.2; python_version < '3.11'
|
|
15
|
+
Provides-Extra: hooks
|
|
16
|
+
Requires-Dist: typed-agent-hooks; extra == 'hooks'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# toolfuncs
|
|
20
|
+
|
|
21
|
+
`toolfuncs` turns typed Python functions into tools that have matching Python and command-line interfaces.
|
|
22
|
+
|
|
23
|
+
A toolfunc is one executable Python script on `PATH`. Its filename is its command name and dynamic Python import name. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves discoverable toolfuncs.
|
|
24
|
+
|
|
25
|
+
## Write a toolfunc
|
|
26
|
+
|
|
27
|
+
Create an extensionless file named `weather`:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
#!/usr/bin/env toolfuncs
|
|
31
|
+
# /// script
|
|
32
|
+
# requires-python = ">=3.10"
|
|
33
|
+
# dependencies = ["weather-client"]
|
|
34
|
+
# [tool.toolfuncs]
|
|
35
|
+
# description = "Read current weather conditions."
|
|
36
|
+
# ///
|
|
37
|
+
|
|
38
|
+
import toolfuncs
|
|
39
|
+
import weather_client
|
|
40
|
+
|
|
41
|
+
app = toolfuncs.App()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command
|
|
45
|
+
def current(city: str) -> dict[str, object]:
|
|
46
|
+
return weather_client.current(city)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Make it executable and place it in any existing `PATH` directory:
|
|
50
|
+
|
|
51
|
+
```console
|
|
52
|
+
chmod +x weather
|
|
53
|
+
mv weather ~/.local/bin/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That is the complete registration operation.
|
|
57
|
+
|
|
58
|
+
## Call it
|
|
59
|
+
|
|
60
|
+
The executable is the CLI:
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
$ weather current London
|
|
64
|
+
{"city": "London", "temperature": 18}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The same file is dynamically importable in a Python process that has `toolfuncs` installed and the same `PATH`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from toolfuncs import weather
|
|
71
|
+
|
|
72
|
+
conditions = weather.current("London")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The explicit equivalent is useful when a tool name conflicts with a public package attribute:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import toolfuncs
|
|
79
|
+
|
|
80
|
+
weather = toolfuncs.load("weather")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Direct Python calls return ordinary Python objects and raise the original exceptions. `toolfuncs.load()` prepares the script's requirements in the running Python environment before import. CLI calls use Cyclopts to parse annotated values and serialize successful results as strict JSON through `pydantic-core`.
|
|
84
|
+
|
|
85
|
+
## Source contract
|
|
86
|
+
|
|
87
|
+
A discoverable toolfunc must:
|
|
88
|
+
|
|
89
|
+
1. be one executable file in a directory on `PATH`;
|
|
90
|
+
2. have an extensionless filename that is a valid non-keyword Python identifier;
|
|
91
|
+
3. start with one of the two exact toolfuncs shebangs described below;
|
|
92
|
+
4. contain exactly one PEP 723 `script` block with `dependencies` and a one-line `[tool.toolfuncs].description`;
|
|
93
|
+
5. define a module-level `app = toolfuncs.App()`;
|
|
94
|
+
6. register each CLI-callable function explicitly with `@app.command`.
|
|
95
|
+
|
|
96
|
+
The PEP 723 dependency list contains the packages needed in addition to the running toolfuncs environment. Listing `toolfuncs` itself is accepted but normally redundant because the shebang has already started the toolfuncs runtime before dependencies are prepared.
|
|
97
|
+
|
|
98
|
+
Git requirements must use a full 40- or 64-hex commit object ID. Branches such as `@main`, tags, abbreviated hashes, and omitted revisions fail before uv resolves them. A tool that deliberately accepts floating-ref refresh and concurrency costs must say so in its source:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
#!/usr/bin/env -S toolfuncs --allow-floating-vcs
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The launcher consumes `--allow-floating-vcs`; the tool's Cyclopts app never sees it. Adjacent `uv lock --script` files are not used because uv does not consult them for `--with-requirements` launches.
|
|
105
|
+
|
|
106
|
+
Only functions registered on `app` become CLI commands. Imported functions and `__all__` do not define the command surface. Function command and option spelling follows Cyclopts' Python-to-kebab-case projection.
|
|
107
|
+
|
|
108
|
+
No `if __name__ == "__main__":` block is required. The operating system passes the source path to the shebang interpreter, and toolfuncs imports the source under its filename before invoking its module-level app. Consequently, `weather ...` is the supported CLI while `python weather ...` merely defines the module and exits, or fails if its dependencies are not already installed.
|
|
109
|
+
|
|
110
|
+
## Package-backed implementations
|
|
111
|
+
|
|
112
|
+
A toolfunc remains one script even when most of its implementation lives in a package:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
#!/usr/bin/env toolfuncs
|
|
116
|
+
# /// script
|
|
117
|
+
# requires-python = ">=3.10"
|
|
118
|
+
# dependencies = ["my-large-package"]
|
|
119
|
+
# [tool.toolfuncs]
|
|
120
|
+
# description = "Run the package's agent operation."
|
|
121
|
+
# ///
|
|
122
|
+
|
|
123
|
+
from my_large_package.agent_tool import app, perform_operation
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The imported `app` and functions are the original Python objects. Toolfuncs does not inspect the package's project layout, metadata, or import structure.
|
|
127
|
+
|
|
128
|
+
## Discovery and precedence
|
|
129
|
+
|
|
130
|
+
Toolfuncs reads the current process's `PATH` from left to right. It considers only executable files and symlinks, reads their first line, and parses PEP 723 metadata only when either exact toolfuncs shebang matches.
|
|
131
|
+
|
|
132
|
+
The first executable filename claims a command name even when it is not a toolfunc. Therefore an ordinary executable earlier on `PATH` hides a same-named toolfunc later on `PATH`, matching what the shell actually executes. Repeated directories are scanned once, inaccessible directories are skipped, and discovered records are sorted by name.
|
|
133
|
+
|
|
134
|
+
`toolfuncs list` returns static JSON records containing `name`, `path`, and `description`. It never imports a tool, prepares its dependencies, or executes its source.
|
|
135
|
+
|
|
136
|
+
## Install the runtime
|
|
137
|
+
|
|
138
|
+
```console
|
|
139
|
+
uvx toolfuncs setup
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Setup writes two managed, auto-updating uvx wrappers by default:
|
|
143
|
+
|
|
144
|
+
- `~/.local/bin/toolfuncs` dispatches tools;
|
|
145
|
+
- `~/.agents/hooks/toolfuncs/hook` advertises the current tool catalog to agent harnesses.
|
|
146
|
+
|
|
147
|
+
For a tool invocation, its effective launch is:
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
#!/bin/sh
|
|
151
|
+
# managed by toolfuncs setup
|
|
152
|
+
exec uvx --quiet --from toolfuncs --with-requirements "$source" \
|
|
153
|
+
toolfuncs __run-source "$source" "$@"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The wrapper preflights Git requirements before that command unless the source explicitly allows floating VCS revisions. Normal uv cache freshness supplies updates without forcing `--refresh-package` on every call. The destination must already be on `PATH`; setup fails with a concrete instruction otherwise. Setup is idempotent, refuses to overwrite unmanaged commands, and does not modify shell profiles.
|
|
157
|
+
|
|
158
|
+
The hook wrapper resolves the optional `toolfuncs[hooks]` environment and is registered at user scope for Codex and Claude Code through `typed-agent-hooks`. Running setup again reconciles those registrations, so the managed launcher remains the configured executable even when provider configuration changes.
|
|
159
|
+
|
|
160
|
+
Setup does not discover, copy, register, link, synchronize, or remove tools. There is no dedicated tools directory, registry, manifest, generated shim, project scope, user scope, or sync command.
|
|
161
|
+
|
|
162
|
+
## Management commands
|
|
163
|
+
|
|
164
|
+
```console
|
|
165
|
+
toolfuncs list
|
|
166
|
+
toolfuncs setup [--bin-dir DIRECTORY]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The source-path argument supplied automatically by `#!/usr/bin/env toolfuncs` is runtime plumbing rather than another user-facing command. Tools are invoked directly by their own names.
|
|
170
|
+
|
|
171
|
+
## Agent integration
|
|
172
|
+
|
|
173
|
+
The installed hook runs at initial session start and at the session-start event emitted after compaction. It discovers the effective tools directly from that harness process's `PATH`, then adds a compact catalog such as:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
Available toolfuncs:
|
|
177
|
+
- `weather`: Read current weather conditions.
|
|
178
|
+
CLI: `weather --help`
|
|
179
|
+
Python: `from toolfuncs import weather`
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The hook reads only executable headers and PEP 723 metadata. It does not import tools or resolve their dependencies. The hook itself is harness infrastructure, not a toolfunc, and therefore does not carry the toolfuncs shebang or appear in the catalog.
|
|
183
|
+
|
|
184
|
+
## Development
|
|
185
|
+
|
|
186
|
+
```console
|
|
187
|
+
uv sync
|
|
188
|
+
uv run pytest
|
|
189
|
+
uv run ruff format --check .
|
|
190
|
+
uv run ruff check .
|
|
191
|
+
uv run basedpyright
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The exact 0.2 guarantees and non-goals are recorded in [the contract](docs/contract.md).
|
|
195
|
+
|
|
196
|
+
## Releasing
|
|
197
|
+
|
|
198
|
+
The GitHub Release is the release control point. Set `[project].version`, merge and push that commit, then publish a GitHub Release whose tag is `v<version>`. The self-hosted release workflow verifies that the tag and package version match, builds the wheel and source distribution, and publishes them to PyPI through Trusted Publishing.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# toolfuncs
|
|
2
|
+
|
|
3
|
+
`toolfuncs` turns typed Python functions into tools that have matching Python and command-line interfaces.
|
|
4
|
+
|
|
5
|
+
A toolfunc is one executable Python script on `PATH`. Its filename is its command name and dynamic Python import name. Its implementation may import any packages declared in its PEP 723 requirements, but packages and projects are not themselves discoverable toolfuncs.
|
|
6
|
+
|
|
7
|
+
## Write a toolfunc
|
|
8
|
+
|
|
9
|
+
Create an extensionless file named `weather`:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
#!/usr/bin/env toolfuncs
|
|
13
|
+
# /// script
|
|
14
|
+
# requires-python = ">=3.10"
|
|
15
|
+
# dependencies = ["weather-client"]
|
|
16
|
+
# [tool.toolfuncs]
|
|
17
|
+
# description = "Read current weather conditions."
|
|
18
|
+
# ///
|
|
19
|
+
|
|
20
|
+
import toolfuncs
|
|
21
|
+
import weather_client
|
|
22
|
+
|
|
23
|
+
app = toolfuncs.App()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command
|
|
27
|
+
def current(city: str) -> dict[str, object]:
|
|
28
|
+
return weather_client.current(city)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Make it executable and place it in any existing `PATH` directory:
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
chmod +x weather
|
|
35
|
+
mv weather ~/.local/bin/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That is the complete registration operation.
|
|
39
|
+
|
|
40
|
+
## Call it
|
|
41
|
+
|
|
42
|
+
The executable is the CLI:
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ weather current London
|
|
46
|
+
{"city": "London", "temperature": 18}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The same file is dynamically importable in a Python process that has `toolfuncs` installed and the same `PATH`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from toolfuncs import weather
|
|
53
|
+
|
|
54
|
+
conditions = weather.current("London")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The explicit equivalent is useful when a tool name conflicts with a public package attribute:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import toolfuncs
|
|
61
|
+
|
|
62
|
+
weather = toolfuncs.load("weather")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Direct Python calls return ordinary Python objects and raise the original exceptions. `toolfuncs.load()` prepares the script's requirements in the running Python environment before import. CLI calls use Cyclopts to parse annotated values and serialize successful results as strict JSON through `pydantic-core`.
|
|
66
|
+
|
|
67
|
+
## Source contract
|
|
68
|
+
|
|
69
|
+
A discoverable toolfunc must:
|
|
70
|
+
|
|
71
|
+
1. be one executable file in a directory on `PATH`;
|
|
72
|
+
2. have an extensionless filename that is a valid non-keyword Python identifier;
|
|
73
|
+
3. start with one of the two exact toolfuncs shebangs described below;
|
|
74
|
+
4. contain exactly one PEP 723 `script` block with `dependencies` and a one-line `[tool.toolfuncs].description`;
|
|
75
|
+
5. define a module-level `app = toolfuncs.App()`;
|
|
76
|
+
6. register each CLI-callable function explicitly with `@app.command`.
|
|
77
|
+
|
|
78
|
+
The PEP 723 dependency list contains the packages needed in addition to the running toolfuncs environment. Listing `toolfuncs` itself is accepted but normally redundant because the shebang has already started the toolfuncs runtime before dependencies are prepared.
|
|
79
|
+
|
|
80
|
+
Git requirements must use a full 40- or 64-hex commit object ID. Branches such as `@main`, tags, abbreviated hashes, and omitted revisions fail before uv resolves them. A tool that deliberately accepts floating-ref refresh and concurrency costs must say so in its source:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
#!/usr/bin/env -S toolfuncs --allow-floating-vcs
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The launcher consumes `--allow-floating-vcs`; the tool's Cyclopts app never sees it. Adjacent `uv lock --script` files are not used because uv does not consult them for `--with-requirements` launches.
|
|
87
|
+
|
|
88
|
+
Only functions registered on `app` become CLI commands. Imported functions and `__all__` do not define the command surface. Function command and option spelling follows Cyclopts' Python-to-kebab-case projection.
|
|
89
|
+
|
|
90
|
+
No `if __name__ == "__main__":` block is required. The operating system passes the source path to the shebang interpreter, and toolfuncs imports the source under its filename before invoking its module-level app. Consequently, `weather ...` is the supported CLI while `python weather ...` merely defines the module and exits, or fails if its dependencies are not already installed.
|
|
91
|
+
|
|
92
|
+
## Package-backed implementations
|
|
93
|
+
|
|
94
|
+
A toolfunc remains one script even when most of its implementation lives in a package:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
#!/usr/bin/env toolfuncs
|
|
98
|
+
# /// script
|
|
99
|
+
# requires-python = ">=3.10"
|
|
100
|
+
# dependencies = ["my-large-package"]
|
|
101
|
+
# [tool.toolfuncs]
|
|
102
|
+
# description = "Run the package's agent operation."
|
|
103
|
+
# ///
|
|
104
|
+
|
|
105
|
+
from my_large_package.agent_tool import app, perform_operation
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The imported `app` and functions are the original Python objects. Toolfuncs does not inspect the package's project layout, metadata, or import structure.
|
|
109
|
+
|
|
110
|
+
## Discovery and precedence
|
|
111
|
+
|
|
112
|
+
Toolfuncs reads the current process's `PATH` from left to right. It considers only executable files and symlinks, reads their first line, and parses PEP 723 metadata only when either exact toolfuncs shebang matches.
|
|
113
|
+
|
|
114
|
+
The first executable filename claims a command name even when it is not a toolfunc. Therefore an ordinary executable earlier on `PATH` hides a same-named toolfunc later on `PATH`, matching what the shell actually executes. Repeated directories are scanned once, inaccessible directories are skipped, and discovered records are sorted by name.
|
|
115
|
+
|
|
116
|
+
`toolfuncs list` returns static JSON records containing `name`, `path`, and `description`. It never imports a tool, prepares its dependencies, or executes its source.
|
|
117
|
+
|
|
118
|
+
## Install the runtime
|
|
119
|
+
|
|
120
|
+
```console
|
|
121
|
+
uvx toolfuncs setup
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Setup writes two managed, auto-updating uvx wrappers by default:
|
|
125
|
+
|
|
126
|
+
- `~/.local/bin/toolfuncs` dispatches tools;
|
|
127
|
+
- `~/.agents/hooks/toolfuncs/hook` advertises the current tool catalog to agent harnesses.
|
|
128
|
+
|
|
129
|
+
For a tool invocation, its effective launch is:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
#!/bin/sh
|
|
133
|
+
# managed by toolfuncs setup
|
|
134
|
+
exec uvx --quiet --from toolfuncs --with-requirements "$source" \
|
|
135
|
+
toolfuncs __run-source "$source" "$@"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The wrapper preflights Git requirements before that command unless the source explicitly allows floating VCS revisions. Normal uv cache freshness supplies updates without forcing `--refresh-package` on every call. The destination must already be on `PATH`; setup fails with a concrete instruction otherwise. Setup is idempotent, refuses to overwrite unmanaged commands, and does not modify shell profiles.
|
|
139
|
+
|
|
140
|
+
The hook wrapper resolves the optional `toolfuncs[hooks]` environment and is registered at user scope for Codex and Claude Code through `typed-agent-hooks`. Running setup again reconciles those registrations, so the managed launcher remains the configured executable even when provider configuration changes.
|
|
141
|
+
|
|
142
|
+
Setup does not discover, copy, register, link, synchronize, or remove tools. There is no dedicated tools directory, registry, manifest, generated shim, project scope, user scope, or sync command.
|
|
143
|
+
|
|
144
|
+
## Management commands
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
toolfuncs list
|
|
148
|
+
toolfuncs setup [--bin-dir DIRECTORY]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The source-path argument supplied automatically by `#!/usr/bin/env toolfuncs` is runtime plumbing rather than another user-facing command. Tools are invoked directly by their own names.
|
|
152
|
+
|
|
153
|
+
## Agent integration
|
|
154
|
+
|
|
155
|
+
The installed hook runs at initial session start and at the session-start event emitted after compaction. It discovers the effective tools directly from that harness process's `PATH`, then adds a compact catalog such as:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
Available toolfuncs:
|
|
159
|
+
- `weather`: Read current weather conditions.
|
|
160
|
+
CLI: `weather --help`
|
|
161
|
+
Python: `from toolfuncs import weather`
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The hook reads only executable headers and PEP 723 metadata. It does not import tools or resolve their dependencies. The hook itself is harness infrastructure, not a toolfunc, and therefore does not carry the toolfuncs shebang or appear in the catalog.
|
|
165
|
+
|
|
166
|
+
## Development
|
|
167
|
+
|
|
168
|
+
```console
|
|
169
|
+
uv sync
|
|
170
|
+
uv run pytest
|
|
171
|
+
uv run ruff format --check .
|
|
172
|
+
uv run ruff check .
|
|
173
|
+
uv run basedpyright
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The exact 0.2 guarantees and non-goals are recorded in [the contract](docs/contract.md).
|
|
177
|
+
|
|
178
|
+
## Releasing
|
|
179
|
+
|
|
180
|
+
The GitHub Release is the release control point. Set `[project].version`, merge and push that commit, then publish a GitHub Release whose tag is `v<version>`. The self-hosted release workflow verifies that the tag and package version match, builds the wheel and source distribution, and publishes them to PyPI through Trusted Publishing.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# toolfuncs 0.2 contract
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Toolfuncs projects one explicitly registered set of typed Python functions into two interfaces: ordinary in-process Python calls and a strict-JSON command line. It is a source convention and runtime, not a transport protocol, package format, registry, plugin system, or filesystem scope.
|
|
6
|
+
|
|
7
|
+
## Tool source
|
|
8
|
+
|
|
9
|
+
A toolfunc is one executable regular file or symlink in a directory on `PATH`. Its first line is one of two exact forms. The ordinary form is:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
#!/usr/bin/env toolfuncs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The explicit floating-VCS form is:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
#!/usr/bin/env -S toolfuncs --allow-floating-vcs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Its extensionless filename is its shell command, discovery name, and dynamic Python import name and must be a valid non-keyword Python identifier.
|
|
22
|
+
|
|
23
|
+
The source contains exactly one PEP 723 `script` block. `dependencies` is a list of valid Python requirement strings, `requires-python` is optional, and `[tool.toolfuncs].description` is a required nonempty one-line string. `description` is the only toolfuncs-specific metadata field.
|
|
24
|
+
|
|
25
|
+
The source defines a module-level `app` as a Cyclopts app, normally `toolfuncs.App`, and registers every CLI-callable operation explicitly. Function signatures, annotations, defaults, docstrings, return values, side effects, and exceptions define the operations. `__all__` retains its ordinary Python meaning and does not register commands.
|
|
26
|
+
|
|
27
|
+
`toolfuncs.App` supplies strict JSON serialization as its default result action. An explicit `result_action=` replaces that default unchanged. Tool authors import only `toolfuncs`; the package re-exports the Cyclopts and `pydantic-core` authoring values needed for advanced interfaces.
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
For CLI execution, the managed wrapper passes the source to `uvx --with-requirements` and starts toolfuncs inside the resulting cached environment. This is one uv-prepared environment containing the toolfuncs runtime and the script's requirements. The runtime then imports the source without installing the requirements again.
|
|
32
|
+
|
|
33
|
+
For direct Python composition, `toolfuncs.load(NAME)` installs script requirements into the running interpreter with pip before importing the source. The current interpreter must satisfy `requires-python`. Dependency preparation and module loading are serialized within a process.
|
|
34
|
+
|
|
35
|
+
The `toolfuncs` distribution may appear in `dependencies`, but it is normally redundant: the shebang has already started a runtime containing toolfuncs before the dependency list is processed. Toolfuncs does not reject or specially interpret that requirement.
|
|
36
|
+
|
|
37
|
+
A script may import any installed dependency, including a package containing most or all of its implementation. The package is not a toolfunc and has no toolfuncs-specific layout or metadata contract.
|
|
38
|
+
|
|
39
|
+
Git requirements are immutable by default. A direct Git URL must name a full 40- or 64-hex commit object ID. A branch, tag, abbreviated hash, or omitted revision raises `InvalidToolError` before dependency resolution. A tool may explicitly accept floating-ref refresh and concurrency costs through the `--allow-floating-vcs` shebang. The launcher consumes that option; it is not passed to the tool's Cyclopts app.
|
|
40
|
+
|
|
41
|
+
An adjacent lock produced by `uv lock --script` is not part of this contract because uv does not consume a script lock when the source is supplied through `--with-requirements`.
|
|
42
|
+
|
|
43
|
+
## Discovery
|
|
44
|
+
|
|
45
|
+
Discovery reads the current value of `PATH`. Directories are visited from left to right, relative entries are resolved against the current working directory, empty entries denote the current working directory, repeated resolved directories are visited once, and missing or unreadable directories are skipped.
|
|
46
|
+
|
|
47
|
+
The first executable filename across `PATH` claims that command name before its shebang is inspected. An earlier non-tool executable therefore hides a later same-named toolfunc. Discovery reads only the first line of ordinary candidates and parses PEP 723 metadata only after one of the two exact shebangs matches.
|
|
48
|
+
|
|
49
|
+
An exact-shebang source with an invalid name or incomplete metadata is an error because it explicitly opted into the contract. `toolfuncs.discover()` returns effective `Tool(name, path, description)` records sorted by name. `toolfuncs list` serializes the same records as JSON without importing source, preparing dependencies, or executing code.
|
|
50
|
+
|
|
51
|
+
## Loading and identity
|
|
52
|
+
|
|
53
|
+
`toolfuncs.load(NAME)` resolves `NAME` through `PATH`, prepares its dependencies, and imports the extensionless source under `NAME`. `from toolfuncs import NAME` is the dynamic shorthand. `import toolfuncs.NAME` is not promised because discovered tools are dynamic package attributes rather than installed submodules.
|
|
54
|
+
|
|
55
|
+
A name loaded once in an interpreter retains one canonical source and module object. Repeated loads return that object. A different source or pre-existing Python module using the same name is a clear conflict rather than an implicit replacement.
|
|
56
|
+
|
|
57
|
+
## CLI execution
|
|
58
|
+
|
|
59
|
+
When the operating system executes a tool, `/usr/bin/env` resolves the managed `toolfuncs` wrapper on `PATH` and passes it the source path followed by the user's arguments. The wrapper validates any unapproved Git requirement before resolution, then executes `uvx --from toolfuncs --with-requirements SOURCE toolfuncs ...`. Toolfuncs validates and imports the prepared source, obtains its module-level app, and invokes the app with the remaining arguments.
|
|
60
|
+
|
|
61
|
+
The source is imported under its filename rather than executed as `__main__`. A main guard is neither required nor used by the supported execution path. Plain `python SOURCE ARGS...` does not invoke the CLI and does not prepare PEP 723 dependencies.
|
|
62
|
+
|
|
63
|
+
Successful default CLI operations emit one strict JSON value. CLI parsing or execution failures exit nonzero. Direct Python calls return original Python objects and raise original exceptions.
|
|
64
|
+
|
|
65
|
+
## Setup
|
|
66
|
+
|
|
67
|
+
`uvx toolfuncs setup` installs one managed uvx wrapper in `~/.local/bin` or an explicit `--bin-dir`. The destination must already resolve from `PATH`. Setup is idempotent, refuses to overwrite an unmanaged command, and never modifies a shell profile. Normal uv cache freshness governs toolfuncs updates; the wrapper does not force `--refresh-package` on every invocation.
|
|
68
|
+
|
|
69
|
+
Setup does not discover, install, copy, link, register, synchronize, or remove tool sources. Adding and removing tools are ordinary filesystem operations in directories already on `PATH`.
|
|
70
|
+
|
|
71
|
+
## Public management interface
|
|
72
|
+
|
|
73
|
+
The management commands are `toolfuncs list` and `toolfuncs setup [--bin-dir DIRECTORY]`. The source-path argument supplied by the shebang is internal runtime behavior. There is no universal name dispatcher, `run`, `exec`, `describe`, or `sync` command.
|
|
74
|
+
|
|
75
|
+
## Non-goals
|
|
76
|
+
|
|
77
|
+
- No discoverable package, project, directory, wheel, archive, remote URL, or configurable import name.
|
|
78
|
+
- No general `import_path` utility.
|
|
79
|
+
- No dedicated tool directory, project scope, user scope, registry, manifest, generated shim, or synchronization operation.
|
|
80
|
+
- No `TOOL.md`, metadata dunders, implicit public-function exposure, or `__main__` execution contract.
|
|
81
|
+
- No subprocess, RPC, or object-proxy isolation for Python composition.
|
|
82
|
+
- No dependency solver spanning several toolfuncs before import.
|
|
83
|
+
- No shell-profile edits, daemon, or implicit harness configuration.
|
|
84
|
+
- Agent event handlers may advertise `toolfuncs list`, but those handlers are integrations rather than toolfuncs.
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "toolfuncs"
|
|
3
|
-
version = "0.1
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "Executable typed Python function tools discovered through PATH"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "Nima Shoghi", email = "nima@boltz.bio" }
|
|
8
8
|
]
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"cyclopts>=4.23.2,<5",
|
|
12
|
-
"fsspec[github,http]>=2025.3.0",
|
|
13
12
|
"packaging>=24",
|
|
14
13
|
"pip>=25",
|
|
15
14
|
"pydantic>=2,<3",
|
|
16
15
|
"pydantic-core>=2,<3",
|
|
16
|
+
"tomli>=2.2,<3; python_version < '3.11'",
|
|
17
17
|
]
|
|
18
18
|
|
|
19
19
|
[project.scripts]
|
|
20
20
|
toolfuncs = "toolfuncs.__main__:main"
|
|
21
|
+
toolfuncs-agent-hook = "toolfuncs.agent_hooks:main"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
hooks = ["typed-agent-hooks"]
|
|
21
25
|
|
|
22
26
|
[project.urls]
|
|
23
27
|
Repository = "https://github.com/nimashoghi/toolfuncs"
|
|
@@ -34,11 +38,12 @@ dev = [
|
|
|
34
38
|
"pytest>=8",
|
|
35
39
|
"pytest-cov>=6",
|
|
36
40
|
"ruff>=0.12",
|
|
41
|
+
"typed-agent-hooks",
|
|
37
42
|
]
|
|
38
43
|
|
|
39
44
|
[tool.ruff]
|
|
40
45
|
line-length = 100
|
|
41
|
-
target-version = "
|
|
46
|
+
target-version = "py310"
|
|
42
47
|
|
|
43
48
|
[tool.ruff.lint]
|
|
44
49
|
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
@@ -48,5 +53,5 @@ addopts = "-ra"
|
|
|
48
53
|
testpaths = ["tests"]
|
|
49
54
|
|
|
50
55
|
[tool.basedpyright]
|
|
51
|
-
pythonVersion = "3.
|
|
56
|
+
pythonVersion = "3.10"
|
|
52
57
|
typeCheckingMode = "standard"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Typed Python functions exposed through matching import and CLI interfaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
from ._errors import (
|
|
8
|
+
DependencyError,
|
|
9
|
+
InvalidToolError,
|
|
10
|
+
SetupError,
|
|
11
|
+
ToolConflictError,
|
|
12
|
+
ToolDefinitionError,
|
|
13
|
+
ToolfuncsError,
|
|
14
|
+
ToolNotFoundError,
|
|
15
|
+
)
|
|
16
|
+
from ._setup import SetupResult, setup
|
|
17
|
+
from ._tools import ALLOW_FLOATING_VCS_SHEBANG, TOOL_SHEBANG, Tool, discover, load
|
|
18
|
+
from .sdk import (
|
|
19
|
+
UNSET,
|
|
20
|
+
App,
|
|
21
|
+
Group,
|
|
22
|
+
Parameter,
|
|
23
|
+
ResultAction,
|
|
24
|
+
Token,
|
|
25
|
+
json_result_action,
|
|
26
|
+
to_jsonable_python,
|
|
27
|
+
types,
|
|
28
|
+
validators,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.3.1"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ALLOW_FLOATING_VCS_SHEBANG",
|
|
35
|
+
"TOOL_SHEBANG",
|
|
36
|
+
"UNSET",
|
|
37
|
+
"App",
|
|
38
|
+
"DependencyError",
|
|
39
|
+
"Group",
|
|
40
|
+
"InvalidToolError",
|
|
41
|
+
"Parameter",
|
|
42
|
+
"ResultAction",
|
|
43
|
+
"SetupError",
|
|
44
|
+
"SetupResult",
|
|
45
|
+
"Token",
|
|
46
|
+
"Tool",
|
|
47
|
+
"ToolConflictError",
|
|
48
|
+
"ToolDefinitionError",
|
|
49
|
+
"ToolNotFoundError",
|
|
50
|
+
"ToolfuncsError",
|
|
51
|
+
"discover",
|
|
52
|
+
"json_result_action",
|
|
53
|
+
"load",
|
|
54
|
+
"setup",
|
|
55
|
+
"to_jsonable_python",
|
|
56
|
+
"types",
|
|
57
|
+
"validators",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
_PUBLIC_API = frozenset(__all__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def __getattr__(name: str) -> ModuleType:
|
|
64
|
+
"""Dynamically expose PATH tools for ``from toolfuncs import name``."""
|
|
65
|
+
|
|
66
|
+
if name.startswith("_"):
|
|
67
|
+
raise AttributeError(name)
|
|
68
|
+
try:
|
|
69
|
+
module = load(name)
|
|
70
|
+
except ToolNotFoundError:
|
|
71
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
72
|
+
globals()[name] = module
|
|
73
|
+
return module
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def __dir__() -> list[str]:
|
|
77
|
+
"""Include effective PATH tools in interactive completion."""
|
|
78
|
+
|
|
79
|
+
return sorted(_PUBLIC_API | {tool.name for tool in discover()})
|