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,569 @@
1
+ """Launch contract — the FROZEN type surface of the command-line layer.
2
+
3
+ This module is the single typed seam between a raw ``argv`` and a fully
4
+ configured run. It owns the *application* command line: the declarative flag
5
+ table vocabulary, the parsed :class:`Invocation` those flags fold into, the
6
+ gathered ``@file`` :class:`Attachments`, and the typed :class:`CredentialFault`
7
+ the sign-in surface raises. It declares *only* shapes plus a few inert, pure
8
+ helpers — no parsing, no terminal I/O, no widgets. The reader
9
+ (:func:`~.invocation.read_invocation`), the usage renderer
10
+ (:func:`~.invocation.render_usage`), the attachment gatherer
11
+ (:func:`~.invocation.gather_attachments`), the model-catalog printer
12
+ (:func:`~.catalog.print_model_catalog`), the resume picker
13
+ (:func:`~.pickers.pick_resume_target`), the settings browser
14
+ (:func:`~.pickers.browse_settings`), and the credential command
15
+ (:func:`~.credentials.run_credential_command`) are each written against the
16
+ names declared here, so the file is intentionally small, append-mostly, and
17
+ stable.
18
+
19
+ Design stance (TS ``src/launch/contract.ts``, ported):
20
+
21
+ - There is exactly **one declarative flag table**
22
+ (:data:`~.invocation.flags.FLAG_SPECS`). The reader walks it to bind tokens;
23
+ the usage renderer generates the help text *from the same table*. Help and
24
+ parsing cannot drift, because there is no second hand-maintained help string
25
+ to drift against.
26
+ - :class:`Invocation` is a superset of the boot-layer minimal invocation: it
27
+ keeps ``mode`` / ``prompt`` / ``flags`` / ``positionals`` and adds the
28
+ resolved, strongly-typed launch fields the runtime reads.
29
+ - Failures are typed unions (:class:`CredentialFault`,
30
+ :class:`~.invocation.attachments.AttachmentError`), never string sentinels.
31
+ - The credential vault (:class:`AuthVault`) is an app-owned **Protocol** here:
32
+ the framework publishes no credential-store type, and the concrete
33
+ multi-account disk vault lands with the boot layer. The credential command
34
+ compiles and is unit-tested against an in-memory stand-in.
35
+
36
+ Port notes
37
+ ----------
38
+ - TS optional-absent fields become explicit ``None`` defaults; TS readonly
39
+ arrays become tuples on the frozen dataclasses.
40
+ - ``OAuthCredentials`` was imported from ``indusagi/ai`` in TS. The Python
41
+ framework has no provider-object OAuth surface (its primitives live in
42
+ :mod:`indusagi.llmgateway.credentials.oauth` and speak ``OAuthTokens``), so
43
+ the credential shape the *vault* stores is declared here, app-owned, with
44
+ the TS field names (``access`` / ``refresh`` / ``expires``). The launch
45
+ OAuth adapter (:mod:`.oauth`) maps framework tokens into this shape.
46
+ - Field names are the mechanical snake_case renames of the TS members
47
+ (``appendSystem`` → ``append_system``, ``noTools`` → ``no_tools``); the
48
+ loose flag bag keeps the TS canonical *flag-name* keys verbatim
49
+ (``"append-system"``, ``"no-tools"``).
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
55
+ from dataclasses import dataclass
56
+ from typing import Any, Final, Literal, Protocol, TypeAlias
57
+
58
+ from indusagi.agent import SessionInfo
59
+ from indusagi.ai import ImageContent, ThinkingLevel
60
+ from indusagi.shell_app import Settings
61
+
62
+ __all__ = [
63
+ "AttachmentOptions",
64
+ "Attachments",
65
+ "AuthVault",
66
+ "CatalogFilter",
67
+ "CredentialFault",
68
+ "CredentialFaultKind",
69
+ "CredentialVerb",
70
+ "FlagDefault",
71
+ "FlagKind",
72
+ "FlagSpec",
73
+ "FlagValue",
74
+ "ImageContent",
75
+ "Invocation",
76
+ "OAuthCredentials",
77
+ "OutputMode",
78
+ "ProviderEntry",
79
+ "ResumeFault",
80
+ "ResumeRef",
81
+ "SessionLoader",
82
+ "Settings",
83
+ "SettingsBrowseOptions",
84
+ "THINKING_EFFORTS",
85
+ "TOOL_NAMES",
86
+ "ThinkingEffort",
87
+ "ThinkingLevel",
88
+ "ToolName",
89
+ "credential_fault",
90
+ "is_output_mode",
91
+ "is_thinking_effort",
92
+ "is_tool_name",
93
+ ]
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Re-exported framework vocabulary
98
+ # ---------------------------------------------------------------------------
99
+
100
+ #: The launch-local alias for the framework session descriptor surfaced by the
101
+ #: resume picker (TS aliased ``SessionInfo as ResumeRef``).
102
+ ResumeRef: TypeAlias = SessionInfo
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Output modes
107
+ # ---------------------------------------------------------------------------
108
+
109
+ #: The three terminal output modes the launch layer can resolve to. These map
110
+ #: one-to-one onto the boot runner ids the orchestrator dispatches on:
111
+ #:
112
+ #: - ``text`` — interactive terminal or a single human-readable answer.
113
+ #: - ``json`` — a single non-interactive request whose result is structured.
114
+ #: - ``rpc`` — the headless line protocol for a driving parent process.
115
+ #:
116
+ #: ``text`` is the interactive default; ``json`` is selected by ``--print``
117
+ #: (without the interactive override), and ``rpc`` by ``--json`` / ``--rpc``.
118
+ OutputMode: TypeAlias = Literal["text", "json", "rpc"]
119
+
120
+
121
+ def is_output_mode(value: str) -> bool:
122
+ """Narrow an arbitrary string to an :data:`OutputMode`."""
123
+ return value == "text" or value == "json" or value == "rpc"
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Reasoning effort
128
+ # ---------------------------------------------------------------------------
129
+
130
+ #: One reasoning-effort rung from :data:`THINKING_EFFORTS`.
131
+ ThinkingEffort: TypeAlias = Literal["off", "minimal", "low", "medium", "high", "xhigh"]
132
+
133
+ #: The ordered reasoning-effort vocabulary accepted by ``--thinking``. ``off``
134
+ #: disables extended reasoning entirely; the remaining rungs ascend in effort.
135
+ #: Ordered so the usage text can enumerate it and :func:`is_thinking_effort`
136
+ #: can validate against it without a second list. It is a superset-compatible
137
+ #: widening of the framework :data:`~indusagi.ai.ThinkingLevel` (which omits
138
+ #: ``off``); callers that hand a level to the framework drop ``off``.
139
+ THINKING_EFFORTS: Final[tuple[ThinkingEffort, ...]] = (
140
+ "off",
141
+ "minimal",
142
+ "low",
143
+ "medium",
144
+ "high",
145
+ "xhigh",
146
+ )
147
+
148
+
149
+ def is_thinking_effort(value: str) -> bool:
150
+ """Pure membership test against :data:`THINKING_EFFORTS`; the model
151
+ resolver reuses it to parse the ``model:effort`` shorthand without
152
+ importing the parser."""
153
+ return value in THINKING_EFFORTS
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Tool roster
158
+ # ---------------------------------------------------------------------------
159
+
160
+ #: One built-in tool identifier from :data:`TOOL_NAMES`.
161
+ ToolName: TypeAlias = Literal[
162
+ "read",
163
+ "write",
164
+ "edit",
165
+ "bash",
166
+ "grep",
167
+ "find",
168
+ "ls",
169
+ "task",
170
+ "todo_read",
171
+ "todo_write",
172
+ "web_fetch",
173
+ "web_search",
174
+ "composio",
175
+ ]
176
+
177
+ #: The closed roster of built-in tool names the ``--tools`` / ``--no-tools``
178
+ #: flags select against. A literal tuple (rather than the live tool map) so
179
+ #: the launch layer can validate a ``--tools`` list before any tool module is
180
+ #: constructed.
181
+ TOOL_NAMES: Final[tuple[ToolName, ...]] = (
182
+ "read",
183
+ "write",
184
+ "edit",
185
+ "bash",
186
+ "grep",
187
+ "find",
188
+ "ls",
189
+ "task",
190
+ "todo_read",
191
+ "todo_write",
192
+ "web_fetch",
193
+ "web_search",
194
+ "composio",
195
+ )
196
+
197
+
198
+ def is_tool_name(value: str) -> bool:
199
+ """Narrow an arbitrary string to a known :data:`ToolName`."""
200
+ return value in TOOL_NAMES
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # Declarative flag table
205
+ # ---------------------------------------------------------------------------
206
+
207
+ #: The value vocabulary a flag binds to its target field.
208
+ #:
209
+ #: - ``boolean`` — a bare switch; presence sets it true (e.g. ``--print``).
210
+ #: - ``string`` — consumes the following token as text (e.g. ``--model``).
211
+ #: - ``number`` — consumes the following token and coerces to a number.
212
+ #: - ``list`` — accumulates; either repeated or one comma-separated token
213
+ #: (e.g. ``--mcp a --mcp b``, or ``--tools read,bash``).
214
+ FlagKind: TypeAlias = Literal["boolean", "string", "number", "list"]
215
+
216
+ #: The default value attached to a :class:`FlagSpec`, narrowed by
217
+ #: :data:`FlagKind`: a boolean default for switches, a string for value flags,
218
+ #: a number for numeric flags, and a string tuple for lists.
219
+ FlagDefault: TypeAlias = bool | str | float | tuple[str, ...]
220
+
221
+
222
+ @dataclass(frozen=True, slots=True)
223
+ class FlagSpec:
224
+ """One row of the single declarative flag table.
225
+
226
+ Every recognised option is described here exactly once. The reader indexes
227
+ the table by :attr:`name` and :attr:`aliases` to bind tokens to
228
+ :attr:`Invocation.flags`; the usage generator walks the same rows to render
229
+ the option reference. There is no second source of truth for either side.
230
+ """
231
+
232
+ # Canonical long spelling, leading dashes included (e.g. "--model"). This
233
+ # is also the key the parsed value lands under in Invocation.flags, sans
234
+ # the leading dashes.
235
+ name: str
236
+ # The value vocabulary this flag binds (see FlagKind).
237
+ kind: FlagKind
238
+ # One-line description rendered verbatim in the generated usage text.
239
+ describe: str
240
+ # Accepted alternate spellings — short forms ("-m") and synonyms ("--rpc"
241
+ # for "--json"). All alias hits normalise to `name`.
242
+ aliases: tuple[str, ...] = ()
243
+ # Optional default folded into Invocation.flags when the flag is absent.
244
+ # Its runtime type must match `kind`.
245
+ default: FlagDefault | None = None
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # Invocation
250
+ # ---------------------------------------------------------------------------
251
+
252
+ #: The runtime value a parsed flag can carry in :attr:`Invocation.flags`: a
253
+ #: boolean switch, a scalar value, a numeric value, or an accumulated list.
254
+ FlagValue: TypeAlias = bool | str | float | list[str]
255
+
256
+
257
+ @dataclass(frozen=True, slots=True)
258
+ class Invocation:
259
+ """The fully parsed command line — the launch layer's enrichment of the
260
+ thin boot routing shape into the complete, strongly-typed surface the
261
+ runtime reads.
262
+
263
+ The first four members (:attr:`mode`, :attr:`prompt`, :attr:`flags`,
264
+ :attr:`positionals`) are the superset of the boot-layer minimal invocation
265
+ (``positionals`` is the renamed ``rest``); everything below is the
266
+ resolved launch configuration. :attr:`flags` retains the loosely-typed bag
267
+ for extension flags and round-tripping, while the named fields give
268
+ consumers a precise, pre-coerced view of the options that matter to the
269
+ runtime. Treat every field as read-only.
270
+ """
271
+
272
+ # Resolved terminal output mode; selects the boot runner.
273
+ mode: OutputMode
274
+ # All parsed switches, keyed by canonical flag name (extension-flag escape
275
+ # hatch).
276
+ flags: Mapping[str, FlagValue]
277
+ # Positional tokens not consumed as flags (the boot layer's `rest`).
278
+ positionals: tuple[str, ...]
279
+ # First user message assembled from positionals, stdin, and attachments.
280
+ prompt: str | None = None
281
+ # `@file` arguments expanded to inline prose plus base64 media, if any.
282
+ attachments: Attachments | None = None
283
+ # Explicit model selector (--model / -m), provider-qualified or bare.
284
+ model: str | None = None
285
+ # Named credential account to authenticate the run with (--account).
286
+ account: str | None = None
287
+ # Working directory the run is scoped to (--cwd); None means process cwd.
288
+ cwd: str | None = None
289
+ # Replacement system prompt (--system); None keeps the built-in.
290
+ system: str | None = None
291
+ # Extra text appended after the system prompt (--append-system).
292
+ append_system: str | None = None
293
+ # Reasoning-effort rung requested via --thinking.
294
+ thinking: ThinkingEffort | None = None
295
+ # Explicit tool allow-list (--tools); None means every built-in tool.
296
+ tools: tuple[ToolName, ...] | None = None
297
+ # Disable all tools for this run (--no-tools).
298
+ no_tools: bool = False
299
+ # External MCP server endpoints to attach (--mcp, repeatable/comma-joined).
300
+ mcp: tuple[str, ...] = ()
301
+ # Run a single request and exit, printing only the result (--print / -p).
302
+ print: bool = False
303
+ # Force the interactive REPL even alongside a prompt (--interactive / -i).
304
+ interactive: bool = False
305
+ # Asking for the usage banner (--help / -h).
306
+ help: bool = False
307
+ # Asking for the version string (--version / -v).
308
+ version: bool = False
309
+
310
+
311
+ # ---------------------------------------------------------------------------
312
+ # Attachments
313
+ # ---------------------------------------------------------------------------
314
+
315
+
316
+ @dataclass(frozen=True, slots=True)
317
+ class Attachments:
318
+ """The result of expanding the ``@file`` arguments collected on the
319
+ command line.
320
+
321
+ Text files are inlined into :attr:`prose` (each wrapped in a delimited
322
+ block keyed by its path); image files are decoded to framework
323
+ :class:`~indusagi.ai.ImageContent` and collected in :attr:`media`. Both
324
+ are concatenated onto the first user message. An invocation with no
325
+ ``@file`` arguments has no :class:`Attachments` at all rather than an
326
+ empty one.
327
+ """
328
+
329
+ # Concatenated text of every inlined file, each wrapped with its path.
330
+ prose: str
331
+ # Base64 image content decoded from every image @file argument.
332
+ media: tuple[ImageContent, ...]
333
+
334
+
335
+ @dataclass(frozen=True, slots=True)
336
+ class AttachmentOptions:
337
+ """Options for :func:`~.invocation.gather_attachments`: the cwd that
338
+ ``@file`` references are resolved against."""
339
+
340
+ # Working directory @file references are resolved relative to.
341
+ cwd: str
342
+
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # Credential command
346
+ # ---------------------------------------------------------------------------
347
+
348
+ #: The two verbs the credential command recognises as the first positional
349
+ #: token. Anything else means the command does not own this invocation and the
350
+ #: caller proceeds to normal launch.
351
+ #:
352
+ #: - ``signin`` — validate and store a credential for a provider / account.
353
+ #: - ``signout`` — remove a stored credential for a provider / account.
354
+ CredentialVerb: TypeAlias = Literal["signin", "signout"]
355
+
356
+
357
+ @dataclass(frozen=True, slots=True)
358
+ class ProviderEntry:
359
+ """A provider entry in the credential directory — the facts the sign-in
360
+ prompts print and validate against. :attr:`env_key` is the conventional
361
+ environment variable :func:`indusagi.ai.get_env_api_key` reads;
362
+ :attr:`docs_url` is where the user obtains a key. These are external
363
+ provider conventions, not derived data."""
364
+
365
+ # Stable provider id matching the framework provider vocabulary.
366
+ id: str
367
+ # Human-facing provider label for menus and prompts.
368
+ label: str
369
+ # Conventional api-key environment variable for this provider.
370
+ env_key: str
371
+ # Page where a user obtains an api key for this provider.
372
+ docs_url: str
373
+
374
+
375
+ #: The closed set of failure categories the credential command can raise.
376
+ #: A consumer switches on :attr:`CredentialFault.kind`, never on message text:
377
+ #:
378
+ #: - ``unknown-provider`` — the named provider is not in the directory.
379
+ #: - ``invalid-key`` — the supplied key failed format validation.
380
+ #: - ``invalid-account`` — the account name failed the naming rules.
381
+ #: - ``name-collision`` — the account name already exists for the provider.
382
+ #: - ``not-found`` — sign-out targeted a credential not stored.
383
+ #: - ``vault`` — the underlying credential store failed.
384
+ #: - ``aborted`` — the user cancelled an interactive prompt.
385
+ CredentialFaultKind: TypeAlias = Literal[
386
+ "unknown-provider",
387
+ "invalid-key",
388
+ "invalid-account",
389
+ "name-collision",
390
+ "not-found",
391
+ "vault",
392
+ "aborted",
393
+ ]
394
+
395
+
396
+ @dataclass(frozen=True, slots=True)
397
+ class CredentialFault:
398
+ """A typed credential failure. :attr:`kind` drives recovery; :attr:`hint`
399
+ carries a single actionable next step for the human (e.g. the env-var to
400
+ set), and :attr:`cause` preserves any wrapped error for diagnostics."""
401
+
402
+ # The failure category (the discriminant).
403
+ kind: CredentialFaultKind
404
+ # Human-readable summary of what failed.
405
+ message: str
406
+ # Optional single actionable suggestion for resolving the fault.
407
+ hint: str | None = None
408
+ # Optional wrapped underlying error.
409
+ cause: object | None = None
410
+
411
+
412
+ def credential_fault(
413
+ kind: CredentialFaultKind,
414
+ message: str,
415
+ *,
416
+ hint: str | None = None,
417
+ cause: object | None = None,
418
+ ) -> CredentialFault:
419
+ """Construct a :class:`CredentialFault`. A tiny inert helper so call sites
420
+ raise a well-formed typed fault without re-spelling the shape."""
421
+ return CredentialFault(kind=kind, message=message, hint=hint, cause=cause)
422
+
423
+
424
+ @dataclass(frozen=True, slots=True)
425
+ class OAuthCredentials:
426
+ """Browser-sign-in credentials as the *vault* stores them (the TS
427
+ ``indusagi/ai`` ``OAuthCredentials`` shape, app-owned in the port — see
428
+ the module docstring). ``expires`` is an absolute epoch-millisecond
429
+ deadline, matching the framework's ``OAuthTokens.expires_at``."""
430
+
431
+ # The live access token.
432
+ access: str
433
+ # The refresh token, when the provider issued one.
434
+ refresh: str | None = None
435
+ # Absolute epoch-millisecond expiry deadline, when known.
436
+ expires: int | None = None
437
+
438
+
439
+ class AuthVault(Protocol):
440
+ """The credential-vault surface the credential command depends on.
441
+
442
+ The framework publishes no credential-store type, so the launch contract
443
+ forward-declares the slice it needs: per-provider, per-account records
444
+ keyed by an account name. A record stores *either* an api key or a set of
445
+ browser-sign-in credentials; the vault refreshes an expired browser token
446
+ before yielding a usable key. The boot layer supplies the concrete
447
+ multi-account disk vault; this Protocol lets the credential command be
448
+ unit-tested against an in-memory stand-in before then.
449
+ """
450
+
451
+ async def list_accounts(self, provider: str) -> list[str]:
452
+ """Stored account names for a provider, in insertion order."""
453
+ ...
454
+
455
+ async def default_account(self, provider: str) -> str | None:
456
+ """The default account name for a provider, if one is set."""
457
+ ...
458
+
459
+ async def put_api_key(
460
+ self,
461
+ provider: str,
462
+ account: str,
463
+ api_key: str,
464
+ make_default: bool = False,
465
+ ) -> None:
466
+ """Persist an api key under a provider / account, optionally as the
467
+ default."""
468
+ ...
469
+
470
+ async def put_oauth(
471
+ self,
472
+ provider: str,
473
+ account: str,
474
+ credentials: OAuthCredentials,
475
+ make_default: bool = False,
476
+ ) -> None:
477
+ """Persist browser-sign-in credentials under a provider / account,
478
+ optionally as the default."""
479
+ ...
480
+
481
+ async def auth_kind(
482
+ self, provider: str, account: str
483
+ ) -> Literal["apiKey", "oauth"] | None:
484
+ """Report whether a stored account holds an api key or browser-sign-in
485
+ credentials, or ``None`` when nothing is stored there."""
486
+ ...
487
+
488
+ async def read_usable_key(self, provider: str, account: str) -> str | None:
489
+ """Resolve a stored account to a live api-key string. An api-key
490
+ record yields its key verbatim; a browser-sign-in record is refreshed
491
+ (persisting any rotated token) before its usable key is returned.
492
+ Resolves ``None`` when nothing usable is stored."""
493
+ ...
494
+
495
+ async def remove(self, provider: str, account: str | None = None) -> bool:
496
+ """Remove a stored credential; resolves False when nothing was
497
+ removed."""
498
+ ...
499
+
500
+
501
+ # ---------------------------------------------------------------------------
502
+ # Model catalog
503
+ # ---------------------------------------------------------------------------
504
+
505
+
506
+ @dataclass(frozen=True, slots=True)
507
+ class CatalogFilter:
508
+ """The filter applied when rendering the ``--list-models`` table. Every
509
+ field is optional; an absent field matches everything. :attr:`search` is a
510
+ plain case-insensitive substring test over the provider/model identifier —
511
+ no fuzzy matcher and no external ranking."""
512
+
513
+ # Restrict to a single provider id.
514
+ provider: str | None = None
515
+ # Keep only models that advertise a reasoning budget.
516
+ thinking_only: bool | None = None
517
+ # Keep only models that accept image input.
518
+ images_only: bool | None = None
519
+ # Case-insensitive substring filter over the model identifier.
520
+ search: str | None = None
521
+
522
+
523
+ # ---------------------------------------------------------------------------
524
+ # Resume picker
525
+ # ---------------------------------------------------------------------------
526
+
527
+ #: A function that loads a set of resumable sessions. The resume picker takes
528
+ #: two: one for the current working directory and one for every directory,
529
+ #: both shaped like the framework session lister.
530
+ SessionLoader: TypeAlias = Callable[[], Awaitable[Sequence[ResumeRef]]]
531
+
532
+
533
+ @dataclass(frozen=True, slots=True)
534
+ class ResumeFault:
535
+ """A launch-time error from the resume flow (the picker failing to mount,
536
+ or a session store read fault). Typed so the orchestrator can fall back
537
+ to a fresh session rather than crash."""
538
+
539
+ # Human-readable summary of what failed.
540
+ message: str
541
+ # Optional wrapped underlying error.
542
+ cause: object | None = None
543
+
544
+
545
+ # ---------------------------------------------------------------------------
546
+ # Settings browser
547
+ # ---------------------------------------------------------------------------
548
+
549
+
550
+ @dataclass(frozen=True, slots=True)
551
+ class SettingsBrowseOptions:
552
+ """Options for :func:`~.pickers.browse_settings`: the resolved framework
553
+ settings, the directories of user-authored resources to enumerate, the
554
+ active cwd, and the profile directory. The browser renders a plain console
555
+ listing of these — no TUI."""
556
+
557
+ # The merged, resolved framework settings.
558
+ settings: Settings
559
+ # Resolved absolute paths of discovered resources, grouped by category.
560
+ resolved_paths: Mapping[str, Sequence[str]]
561
+ # The active working directory.
562
+ cwd: str
563
+ # The profile directory the settings were loaded from.
564
+ profile_dir: str
565
+
566
+
567
+ # Quiet "imported but unused" for the re-exported framework names: they are
568
+ # part of this contract's public surface (`__all__`).
569
+ _REEXPORTS: Final[tuple[Any, ...]] = (ImageContent, ThinkingLevel, Settings, SessionInfo)