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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""The built-in slash catalog — the single ordered source of truth.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/slash/builtins.ts``. The catalog is assembled from
|
|
4
|
+
three static topic groups, each its own module in this package: transcript /
|
|
5
|
+
session control, the workbench pickers, and the integration bridges. On top
|
|
6
|
+
of those it splices in the *dynamic* rows — one ``/skill:<name>`` per
|
|
7
|
+
discovered Agent-Skills capability card and one ``/<template-name>`` per
|
|
8
|
+
discovered prompt-template macro — produced by
|
|
9
|
+
:func:`~induscode.console.slash_commands.dynamic.build_dynamic_commands`.
|
|
10
|
+
|
|
11
|
+
There is no command ``if``-ladder anywhere: adding a static command is
|
|
12
|
+
appending a row to one of the groups, and the dynamic rows are discovered
|
|
13
|
+
from the standard skill/macro roots (returning nothing, never an error, when
|
|
14
|
+
those roots are absent). The registry index, the completion matcher, and the
|
|
15
|
+
family grouping are all *derived* from the assembled catalog (see
|
|
16
|
+
:mod:`induscode.console_slash.registry`).
|
|
17
|
+
|
|
18
|
+
Listing order is intentional: transcript controls first (the verbs a new
|
|
19
|
+
user meets), then the workbench pickers, then the integration bridges, and
|
|
20
|
+
finally the discovered skills and templates.
|
|
21
|
+
|
|
22
|
+
Port deltas, locked by the plan (cross-cutting rule 4 / analysis 03 §6.5):
|
|
23
|
+
|
|
24
|
+
- **Assembly is the explicit** :func:`build_catalog` ``(cwd, home)``
|
|
25
|
+
function — the TS module ran discovery at import time; import-time fs
|
|
26
|
+
scans are banned here, and the explicit roots make the discovery testable
|
|
27
|
+
against a temp tree.
|
|
28
|
+
- **The defaults are lazy.** ``SLASH_COMMANDS`` and
|
|
29
|
+
``DEFAULT_SLASH_REGISTRY`` remain importable for parity, but they are
|
|
30
|
+
minted on first attribute access via module ``__getattr__`` (and cached so
|
|
31
|
+
the TS identity pins — ``DEFAULT_SLASH_REGISTRY.commands is
|
|
32
|
+
SLASH_COMMANDS`` — keep holding). :func:`reset_default_registry` drops the
|
|
33
|
+
cache for tests.
|
|
34
|
+
- **Per-console bridge state**: the integrations group is minted against an
|
|
35
|
+
:class:`~induscode.console.slash_commands.integrations.IntegrationsRuntime`
|
|
36
|
+
holder per :func:`build_catalog` call, replacing the TS module globals.
|
|
37
|
+
- Building the default registry also installs it as the ``/help`` catalog
|
|
38
|
+
provider (the late-bound provider that replaced the TS dynamic-import
|
|
39
|
+
cycle-breaker — see :mod:`.workbench`), so ``/help`` lists the dynamic
|
|
40
|
+
rows exactly as the TS ``SLASH_COMMANDS`` listing did.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import os
|
|
46
|
+
from dataclasses import dataclass
|
|
47
|
+
from typing import Final
|
|
48
|
+
|
|
49
|
+
from induscode.briefing import Macro, MacroOrigin, SkillRoot, gather_skill_cards, load_macros
|
|
50
|
+
from induscode.console_slash import SlashCommand, SlashRegistry, build_registry
|
|
51
|
+
|
|
52
|
+
from .dynamic import DynamicCommandSources, build_dynamic_commands
|
|
53
|
+
from .integrations import IntegrationsRuntime, build_integration_commands
|
|
54
|
+
from .transcript import transcript_commands
|
|
55
|
+
from .workbench import set_help_registry_provider, workbench_commands
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"DEFAULT_SLASH_REGISTRY",
|
|
59
|
+
"DynamicRoots",
|
|
60
|
+
"MacroRoot",
|
|
61
|
+
"PROJECT_DIR",
|
|
62
|
+
"SLASH_COMMANDS",
|
|
63
|
+
"build_catalog",
|
|
64
|
+
"build_default_registry",
|
|
65
|
+
"discover_dynamic_sources",
|
|
66
|
+
"dynamic_roots",
|
|
67
|
+
"reset_default_registry",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Dynamic discovery
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
#: The per-project settings directory dynamic roots are resolved under.
|
|
76
|
+
PROJECT_DIR: Final = ".indusagi"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class MacroRoot:
|
|
81
|
+
"""One prompt-template root to scan, with its origin tag and the label
|
|
82
|
+
woven into derived descriptions."""
|
|
83
|
+
|
|
84
|
+
dir: str
|
|
85
|
+
origin: MacroOrigin
|
|
86
|
+
label: str
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class DynamicRoots:
|
|
91
|
+
"""The skill and macro roots to scan, in precedence order."""
|
|
92
|
+
|
|
93
|
+
skills: tuple[SkillRoot, ...]
|
|
94
|
+
macros: tuple[MacroRoot, ...]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def dynamic_roots(cwd: str, home: str) -> DynamicRoots:
|
|
98
|
+
"""The skill and macro roots to scan, in precedence order (project
|
|
99
|
+
before user so a project-local card or template shadows a user-global
|
|
100
|
+
one of the same name). Every root is optional; a missing directory
|
|
101
|
+
simply yields nothing.
|
|
102
|
+
|
|
103
|
+
:param cwd: the workspace directory (project roots resolve under it)
|
|
104
|
+
:param home: the user's home directory (user roots resolve under it)
|
|
105
|
+
"""
|
|
106
|
+
return DynamicRoots(
|
|
107
|
+
skills=(
|
|
108
|
+
SkillRoot(dir=os.path.join(cwd, PROJECT_DIR, "skills"), origin="project"),
|
|
109
|
+
SkillRoot(dir=os.path.join(home, PROJECT_DIR, "skills"), origin="user"),
|
|
110
|
+
),
|
|
111
|
+
macros=(
|
|
112
|
+
MacroRoot(
|
|
113
|
+
dir=os.path.join(cwd, PROJECT_DIR, "commands"),
|
|
114
|
+
origin="project",
|
|
115
|
+
label="project",
|
|
116
|
+
),
|
|
117
|
+
MacroRoot(
|
|
118
|
+
dir=os.path.join(home, PROJECT_DIR, "commands"),
|
|
119
|
+
origin="user",
|
|
120
|
+
label="user",
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def discover_dynamic_sources(cwd: str, home: str) -> DynamicCommandSources:
|
|
127
|
+
"""Discover the skill cards and prompt-template macros available on disk.
|
|
128
|
+
|
|
129
|
+
Never throws: the underlying loaders treat a missing or unreadable root
|
|
130
|
+
as an empty result, so this returns whatever was found and an empty set
|
|
131
|
+
otherwise. Templates are deduped by name across roots (first root wins —
|
|
132
|
+
project shadows user).
|
|
133
|
+
|
|
134
|
+
:param cwd: the workspace directory
|
|
135
|
+
:param home: the user's home directory
|
|
136
|
+
"""
|
|
137
|
+
roots = dynamic_roots(cwd, home)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
skills = tuple(gather_skill_cards(roots.skills).cards)
|
|
141
|
+
except Exception:
|
|
142
|
+
skills = ()
|
|
143
|
+
|
|
144
|
+
templates: list[Macro] = []
|
|
145
|
+
claimed: set[str] = set()
|
|
146
|
+
for root in roots.macros:
|
|
147
|
+
try:
|
|
148
|
+
loaded = load_macros(root.dir, origin=root.origin, label=root.label)
|
|
149
|
+
except Exception:
|
|
150
|
+
loaded = []
|
|
151
|
+
for macro in loaded:
|
|
152
|
+
if macro.name in claimed:
|
|
153
|
+
continue
|
|
154
|
+
claimed.add(macro.name)
|
|
155
|
+
templates.append(macro)
|
|
156
|
+
|
|
157
|
+
return DynamicCommandSources(skills=skills, templates=tuple(templates))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# The catalog
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_catalog(
|
|
166
|
+
cwd: str,
|
|
167
|
+
home: str,
|
|
168
|
+
*,
|
|
169
|
+
integrations: IntegrationsRuntime | None = None,
|
|
170
|
+
) -> tuple[SlashCommand, ...]:
|
|
171
|
+
"""Assemble the full command catalog — every row in listing order.
|
|
172
|
+
|
|
173
|
+
The static command groups come first (transcript → workbench →
|
|
174
|
+
integrations), then the dynamic rows discovered under ``cwd``/``home``.
|
|
175
|
+
Every token (name + aliases) the static catalog already claims is
|
|
176
|
+
reserved: a discovered skill or template whose token would collide with
|
|
177
|
+
one of them is dropped, so splicing the dynamic rows can never make
|
|
178
|
+
``build_registry`` raise on a duplicate.
|
|
179
|
+
|
|
180
|
+
Consumers fold the result into a
|
|
181
|
+
:class:`~induscode.console_slash.SlashRegistry` via ``build_registry``;
|
|
182
|
+
the index by name/alias, the family grouping, and the completion matcher
|
|
183
|
+
are all derived from this one tuple.
|
|
184
|
+
|
|
185
|
+
:param cwd: the workspace directory dynamic roots resolve under
|
|
186
|
+
:param home: the user's home directory for user-global roots
|
|
187
|
+
:param integrations: the per-console bridge-state holder; fresh when
|
|
188
|
+
omitted
|
|
189
|
+
"""
|
|
190
|
+
static_commands: tuple[SlashCommand, ...] = (
|
|
191
|
+
*transcript_commands,
|
|
192
|
+
*workbench_commands,
|
|
193
|
+
*build_integration_commands(integrations),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
reserved_tokens: set[str] = set()
|
|
197
|
+
for command in static_commands:
|
|
198
|
+
reserved_tokens.add(command.name.lower())
|
|
199
|
+
for alias in command.aliases:
|
|
200
|
+
reserved_tokens.add(alias.lower())
|
|
201
|
+
|
|
202
|
+
dynamic_commands = tuple(
|
|
203
|
+
command
|
|
204
|
+
for command in build_dynamic_commands(discover_dynamic_sources(cwd, home))
|
|
205
|
+
if command.name.lower() not in reserved_tokens
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return (*static_commands, *dynamic_commands)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# The lazy defaults (TS `SLASH_COMMANDS` / `DEFAULT_SLASH_REGISTRY`)
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
_catalog_cache: tuple[SlashCommand, ...] | None = None
|
|
216
|
+
_registry_cache: SlashRegistry | None = None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_default_registry() -> SlashRegistry:
|
|
220
|
+
"""The process-default registry, assembled once from the real cwd/home
|
|
221
|
+
roots and cached (the Python rendering of the TS module-level
|
|
222
|
+
``DEFAULT_SLASH_REGISTRY``).
|
|
223
|
+
|
|
224
|
+
First build also installs the assembled catalog as the ``/help``
|
|
225
|
+
provider so the palette lists the dynamic rows too.
|
|
226
|
+
"""
|
|
227
|
+
global _catalog_cache, _registry_cache
|
|
228
|
+
if _registry_cache is None:
|
|
229
|
+
if _catalog_cache is None:
|
|
230
|
+
_catalog_cache = build_catalog(os.getcwd(), os.path.expanduser("~"))
|
|
231
|
+
registry = build_registry(_catalog_cache)
|
|
232
|
+
set_help_registry_provider(lambda: registry.commands)
|
|
233
|
+
_registry_cache = registry
|
|
234
|
+
return _registry_cache
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def reset_default_registry() -> None:
|
|
238
|
+
"""Drop the cached default catalog/registry and the ``/help`` provider
|
|
239
|
+
(tests; a fresh access re-discovers against the then-current cwd)."""
|
|
240
|
+
global _catalog_cache, _registry_cache
|
|
241
|
+
_catalog_cache = None
|
|
242
|
+
_registry_cache = None
|
|
243
|
+
set_help_registry_provider(None)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def __getattr__(name: str) -> object:
|
|
247
|
+
"""Lazy module attributes: the TS import-time catalog constants, minted
|
|
248
|
+
on first access (no import-time fs scans — plan cross-cutting rule 4)."""
|
|
249
|
+
if name == "SLASH_COMMANDS":
|
|
250
|
+
build_default_registry()
|
|
251
|
+
return _catalog_cache
|
|
252
|
+
if name == "DEFAULT_SLASH_REGISTRY":
|
|
253
|
+
return build_default_registry()
|
|
254
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Dynamic slash commands — capability cards and prompt templates as registry
|
|
2
|
+
rows.
|
|
3
|
+
|
|
4
|
+
Port of TS ``src/console/slash/commands/dynamic.ts``. The static catalog
|
|
5
|
+
(``.builtins``) is hand-maintained, but two command families are *discovered*
|
|
6
|
+
rather than written by hand: the Agent-Skills capability cards
|
|
7
|
+
(:class:`~induscode.briefing.SkillCard` rows loaded from ``SKILL.md`` files)
|
|
8
|
+
and the user/project prompt templates (:class:`~induscode.briefing.Macro`
|
|
9
|
+
rows loaded from ``*.md`` macro files). This module turns whatever was
|
|
10
|
+
discovered into ordinary :class:`~induscode.console_slash.SlashCommand` rows
|
|
11
|
+
so they sit in the same registry as the built-ins, complete the same way,
|
|
12
|
+
and resolve through the same dispatcher.
|
|
13
|
+
|
|
14
|
+
Two row shapes are produced:
|
|
15
|
+
|
|
16
|
+
- ``/skill:<name>`` — invoking it submits an Agent-Skills *invocation block*
|
|
17
|
+
(``<skill name="…" location="…">body</skill>``) as a normal turn, which is
|
|
18
|
+
exactly what the agent loop recognises to load and run that skill. The
|
|
19
|
+
trailing argument string becomes the skill body, so ``/skill:commit tidy
|
|
20
|
+
the diff`` runs the ``commit`` card against "tidy the diff".
|
|
21
|
+
- ``/<template-name>`` — invoking it expands the macro body against the
|
|
22
|
+
trailing arguments (the single-pass ``$arg`` model from the briefing
|
|
23
|
+
layer) and submits the result as a normal turn.
|
|
24
|
+
|
|
25
|
+
Both return a :class:`~induscode.console_slash.Prompt` outcome so the
|
|
26
|
+
dispatcher hands the produced text to the conductor as a turn; neither
|
|
27
|
+
reaches into a backend the console does not own. When nothing was
|
|
28
|
+
discovered, :func:`build_dynamic_commands` returns an empty list, so
|
|
29
|
+
splicing it into the catalog is always safe.
|
|
30
|
+
|
|
31
|
+
Port delta (locked): per the uniform calling convention every generated
|
|
32
|
+
``run`` is ``async def``; the TS optional-fields source bag becomes the
|
|
33
|
+
frozen :class:`DynamicCommandSources` dataclass with empty-tuple defaults.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
from typing import Final
|
|
41
|
+
|
|
42
|
+
from induscode.briefing import Macro, SkillCard, apply_macros
|
|
43
|
+
from induscode.console_slash import Prompt, SlashCommand, SlashContext, SlashOutcome
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"DynamicCommandSources",
|
|
47
|
+
"build_dynamic_commands",
|
|
48
|
+
"clamp_summary",
|
|
49
|
+
"skill_invocation_block",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Sources
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class DynamicCommandSources:
|
|
60
|
+
"""The discovered material a dynamic catalog is built from.
|
|
61
|
+
|
|
62
|
+
Both fields default to empty: a caller that only loaded skills passes
|
|
63
|
+
``DynamicCommandSources(skills=…)``, one that only loaded templates
|
|
64
|
+
passes ``DynamicCommandSources(templates=…)``, and a caller with neither
|
|
65
|
+
passes nothing and gets back ``[]``.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
# Capability cards discovered from `SKILL.md` files.
|
|
69
|
+
skills: tuple[SkillCard, ...] = ()
|
|
70
|
+
# Prompt templates discovered from `*.md` macro files.
|
|
71
|
+
templates: tuple[Macro, ...] = ()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
#: The namespace prefix every skill command answers to.
|
|
75
|
+
SKILL_PREFIX: Final = "skill:"
|
|
76
|
+
|
|
77
|
+
#: Trim a one-line summary down to a length the completion window can show.
|
|
78
|
+
SUMMARY_BUDGET: Final = 88
|
|
79
|
+
|
|
80
|
+
#: Whitespace-run collapser for the one-line summary.
|
|
81
|
+
_WHITESPACE_RUNS: Final = re.compile(r"\s+")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def clamp_summary(text: str) -> str:
|
|
85
|
+
"""Cap a description to :data:`SUMMARY_BUDGET`, suffixing an ellipsis
|
|
86
|
+
when cut."""
|
|
87
|
+
one_line = _WHITESPACE_RUNS.sub(" ", text).strip()
|
|
88
|
+
if len(one_line) <= SUMMARY_BUDGET:
|
|
89
|
+
return one_line
|
|
90
|
+
return one_line[: SUMMARY_BUDGET - 1].rstrip() + "…"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Skill-invocation block
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _escape_attr(value: str) -> str:
|
|
99
|
+
"""Escape the XML attribute-significant characters for the opener tag."""
|
|
100
|
+
return (
|
|
101
|
+
value.replace("&", "&")
|
|
102
|
+
.replace("<", "<")
|
|
103
|
+
.replace(">", ">")
|
|
104
|
+
.replace('"', """)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def skill_invocation_block(card: SkillCard, body: str) -> str:
|
|
109
|
+
"""Render the Agent-Skills invocation block for a card and an optional
|
|
110
|
+
body.
|
|
111
|
+
|
|
112
|
+
The opener carries the card ``name`` and its on-disk ``location`` so the
|
|
113
|
+
agent loop can locate and read the full instructions; the body (the
|
|
114
|
+
user's trailing argument) sits between the opener and ``</skill>``. A
|
|
115
|
+
bare invocation produces a block with an empty body — the skill still
|
|
116
|
+
runs, just without a task message.
|
|
117
|
+
|
|
118
|
+
:param card: the capability card to invoke
|
|
119
|
+
:param body: the trailing user message the skill should act on (may be
|
|
120
|
+
empty)
|
|
121
|
+
"""
|
|
122
|
+
opener = (
|
|
123
|
+
f'<skill name="{_escape_attr(card.name)}" '
|
|
124
|
+
f'location="{_escape_attr(card.location)}">'
|
|
125
|
+
)
|
|
126
|
+
return f"{opener}\n{body}\n</skill>"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _skill_command(card: SkillCard) -> SlashCommand:
|
|
130
|
+
"""Build the ``/skill:<name>`` row for one capability card.
|
|
131
|
+
|
|
132
|
+
Its ``run`` mints the invocation block against the trailing argument
|
|
133
|
+
string and returns it as a ``prompt`` outcome, so the dispatcher submits
|
|
134
|
+
it as a turn.
|
|
135
|
+
|
|
136
|
+
:param card: the capability card to expose
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
async def run(ctx: SlashContext) -> SlashOutcome:
|
|
140
|
+
body = ctx.args.strip()
|
|
141
|
+
return Prompt(text=skill_invocation_block(card, body))
|
|
142
|
+
|
|
143
|
+
return SlashCommand(
|
|
144
|
+
name=f"{SKILL_PREFIX}{card.name}",
|
|
145
|
+
summary=clamp_summary(card.description),
|
|
146
|
+
run=run,
|
|
147
|
+
family="skill",
|
|
148
|
+
takes_args=True,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Prompt templates
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _template_command(macro: Macro) -> SlashCommand:
|
|
158
|
+
"""Build the ``/<template-name>`` row for one prompt-template macro.
|
|
159
|
+
|
|
160
|
+
Its ``run`` expands the macro body against the trailing argument string
|
|
161
|
+
via the single-pass ``$arg`` resolver and returns the expansion as a
|
|
162
|
+
``prompt`` outcome.
|
|
163
|
+
|
|
164
|
+
:param macro: the prompt template to expose
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
async def run(ctx: SlashContext) -> SlashOutcome:
|
|
168
|
+
return Prompt(text=apply_macros(macro.body, ctx.args))
|
|
169
|
+
|
|
170
|
+
return SlashCommand(
|
|
171
|
+
name=macro.name,
|
|
172
|
+
summary=clamp_summary(macro.description),
|
|
173
|
+
run=run,
|
|
174
|
+
family="template",
|
|
175
|
+
takes_args=True,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Assembly
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_dynamic_commands(
|
|
185
|
+
sources: DynamicCommandSources | None = None,
|
|
186
|
+
) -> list[SlashCommand]:
|
|
187
|
+
"""Project discovered skills and templates into command rows.
|
|
188
|
+
|
|
189
|
+
Skill rows are listed first (namespaced under ``skill:``), then the
|
|
190
|
+
template rows. A name claimed by an earlier row — including any of the
|
|
191
|
+
static built-ins a caller will concatenate these ahead of — is the
|
|
192
|
+
registry's problem to reject at assembly time; this function only
|
|
193
|
+
suppresses *self*-collisions so a template whose name duplicates another
|
|
194
|
+
template (first wins) cannot crash the build. When no sources are
|
|
195
|
+
supplied the result is an empty list, never an error.
|
|
196
|
+
|
|
197
|
+
:param sources: the discovered skills and templates
|
|
198
|
+
:returns: the dynamic command rows, skills first then templates
|
|
199
|
+
"""
|
|
200
|
+
resolved = sources if sources is not None else DynamicCommandSources()
|
|
201
|
+
rows: list[SlashCommand] = []
|
|
202
|
+
claimed: set[str] = set()
|
|
203
|
+
|
|
204
|
+
for card in resolved.skills:
|
|
205
|
+
command = _skill_command(card)
|
|
206
|
+
if command.name in claimed:
|
|
207
|
+
continue
|
|
208
|
+
claimed.add(command.name)
|
|
209
|
+
rows.append(command)
|
|
210
|
+
|
|
211
|
+
for macro in resolved.templates:
|
|
212
|
+
if macro.name in claimed:
|
|
213
|
+
continue
|
|
214
|
+
claimed.add(macro.name)
|
|
215
|
+
rows.append(_template_command(macro))
|
|
216
|
+
|
|
217
|
+
return rows
|