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.
Files changed (167) hide show
  1. induscode/__init__.py +56 -0
  2. induscode/addons/__init__.py +176 -0
  3. induscode/addons/contract.py +923 -0
  4. induscode/addons/dispatch/__init__.py +43 -0
  5. induscode/addons/dispatch/event_dispatcher.py +348 -0
  6. induscode/addons/dispatch/tool_interceptor.py +349 -0
  7. induscode/addons/host.py +469 -0
  8. induscode/addons/loader.py +314 -0
  9. induscode/addons/manifest.py +232 -0
  10. induscode/addons/surface.py +199 -0
  11. induscode/boot/__init__.py +108 -0
  12. induscode/boot/auth_vault.py +323 -0
  13. induscode/boot/boot.py +210 -0
  14. induscode/boot/contract.py +223 -0
  15. induscode/boot/invocation.py +117 -0
  16. induscode/boot/runners/__init__.py +42 -0
  17. induscode/boot/runners/link_runner.py +82 -0
  18. induscode/boot/runners/oneshot_runner.py +85 -0
  19. induscode/boot/runners/registry.py +46 -0
  20. induscode/boot/runners/repl_runner.py +340 -0
  21. induscode/boot/runners/session.py +549 -0
  22. induscode/boot/stages.py +198 -0
  23. induscode/boot/upgrade/__init__.py +36 -0
  24. induscode/boot/upgrade/apply.py +125 -0
  25. induscode/boot/upgrade/upgrades.py +136 -0
  26. induscode/briefing/__init__.py +115 -0
  27. induscode/briefing/compose.py +414 -0
  28. induscode/briefing/contract.py +528 -0
  29. induscode/briefing/macros.py +721 -0
  30. induscode/briefing/skills.py +417 -0
  31. induscode/capability_deck/__init__.py +233 -0
  32. induscode/capability_deck/bridge_ledger/__init__.py +66 -0
  33. induscode/capability_deck/bridge_ledger/key.py +181 -0
  34. induscode/capability_deck/bridge_ledger/ledger.py +276 -0
  35. induscode/capability_deck/bridge_ledger/network.py +336 -0
  36. induscode/capability_deck/builtin_bridge.py +358 -0
  37. induscode/capability_deck/cards/__init__.py +116 -0
  38. induscode/capability_deck/cards/bg_process.py +482 -0
  39. induscode/capability_deck/cards/memory.py +226 -0
  40. induscode/capability_deck/cards/saas.py +280 -0
  41. induscode/capability_deck/cards/task.py +256 -0
  42. induscode/capability_deck/cards/todo.py +312 -0
  43. induscode/capability_deck/contract.py +450 -0
  44. induscode/capability_deck/manifest.py +126 -0
  45. induscode/capability_deck/provision.py +217 -0
  46. induscode/channels/__init__.py +146 -0
  47. induscode/channels/contract.py +585 -0
  48. induscode/channels/framer.py +132 -0
  49. induscode/channels/link/__init__.py +50 -0
  50. induscode/channels/link/dialog.py +246 -0
  51. induscode/channels/link/driver.py +308 -0
  52. induscode/channels/link/server.py +217 -0
  53. induscode/channels/oneshot.py +178 -0
  54. induscode/channels/ops.py +140 -0
  55. induscode/channels/session_ops.py +172 -0
  56. induscode/conductor/__init__.py +240 -0
  57. induscode/conductor/catalog.py +309 -0
  58. induscode/conductor/conductor.py +1084 -0
  59. induscode/conductor/contract.py +1035 -0
  60. induscode/conductor/matcher.py +291 -0
  61. induscode/conductor/serialize.py +575 -0
  62. induscode/conductor/signal_hub.py +382 -0
  63. induscode/conductor/skill_parse.py +294 -0
  64. induscode/conductor/transcript_store.py +449 -0
  65. induscode/console/__init__.py +236 -0
  66. induscode/console/app.py +1677 -0
  67. induscode/console/components/__init__.py +62 -0
  68. induscode/console/components/banner.py +499 -0
  69. induscode/console/components/banner_sweep.py +188 -0
  70. induscode/console/components/emblem.py +181 -0
  71. induscode/console/components/status_bar.py +102 -0
  72. induscode/console/contract.py +836 -0
  73. induscode/console/input/__init__.py +107 -0
  74. induscode/console/input/chord.py +197 -0
  75. induscode/console/input/dir_reader.py +113 -0
  76. induscode/console/input/intents.py +258 -0
  77. induscode/console/input/providers.py +469 -0
  78. induscode/console/mount.py +137 -0
  79. induscode/console/overlays/__init__.py +94 -0
  80. induscode/console/overlays/auth.py +503 -0
  81. induscode/console/overlays/pickers.py +526 -0
  82. induscode/console/overlays/router.py +129 -0
  83. induscode/console/overlays/sessions.py +232 -0
  84. induscode/console/reducer.py +145 -0
  85. induscode/console/resume_picker.py +156 -0
  86. induscode/console/slash_commands/__init__.py +78 -0
  87. induscode/console/slash_commands/builtins.py +254 -0
  88. induscode/console/slash_commands/dynamic.py +217 -0
  89. induscode/console/slash_commands/integrations.py +949 -0
  90. induscode/console/slash_commands/transcript.py +404 -0
  91. induscode/console/slash_commands/workbench.py +430 -0
  92. induscode/console/startup.py +434 -0
  93. induscode/console/theme/__init__.py +44 -0
  94. induscode/console/theme/adapter.py +168 -0
  95. induscode/console/theme/palette.py +128 -0
  96. induscode/console/theme/resolve.py +123 -0
  97. induscode/console/theme/tokens.py +185 -0
  98. induscode/console_slash/__init__.py +111 -0
  99. induscode/console_slash/contract.py +185 -0
  100. induscode/console_slash/registry.py +140 -0
  101. induscode/console_slash/resolve.py +194 -0
  102. induscode/console_slash/shared.py +172 -0
  103. induscode/entry.py +108 -0
  104. induscode/insight/__init__.py +153 -0
  105. induscode/insight/collector.py +73 -0
  106. induscode/insight/replay.py +305 -0
  107. induscode/insight/wrapper.py +1115 -0
  108. induscode/kit/__init__.py +82 -0
  109. induscode/kit/clipboard_image.py +215 -0
  110. induscode/kit/external_editor.py +120 -0
  111. induscode/kit/image.py +188 -0
  112. induscode/kit/shell.py +89 -0
  113. induscode/kit/tool_fetch.py +288 -0
  114. induscode/launch/__init__.py +224 -0
  115. induscode/launch/catalog.py +310 -0
  116. induscode/launch/contract.py +569 -0
  117. induscode/launch/credentials.py +852 -0
  118. induscode/launch/invocation/__init__.py +39 -0
  119. induscode/launch/invocation/attachments.py +281 -0
  120. induscode/launch/invocation/flags.py +210 -0
  121. induscode/launch/invocation/read.py +369 -0
  122. induscode/launch/invocation/usage.py +110 -0
  123. induscode/launch/oauth.py +808 -0
  124. induscode/launch/packages.py +299 -0
  125. induscode/launch/pickers.py +291 -0
  126. induscode/py.typed +0 -0
  127. induscode/runtime_bridge/__init__.py +166 -0
  128. induscode/runtime_bridge/bridges/__init__.py +66 -0
  129. induscode/runtime_bridge/bridges/_drive.py +268 -0
  130. induscode/runtime_bridge/bridges/builtins.py +177 -0
  131. induscode/runtime_bridge/bridges/claude_cli.py +198 -0
  132. induscode/runtime_bridge/bridges/codex_cli.py +203 -0
  133. induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
  134. induscode/runtime_bridge/broker.py +397 -0
  135. induscode/runtime_bridge/contract.py +734 -0
  136. induscode/runtime_bridge/sink.py +351 -0
  137. induscode/sessions/__init__.py +25 -0
  138. induscode/sessions/contract.py +119 -0
  139. induscode/sessions/library.py +350 -0
  140. induscode/settings/__init__.py +47 -0
  141. induscode/settings/contract.py +313 -0
  142. induscode/settings/manager.py +268 -0
  143. induscode/transcript_export/__init__.py +109 -0
  144. induscode/transcript_export/contract.py +522 -0
  145. induscode/transcript_export/publish.py +455 -0
  146. induscode/transcript_export/sgr.py +566 -0
  147. induscode/transcript_export/template.py +319 -0
  148. induscode/transcript_export/theme_bridge.py +325 -0
  149. induscode/window_budget/__init__.py +76 -0
  150. induscode/window_budget/budget/__init__.py +26 -0
  151. induscode/window_budget/budget/estimate.py +273 -0
  152. induscode/window_budget/budget/gate.py +60 -0
  153. induscode/window_budget/budget/slice.py +145 -0
  154. induscode/window_budget/condenser.py +170 -0
  155. induscode/window_budget/contract.py +329 -0
  156. induscode/window_budget/summarize/__init__.py +33 -0
  157. induscode/window_budget/summarize/condense.py +212 -0
  158. induscode/window_budget/summarize/prompt.py +241 -0
  159. induscode/workspace/__init__.py +30 -0
  160. induscode/workspace/brand.py +96 -0
  161. induscode/workspace/locator.py +269 -0
  162. induscode-0.1.0.dist-info/METADATA +97 -0
  163. induscode-0.1.0.dist-info/RECORD +167 -0
  164. induscode-0.1.0.dist-info/WHEEL +4 -0
  165. induscode-0.1.0.dist-info/entry_points.txt +3 -0
  166. induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
  167. 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
+ )