aiscript 1.4.0__tar.gz → 1.5.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.
Files changed (41) hide show
  1. aiscript-1.5.0/PKG-INFO +206 -0
  2. aiscript-1.5.0/README.md +189 -0
  3. {aiscript-1.4.0 → aiscript-1.5.0}/pyproject.toml +11 -1
  4. {aiscript-1.4.0 → aiscript-1.5.0}/pyproject.toml.orig +12 -1
  5. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/__init__.py +1 -1
  6. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/_shim/aiscript/__init__.py +38 -7
  7. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/ask.py +5 -2
  8. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/ask_cli.py +30 -2
  9. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/cli.py +55 -9
  10. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/config.py +253 -25
  11. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/init_cmd.py +49 -4
  12. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/llm.py +23 -0
  13. aiscript-1.5.0/src/aiscript/onboarding.py +112 -0
  14. aiscript-1.5.0/src/aiscript/provider_factory.py +111 -0
  15. aiscript-1.5.0/src/aiscript/providers/__init__.py +1 -0
  16. aiscript-1.5.0/src/aiscript/providers/acp.py +436 -0
  17. aiscript-1.5.0/src/aiscript/providers/anthropic.py +111 -0
  18. aiscript-1.5.0/src/aiscript/providers/contracts.py +80 -0
  19. aiscript-1.5.0/src/aiscript/providers/gemini.py +70 -0
  20. aiscript-1.5.0/src/aiscript/qsiv.py +78 -0
  21. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/reviewer.py +107 -8
  22. aiscript-1.4.0/PKG-INFO +0 -102
  23. aiscript-1.4.0/README.md +0 -88
  24. {aiscript-1.4.0 → aiscript-1.5.0}/LICENSE +0 -0
  25. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/__main__.py +0 -0
  26. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/_shim/__init__.py +0 -0
  27. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/ask_counter.py +0 -0
  28. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/ask_openai.py +0 -0
  29. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/ask_schema.py +0 -0
  30. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/builder.py +0 -0
  31. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/decisions.py +0 -0
  32. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/env.py +0 -0
  33. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/errors.py +0 -0
  34. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/file_contracts.py +0 -0
  35. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/listing.py +0 -0
  36. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/paths.py +0 -0
  37. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/prompt_file.py +0 -0
  38. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/pysetup.py +0 -0
  39. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/runner.py +0 -0
  40. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/secret_scan.py +0 -0
  41. {aiscript-1.4.0 → aiscript-1.5.0}/src/aiscript/sweep.py +0 -0
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiscript
3
+ Version: 1.5.0
4
+ Summary: JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM.
5
+ Author: Daniel Illescas Romero
6
+ Author-email: Daniel Illescas Romero <illescas.daniel@protonmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: httpx2>=2.14.0
10
+ Requires-Dist: openai>=3.35.1
11
+ Requires-Dist: pyyaml>=6.0.3
12
+ Requires-Dist: typesafe-sdk>=0.7.5
13
+ Requires-Dist: qsiv-runtime>=0.1,<0.2 ; extra == 'qsiv'
14
+ Requires-Python: >=3.10
15
+ Provides-Extra: qsiv
16
+ Description-Content-Type: text/markdown
17
+
18
+ # aiscript
19
+
20
+ [![PyPI version](https://img.shields.io/pypi/v/aiscript)](https://pypi.org/project/aiscript/)
21
+ [![Python versions](https://img.shields.io/pypi/pyversions/aiscript)](https://pypi.org/project/aiscript/)
22
+
23
+ Write scripts in plain English and run them directly. `aiscript` generates and reviews a shell
24
+ or Python program from your description, caches it, and runs it. Later runs reuse the cached build.
25
+
26
+ ## Install
27
+
28
+ Requires Python 3.10+. Install using one of:
29
+
30
+ ```sh
31
+ uv tool install aiscript
32
+ # or
33
+ pipx install aiscript
34
+ # or
35
+ pip install aiscript
36
+ ```
37
+
38
+ To update to the latest version, use the same tool you installed with:
39
+
40
+ ```sh
41
+ uv tool upgrade aiscript
42
+ # or
43
+ pipx upgrade aiscript
44
+ # or
45
+ pip install --upgrade aiscript
46
+ ```
47
+
48
+ See [installation and PATH setup](docs/installation.md) for troubleshooting.
49
+
50
+ ## Quickstart
51
+
52
+ Run `aiscript --init` to choose a provider and create a starter configuration at
53
+ `~/.config/aiscript/config.yaml`. On an interactive first run that needs script generation,
54
+ aiscript also offers this choice if there is no configuration yet. The menu checks for local
55
+ executables and environment keys; a detected item is not necessarily signed in or ready to use.
56
+ You can select an undetected provider and finish its setup afterwards.
57
+
58
+ Existing configuration and unattended runs do not show the menu. Non-interactive `--init`
59
+ creates the OpenAI starter configuration. See [configuration](#configuration) and the
60
+ [configuration reference](docs/configuration.md) for provider setup and API keys.
61
+
62
+ You can create a named command interactively:
63
+
64
+ ```sh
65
+ aiscript sizes
66
+ # Describe what it should do when prompted, then pass arguments on later runs:
67
+ aiscript sizes ~/Downloads --top 5
68
+ ```
69
+
70
+ ## Executable prompt files
71
+
72
+ On Linux or macOS, save this plain-English script as `biggest` in a folder such as `~/Documents`:
73
+
74
+ ```text
75
+ #!/usr/bin/env aiscript
76
+
77
+ List the 10 largest immediate subfolders of the directory given as the first argument. Show their sizes in human-readable form, largest first. Do not modify any files.
78
+ ```
79
+
80
+ In that folder, make it executable and run it:
81
+
82
+ ```sh
83
+ chmod +x biggest
84
+ ./biggest ~/Downloads
85
+ ```
86
+
87
+ The first run generates and reviews the program. Inspect it if approval is requested; later runs
88
+ reuse the cached build. See [prompt files](docs/prompt-files.md) for PATH setup, Windows,
89
+ arguments, caching and rebuilding.
90
+
91
+ ## AI inside scripts
92
+
93
+ Generated scripts can ask your configured model to summarise, classify or extract data through
94
+ `aiscript.ask` (Python) or `aiscript --ask` (shell). Their input is sent to the selected provider
95
+ on every run. A status line on stderr reports the configured timeout when a request starts; the
96
+ answer remains on stdout or is returned to the Python caller. API timeouts apply per attempt, so
97
+ retries can take longer. ACP timeouts are total request deadlines. See
98
+ [usage](docs/usage.md#ai-inside-scripts) and [privacy](docs/safety.md#privacy).
99
+
100
+ For example, save this as `standup`. Git can list commits but can't summarise them, so the prompt
101
+ asks for that step explicitly:
102
+
103
+ ```text
104
+ #!/usr/bin/env aiscript
105
+
106
+ Print a short standup note for the git repository in the current directory. Collect my commits
107
+ from the last 24 hours (by the configured git user, all branches), then use AI to summarise them
108
+ into "Done" and "Next" bullet points, at most 5 bullets in total. If there are no commits, say so
109
+ without calling AI. Do not modify the repository.
110
+ ```
111
+
112
+ ```sh
113
+ chmod +x standup
114
+ cd ~/projects/my-app && ~/Documents/standup
115
+ ```
116
+
117
+ The generated program runs `git log` itself and hands only that output to the model through
118
+ `aiscript.ask`. Only the commit data is sent to your provider, and only when the script runs.
119
+
120
+ Another example: save this as `explain-error` to get a plain-language diagnosis of any failing
121
+ command:
122
+
123
+ ```text
124
+ #!/usr/bin/env aiscript
125
+
126
+ Run the command given as the arguments and capture its output and exit code. If it succeeds, print
127
+ its output unchanged and exit 0. If it fails, print its output, then use AI to explain the likely
128
+ cause in one short paragraph and suggest one concrete fix. Exit with the command's exit code. Do
129
+ not modify any files.
130
+ ```
131
+
132
+ ```sh
133
+ chmod +x explain-error
134
+ ./explain-error npm run build
135
+ ```
136
+
137
+ Only the failing command's output is sent to your provider, so avoid running it on commands that
138
+ print secrets.
139
+
140
+ ### How it works
141
+
142
+ ```mermaid
143
+ flowchart TD
144
+ A["./standup"] --> B{"Cached build?"}
145
+ B -- no --> C["Generate, review and<br/>approve if risky, then cache"]
146
+ B -- yes --> D["Run the program"]
147
+ C --> D
148
+ D --> E["aiscript.ask(task, data)"]
149
+ E --> F["Your configured provider<br/>(API key stays in aiscript)"]
150
+ F --> G["Script prints the answer"]
151
+ ```
152
+
153
+ Generation and review happen once. Every later run skips straight to the cached program, and only
154
+ the `ask` call talks to the model.
155
+
156
+ ## Safety review / decision models
157
+
158
+ Generated programs are checked before they are saved or run. Risky programs need approval; review
159
+ failures and unavailable reviewers also require approval. Review is not a sandbox. The reviewer can be a hosted or local decision model, including the
160
+ optional local GPU model QSIV (`aiscript[qsiv]`). See
161
+ [safety review](docs/safety.md) for the full behavior.
162
+
163
+ ## Configuration
164
+
165
+ Set `default_provider` in `config.yaml` to choose the provider that writes your scripts and
166
+ answers AI calls inside them. The starter config (`aiscript --init`) has an entry for each choice.
167
+
168
+ - **APIs:** `openai` (or any OpenAI-compatible endpoint), `anthropic` and `gemini`. Put the API key
169
+ and model in the config.
170
+ - **Installed agents (ACP):** `codex-acp`, `claude-acp`, `gemini-acp`, `opencode-go-acp` and
171
+ `glm-acp` use the agent you already have installed and signed in to, with no `providers:` entry.
172
+ The agent chooses its model. OpenCode Go needs `"model": "opencode-go/<model>"` in your OpenCode
173
+ config. Agents are trusted local programs, not a sandbox. Install and sign in to the agent
174
+ first: see [installing ACP agents](docs/configuration.md#installing-acp-agents).
175
+
176
+ ```yaml
177
+ default_provider: claude-acp
178
+ ```
179
+
180
+ See the [configuration reference](docs/configuration.md) for API keys, ACP limits, the optional
181
+ safety reviewer and environment variables, and [safety](docs/safety.md#acp-agents) for agent trust.
182
+
183
+ ## Usage
184
+
185
+ Run `aiscript --help` for available commands. See [usage and storage](docs/usage.md) for command
186
+ management, Python dependencies and cache locations.
187
+
188
+ ## Development
189
+
190
+ ```sh
191
+ uv sync # install dependencies
192
+ uv run task test # tests
193
+ uv run task dev --init # try your checkout with a private, gitignored config in .dev/
194
+ ```
195
+
196
+ Edit `.dev/config.yaml` to add an API key, then run any command with `uv run task dev` (for
197
+ example `uv run task dev sizes ~/Downloads`). See [development](docs/development.md).
198
+
199
+ ## Docs
200
+
201
+ - [Installation](docs/installation.md)
202
+ - [Executable prompt files](docs/prompt-files.md)
203
+ - [Configuration reference](docs/configuration.md)
204
+ - [Safety review](docs/safety.md)
205
+ - [Usage and storage](docs/usage.md)
206
+ - [Development](docs/development.md)
@@ -0,0 +1,189 @@
1
+ # aiscript
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/aiscript)](https://pypi.org/project/aiscript/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/aiscript)](https://pypi.org/project/aiscript/)
5
+
6
+ Write scripts in plain English and run them directly. `aiscript` generates and reviews a shell
7
+ or Python program from your description, caches it, and runs it. Later runs reuse the cached build.
8
+
9
+ ## Install
10
+
11
+ Requires Python 3.10+. Install using one of:
12
+
13
+ ```sh
14
+ uv tool install aiscript
15
+ # or
16
+ pipx install aiscript
17
+ # or
18
+ pip install aiscript
19
+ ```
20
+
21
+ To update to the latest version, use the same tool you installed with:
22
+
23
+ ```sh
24
+ uv tool upgrade aiscript
25
+ # or
26
+ pipx upgrade aiscript
27
+ # or
28
+ pip install --upgrade aiscript
29
+ ```
30
+
31
+ See [installation and PATH setup](docs/installation.md) for troubleshooting.
32
+
33
+ ## Quickstart
34
+
35
+ Run `aiscript --init` to choose a provider and create a starter configuration at
36
+ `~/.config/aiscript/config.yaml`. On an interactive first run that needs script generation,
37
+ aiscript also offers this choice if there is no configuration yet. The menu checks for local
38
+ executables and environment keys; a detected item is not necessarily signed in or ready to use.
39
+ You can select an undetected provider and finish its setup afterwards.
40
+
41
+ Existing configuration and unattended runs do not show the menu. Non-interactive `--init`
42
+ creates the OpenAI starter configuration. See [configuration](#configuration) and the
43
+ [configuration reference](docs/configuration.md) for provider setup and API keys.
44
+
45
+ You can create a named command interactively:
46
+
47
+ ```sh
48
+ aiscript sizes
49
+ # Describe what it should do when prompted, then pass arguments on later runs:
50
+ aiscript sizes ~/Downloads --top 5
51
+ ```
52
+
53
+ ## Executable prompt files
54
+
55
+ On Linux or macOS, save this plain-English script as `biggest` in a folder such as `~/Documents`:
56
+
57
+ ```text
58
+ #!/usr/bin/env aiscript
59
+
60
+ List the 10 largest immediate subfolders of the directory given as the first argument. Show their sizes in human-readable form, largest first. Do not modify any files.
61
+ ```
62
+
63
+ In that folder, make it executable and run it:
64
+
65
+ ```sh
66
+ chmod +x biggest
67
+ ./biggest ~/Downloads
68
+ ```
69
+
70
+ The first run generates and reviews the program. Inspect it if approval is requested; later runs
71
+ reuse the cached build. See [prompt files](docs/prompt-files.md) for PATH setup, Windows,
72
+ arguments, caching and rebuilding.
73
+
74
+ ## AI inside scripts
75
+
76
+ Generated scripts can ask your configured model to summarise, classify or extract data through
77
+ `aiscript.ask` (Python) or `aiscript --ask` (shell). Their input is sent to the selected provider
78
+ on every run. A status line on stderr reports the configured timeout when a request starts; the
79
+ answer remains on stdout or is returned to the Python caller. API timeouts apply per attempt, so
80
+ retries can take longer. ACP timeouts are total request deadlines. See
81
+ [usage](docs/usage.md#ai-inside-scripts) and [privacy](docs/safety.md#privacy).
82
+
83
+ For example, save this as `standup`. Git can list commits but can't summarise them, so the prompt
84
+ asks for that step explicitly:
85
+
86
+ ```text
87
+ #!/usr/bin/env aiscript
88
+
89
+ Print a short standup note for the git repository in the current directory. Collect my commits
90
+ from the last 24 hours (by the configured git user, all branches), then use AI to summarise them
91
+ into "Done" and "Next" bullet points, at most 5 bullets in total. If there are no commits, say so
92
+ without calling AI. Do not modify the repository.
93
+ ```
94
+
95
+ ```sh
96
+ chmod +x standup
97
+ cd ~/projects/my-app && ~/Documents/standup
98
+ ```
99
+
100
+ The generated program runs `git log` itself and hands only that output to the model through
101
+ `aiscript.ask`. Only the commit data is sent to your provider, and only when the script runs.
102
+
103
+ Another example: save this as `explain-error` to get a plain-language diagnosis of any failing
104
+ command:
105
+
106
+ ```text
107
+ #!/usr/bin/env aiscript
108
+
109
+ Run the command given as the arguments and capture its output and exit code. If it succeeds, print
110
+ its output unchanged and exit 0. If it fails, print its output, then use AI to explain the likely
111
+ cause in one short paragraph and suggest one concrete fix. Exit with the command's exit code. Do
112
+ not modify any files.
113
+ ```
114
+
115
+ ```sh
116
+ chmod +x explain-error
117
+ ./explain-error npm run build
118
+ ```
119
+
120
+ Only the failing command's output is sent to your provider, so avoid running it on commands that
121
+ print secrets.
122
+
123
+ ### How it works
124
+
125
+ ```mermaid
126
+ flowchart TD
127
+ A["./standup"] --> B{"Cached build?"}
128
+ B -- no --> C["Generate, review and<br/>approve if risky, then cache"]
129
+ B -- yes --> D["Run the program"]
130
+ C --> D
131
+ D --> E["aiscript.ask(task, data)"]
132
+ E --> F["Your configured provider<br/>(API key stays in aiscript)"]
133
+ F --> G["Script prints the answer"]
134
+ ```
135
+
136
+ Generation and review happen once. Every later run skips straight to the cached program, and only
137
+ the `ask` call talks to the model.
138
+
139
+ ## Safety review / decision models
140
+
141
+ Generated programs are checked before they are saved or run. Risky programs need approval; review
142
+ failures and unavailable reviewers also require approval. Review is not a sandbox. The reviewer can be a hosted or local decision model, including the
143
+ optional local GPU model QSIV (`aiscript[qsiv]`). See
144
+ [safety review](docs/safety.md) for the full behavior.
145
+
146
+ ## Configuration
147
+
148
+ Set `default_provider` in `config.yaml` to choose the provider that writes your scripts and
149
+ answers AI calls inside them. The starter config (`aiscript --init`) has an entry for each choice.
150
+
151
+ - **APIs:** `openai` (or any OpenAI-compatible endpoint), `anthropic` and `gemini`. Put the API key
152
+ and model in the config.
153
+ - **Installed agents (ACP):** `codex-acp`, `claude-acp`, `gemini-acp`, `opencode-go-acp` and
154
+ `glm-acp` use the agent you already have installed and signed in to, with no `providers:` entry.
155
+ The agent chooses its model. OpenCode Go needs `"model": "opencode-go/<model>"` in your OpenCode
156
+ config. Agents are trusted local programs, not a sandbox. Install and sign in to the agent
157
+ first: see [installing ACP agents](docs/configuration.md#installing-acp-agents).
158
+
159
+ ```yaml
160
+ default_provider: claude-acp
161
+ ```
162
+
163
+ See the [configuration reference](docs/configuration.md) for API keys, ACP limits, the optional
164
+ safety reviewer and environment variables, and [safety](docs/safety.md#acp-agents) for agent trust.
165
+
166
+ ## Usage
167
+
168
+ Run `aiscript --help` for available commands. See [usage and storage](docs/usage.md) for command
169
+ management, Python dependencies and cache locations.
170
+
171
+ ## Development
172
+
173
+ ```sh
174
+ uv sync # install dependencies
175
+ uv run task test # tests
176
+ uv run task dev --init # try your checkout with a private, gitignored config in .dev/
177
+ ```
178
+
179
+ Edit `.dev/config.yaml` to add an API key, then run any command with `uv run task dev` (for
180
+ example `uv run task dev sizes ~/Downloads`). See [development](docs/development.md).
181
+
182
+ ## Docs
183
+
184
+ - [Installation](docs/installation.md)
185
+ - [Executable prompt files](docs/prompt-files.md)
186
+ - [Configuration reference](docs/configuration.md)
187
+ - [Safety review](docs/safety.md)
188
+ - [Usage and storage](docs/usage.md)
189
+ - [Development](docs/development.md)
@@ -1,12 +1,13 @@
1
1
  [project]
2
2
  name = "aiscript"
3
- version = "1.4.0"
3
+ version = "1.5.0"
4
4
  description = "JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
+ "httpx2>=2.14.0",
10
11
  "openai>=3.35.1",
11
12
  "pyyaml>=6.0.3",
12
13
  "typesafe-sdk>=0.7.5",
@@ -16,6 +17,9 @@ dependencies = [
16
17
  name = "Daniel Illescas Romero"
17
18
  email = "illescas.daniel@protonmail.com"
18
19
 
20
+ [project.optional-dependencies]
21
+ qsiv = ["qsiv-runtime>=0.1,<0.2"]
22
+
19
23
  [project.scripts]
20
24
  aiscript = "aiscript.cli:main"
21
25
 
@@ -28,6 +32,7 @@ dev = [
28
32
  "mypy>=2.4.0",
29
33
  "pytest>=9.1.1",
30
34
  "ruff>=0.17.1",
35
+ "taskipy>=1.14.1",
31
36
  ]
32
37
 
33
38
  [tool.ruff]
@@ -63,3 +68,8 @@ testpaths = ["tests"]
63
68
  python_version = "3.10"
64
69
  ignore_missing_imports = true
65
70
  warn_unused_ignores = true
71
+
72
+ [tool.taskipy.tasks]
73
+ dev = "python scripts/dev.py"
74
+ test = "pytest -q"
75
+ lint = "ruff check && ruff format --check && mypy src tests"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiscript"
3
- version = "1.4.0"
3
+ version = "1.5.0"
4
4
  description = "JIT-compile plain-English intents into cached, reviewed shell or Python scripts via an LLM."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -10,11 +10,16 @@ authors = [
10
10
  ]
11
11
  requires-python = ">=3.10"
12
12
  dependencies = [
13
+ "httpx2>=2.14.0",
13
14
  "openai>=3.35.1",
14
15
  "pyyaml>=6.0.3",
15
16
  "typesafe-sdk>=0.7.5",
16
17
  ]
17
18
 
19
+ [project.optional-dependencies]
20
+ # Local QSIV safety reviewer: Linux x86_64 and an NVIDIA GPU only, so it is not a default dependency.
21
+ qsiv = ["qsiv-runtime>=0.1,<0.2"]
22
+
18
23
  [project.scripts]
19
24
  aiscript = "aiscript.cli:main"
20
25
 
@@ -27,6 +32,7 @@ dev = [
27
32
  "mypy>=2.4.0",
28
33
  "pytest>=9.1.1",
29
34
  "ruff>=0.17.1",
35
+ "taskipy>=1.14.1",
30
36
  ]
31
37
 
32
38
  [tool.ruff]
@@ -52,3 +58,8 @@ testpaths = ["tests"]
52
58
  python_version = "3.10"
53
59
  ignore_missing_imports = true
54
60
  warn_unused_ignores = true
61
+
62
+ [tool.taskipy.tasks]
63
+ dev = "python scripts/dev.py"
64
+ test = "pytest -q"
65
+ lint = "ruff check && ruff format --check && mypy src tests"
@@ -1,3 +1,3 @@
1
1
  """aiscript: JIT-compile plain-English intents into cached shell scripts."""
2
2
 
3
- __version__ = "1.4.0"
3
+ __version__ = "1.5.0"
@@ -5,9 +5,12 @@
5
5
 
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import shutil
9
10
  import subprocess
10
- from typing import Any, Dict, Optional, Union
11
+ import sys
12
+ import threading
13
+ from typing import Any, Dict, List, Optional, Union
11
14
 
12
15
  _STDERR_LIMIT = 500
13
16
 
@@ -104,6 +107,39 @@ def _error_for(code: int, stderr: bytes) -> AskError:
104
107
  return error
105
108
 
106
109
 
110
+ def _run_child(argv: List[str], data: bytes, env: Dict[str, str]) -> subprocess.CompletedProcess:
111
+ """Stream only the known status while subprocess.run drains stdout and stdin."""
112
+ read_fd, write_fd = os.pipe()
113
+ stderr = bytearray()
114
+ notice = re.compile(
115
+ rb"aiscript: (?:This request can take up to [0-9]+ seconds\.|"
116
+ rb"Timeout [0-9]+ seconds per attempt; retries may take longer\.)\r?\n$"
117
+ )
118
+
119
+ def read_errors() -> None:
120
+ shown = False
121
+ with os.fdopen(read_fd, "rb") as stream:
122
+ while True:
123
+ line = stream.readline(1024)
124
+ if not line:
125
+ break
126
+ if not shown and notice.fullmatch(line):
127
+ sys.stderr.write(line.decode("ascii"))
128
+ sys.stderr.flush()
129
+ shown = True
130
+ elif line.startswith(b"aiscript: error: ") and len(stderr) < _STDERR_LIMIT:
131
+ stderr.extend(line[: _STDERR_LIMIT - len(stderr)])
132
+
133
+ thread = threading.Thread(target=read_errors, daemon=True)
134
+ thread.start()
135
+ try:
136
+ result = subprocess.run(argv, input=data, stdout=subprocess.PIPE, stderr=write_fd, env=env)
137
+ finally:
138
+ os.close(write_fd)
139
+ thread.join()
140
+ return subprocess.CompletedProcess(argv, result.returncode, result.stdout, bytes(stderr))
141
+
142
+
107
143
  def ask(
108
144
  task: str,
109
145
  data: str,
@@ -128,12 +164,7 @@ def ask(
128
164
  raise AskNotFoundError(
129
165
  "aiscript was not found on PATH; install aiscript so that `aiscript` is on PATH."
130
166
  )
131
- proc = subprocess.run(
132
- [exe] + args,
133
- input=data.encode("utf-8"),
134
- capture_output=True,
135
- env=_child_env(),
136
- )
167
+ proc = _run_child([exe] + args, data.encode("utf-8"), _child_env())
137
168
  if proc.returncode != 0:
138
169
  raise _error_for(proc.returncode, proc.stderr)
139
170
  out = proc.stdout.decode("utf-8")
@@ -88,10 +88,13 @@ class ProviderReply:
88
88
  class ProviderCallError(Exception):
89
89
  """Bounded provider failure without a payload."""
90
90
 
91
- def __init__(self, kind: ProviderFailure, status_code: int | None = None) -> None:
91
+ def __init__(
92
+ self, kind: ProviderFailure, status_code: int | None = None, detail: str | None = None
93
+ ) -> None:
92
94
  super().__init__(kind)
93
95
  self.kind: ProviderFailure = kind
94
96
  self.status_code: int | None = status_code
97
+ self.detail: str | None = detail
95
98
 
96
99
 
97
100
  class AskProvider(Protocol):
@@ -183,7 +186,7 @@ def provider_error(exc: ProviderCallError, *, where: str, schema_sent: bool) ->
183
186
  "server": "the server reported an error",
184
187
  "bad_request": "the request was rejected",
185
188
  }
186
- reason = reasons.get(exc.kind, "an unexpected error occurred")
189
+ reason = exc.detail or reasons.get(exc.kind, "an unexpected error occurred")
187
190
  message = f"The call to {where} failed: {reason}."
188
191
  if exc.kind == "bad_request" and schema_sent:
189
192
  message += " The server may not support strict JSON schemas; simplify or remove the schema."
@@ -8,14 +8,17 @@ from collections.abc import Mapping
8
8
  from typing import IO
9
9
 
10
10
  from aiscript import ask_counter, ask_openai
11
- from aiscript.ask import AskRequest, JsonSchema, decode_input, run_ask, validate_call
11
+ from aiscript.ask import AskProvider, AskRequest, JsonSchema, decode_input, run_ask, validate_call
12
12
  from aiscript.config import (
13
13
  identify_ask_target,
14
+ load_acp_settings,
14
15
  load_ai_settings,
15
16
  load_raw_config,
16
17
  resolve_ask_key,
17
18
  )
18
19
  from aiscript.errors import AskConfigError, AskError, AskUsageError, ConfigError
20
+ from aiscript.provider_factory import AskBridge, NoticeProvider, make_provider
21
+ from aiscript.providers.contracts import CompletionError
19
22
 
20
23
  _MAX_MESSAGE = 600
21
24
 
@@ -45,13 +48,38 @@ def _ask(
45
48
  try:
46
49
  raw = load_raw_config()
47
50
  limits = load_ai_settings(raw)
51
+ acp = load_acp_settings(raw)
48
52
  except ConfigError as exc:
49
53
  raise AskConfigError(str(exc)) from None
50
54
  target = identify_ask_target(raw) # F7
51
55
  key = resolve_ask_key(target, raw, environ)
52
56
  max_bytes = limits.max_input_kb * 1024
53
57
  data = decode_input(stdin.read(max_bytes), max_bytes) # F8
54
- provider = ask_openai.make_openai_provider(target, key, limits)
58
+ provider: AskProvider
59
+ if target.kind == "openai":
60
+ provider = NoticeProvider(
61
+ ask_openai.make_openai_provider(target, key, limits), limits.timeout_seconds
62
+ )
63
+ else:
64
+ try:
65
+ neutral = make_provider(
66
+ target.kind,
67
+ target.provider,
68
+ target.model,
69
+ target.base_url,
70
+ key,
71
+ limits.timeout_seconds,
72
+ acp,
73
+ )
74
+ except CompletionError:
75
+ raise AskConfigError(
76
+ f"Provider '{target.provider}' is not yet available; check its connector setup."
77
+ ) from None
78
+ provider = AskBridge(
79
+ neutral,
80
+ is_acp=target.kind == "acp",
81
+ timeout=acp.timeout_seconds if target.kind == "acp" else limits.timeout_seconds,
82
+ )
55
83
  result = run_ask( # F13, then send (F9-F12)
56
84
  AskRequest(task, data, data_description, schema),
57
85
  limits=limits,