induscode 0.1.0__py3-none-any.whl
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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Invocation subsystem — public barrel.
|
|
2
|
+
|
|
3
|
+
Bundles the command-line behavior modules that all read the single
|
|
4
|
+
declarative :data:`~.flags.FLAG_SPECS` table:
|
|
5
|
+
|
|
6
|
+
- :func:`~.read.read_invocation` — the generic, table-driven argv parser that
|
|
7
|
+
folds a sliced argument vector into a typed
|
|
8
|
+
:class:`~induscode.launch.contract.Invocation`;
|
|
9
|
+
- :func:`~.usage.render_usage` — the ``--help`` banner generated from that
|
|
10
|
+
same table, so help and parsing share one source of truth;
|
|
11
|
+
- :func:`~.attachments.gather_attachments` — the ``@file`` expander that
|
|
12
|
+
resolves references through the inlined path resolver into inlined prose
|
|
13
|
+
plus base64 media, raising a typed
|
|
14
|
+
:class:`~.attachments.AttachmentError` on failure.
|
|
15
|
+
|
|
16
|
+
Consumers import the invocation surface from ``induscode.launch.invocation``
|
|
17
|
+
rather than reaching into the individual modules.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .attachments import AttachmentError, AttachmentErrorKind, gather_attachments
|
|
23
|
+
from .flags import FLAG_GROUPS, FLAG_SPECS, FlagGroup, FlagGroupEntry, GroupedFlagSpec
|
|
24
|
+
from .read import read_file_references, read_invocation
|
|
25
|
+
from .usage import render_usage
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AttachmentError",
|
|
29
|
+
"AttachmentErrorKind",
|
|
30
|
+
"FLAG_GROUPS",
|
|
31
|
+
"FLAG_SPECS",
|
|
32
|
+
"FlagGroup",
|
|
33
|
+
"FlagGroupEntry",
|
|
34
|
+
"GroupedFlagSpec",
|
|
35
|
+
"gather_attachments",
|
|
36
|
+
"read_file_references",
|
|
37
|
+
"read_invocation",
|
|
38
|
+
"render_usage",
|
|
39
|
+
]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""The attachment gatherer — expands ``@file`` references into one
|
|
2
|
+
:class:`~induscode.launch.contract.Attachments` value.
|
|
3
|
+
|
|
4
|
+
Each ``@file`` token collected by the reader is resolved through the inlined
|
|
5
|
+
path resolver (:func:`_resolve_read_path`, which handles ``~`` expansion and
|
|
6
|
+
the cwd — the framework publishes no ``resolveReadPath`` export, so the
|
|
7
|
+
~5-line shim lives here per the port plan), classified by extension, and
|
|
8
|
+
folded into the result:
|
|
9
|
+
|
|
10
|
+
- **text files** are read as UTF-8 and inlined into ``Attachments.prose``
|
|
11
|
+
wrapped in a delimited block keyed by the resolved path, so the model sees
|
|
12
|
+
which file each block came from;
|
|
13
|
+
- **image files** are read as bytes, base64-encoded, and collected as
|
|
14
|
+
framework :class:`~indusagi.ai.ImageContent` in ``Attachments.media``.
|
|
15
|
+
|
|
16
|
+
Guard rails:
|
|
17
|
+
|
|
18
|
+
- an unknown extension, an oversized file, a missing file, or a read error
|
|
19
|
+
raises a typed :class:`AttachmentError` (with an :data:`AttachmentErrorKind`
|
|
20
|
+
discriminant) rather than a string or a process exit;
|
|
21
|
+
- size caps bound the inlined text and the decoded image before either is
|
|
22
|
+
materialised onto the message;
|
|
23
|
+
- an empty file is skipped silently — it contributes neither prose nor media.
|
|
24
|
+
|
|
25
|
+
The ``@file`` syntax, the extension classification, and base64 image
|
|
26
|
+
attachment are kept behaviours; only the typed-error model and the explicit
|
|
27
|
+
caps are owned here. (Port of TS ``src/launch/invocation/attachments.ts``.)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
import base64
|
|
34
|
+
import os
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Final, Literal, Sequence, TypeAlias
|
|
37
|
+
|
|
38
|
+
from indusagi.ai import ImageContent
|
|
39
|
+
|
|
40
|
+
from ..contract import AttachmentOptions, Attachments
|
|
41
|
+
|
|
42
|
+
__all__ = ["AttachmentError", "AttachmentErrorKind", "gather_attachments"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#: Upper bound on an inlined text file: larger files are rejected, not
|
|
46
|
+
#: truncated.
|
|
47
|
+
_MAX_TEXT_BYTES: Final[int] = 10 * 1024 * 1024
|
|
48
|
+
|
|
49
|
+
#: Upper bound on a decoded image file before base64 encoding.
|
|
50
|
+
_MAX_IMAGE_BYTES: Final[int] = 20 * 1024 * 1024
|
|
51
|
+
|
|
52
|
+
#: Lower-cased file extensions inlined as text prose.
|
|
53
|
+
_TEXT_EXTENSIONS: Final[frozenset[str]] = frozenset(
|
|
54
|
+
{
|
|
55
|
+
".txt",
|
|
56
|
+
".md",
|
|
57
|
+
".markdown",
|
|
58
|
+
".json",
|
|
59
|
+
".jsonc",
|
|
60
|
+
".yaml",
|
|
61
|
+
".yml",
|
|
62
|
+
".toml",
|
|
63
|
+
".ini",
|
|
64
|
+
".csv",
|
|
65
|
+
".tsv",
|
|
66
|
+
".xml",
|
|
67
|
+
".html",
|
|
68
|
+
".htm",
|
|
69
|
+
".css",
|
|
70
|
+
".scss",
|
|
71
|
+
".ts",
|
|
72
|
+
".tsx",
|
|
73
|
+
".js",
|
|
74
|
+
".jsx",
|
|
75
|
+
".mjs",
|
|
76
|
+
".cjs",
|
|
77
|
+
".py",
|
|
78
|
+
".rb",
|
|
79
|
+
".go",
|
|
80
|
+
".rs",
|
|
81
|
+
".java",
|
|
82
|
+
".kt",
|
|
83
|
+
".c",
|
|
84
|
+
".h",
|
|
85
|
+
".cc",
|
|
86
|
+
".cpp",
|
|
87
|
+
".hpp",
|
|
88
|
+
".cs",
|
|
89
|
+
".php",
|
|
90
|
+
".sh",
|
|
91
|
+
".bash",
|
|
92
|
+
".zsh",
|
|
93
|
+
".sql",
|
|
94
|
+
".log",
|
|
95
|
+
".env",
|
|
96
|
+
".gitignore",
|
|
97
|
+
".dockerfile",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
#: Lower-cased image extensions mapped to their media type for
|
|
102
|
+
#: :class:`~indusagi.ai.ImageContent`.
|
|
103
|
+
_IMAGE_MIME_BY_EXT: Final[dict[str, str]] = {
|
|
104
|
+
".png": "image/png",
|
|
105
|
+
".jpg": "image/jpeg",
|
|
106
|
+
".jpeg": "image/jpeg",
|
|
107
|
+
".gif": "image/gif",
|
|
108
|
+
".webp": "image/webp",
|
|
109
|
+
".bmp": "image/bmp",
|
|
110
|
+
".svg": "image/svg+xml",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
#: The closed set of attachment failure categories. A consumer switches on
|
|
115
|
+
#: :attr:`AttachmentError.kind`, never on message text:
|
|
116
|
+
#:
|
|
117
|
+
#: - ``unsupported`` — the file extension is neither text nor image.
|
|
118
|
+
#: - ``too-large`` — the file exceeds its kind's size cap.
|
|
119
|
+
#: - ``not-found`` — the resolved path does not exist.
|
|
120
|
+
#: - ``read-failed`` — the file could not be read or stat-ed.
|
|
121
|
+
AttachmentErrorKind: TypeAlias = Literal[
|
|
122
|
+
"unsupported", "too-large", "not-found", "read-failed"
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class AttachmentError(Exception):
|
|
127
|
+
"""A typed ``@file`` expansion failure. Carries the offending reference
|
|
128
|
+
and the resolved path for diagnostics; :attr:`cause` preserves any wrapped
|
|
129
|
+
error."""
|
|
130
|
+
|
|
131
|
+
# The failure category (the discriminant).
|
|
132
|
+
kind: AttachmentErrorKind
|
|
133
|
+
# The raw @file reference as supplied on the command line.
|
|
134
|
+
reference: str
|
|
135
|
+
# The resolved absolute path, when resolution succeeded.
|
|
136
|
+
resolved_path: str | None
|
|
137
|
+
# The wrapped underlying error, if any.
|
|
138
|
+
cause: object | None
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
kind: AttachmentErrorKind,
|
|
143
|
+
message: str,
|
|
144
|
+
*,
|
|
145
|
+
reference: str,
|
|
146
|
+
resolved_path: str | None = None,
|
|
147
|
+
cause: object | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
super().__init__(message)
|
|
150
|
+
self.kind = kind
|
|
151
|
+
self.reference = reference
|
|
152
|
+
self.resolved_path = resolved_path
|
|
153
|
+
self.cause = cause
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_read_path(reference: str, cwd: str) -> str:
|
|
157
|
+
"""The inlined path resolver (the framework's ``resolveReadPath`` has no
|
|
158
|
+
Python export — port plan §1 gap #3): expand a leading ``~``, keep an
|
|
159
|
+
absolute reference verbatim, and resolve a relative one against ``cwd``
|
|
160
|
+
(lexically — no symlink resolution, so the path the user supplied is the
|
|
161
|
+
path reported back)."""
|
|
162
|
+
expanded = Path(reference).expanduser()
|
|
163
|
+
if expanded.is_absolute():
|
|
164
|
+
return str(expanded)
|
|
165
|
+
return os.path.normpath(os.path.join(cwd, str(expanded)))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _extension_of(path: str) -> str:
|
|
169
|
+
"""The lower-cased extension of a path, including the leading dot."""
|
|
170
|
+
return Path(path).suffix.lower()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def _size_of(resolved_path: str, reference: str) -> int:
|
|
174
|
+
"""Stat a resolved path, mapping a missing file to a typed
|
|
175
|
+
:class:`AttachmentError`."""
|
|
176
|
+
try:
|
|
177
|
+
info = await asyncio.to_thread(os.stat, resolved_path)
|
|
178
|
+
return info.st_size
|
|
179
|
+
except FileNotFoundError as error:
|
|
180
|
+
raise AttachmentError(
|
|
181
|
+
"not-found",
|
|
182
|
+
f"No such file: {reference}",
|
|
183
|
+
reference=reference,
|
|
184
|
+
resolved_path=resolved_path,
|
|
185
|
+
cause=error,
|
|
186
|
+
) from error
|
|
187
|
+
except OSError as error:
|
|
188
|
+
raise AttachmentError(
|
|
189
|
+
"read-failed",
|
|
190
|
+
f"Could not access file: {reference}",
|
|
191
|
+
reference=reference,
|
|
192
|
+
resolved_path=resolved_path,
|
|
193
|
+
cause=error,
|
|
194
|
+
) from error
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _wrap_text_block(resolved_path: str, body: str) -> str:
|
|
198
|
+
"""Wrap one resolved text file in a path-keyed delimited block for the
|
|
199
|
+
prose stream."""
|
|
200
|
+
return f'<file path="{resolved_path}">\n{body}\n</file>'
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def gather_attachments(
|
|
204
|
+
references: Sequence[str],
|
|
205
|
+
options: AttachmentOptions,
|
|
206
|
+
) -> Attachments:
|
|
207
|
+
"""Expand the ``@file`` references into a single
|
|
208
|
+
:class:`~induscode.launch.contract.Attachments` value.
|
|
209
|
+
|
|
210
|
+
Resolves each reference against ``options.cwd``, classifies it by
|
|
211
|
+
extension, enforces the per-kind size cap, and appends either a text block
|
|
212
|
+
to the prose or an :class:`~indusagi.ai.ImageContent` to the media. The
|
|
213
|
+
references list is taken in order; the prose blocks are joined with a
|
|
214
|
+
blank line between them. An empty references list yields an empty
|
|
215
|
+
:class:`Attachments` (the caller decides whether to attach it at all).
|
|
216
|
+
|
|
217
|
+
:param references: the raw ``@file`` paths (without the leading ``@``)
|
|
218
|
+
:param options: the cwd the references resolve against
|
|
219
|
+
:raises AttachmentError: on an unsupported extension, an oversized file, a
|
|
220
|
+
missing file, or a read failure
|
|
221
|
+
"""
|
|
222
|
+
blocks: list[str] = []
|
|
223
|
+
media: list[ImageContent] = []
|
|
224
|
+
|
|
225
|
+
for reference in references:
|
|
226
|
+
resolved_path = _resolve_read_path(reference, options.cwd)
|
|
227
|
+
ext = _extension_of(resolved_path)
|
|
228
|
+
|
|
229
|
+
image_mime = _IMAGE_MIME_BY_EXT.get(ext)
|
|
230
|
+
is_text = ext in _TEXT_EXTENSIONS
|
|
231
|
+
|
|
232
|
+
if image_mime is None and not is_text:
|
|
233
|
+
raise AttachmentError(
|
|
234
|
+
"unsupported",
|
|
235
|
+
f"Unsupported attachment type: {reference}",
|
|
236
|
+
reference=reference,
|
|
237
|
+
resolved_path=resolved_path,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
size = await _size_of(resolved_path, reference)
|
|
241
|
+
if size == 0:
|
|
242
|
+
continue # Skip empty files silently.
|
|
243
|
+
|
|
244
|
+
cap = _MAX_IMAGE_BYTES if image_mime is not None else _MAX_TEXT_BYTES
|
|
245
|
+
if size > cap:
|
|
246
|
+
raise AttachmentError(
|
|
247
|
+
"too-large",
|
|
248
|
+
f"Attachment exceeds the size limit: {reference}",
|
|
249
|
+
reference=reference,
|
|
250
|
+
resolved_path=resolved_path,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
if image_mime is not None:
|
|
255
|
+
data = await asyncio.to_thread(Path(resolved_path).read_bytes)
|
|
256
|
+
media.append(
|
|
257
|
+
ImageContent(
|
|
258
|
+
data=base64.b64encode(data).decode("ascii"),
|
|
259
|
+
mimeType=image_mime,
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
# errors="replace" mirrors Buffer.toString("utf8"): invalid
|
|
264
|
+
# sequences degrade to replacement characters, never raise.
|
|
265
|
+
body = await asyncio.to_thread(
|
|
266
|
+
Path(resolved_path).read_text,
|
|
267
|
+
encoding="utf-8",
|
|
268
|
+
errors="replace",
|
|
269
|
+
)
|
|
270
|
+
if len(body) > 0:
|
|
271
|
+
blocks.append(_wrap_text_block(resolved_path, body))
|
|
272
|
+
except OSError as error:
|
|
273
|
+
raise AttachmentError(
|
|
274
|
+
"read-failed",
|
|
275
|
+
f"Could not read file: {reference}",
|
|
276
|
+
reference=reference,
|
|
277
|
+
resolved_path=resolved_path,
|
|
278
|
+
cause=error,
|
|
279
|
+
) from error
|
|
280
|
+
|
|
281
|
+
return Attachments(prose="\n\n".join(blocks), media=tuple(media))
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""The single declarative flag table.
|
|
2
|
+
|
|
3
|
+
Every recognised command-line option is described here exactly once, as one
|
|
4
|
+
:class:`GroupedFlagSpec` row. Two consumers walk this same table:
|
|
5
|
+
|
|
6
|
+
- the reader (:func:`~.read.read_invocation`) indexes it by canonical name
|
|
7
|
+
and alias to bind raw tokens into the parsed
|
|
8
|
+
:class:`~induscode.launch.contract.Invocation`; and
|
|
9
|
+
- the usage generator (:func:`~.usage.render_usage`) enumerates it to render
|
|
10
|
+
the option reference — there is no second, hand-maintained help string for
|
|
11
|
+
it to drift against.
|
|
12
|
+
|
|
13
|
+
Because both sides read the same rows, the help text and the parser cannot
|
|
14
|
+
disagree about which flags exist, what they take, or what they mean. The order
|
|
15
|
+
of the rows is the order options appear in the generated usage, grouped by the
|
|
16
|
+
:data:`FLAG_GROUPS` sidecar list below; keeping that order intentional is the
|
|
17
|
+
only editorial concern when a row is added.
|
|
18
|
+
|
|
19
|
+
Flag names and their semantics are a stable product contract (and largely an
|
|
20
|
+
industry convention), so they are kept as-is; only the table that declares
|
|
21
|
+
them and the code that reads it are owned here. (Port of TS
|
|
22
|
+
``src/launch/invocation/flags.ts`` — all 17 rows verbatim.)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Final, Literal, TypeAlias
|
|
29
|
+
|
|
30
|
+
from ..contract import THINKING_EFFORTS, FlagSpec
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"FLAG_GROUPS",
|
|
34
|
+
"FLAG_SPECS",
|
|
35
|
+
"FlagGroup",
|
|
36
|
+
"FlagGroupEntry",
|
|
37
|
+
"GroupedFlagSpec",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: The heading a flag is filed under in the generated usage. Purely an
|
|
42
|
+
#: editorial grouping for the renderer; it has no effect on parsing.
|
|
43
|
+
FlagGroup: TypeAlias = Literal["output", "model", "context", "tools", "meta"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class GroupedFlagSpec(FlagSpec):
|
|
48
|
+
"""A :class:`~induscode.launch.contract.FlagSpec` carrying its usage
|
|
49
|
+
section. The extra :attr:`group` field is inert at parse time; the reader
|
|
50
|
+
ignores it and only the renderer reads it."""
|
|
51
|
+
|
|
52
|
+
# The usage section this row is rendered under.
|
|
53
|
+
group: FlagGroup = field(kw_only=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class FlagGroupEntry:
|
|
58
|
+
"""One usage section: its :data:`FlagGroup` id and human-readable title."""
|
|
59
|
+
|
|
60
|
+
id: FlagGroup
|
|
61
|
+
title: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
#: Human-readable titles for each :data:`FlagGroup`, in render order. The
|
|
65
|
+
#: usage generator walks this list, then within each group emits the matching
|
|
66
|
+
#: rows from :data:`FLAG_SPECS` in declaration order.
|
|
67
|
+
FLAG_GROUPS: Final[tuple[FlagGroupEntry, ...]] = (
|
|
68
|
+
FlagGroupEntry(id="output", title="Output"),
|
|
69
|
+
FlagGroupEntry(id="model", title="Model"),
|
|
70
|
+
FlagGroupEntry(id="context", title="Context"),
|
|
71
|
+
FlagGroupEntry(id="tools", title="Tools"),
|
|
72
|
+
FlagGroupEntry(id="meta", title="Meta"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#: The ascending reasoning-effort rungs accepted by ``--thinking``, rendered
|
|
77
|
+
#: into its description so the usage text enumerates the accepted values from
|
|
78
|
+
#: the same source the validator uses.
|
|
79
|
+
_THINKING_CHOICES: Final[str] = ", ".join(THINKING_EFFORTS)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
#: The one declarative option table. Every recognised flag is one row; the
|
|
83
|
+
#: reader and the usage generator both index this and nothing else.
|
|
84
|
+
#:
|
|
85
|
+
#: Conventions:
|
|
86
|
+
#:
|
|
87
|
+
#: - ``name`` is the canonical long spelling (leading dashes included) and
|
|
88
|
+
#: also the key the parsed value lands under in ``Invocation.flags`` (sans
|
|
89
|
+
#: the dashes).
|
|
90
|
+
#: - ``aliases`` hold short forms and synonyms; every alias hit normalises to
|
|
91
|
+
#: ``name``.
|
|
92
|
+
#: - ``kind`` fixes the value vocabulary: ``boolean`` switches, ``string`` /
|
|
93
|
+
#: ``number`` value flags, and accumulating ``list`` flags.
|
|
94
|
+
FLAG_SPECS: Final[tuple[GroupedFlagSpec, ...]] = (
|
|
95
|
+
# ---- Output -----------------------------------------------------------
|
|
96
|
+
GroupedFlagSpec(
|
|
97
|
+
name="--print",
|
|
98
|
+
aliases=("-p",),
|
|
99
|
+
kind="boolean",
|
|
100
|
+
group="output",
|
|
101
|
+
describe="Run a single request, print only the result, and exit.",
|
|
102
|
+
),
|
|
103
|
+
GroupedFlagSpec(
|
|
104
|
+
name="--json",
|
|
105
|
+
aliases=("--rpc",),
|
|
106
|
+
kind="boolean",
|
|
107
|
+
group="output",
|
|
108
|
+
describe="Speak the headless line protocol for a driving parent process.",
|
|
109
|
+
),
|
|
110
|
+
GroupedFlagSpec(
|
|
111
|
+
name="--interactive",
|
|
112
|
+
aliases=("-i",),
|
|
113
|
+
kind="boolean",
|
|
114
|
+
group="output",
|
|
115
|
+
describe="Force the interactive session even when a prompt is supplied.",
|
|
116
|
+
),
|
|
117
|
+
# ---- Model ------------------------------------------------------------
|
|
118
|
+
GroupedFlagSpec(
|
|
119
|
+
name="--model",
|
|
120
|
+
aliases=("-m",),
|
|
121
|
+
kind="string",
|
|
122
|
+
group="model",
|
|
123
|
+
describe="Select the model, provider-qualified or bare (e.g. provider/name).",
|
|
124
|
+
),
|
|
125
|
+
GroupedFlagSpec(
|
|
126
|
+
name="--account",
|
|
127
|
+
kind="string",
|
|
128
|
+
group="model",
|
|
129
|
+
describe="Authenticate the run with a named stored credential account.",
|
|
130
|
+
),
|
|
131
|
+
GroupedFlagSpec(
|
|
132
|
+
name="--thinking",
|
|
133
|
+
kind="string",
|
|
134
|
+
group="model",
|
|
135
|
+
describe=f"Set the reasoning effort (one of: {_THINKING_CHOICES}).",
|
|
136
|
+
),
|
|
137
|
+
GroupedFlagSpec(
|
|
138
|
+
name="--list-models",
|
|
139
|
+
kind="string",
|
|
140
|
+
group="model",
|
|
141
|
+
describe="List the available models (optionally filtered by a substring) and exit.",
|
|
142
|
+
),
|
|
143
|
+
# ---- Context ----------------------------------------------------------
|
|
144
|
+
GroupedFlagSpec(
|
|
145
|
+
name="--cwd",
|
|
146
|
+
kind="string",
|
|
147
|
+
group="context",
|
|
148
|
+
describe="Scope the run to a working directory (default: the current directory).",
|
|
149
|
+
),
|
|
150
|
+
GroupedFlagSpec(
|
|
151
|
+
name="--system",
|
|
152
|
+
kind="string",
|
|
153
|
+
group="context",
|
|
154
|
+
describe="Replace the built-in system prompt with the given text.",
|
|
155
|
+
),
|
|
156
|
+
GroupedFlagSpec(
|
|
157
|
+
name="--append-system",
|
|
158
|
+
kind="string",
|
|
159
|
+
group="context",
|
|
160
|
+
describe="Append extra text after the system prompt.",
|
|
161
|
+
),
|
|
162
|
+
GroupedFlagSpec(
|
|
163
|
+
name="--resume",
|
|
164
|
+
aliases=("-r",),
|
|
165
|
+
kind="boolean",
|
|
166
|
+
group="context",
|
|
167
|
+
describe="Pick a previous session to resume.",
|
|
168
|
+
),
|
|
169
|
+
GroupedFlagSpec(
|
|
170
|
+
name="--continue",
|
|
171
|
+
aliases=("-c",),
|
|
172
|
+
kind="boolean",
|
|
173
|
+
group="context",
|
|
174
|
+
describe="Continue the most recent session in this directory.",
|
|
175
|
+
),
|
|
176
|
+
# ---- Tools ------------------------------------------------------------
|
|
177
|
+
GroupedFlagSpec(
|
|
178
|
+
name="--tools",
|
|
179
|
+
kind="list",
|
|
180
|
+
group="tools",
|
|
181
|
+
describe="Allow only the named built-in tools (comma-separated or repeated).",
|
|
182
|
+
),
|
|
183
|
+
GroupedFlagSpec(
|
|
184
|
+
name="--no-tools",
|
|
185
|
+
kind="boolean",
|
|
186
|
+
group="tools",
|
|
187
|
+
describe="Disable every built-in tool for this run.",
|
|
188
|
+
),
|
|
189
|
+
GroupedFlagSpec(
|
|
190
|
+
name="--mcp",
|
|
191
|
+
kind="list",
|
|
192
|
+
group="tools",
|
|
193
|
+
describe="Attach an external MCP server endpoint (comma-separated or repeated).",
|
|
194
|
+
),
|
|
195
|
+
# ---- Meta -------------------------------------------------------------
|
|
196
|
+
GroupedFlagSpec(
|
|
197
|
+
name="--help",
|
|
198
|
+
aliases=("-h",),
|
|
199
|
+
kind="boolean",
|
|
200
|
+
group="meta",
|
|
201
|
+
describe="Show this usage and exit.",
|
|
202
|
+
),
|
|
203
|
+
GroupedFlagSpec(
|
|
204
|
+
name="--version",
|
|
205
|
+
aliases=("-v",),
|
|
206
|
+
kind="boolean",
|
|
207
|
+
group="meta",
|
|
208
|
+
describe="Show the version and exit.",
|
|
209
|
+
),
|
|
210
|
+
)
|