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.
Files changed (37) hide show
  1. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/.github/workflows/release.yml +2 -2
  2. toolfuncs-0.3.1/PKG-INFO +198 -0
  3. toolfuncs-0.3.1/README.md +180 -0
  4. toolfuncs-0.3.1/docs/contract.md +84 -0
  5. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/pyproject.toml +11 -6
  6. toolfuncs-0.3.1/src/toolfuncs/__init__.py +79 -0
  7. toolfuncs-0.3.1/src/toolfuncs/__main__.py +88 -0
  8. toolfuncs-0.3.1/src/toolfuncs/_errors.py +40 -0
  9. toolfuncs-0.3.1/src/toolfuncs/_setup.py +159 -0
  10. toolfuncs-0.3.1/src/toolfuncs/_tools.py +377 -0
  11. toolfuncs-0.3.1/src/toolfuncs/agent_hooks.py +75 -0
  12. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/src/toolfuncs/sdk.py +5 -13
  13. toolfuncs-0.3.1/tests/test_agent_hooks.py +133 -0
  14. toolfuncs-0.3.1/tests/test_sdk_cli.py +393 -0
  15. toolfuncs-0.3.1/tests/test_tools.py +294 -0
  16. toolfuncs-0.3.1/uv.lock +694 -0
  17. toolfuncs-0.1.4/PKG-INFO +0 -217
  18. toolfuncs-0.1.4/README.md +0 -201
  19. toolfuncs-0.1.4/docs/contract.md +0 -70
  20. toolfuncs-0.1.4/src/toolfuncs/__init__.py +0 -91
  21. toolfuncs-0.1.4/src/toolfuncs/__main__.py +0 -84
  22. toolfuncs-0.1.4/src/toolfuncs/_discovery.py +0 -152
  23. toolfuncs-0.1.4/src/toolfuncs/_errors.py +0 -50
  24. toolfuncs-0.1.4/src/toolfuncs/_importer.py +0 -609
  25. toolfuncs-0.1.4/src/toolfuncs/_metadata.py +0 -241
  26. toolfuncs-0.1.4/src/toolfuncs/_redaction.py +0 -18
  27. toolfuncs-0.1.4/src/toolfuncs/_shims.py +0 -139
  28. toolfuncs-0.1.4/tests/test_discovery.py +0 -192
  29. toolfuncs-0.1.4/tests/test_importer_direct.py +0 -160
  30. toolfuncs-0.1.4/tests/test_importer_projects.py +0 -233
  31. toolfuncs-0.1.4/tests/test_sdk_cli.py +0 -294
  32. toolfuncs-0.1.4/uv.lock +0 -1382
  33. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/.gitignore +0 -0
  34. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/.python-version +0 -0
  35. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/src/toolfuncs/py.typed +0 -0
  36. {toolfuncs-0.1.4 → toolfuncs-0.3.1}/tests/__init__.py +0 -0
  37. {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: ubuntu-latest
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: ubuntu-latest
58
+ runs-on: [self-hosted, Linux, X64]
59
59
  environment:
60
60
  name: pypi
61
61
  url: https://pypi.org/project/toolfuncs/
@@ -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"
4
- description = "Function-first Python tools with matching import and command-line interfaces"
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.11"
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 = "py311"
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.11"
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()})