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,350 @@
1
+ """SessionLibrary — the catalog-and-navigation layer over persisted
2
+ transcripts (port of TS ``src/sessions/library.ts``).
3
+
4
+ The conductor owns a single session at a time as a
5
+ :class:`~induscode.conductor.transcript_store.TranscriptStore`. The library
6
+ owns the *collection*: it scans the workspace ``sessions/`` directory for the
7
+ NDJSON files the conductor's filesystem backend writes, lists them as
8
+ :class:`~induscode.sessions.contract.SavedSession` catalog rows, opens any
9
+ one back into a live store via the store's own opener +
10
+ :func:`~induscode.conductor.transcript_store.replay` reducer, and offers the
11
+ two read projections a chooser UI needs — a flattened
12
+ :class:`~induscode.sessions.contract.BranchNode` tree for a branch navigator
13
+ and a :class:`~induscode.sessions.contract.PriorTurn` list for re-asking an
14
+ earlier prompt. It also performs the two file-level mutations a manager
15
+ needs: renaming and deleting a session on disk.
16
+
17
+ The library never re-implements transcript parsing or message serialization —
18
+ it delegates entirely to the conductor's transcript-store surface
19
+ (:class:`TranscriptStore`, :func:`fs_backend`, :func:`replay`). Its own logic
20
+ is purely the directory enumeration, the tree flatten, and the text
21
+ reduction.
22
+
23
+ Seam note: the per-cwd session scope directory (the ``--<cwd slug>--`` layout
24
+ under the workspace ``sessions/`` root) is **boot's** helper — TS
25
+ ``boot/runners/session.ts#sessionScopeDir`` — not the library's. The library
26
+ takes the already-scoped directory; the M4 boot wave ports the slug helper
27
+ and must agree with the conductor (writer) on it.
28
+
29
+ Port note: disk I/O runs through :func:`asyncio.to_thread` (matching the
30
+ conductor's ``fs_backend`` discipline) so the async surface never blocks the
31
+ loop.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import asyncio
37
+ import os
38
+ import re
39
+ from collections.abc import Mapping, Sequence
40
+ from pathlib import Path
41
+ from typing import Any, Final
42
+
43
+ from induscode.conductor.contract import TranscriptEntry
44
+ from induscode.conductor.transcript_store import TranscriptStore, fs_backend
45
+ from induscode.sessions.contract import BranchNode, PriorTurn, SavedSession
46
+
47
+ __all__ = [
48
+ "SessionLibrary",
49
+ ]
50
+
51
+
52
+ #: Suffix the conductor's filesystem backend gives every session file.
53
+ _SESSION_FILE_EXT: Final[str] = ".ndjson"
54
+
55
+ #: Default cap for the single-line previews this library renders.
56
+ _PREVIEW_LIMIT: Final[int] = 72
57
+
58
+
59
+ class SessionLibrary:
60
+ """The collection-level handle over a workspace's persisted sessions.
61
+
62
+ Construct it with the resolved ``sessions_dir`` (from the workspace
63
+ locator, already cwd-scoped by boot), then call :meth:`list` /
64
+ :meth:`open` / :meth:`rename` / :meth:`remove` to manage files, and
65
+ :meth:`tree` / :meth:`prior_turns` to project a loaded transcript for
66
+ navigation and forking.
67
+ """
68
+
69
+ __slots__ = ("_dir", "_backend")
70
+
71
+ def __init__(self, *, sessions_dir: str) -> None:
72
+ """:param sessions_dir: absolute path to the workspace ``sessions/``
73
+ directory (TS ``SessionLibraryOptions.sessionsDir``)."""
74
+ self._dir = sessions_dir
75
+ self._backend = fs_backend(sessions_dir)
76
+
77
+ @property
78
+ def directory(self) -> str:
79
+ """The sessions directory this library is rooted at."""
80
+ return self._dir
81
+
82
+ async def list(self, *, deep: bool = False) -> list[SavedSession]:
83
+ """Enumerate persisted sessions as catalog rows, newest-modified
84
+ first.
85
+
86
+ Scans the directory for ``*.ndjson`` files and stats each for its
87
+ size and modification time. When ``deep`` is set, it also opens each
88
+ file to fill in ``messageCount`` and the opening-turn ``preview``;
89
+ otherwise it returns the shallow rows (id/path/size/lastModified) so
90
+ a large directory lists fast. A missing directory yields an empty
91
+ list rather than an error.
92
+ """
93
+ ids = await self._session_ids()
94
+ rows = await asyncio.gather(
95
+ *[self._deep_row(id) if deep else self._shallow_row(id) for id in ids]
96
+ )
97
+ return sorted(rows, key=_most_recent_key)
98
+
99
+ async def open(self, id: str) -> TranscriptStore | None:
100
+ """Open a persisted session back into a live, hydrated
101
+ :class:`TranscriptStore`, or return ``None`` when no file with that
102
+ id exists.
103
+
104
+ Delegates to the store's own opener, which reads the backend and
105
+ rebuilds state through the ``replay`` reducer — the library adds
106
+ nothing to the rehydration beyond binding the filesystem backend.
107
+ """
108
+ return await TranscriptStore.open(id, backend=self._backend)
109
+
110
+ async def rename(self, from_id: str, to_id: str) -> str:
111
+ """Rename a session file from one id to another, returning the new
112
+ absolute path. The on-disk identifier *is* the filename stem, so this
113
+ is a plain file move; the in-file head still names the old id, which
114
+ the store tolerates on the next load (the head's leaf is what
115
+ matters, not its session label).
116
+
117
+ :raises ValueError: if the source file is missing or the target id
118
+ already exists.
119
+ """
120
+ from_path = self.path_of(from_id)
121
+ to_path = self.path_of(to_id)
122
+ if await self._missing(from_path):
123
+ raise ValueError(f'SessionLibrary.rename: no session "{from_id}"')
124
+ if not await self._missing(to_path):
125
+ raise ValueError(f'SessionLibrary.rename: session "{to_id}" already exists')
126
+
127
+ def _move() -> None:
128
+ Path(self._dir).mkdir(parents=True, exist_ok=True)
129
+ os.rename(from_path, to_path)
130
+
131
+ await asyncio.to_thread(_move)
132
+ return to_path
133
+
134
+ async def remove(self, id: str) -> bool:
135
+ """Delete a session file. Returns ``True`` when a file was removed,
136
+ ``False`` when there was nothing to delete — never raises on an
137
+ already-absent session."""
138
+ path = self.path_of(id)
139
+ if await self._missing(path):
140
+ return False
141
+
142
+ def _unlink() -> None:
143
+ Path(path).unlink(missing_ok=True)
144
+
145
+ await asyncio.to_thread(_unlink)
146
+ return True
147
+
148
+ def path_of(self, id: str) -> str:
149
+ """Absolute path a session id persists to."""
150
+ return str(Path(self._dir) / f"{id}{_SESSION_FILE_EXT}")
151
+
152
+ def tree(self, store: TranscriptStore) -> list[BranchNode]:
153
+ """Flatten a loaded transcript into an ordered :class:`BranchNode`
154
+ list for a tree navigator.
155
+
156
+ Walks every node from each root downward in a stable depth-first
157
+ order, emitting one row per node with its render label, depth
158
+ indentation, leaf flag, and a current-marker derived from the store's
159
+ active head. A node is a leaf when no other node names it as
160
+ ``parent``. Pure — no I/O.
161
+ """
162
+ nodes = list(store.state().nodes.values())
163
+ current_leaf = store.head.leaf
164
+
165
+ children_of: dict[str | None, list[TranscriptEntry]] = {}
166
+ for node in nodes:
167
+ children_of.setdefault(node.parent, []).append(node)
168
+ has_children: set[str] = {node.parent for node in nodes if node.parent is not None}
169
+
170
+ out: list[BranchNode] = []
171
+
172
+ def walk(entry: TranscriptEntry, depth: int) -> None:
173
+ out.append(
174
+ BranchNode(
175
+ id=entry.id,
176
+ parent=entry.parent,
177
+ label=_label_for(entry, depth),
178
+ depth=depth,
179
+ isLeaf=entry.id not in has_children,
180
+ isCurrent=entry.id == current_leaf,
181
+ )
182
+ )
183
+ for child in children_of.get(entry.id, []):
184
+ walk(child, depth + 1)
185
+
186
+ for root in children_of.get(None, []):
187
+ walk(root, 0)
188
+ return out
189
+
190
+ def prior_turns(self, store: TranscriptStore) -> list[PriorTurn]:
191
+ """Extract the user prompts along a loaded transcript's active branch
192
+ as fork candidates, in chronological order.
193
+
194
+ Reads the root→leaf branch the store currently points at (the
195
+ conversation the agent would replay) and keeps only the user-role
196
+ nodes that carry real text, projecting each to a :class:`PriorTurn` a
197
+ forking picker can offer. Pure — no I/O.
198
+ """
199
+ out: list[PriorTurn] = []
200
+ for entry in store.path_to():
201
+ if entry.role != "user":
202
+ continue
203
+ text = _message_text(entry.content).strip()
204
+ if len(text) == 0:
205
+ continue
206
+ out.append(PriorTurn(entryId=entry.id, text=text, preview=_preview_of(text)))
207
+ return out
208
+
209
+ # -- internals ------------------------------------------------------------
210
+
211
+ async def _session_ids(self) -> list[str]:
212
+ """Bare session ids present in the directory (no extension), or
213
+ ``[]``."""
214
+
215
+ def _scan() -> list[str]:
216
+ try:
217
+ names = os.listdir(self._dir)
218
+ except OSError:
219
+ return []
220
+ return [
221
+ name[: -len(_SESSION_FILE_EXT)]
222
+ for name in names
223
+ if name.endswith(_SESSION_FILE_EXT)
224
+ ]
225
+
226
+ return await asyncio.to_thread(_scan)
227
+
228
+ async def _shallow_row(self, id: str) -> SavedSession:
229
+ """Catalog row from file metadata only — no parse."""
230
+ path = self.path_of(id)
231
+ size, last_modified = await self._file_meta(path)
232
+ return SavedSession(id=id, path=path, size=size, lastModified=last_modified)
233
+
234
+ async def _deep_row(self, id: str) -> SavedSession:
235
+ """Catalog row enriched by opening the transcript and reducing its
236
+ text."""
237
+ path = self.path_of(id)
238
+ size, last_modified = await self._file_meta(path)
239
+ store = await self.open(id)
240
+ if store is None:
241
+ return SavedSession(id=id, path=path, size=size, lastModified=last_modified)
242
+
243
+ branch = store.path_to()
244
+ conversational = [entry for entry in branch if _is_conversational(entry)]
245
+ opener = next((entry for entry in conversational if entry.role == "user"), None)
246
+ preview = _preview_of(_message_text(opener.content)) if opener is not None else None
247
+ has_preview = preview is not None and len(preview) > 0
248
+ return SavedSession(
249
+ id=id,
250
+ path=path,
251
+ size=size,
252
+ lastModified=last_modified,
253
+ messageCount=len(conversational),
254
+ preview=preview if has_preview else None,
255
+ name=preview if has_preview else None,
256
+ )
257
+
258
+ async def _file_meta(self, path: str) -> tuple[int | None, float | None]:
259
+ """Best-effort ``(size, lastModified-ms)``; ``(None, None)`` when the
260
+ file is gone."""
261
+
262
+ def _stat() -> tuple[int | None, float | None]:
263
+ try:
264
+ info = os.stat(path)
265
+ return info.st_size, info.st_mtime * 1000.0
266
+ except OSError:
267
+ return None, None
268
+
269
+ return await asyncio.to_thread(_stat)
270
+
271
+ async def _missing(self, path: str) -> bool:
272
+ """True when a path does not resolve to a readable file."""
273
+ return not await asyncio.to_thread(os.path.isfile, path)
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Pure helpers
278
+ # ---------------------------------------------------------------------------
279
+
280
+
281
+ def _is_conversational(entry: TranscriptEntry) -> bool:
282
+ """Conversational roles count toward a session's message total."""
283
+ return entry.role in ("user", "assistant", "tool")
284
+
285
+
286
+ def _most_recent_key(row: SavedSession) -> tuple[float, str]:
287
+ """Sort key: most recently modified first, ties broken by id."""
288
+ return (-(row.lastModified if row.lastModified is not None else 0), row.id)
289
+
290
+
291
+ def _label_for(entry: TranscriptEntry, depth: int) -> str:
292
+ """Build a depth-indented, role-aware label for a transcript node."""
293
+ indent = " " * depth
294
+ match entry.role:
295
+ case "user":
296
+ return f"{indent}user: {_preview_of(_message_text(entry.content))}"
297
+ case "assistant":
298
+ return f"{indent}assistant"
299
+ case "tool":
300
+ return f"{indent}tool: {_tool_name_of(entry.content)}"
301
+ case "condense":
302
+ return f"{indent}condense"
303
+ case "system":
304
+ return f"{indent}system"
305
+ case _:
306
+ return f"{indent}note"
307
+
308
+
309
+ def _read(value: Any, name: str) -> Any:
310
+ """Field access tolerant of both frozen dataclasses and raw mappings."""
311
+ if isinstance(value, Mapping):
312
+ return value.get(name)
313
+ return getattr(value, name, None)
314
+
315
+
316
+ def _tool_name_of(message: Any) -> str:
317
+ """Tool name carried by a tool-result message, or a placeholder."""
318
+ candidate = _read(message, "toolName")
319
+ return candidate if isinstance(candidate, str) and len(candidate) > 0 else "tool"
320
+
321
+
322
+ def _message_text(message: Any) -> str:
323
+ """Reduce any framework ``AgentMessage`` to its plain text.
324
+
325
+ Messages carry ``content`` as either a raw string or a sequence of typed
326
+ blocks; we concatenate the text-bearing blocks and ignore images,
327
+ thinking, and tool calls. Defensive about shape so a malformed or custom
328
+ message yields ``""`` rather than raising.
329
+ """
330
+ content = _read(message, "content")
331
+ if isinstance(content, str):
332
+ return content
333
+ if not isinstance(content, Sequence) or isinstance(content, (str, bytes)):
334
+ return ""
335
+ parts: list[str] = []
336
+ for block in content:
337
+ if _read(block, "type") == "text":
338
+ text = _read(block, "text")
339
+ if isinstance(text, str):
340
+ parts.append(text)
341
+ return "".join(parts)
342
+
343
+
344
+ def _preview_of(text: str, limit: int = _PREVIEW_LIMIT) -> str:
345
+ """Collapse text to a single trimmed line and cap its length for
346
+ display."""
347
+ flat = re.sub(r"\s+", " ", text).strip()
348
+ if len(flat) <= limit:
349
+ return flat
350
+ return flat[: max(0, limit - 1)] + "…"
@@ -0,0 +1,47 @@
1
+ """Settings subsystem — public barrel (port of the TS ``src/settings``
2
+ barrel).
3
+
4
+ One import site for the typed :class:`Preferences` record and its
5
+ :data:`SettingKey` / :data:`EscapeAction` / :data:`DeliveryMode` /
6
+ ``ThinkingLevel`` vocabularies, the frozen :data:`DEFAULT_PREFERENCES`, the
7
+ explicit snake_case ↔ camelCase alias maps, and the two-tier
8
+ :class:`PreferenceStore` reader/writer. Console surfaces and boot stages
9
+ depend on this module rather than reaching into the individual files.
10
+ """
11
+
12
+ from .contract import (
13
+ DEFAULT_PREFERENCES,
14
+ DELIVERY_MODES,
15
+ ESCAPE_ACTIONS,
16
+ FIELD_TO_JSON_KEY,
17
+ JSON_TO_FIELD_KEY,
18
+ SETTING_KEYS,
19
+ DeliveryMode,
20
+ EscapeAction,
21
+ Preferences,
22
+ SettingKey,
23
+ ThinkingLevel,
24
+ canonical_key,
25
+ is_delivery_mode,
26
+ is_escape_action,
27
+ )
28
+ from .manager import PreferenceLocations, PreferenceStore
29
+
30
+ __all__ = [
31
+ "DEFAULT_PREFERENCES",
32
+ "DELIVERY_MODES",
33
+ "DeliveryMode",
34
+ "ESCAPE_ACTIONS",
35
+ "EscapeAction",
36
+ "FIELD_TO_JSON_KEY",
37
+ "JSON_TO_FIELD_KEY",
38
+ "PreferenceLocations",
39
+ "PreferenceStore",
40
+ "Preferences",
41
+ "SETTING_KEYS",
42
+ "SettingKey",
43
+ "ThinkingLevel",
44
+ "canonical_key",
45
+ "is_delivery_mode",
46
+ "is_escape_action",
47
+ ]