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,310 @@
1
+ """Model catalog printer — :func:`print_model_catalog`.
2
+
3
+ Renders the available models as a padded text table. The data comes from the
4
+ conductor's :class:`~induscode.conductor.ModelCatalog` (itself a normalized
5
+ view over the framework :func:`indusagi.ai.get_providers` /
6
+ :func:`indusagi.ai.get_models`); this module re-derives the rows it needs,
7
+ applies a plain case-insensitive :class:`~.contract.CatalogFilter`, sorts by
8
+ provider then model id, and lays the result out as aligned columns with a
9
+ Title-Case header and a rule row beneath it.
10
+
11
+ Filtering is a literal substring test — no fuzzy matcher, no external
12
+ ranking. The column set (provider, model, context window, max output,
13
+ thinking, images) is the established model-table shape; only the headers and
14
+ ordering are this module's own.
15
+
16
+ Port note: the TS module sat on the framework ``ModelRegistry``; the port
17
+ plan routes the printer through the conductor catalog so there is exactly one
18
+ catalog path in the app (plan §3 "Model catalog"). The injectable
19
+ :class:`CatalogModelSource` seam keeps the TS test shape: a test hands in a
20
+ plain provider→models map and the layout is exercised deterministically.
21
+
22
+ (Port of TS ``src/launch/catalog.ts``.)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import math
28
+ import sys
29
+ from collections.abc import Mapping, Sequence
30
+ from dataclasses import dataclass
31
+ from typing import Any, Final, Protocol
32
+
33
+ from ..conductor import ModelCatalog
34
+ from .contract import CatalogFilter
35
+
36
+ __all__ = [
37
+ "CatalogIo",
38
+ "CatalogModelSource",
39
+ "catalog_source",
40
+ "default_catalog_io",
41
+ "print_model_catalog",
42
+ ]
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Output seam
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class CatalogIo(Protocol):
51
+ """The minimal sink the catalog prints onto. A test captures the lines
52
+ into a list; production passes a writer over stdout."""
53
+
54
+ def print(self, line: str) -> None:
55
+ """Emit one line of table text."""
56
+ ...
57
+
58
+
59
+ class _DefaultCatalogIo:
60
+ """The default :class:`CatalogIo` backed by stdout."""
61
+
62
+ def print(self, line: str) -> None:
63
+ sys.stdout.write(line if line.endswith("\n") else line + "\n")
64
+
65
+
66
+ def default_catalog_io() -> CatalogIo:
67
+ """Build the live stdout-backed :class:`CatalogIo`."""
68
+ return _DefaultCatalogIo()
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Row projection
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ @dataclass(frozen=True, slots=True)
77
+ class _CatalogRow:
78
+ """One normalized table row, derived from a framework model record."""
79
+
80
+ provider: str
81
+ model_id: str
82
+ context_window: int
83
+ max_output: int
84
+ thinking: bool
85
+ images: bool
86
+
87
+
88
+ #: The table column headers, in render order.
89
+ _HEADERS: Final[tuple[str, ...]] = (
90
+ "Provider",
91
+ "Model",
92
+ "Context",
93
+ "Max Output",
94
+ "Thinking",
95
+ "Images",
96
+ )
97
+
98
+ #: Sentinel marking "field not present at all" on a raw record.
99
+ _ABSENT: Final[Any] = object()
100
+
101
+
102
+ def _field_of(raw: Any, name: str) -> Any:
103
+ """Read ``name`` off a raw model record — mapping key or dataclass
104
+ attribute — returning the absent sentinel when the field does not exist.
105
+ Mirrors the conductor catalog's tolerant accessor so test fakes can be
106
+ plain mappings."""
107
+ if isinstance(raw, Mapping):
108
+ return raw.get(name, _ABSENT)
109
+ return getattr(raw, name, _ABSENT)
110
+
111
+
112
+ def _as_int(value: Any) -> int:
113
+ """A number field as an int, 0 for anything non-numeric."""
114
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
115
+ return 0
116
+ return int(value)
117
+
118
+
119
+ def _to_row(model: Any) -> _CatalogRow:
120
+ """Project a framework model record down to a :class:`_CatalogRow`."""
121
+ raw_inputs = _field_of(model, "input")
122
+ inputs = (
123
+ list(raw_inputs)
124
+ if isinstance(raw_inputs, Sequence) and not isinstance(raw_inputs, (str, bytes))
125
+ else []
126
+ )
127
+ return _CatalogRow(
128
+ provider=str(_field_of(model, "provider")),
129
+ model_id=str(_field_of(model, "id")),
130
+ context_window=_as_int(_field_of(model, "contextWindow")),
131
+ max_output=_as_int(_field_of(model, "maxTokens")),
132
+ thinking=_field_of(model, "reasoning") is True,
133
+ images="image" in inputs,
134
+ )
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Filtering & sorting
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def _filter_rows(
143
+ rows: Sequence[_CatalogRow], filter: CatalogFilter
144
+ ) -> list[_CatalogRow]:
145
+ """Keep the rows that satisfy every set field of ``filter``. An absent
146
+ field matches everything; ``search`` is a case-insensitive substring test
147
+ over ``"provider/modelId"``."""
148
+ provider_needle = (
149
+ str(filter.provider).lower() if filter.provider is not None else None
150
+ )
151
+ search_needle = filter.search.strip().lower() if filter.search is not None else None
152
+ selected: list[_CatalogRow] = []
153
+ for row in rows:
154
+ if provider_needle is not None and row.provider.lower() != provider_needle:
155
+ continue
156
+ if filter.thinking_only is True and not row.thinking:
157
+ continue
158
+ if filter.images_only is True and not row.images:
159
+ continue
160
+ if search_needle:
161
+ haystack = f"{row.provider}/{row.model_id}".lower()
162
+ if search_needle not in haystack:
163
+ continue
164
+ selected.append(row)
165
+ return selected
166
+
167
+
168
+ def _sort_rows(rows: Sequence[_CatalogRow]) -> list[_CatalogRow]:
169
+ """Order rows by provider, then by model id, both ascending."""
170
+ return sorted(rows, key=lambda row: (row.provider, row.model_id))
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # Formatting
175
+ # ---------------------------------------------------------------------------
176
+
177
+
178
+ def _format_token_count(tokens: int) -> str:
179
+ """Format a token count compactly: ``128K`` / ``1.0M`` for large windows,
180
+ the bare number otherwise. A dash stands in for an unknown (zero)
181
+ count."""
182
+ if tokens <= 0:
183
+ return "-"
184
+ if tokens >= 1_000_000:
185
+ millions = tokens / 1_000_000
186
+ rendered = f"{millions:.0f}" if millions % 1 == 0 else f"{millions:.1f}"
187
+ return f"{rendered}M"
188
+ if tokens >= 1_000:
189
+ # Math.round semantics (half away from zero for positives).
190
+ return f"{math.floor(tokens / 1_000 + 0.5)}K"
191
+ return str(tokens)
192
+
193
+
194
+ def _cells(row: _CatalogRow) -> tuple[str, ...]:
195
+ """Render one row to its six string cells, in header order."""
196
+ return (
197
+ row.provider,
198
+ row.model_id,
199
+ _format_token_count(row.context_window),
200
+ _format_token_count(row.max_output),
201
+ "yes" if row.thinking else "no",
202
+ "yes" if row.images else "no",
203
+ )
204
+
205
+
206
+ def _pad(value: str, width: int) -> str:
207
+ """Pad a cell to a fixed width on the right."""
208
+ return value if len(value) >= width else value + " " * (width - len(value))
209
+
210
+
211
+ def _layout(rows: Sequence[_CatalogRow]) -> list[str]:
212
+ """Lay out a header plus body as an aligned table: compute each column
213
+ width from the widest cell, emit the Title-Case header, a rule row of
214
+ dashes, then every body row. Pure; returns the lines for the caller to
215
+ print."""
216
+ body = [_cells(row) for row in rows]
217
+ widths = [
218
+ max(len(header), *(len(row[col]) for row in body)) if body else len(header)
219
+ for col, header in enumerate(_HEADERS)
220
+ ]
221
+
222
+ def render(cells: Sequence[str]) -> str:
223
+ return " ".join(_pad(cell, widths[col]) for col, cell in enumerate(cells)).rstrip()
224
+
225
+ rule = " ".join("-" * width for width in widths)
226
+ return [render(_HEADERS), rule, *(render(row) for row in body)]
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Data source
231
+ # ---------------------------------------------------------------------------
232
+
233
+
234
+ class CatalogModelSource(Protocol):
235
+ """The seam the catalog sources its raw models from. Defaults to the
236
+ conductor :class:`~induscode.conductor.ModelCatalog`; tests inject a
237
+ stand-in to drive the layout deterministically."""
238
+
239
+ def providers(self) -> Sequence[str]:
240
+ """Enumerate the providers to scan."""
241
+ ...
242
+
243
+ def models(self, provider: str) -> Sequence[Any]:
244
+ """Pull every model for one provider."""
245
+ ...
246
+
247
+
248
+ class _ModelCatalogSource:
249
+ """A :class:`CatalogModelSource` over a conductor
250
+ :class:`~induscode.conductor.ModelCatalog`: providers come from the
251
+ catalog's grouping and each card contributes its retained framework model
252
+ record."""
253
+
254
+ def __init__(self, catalog: ModelCatalog) -> None:
255
+ self._catalog = catalog
256
+
257
+ def providers(self) -> Sequence[str]:
258
+ return self._catalog.providers()
259
+
260
+ def models(self, provider: str) -> Sequence[Any]:
261
+ return [card.model for card in self._catalog.by_provider(provider)]
262
+
263
+
264
+ def catalog_source(catalog: ModelCatalog | None = None) -> CatalogModelSource:
265
+ """Build a :class:`CatalogModelSource` over a conductor model catalog
266
+ (a fresh one over the live framework registry when none is given)."""
267
+ return _ModelCatalogSource(catalog if catalog is not None else ModelCatalog())
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # Public entry
272
+ # ---------------------------------------------------------------------------
273
+
274
+
275
+ def print_model_catalog(
276
+ io: CatalogIo | None = None,
277
+ filter: CatalogFilter | None = None,
278
+ source: CatalogModelSource | None = None,
279
+ ) -> None:
280
+ """Print the model catalog table.
281
+
282
+ Pulls every model from the source, applies ``filter``, sorts, and writes
283
+ the aligned table through ``io``. When the filter matches nothing, a
284
+ single explanatory line is printed instead of an empty table.
285
+
286
+ :param io: the line sink (defaults to stdout)
287
+ :param filter: optional provider / capability / substring narrowing
288
+ :param source: optional model source (defaults to the conductor catalog
289
+ over the live framework registry)
290
+ """
291
+ live_io = io if io is not None else default_catalog_io()
292
+ live_filter = filter if filter is not None else CatalogFilter()
293
+ live_source = source if source is not None else catalog_source()
294
+
295
+ all_rows: list[_CatalogRow] = []
296
+ for provider in live_source.providers():
297
+ try:
298
+ models = live_source.models(str(provider))
299
+ except Exception:
300
+ continue
301
+ for model in models:
302
+ all_rows.append(_to_row(model))
303
+
304
+ selected = _sort_rows(_filter_rows(all_rows, live_filter))
305
+ if not selected:
306
+ live_io.print("No models match the given filter.")
307
+ return
308
+
309
+ for line in _layout(selected):
310
+ live_io.print(line)