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,309 @@
1
+ """Model catalog — :class:`ModelCatalog` (port of TS ``src/conductor/catalog/catalog.ts``).
2
+
3
+ A transform pipeline over the framework model list. The framework publishes
4
+ its catalog per provider via :func:`indusagi.ai.get_models`; each entry is a
5
+ rich :class:`indusagi.ai.Model` record. This module **re-derives** that data
6
+ into the conductor's own normalized shape — a :class:`CatalogCard` — through a
7
+ validate → normalize → key pipeline, then exposes a small lookup surface
8
+ (:meth:`ModelCatalog.all`, :meth:`ModelCatalog.by_provider`,
9
+ :meth:`ModelCatalog.get`) keyed by a canonical ``"provider/modelId"`` id.
10
+
11
+ The full framework :class:`~indusagi.ai.Model` is retained on each card so the
12
+ conductor can hand a complete model object to the agent without a second
13
+ registry round-trip.
14
+
15
+ Port notes
16
+ ----------
17
+ - The TS zod gate (``rawModelGate.safeParse``) becomes a **manual tolerant
18
+ probe** (:func:`_clears_gate`): a malformed provider record is silently
19
+ DROPPED, never raised on. pydantic is deliberately not pulled in for six
20
+ field checks. # parity: the gate drops invalid cards — it must never throw.
21
+ - Raw records may be the framework's frozen :class:`~indusagi.ai.Model`
22
+ dataclasses (the live source) *or* plain mappings (test sources); every
23
+ field read goes through the tolerant :func:`_field_of` accessor, mirroring
24
+ how zod validated plain objects structurally.
25
+ - TS ``z.<type>().optional()`` admits *undefined* but rejects *null*; the
26
+ Python probe mirrors that by distinguishing an absent field (passes) from a
27
+ present ``None`` (rejects).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from collections.abc import Callable, Mapping, Sequence
33
+ from dataclasses import dataclass
34
+ from typing import Any
35
+
36
+ from indusagi.ai import get_models, get_providers
37
+
38
+ from induscode.conductor.contract import ModelCardRef
39
+
40
+ __all__ = [
41
+ "CatalogCard",
42
+ "CatalogSource",
43
+ "ModelCatalog",
44
+ "canonical_id",
45
+ "to_card_ref",
46
+ ]
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Card shape
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class CatalogCard:
56
+ """One normalized entry in the catalog (TS ``CatalogCard``).
57
+
58
+ Extends the lightweight :class:`~induscode.conductor.contract.ModelCardRef`
59
+ identity projection with the extra capability facets the matcher filters
60
+ on, plus the original framework ``Model``. ``ref`` is the slice a UI
61
+ lists/labels with; ``model`` is the full record the conductor binds to the
62
+ agent. Field names keep the TS spelling.
63
+ """
64
+
65
+ # Canonical "provider/modelId" identifier — the catalog key.
66
+ id: str
67
+ # Owning provider slug.
68
+ provider: str
69
+ # Provider-scoped model id (e.g. "claude-sonnet-4").
70
+ modelId: str
71
+ # Human-readable display name.
72
+ name: str
73
+ # Whether the model exposes a reasoning/thinking budget.
74
+ reasoning: bool
75
+ # Whether the model accepts image input.
76
+ acceptsImages: bool
77
+ # Context window size in tokens (0 when the framework omits it).
78
+ contextTokens: int
79
+ # The framework model record, retained verbatim for binding.
80
+ model: Any
81
+
82
+
83
+ def to_card_ref(card: CatalogCard) -> ModelCardRef:
84
+ """Project a :class:`CatalogCard` down to its public ``ModelCardRef`` slice."""
85
+ return ModelCardRef(
86
+ id=card.id,
87
+ provider=card.provider,
88
+ modelId=card.modelId,
89
+ name=card.name,
90
+ reasoning=card.reasoning,
91
+ )
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Validation gate (the zod gate, hand-probed)
96
+ # ---------------------------------------------------------------------------
97
+
98
+ #: Sentinel marking "field not present at all" (TS ``undefined``).
99
+ _ABSENT: Any = object()
100
+
101
+
102
+ def _field_of(raw: Any, name: str) -> Any:
103
+ """Read ``name`` off a raw record — mapping key or dataclass attribute —
104
+ returning :data:`_ABSENT` when the field does not exist."""
105
+ if isinstance(raw, Mapping):
106
+ return raw.get(name, _ABSENT)
107
+ return getattr(raw, name, _ABSENT)
108
+
109
+
110
+ def _is_nonempty_str(value: Any) -> bool:
111
+ return isinstance(value, str) and len(value) >= 1
112
+
113
+
114
+ def _clears_gate(raw: Any) -> bool:
115
+ """The minimal slice of a framework ``Model`` the catalog depends on.
116
+
117
+ Run every raw record through this gate so a malformed provider entry is
118
+ dropped rather than poisoning lookups downstream. Unknown extra fields are
119
+ tolerated (only named keys are read). Mirrors the TS zod gate exactly:
120
+
121
+ - ``id`` / ``name`` / ``provider``: required non-empty strings
122
+ (``provider`` admits any non-empty string, matching ``z.string()`` over
123
+ the ``KnownProvider | string`` union);
124
+ - ``reasoning``: optional bool;
125
+ - ``input``: optional array of strings;
126
+ - ``contextWindow``: optional number.
127
+ """
128
+ if raw is None or isinstance(raw, (str, int, float, bool)):
129
+ return False
130
+ if not _is_nonempty_str(_field_of(raw, "id")):
131
+ return False
132
+ if not _is_nonempty_str(_field_of(raw, "name")):
133
+ return False
134
+ if not _is_nonempty_str(_field_of(raw, "provider")):
135
+ return False
136
+ reasoning = _field_of(raw, "reasoning")
137
+ if reasoning is not _ABSENT and not isinstance(reasoning, bool):
138
+ return False
139
+ inputs = _field_of(raw, "input")
140
+ if inputs is not _ABSENT:
141
+ if not isinstance(inputs, Sequence) or isinstance(inputs, (str, bytes)):
142
+ return False
143
+ if not all(isinstance(item, str) for item in inputs):
144
+ return False
145
+ context_window = _field_of(raw, "contextWindow")
146
+ if context_window is not _ABSENT and (
147
+ isinstance(context_window, bool) or not isinstance(context_window, (int, float))
148
+ ):
149
+ return False
150
+ return True
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Pipeline helpers
155
+ # ---------------------------------------------------------------------------
156
+
157
+
158
+ def canonical_id(provider: str, model_id: str) -> str:
159
+ """Compose the canonical catalog key from a provider + model id."""
160
+ return f"{provider}/{model_id}"
161
+
162
+
163
+ def _normalize_card(raw: Any) -> CatalogCard:
164
+ """Normalize one validated raw record into a :class:`CatalogCard`.
165
+
166
+ Pure: the same input always yields the same card. Capability flags are
167
+ derived defensively from the (optional) framework fields.
168
+ """
169
+ provider = str(_field_of(raw, "provider"))
170
+ model_id = _field_of(raw, "id")
171
+ raw_inputs = _field_of(raw, "input")
172
+ inputs = (
173
+ list(raw_inputs)
174
+ if isinstance(raw_inputs, Sequence) and not isinstance(raw_inputs, (str, bytes))
175
+ else []
176
+ )
177
+ context_window = _field_of(raw, "contextWindow")
178
+ return CatalogCard(
179
+ id=canonical_id(provider, model_id),
180
+ provider=provider,
181
+ modelId=model_id,
182
+ name=_field_of(raw, "name"),
183
+ reasoning=_field_of(raw, "reasoning") is True,
184
+ acceptsImages="image" in inputs,
185
+ contextTokens=(
186
+ int(context_window)
187
+ if isinstance(context_window, (int, float)) and not isinstance(context_window, bool)
188
+ else 0
189
+ ),
190
+ model=raw,
191
+ )
192
+
193
+
194
+ def _build_index(
195
+ providers: Sequence[str],
196
+ pull: Callable[[str], Sequence[Any]],
197
+ ) -> dict[str, CatalogCard]:
198
+ """The transform pipeline: pull every provider's raw model list, validate,
199
+ drop rejects, normalize, and de-duplicate by canonical id (first wins).
200
+ The result is a stable, insertion-ordered map."""
201
+ index: dict[str, CatalogCard] = {}
202
+ for provider in providers:
203
+ try:
204
+ raws = pull(str(provider))
205
+ except Exception:
206
+ continue # a provider that can't be enumerated is simply absent
207
+ for raw in raws:
208
+ if not _clears_gate(raw):
209
+ continue
210
+ card = _normalize_card(raw)
211
+ if card.id not in index:
212
+ index[card.id] = card
213
+ return index
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # ModelCatalog
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ @dataclass(frozen=True, slots=True)
222
+ class CatalogSource:
223
+ """Injection seam for tests: how the catalog sources its raw data
224
+ (TS ``CatalogSource``)."""
225
+
226
+ # Enumerate the providers to scan.
227
+ providers: Callable[[], Sequence[str]]
228
+ # Pull the raw model list for one provider.
229
+ models: Callable[[str], Sequence[Any]]
230
+
231
+
232
+ #: Providers excluded from the live catalog.
233
+ #:
234
+ #: ``mock`` is the framework's echo provider (its one model, ``mock-default``,
235
+ #: replies "Mock response to: …"). It is invaluable in tests but must never
236
+ #: surface to a real session — neither as the auto-selected default nor as a
237
+ #: row in the model picker — so it is filtered out of the live source. Tests
238
+ #: that want it inject their own :class:`CatalogSource`.
239
+ _EXCLUDED_PROVIDERS: frozenset[str] = frozenset({"mock"})
240
+
241
+
242
+ def _framework_source() -> CatalogSource:
243
+ """The live framework source — ``get_providers`` + ``get_models`` from
244
+ :mod:`indusagi.ai`. Built lazily per call site: no import-time registry I/O."""
245
+ return CatalogSource(
246
+ providers=lambda: [
247
+ p for p in get_providers() if str(p) not in _EXCLUDED_PROVIDERS
248
+ ],
249
+ models=lambda provider: get_models(provider),
250
+ )
251
+
252
+
253
+ class ModelCatalog:
254
+ """A normalized, validated view of the framework model list.
255
+
256
+ Construct once (the build pipeline runs eagerly in the constructor) and
257
+ query many times. The instance is immutable after construction; call
258
+ :meth:`refreshed` to rebuild against the current framework state.
259
+ """
260
+
261
+ __slots__ = ("_index", "_by_provider")
262
+
263
+ def __init__(self, source: CatalogSource | None = None) -> None:
264
+ """``source`` is the raw-data seam; defaults to the live
265
+ :mod:`indusagi.ai` registry."""
266
+ src = source if source is not None else _framework_source()
267
+ self._index: dict[str, CatalogCard] = _build_index(src.providers(), src.models)
268
+ grouped: dict[str, list[CatalogCard]] = {}
269
+ for card in self._index.values():
270
+ grouped.setdefault(card.provider, []).append(card)
271
+ self._by_provider: dict[str, list[CatalogCard]] = grouped
272
+
273
+ @property
274
+ def size(self) -> int:
275
+ """Total number of cards in the catalog."""
276
+ return len(self._index)
277
+
278
+ def all(self) -> list[CatalogCard]:
279
+ """Every card, in insertion order (provider scan order)."""
280
+ return list(self._index.values())
281
+
282
+ def by_provider(self, provider: str) -> list[CatalogCard]:
283
+ """Cards owned by one provider (empty list if the provider has none)."""
284
+ return list(self._by_provider.get(str(provider), []))
285
+
286
+ def providers(self) -> list[str]:
287
+ """The providers that contributed at least one card, in scan order."""
288
+ return list(self._by_provider.keys())
289
+
290
+ def get(self, id: str) -> CatalogCard | None:
291
+ """Look up one card by its canonical ``"provider/modelId"`` id. Also
292
+ accepts a bare provider-scoped model id when it is unambiguous across
293
+ providers."""
294
+ direct = self._index.get(id)
295
+ if direct is not None:
296
+ return direct
297
+ if "/" not in id:
298
+ matches = [c for c in self._index.values() if c.modelId == id]
299
+ if len(matches) == 1:
300
+ return matches[0]
301
+ return None
302
+
303
+ def has(self, id: str) -> bool:
304
+ """True when the canonical id resolves to a card."""
305
+ return self.get(id) is not None
306
+
307
+ def refreshed(self, source: CatalogSource | None = None) -> "ModelCatalog":
308
+ """Rebuild against the current framework state, returning a fresh catalog."""
309
+ return ModelCatalog(source if source is not None else _framework_source())