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,291 @@
1
+ """Model matcher — :class:`ModelMatcher` (port of TS ``src/conductor/catalog/matcher.ts``).
2
+
3
+ A scored-candidate resolver over a :class:`~induscode.conductor.catalog.ModelCatalog`.
4
+ Given a free-form query (a string, or a structured
5
+ :class:`~induscode.conductor.contract.MatchQuery`), it produces the single
6
+ best-matching :class:`~induscode.conductor.contract.ModelCardRef`, or ``None``
7
+ when nothing clears the bar.
8
+
9
+ Resolution is a *prioritized scoring pipeline*, not a chain of single-purpose
10
+ match helpers: each candidate card is scored against the query by a set of
11
+ independent strategy probes (exact-canonical, exact-id, provider-prefix,
12
+ alias-substring, glob), the capability filters gate the field, and the highest
13
+ scorer (with deterministic tie-breaks) wins.
14
+
15
+ Port notes
16
+ ----------
17
+ - The TS glob compiler is ported character-by-character (an index loop, the
18
+ same ``[^/]`` classes, the same escape set) and memoized via
19
+ :func:`functools.lru_cache` in place of the TS module-level ``Map`` cache.
20
+ - Scoring weights and the tie bonus are VERBATIM:
21
+ pin 1000 / exact-canonical 900 / exact-id 800 / provider 600 / glob 400 /
22
+ substring 200, plus ``max(0, 40 - min(|len(id) - len(needle)|, 40))`` and a
23
+ ``+3`` reasoning nudge.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import re
29
+ from dataclasses import dataclass
30
+ from functools import lru_cache
31
+ from typing import Union
32
+
33
+ from induscode.conductor.contract import MatchQuery, ModelCardRef
34
+
35
+ from induscode.conductor.catalog import CatalogCard, ModelCatalog, to_card_ref
36
+
37
+ __all__ = [
38
+ "ModelMatcher",
39
+ "ResolveInput",
40
+ ]
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Query shape
45
+ # ---------------------------------------------------------------------------
46
+
47
+ #: Either a raw selector string or the structured ``MatchQuery``.
48
+ ResolveInput = Union[str, MatchQuery]
49
+
50
+
51
+ def _lift_query(input: ResolveInput) -> MatchQuery:
52
+ """Lift a bare selector string into a ``MatchQuery``."""
53
+ return MatchQuery(pattern=input) if isinstance(input, str) else input
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Scoring scale
58
+ # ---------------------------------------------------------------------------
59
+
60
+ # Strategy weights, highest-confidence first. A candidate's score is the single
61
+ # best strategy that fires (strategies do not stack), so the order here defines
62
+ # the resolution priority. The gap between tiers is wide enough that a weaker
63
+ # probe never out-ranks a stronger one regardless of secondary bonuses.
64
+ _SCALE_PINNED_PAIR = 1000 # explicit provider + modelId pin
65
+ _SCALE_EXACT_CANONICAL = 900 # pattern == "provider/modelId"
66
+ _SCALE_EXACT_MODEL_ID = 800 # pattern == modelId (provider-scoped id)
67
+ _SCALE_PROVIDER_SCOPED = 600 # pattern == "provider/" or provider name
68
+ _SCALE_GLOB_PATTERN = 400 # pattern is a glob that matches the id
69
+ _SCALE_ALIAS_SUBSTRING = 200 # pattern is a case-folded substring of id/name
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class _ScoredCard:
74
+ """A scored candidate awaiting tie-break (TS ``ScoredCard``)."""
75
+
76
+ card: CatalogCard
77
+ score: int
78
+ bonus: int
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Capability gate
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ def _clears_constraints(card: CatalogCard, q: MatchQuery) -> bool:
87
+ """Reject a card that fails a hard capability or provider constraint.
88
+
89
+ These are gates, not scores: a card that fails any constraint is removed
90
+ from the field entirely rather than merely down-ranked.
91
+ """
92
+ if q.provider is not None and card.provider != str(q.provider):
93
+ return False
94
+ if q.modelId is not None and card.modelId != q.modelId:
95
+ return False
96
+ if q.reasoning is True and not card.reasoning:
97
+ return False
98
+ if q.supportsImageInput is True and not card.acceptsImages:
99
+ return False
100
+ return True
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Glob compiler (standard technique — no fnmatch dependency, ported verbatim)
105
+ # ---------------------------------------------------------------------------
106
+
107
+ _GLOBBY = re.compile(r"[*?\[\]]")
108
+
109
+ # The TS literal-character escape set: /[.+^${}()|\\]/
110
+ _NEEDS_ESCAPE = frozenset(".+^${}()|\\")
111
+
112
+
113
+ def _looks_globby(pattern: str) -> bool:
114
+ """True when a selector contains glob metacharacters worth compiling."""
115
+ return _GLOBBY.search(pattern) is not None
116
+
117
+
118
+ @lru_cache(maxsize=None)
119
+ def _compile_glob(pattern: str) -> re.Pattern[str] | None:
120
+ """Compile a shell-style glob (``*``, ``?``, ``[set]``) to an anchored,
121
+ case-folded regex. Compilation is memoized. A ``/`` in the pattern is
122
+ matched literally, so ``anthropic/*`` scopes to a provider. Returns
123
+ ``None`` for an uncompilable pattern.
124
+
125
+ Ported char-by-char from the TS compiler (index loop, same classes).
126
+ """
127
+ body = ""
128
+ i = 0
129
+ while i < len(pattern):
130
+ ch = pattern[i]
131
+ if ch == "*":
132
+ body += "[^/]*"
133
+ elif ch == "?":
134
+ body += "[^/]"
135
+ elif ch == "[":
136
+ close = pattern.find("]", i + 1)
137
+ if close == -1:
138
+ body += "\\["
139
+ else:
140
+ body += "[" + pattern[i + 1 : close].replace("\\", "\\\\") + "]"
141
+ i = close
142
+ else:
143
+ body += "\\" + ch if ch in _NEEDS_ESCAPE else ch
144
+ i += 1
145
+ try:
146
+ return re.compile(f"^{body}$", re.IGNORECASE)
147
+ except re.error:
148
+ return None
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Strategy probes (independent; one fires per card, the strongest wins)
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def _probe_pattern(card: CatalogCard, pattern: str) -> int:
157
+ """Probe a single card against the selector pattern, returning the best
158
+ strategy score (or 0 if no probe fires). The probes are tried
159
+ strongest-first and the first hit short-circuits."""
160
+ needle = pattern.strip()
161
+ if needle == "":
162
+ return 0
163
+ folded = needle.lower()
164
+
165
+ if card.id == needle:
166
+ return _SCALE_EXACT_CANONICAL
167
+ if card.modelId == needle:
168
+ return _SCALE_EXACT_MODEL_ID
169
+
170
+ # provider-scoped: "anthropic/" or the bare provider slug
171
+ provider_prefix = f"{card.provider}/"
172
+ if needle == provider_prefix or folded == card.provider.lower():
173
+ return _SCALE_PROVIDER_SCOPED
174
+
175
+ if _looks_globby(needle):
176
+ compiled = _compile_glob(needle)
177
+ if compiled is not None and (
178
+ compiled.match(card.id) is not None or compiled.match(card.modelId) is not None
179
+ ):
180
+ return _SCALE_GLOB_PATTERN
181
+
182
+ if folded in card.id.lower() or folded in card.name.lower():
183
+ return _SCALE_ALIAS_SUBSTRING
184
+
185
+ return 0
186
+
187
+
188
+ def _tie_bonus(card: CatalogCard, pattern: str) -> int:
189
+ """Secondary bonus for tie-breaking within a strategy tier. Rewards a card
190
+ whose id is *closer* to the pattern length (a tighter alias match), and
191
+ gently prefers reasoning-capable cards. Always small relative to a tier
192
+ gap."""
193
+ needle = pattern.strip().lower()
194
+ bonus = 0
195
+ if len(needle) > 0:
196
+ # tighter substring matches (less surrounding noise) rank higher
197
+ slack = abs(len(card.id) - len(needle))
198
+ bonus += max(0, 40 - min(slack, 40))
199
+ if card.reasoning:
200
+ bonus += 3
201
+ return bonus
202
+
203
+
204
+ def _rank_key(scored: _ScoredCard) -> tuple[int, int, str]:
205
+ """Deterministic candidate ordering: higher strategy tier first, then
206
+ higher tie-bonus, then canonical id lexical order so equal scores never
207
+ depend on catalog iteration order (TS ``rankCandidates``)."""
208
+ return (-scored.score, -scored.bonus, scored.card.id)
209
+
210
+
211
+ # ---------------------------------------------------------------------------
212
+ # ModelMatcher
213
+ # ---------------------------------------------------------------------------
214
+
215
+
216
+ class ModelMatcher:
217
+ """Resolves model queries against a ``ModelCatalog`` by scored candidates.
218
+
219
+ Construct once with a catalog, then call :meth:`resolve` per query. The
220
+ matcher holds no mutable state; it is safe to share.
221
+ """
222
+
223
+ __slots__ = ("_catalog",)
224
+
225
+ def __init__(self, catalog: ModelCatalog) -> None:
226
+ self._catalog = catalog
227
+
228
+ def resolve(self, input: ResolveInput) -> ModelCardRef | None:
229
+ """Resolve a query to the single best ``ModelCardRef``, or ``None``.
230
+
231
+ Pipeline:
232
+ 1. lift the input to a ``MatchQuery``;
233
+ 2. honor an explicit ``provider``+``modelId`` *pin* (highest tier)
234
+ up front;
235
+ 3. gate every card through the constraint filter;
236
+ 4. with no pattern, return the leading surviving candidate (default
237
+ pick);
238
+ 5. otherwise score each survivor (probe + tie bonus) and return the
239
+ top scorer (deterministic tie-break by tier, bonus, id).
240
+ """
241
+ q = _lift_query(input)
242
+ card = self.resolve_card(q)
243
+ return to_card_ref(card) if card is not None else None
244
+
245
+ def resolve_card(self, input: ResolveInput) -> CatalogCard | None:
246
+ """As :meth:`resolve`, but returns the full :class:`CatalogCard`."""
247
+ q = _lift_query(input)
248
+
249
+ # Tier 0: an explicit provider+modelId pin resolves directly.
250
+ if q.provider is not None and q.modelId is not None:
251
+ pinned = self._catalog.get(f"{q.provider}/{q.modelId}")
252
+ if pinned is not None and _clears_constraints(pinned, q):
253
+ return pinned
254
+
255
+ field = [c for c in self._catalog.all() if _clears_constraints(c, q)]
256
+ if len(field) == 0:
257
+ return None
258
+
259
+ pattern = q.pattern.strip() if q.pattern is not None else None
260
+ if not pattern:
261
+ # No selector: the default candidate is the leading survivor,
262
+ # nudged toward a reasoning model when the field offers one.
263
+ return next((c for c in field if c.reasoning), field[0])
264
+
265
+ scored = [
266
+ _ScoredCard(card=c, score=score, bonus=_tie_bonus(c, pattern))
267
+ for c in field
268
+ if (score := _probe_pattern(c, pattern)) > 0
269
+ ]
270
+ if len(scored) == 0:
271
+ return None
272
+
273
+ scored.sort(key=_rank_key)
274
+ return scored[0].card
275
+
276
+ def resolve_all(self, input: ResolveInput) -> list[ModelCardRef]:
277
+ """Resolve, returning *all* surviving candidates ranked best-first
278
+ (the field a "did you mean" picker would show). Useful for ambiguous
279
+ selectors."""
280
+ q = _lift_query(input)
281
+ field = [c for c in self._catalog.all() if _clears_constraints(c, q)]
282
+ pattern = q.pattern.strip() if q.pattern is not None else None
283
+ if not pattern:
284
+ return [to_card_ref(c) for c in field]
285
+ scored = [
286
+ _ScoredCard(card=c, score=score, bonus=_tie_bonus(c, pattern))
287
+ for c in field
288
+ if (score := _probe_pattern(c, pattern)) > 0
289
+ ]
290
+ scored.sort(key=_rank_key)
291
+ return [to_card_ref(s.card) for s in scored]