lingo-loop 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 (57) hide show
  1. language_tutor/__init__.py +3 -0
  2. language_tutor/_assets/.claude-plugin/plugin.json +9 -0
  3. language_tutor/_assets/.codex-plugin/plugin.json +12 -0
  4. language_tutor/_assets/hermes-profile/SOUL.md +30 -0
  5. language_tutor/_assets/hermes-profile/config.yaml +37 -0
  6. language_tutor/_assets/hermes-profile/distribution.yaml +49 -0
  7. language_tutor/_assets/openclaw-plugin/openclaw.plugin.json +47 -0
  8. language_tutor/_assets/openclaw-plugin/package.json +39 -0
  9. language_tutor/_assets/openclaw-plugin/src/index.ts +68 -0
  10. language_tutor/_assets/openclaw-plugin/tsconfig.json +17 -0
  11. language_tutor/adapters/__init__.py +1 -0
  12. language_tutor/adapters/base.py +178 -0
  13. language_tutor/adapters/claude.py +21 -0
  14. language_tutor/adapters/codex.py +16 -0
  15. language_tutor/adapters/hermes.py +16 -0
  16. language_tutor/adapters/openclaw.py +16 -0
  17. language_tutor/adapters/registry.py +81 -0
  18. language_tutor/boot_context.py +114 -0
  19. language_tutor/cli.py +1039 -0
  20. language_tutor/dal/__init__.py +1 -0
  21. language_tutor/dal/migrations.py +77 -0
  22. language_tutor/dal/paths.py +48 -0
  23. language_tutor/dal/repositories.py +1117 -0
  24. language_tutor/dal/sqlite_store.py +29 -0
  25. language_tutor/dal/yaml_store.py +54 -0
  26. language_tutor/errors.py +101 -0
  27. language_tutor/evaluators.py +25 -0
  28. language_tutor/feedback.py +207 -0
  29. language_tutor/health.py +70 -0
  30. language_tutor/installer/__init__.py +35 -0
  31. language_tutor/installer/assets.py +100 -0
  32. language_tutor/installer/protocol.py +52 -0
  33. language_tutor/installer/providers/__init__.py +1 -0
  34. language_tutor/installer/providers/base.py +326 -0
  35. language_tutor/installer/providers/claude.py +19 -0
  36. language_tutor/installer/providers/codex.py +19 -0
  37. language_tutor/installer/providers/hermes.py +19 -0
  38. language_tutor/installer/providers/openclaw.py +24 -0
  39. language_tutor/installer/registry.py +44 -0
  40. language_tutor/installer/seams.py +124 -0
  41. language_tutor/installer/service.py +70 -0
  42. language_tutor/lessons.py +47 -0
  43. language_tutor/lifecycle.py +91 -0
  44. language_tutor/progress.py +426 -0
  45. language_tutor/progress_rendering.py +137 -0
  46. language_tutor/reading.py +66 -0
  47. language_tutor/schemas.py +1442 -0
  48. language_tutor/setup.py +43 -0
  49. language_tutor/srs.py +46 -0
  50. language_tutor/text_modalities.py +288 -0
  51. language_tutor/vocab.py +505 -0
  52. language_tutor/writing.py +35 -0
  53. lingo_loop-0.1.0.dist-info/METADATA +130 -0
  54. lingo_loop-0.1.0.dist-info/RECORD +57 -0
  55. lingo_loop-0.1.0.dist-info/WHEEL +4 -0
  56. lingo_loop-0.1.0.dist-info/entry_points.txt +2 -0
  57. lingo_loop-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """Local-first language tutor core."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "language-tutor",
3
+ "version": "0.1.0",
4
+ "description": "Local-first language tutor for Claude Code",
5
+ "author": {
6
+ "name": "language-tutor contributors"
7
+ },
8
+ "license": "MIT"
9
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "name": "language-tutor",
4
+ "version": "0.1.0",
5
+ "description": "Local-first language tutor packaged as a Codex plugin (reuses root skills/).",
6
+ "author": "language-tutor contributors",
7
+ "license": "MIT",
8
+ "skills": "./skills/",
9
+ "features": {
10
+ "plugin_hooks": false
11
+ }
12
+ }
@@ -0,0 +1,30 @@
1
+ # Language Tutor (Hermes Profile Prompt)
2
+
3
+ You are a patient, source-backed language tutor running inside the Hermes
4
+ host. You operate text-only: reading, lessons, transcripts, vocabulary,
5
+ writing, and progress reporting. You do not produce or consume audio or
6
+ images.
7
+
8
+ ## Lifecycle
9
+
10
+ Hermes does not provide SessionStart/SessionEnd hooks for this profile.
11
+ Boot context is built on demand via an explicit tutor command (the
12
+ operator runs the tutor's boot command after `hermes profile install`).
13
+ There is no automatic session-end hook; end-of-session work is invoked
14
+ explicitly when requested.
15
+
16
+ ## Boot context
17
+
18
+ When the explicit tutor boot command runs, load the learner profile and
19
+ preferences through the tutor core and render the boot context sections.
20
+ Never invent learner state; rely on the core's persisted data, which lives
21
+ outside this distribution.
22
+
23
+ ## Behavior contract
24
+
25
+ - Keep all interaction text-only and terminal-readable.
26
+ - Defer pedagogy, feedback, progress, and persistence to the tutor core.
27
+ - Never echo, package, or export learner secrets, memories, sessions,
28
+ database state, logs, or local overrides.
29
+ - Stay within the declared capability profile: no audio, no images, no
30
+ host-specific side effects beyond the documented text flows.
@@ -0,0 +1,37 @@
1
+ # Hermes profile configuration defaults for the language-tutor agent.
2
+ # Defaults only. No learner data, no secrets, no machine-local state.
3
+ schema_version: 1
4
+
5
+ # Capability declaration mirrors src/language_tutor/adapters/registry.py
6
+ # (HostId.HERMES). Source of truth stays in the registry; this is a
7
+ # documentation echo for operators inspecting the distribution.
8
+ capabilities:
9
+ text_support: supported
10
+ audio_support: unsupported
11
+ image_support: unsupported
12
+ lifecycle_start: explicit_command
13
+ lifecycle_end: not_available
14
+ boot_context_trigger: explicit_tutor_command
15
+
16
+ # Tutor flows exposed by this profile. All text-only.
17
+ flows:
18
+ - reading
19
+ - lesson
20
+ - transcript
21
+ - vocab
22
+ - writing
23
+ - progress
24
+
25
+ # Hermes profile lifecycle commands (documentation for operators).
26
+ commands:
27
+ install: hermes profile install
28
+ update: hermes profile update
29
+ inspect: hermes profile info language-tutor
30
+ list: hermes profile list
31
+ remove: hermes profile delete language-tutor
32
+
33
+ # Operator-supplied environment is read from the operator's own untracked
34
+ # .env at runtime. This distribution ships no values.
35
+ env:
36
+ source: operator_local_env
37
+ bundled_values: false
@@ -0,0 +1,49 @@
1
+ # Hermes profile distribution manifest for the language-tutor agent.
2
+ # Distribution metadata and defaults only. Contains NO learner-owned data.
3
+ # Installed/updated via `hermes profile install` / `hermes profile update`.
4
+ schema_version: 1
5
+ name: language-tutor
6
+ display_name: Language Tutor
7
+ version: 0.1.0
8
+ description: >-
9
+ Source-backed language-learning tutor profile for Hermes. Provides
10
+ text-only reading, lesson, transcript, vocab, writing, and progress
11
+ flows backed by the host-independent tutor core. No audio or image
12
+ capabilities.
13
+ license: MIT
14
+ source_url: https://github.com/artemVeduta/lingo-loop
15
+
16
+ # Whole-agent package contents (git-backed distribution).
17
+ profile:
18
+ prompt: SOUL.md
19
+ config: config.yaml
20
+ # Tutor skills are reused from the repository root `skills/` surface; this
21
+ # distribution does not ship its own skills/ tree.
22
+ skills: ../skills
23
+
24
+ # Environment variables the operator must supply locally before launch.
25
+ # Declared here for documentation only; values are never bundled. The actual
26
+ # secrets live in the operator's own untracked .env (NOT shipped).
27
+ env_requires:
28
+ - name: ANTHROPIC_API_KEY
29
+ required: true
30
+ description: API key for the tutor model provider. Operator-supplied; never bundled.
31
+
32
+ # Hard exclusions: data that must never be packaged, copied, or published.
33
+ exclude:
34
+ - ".env"
35
+ - "*.env"
36
+ - secrets
37
+ - memories
38
+ - sessions
39
+ - "*.sqlite"
40
+ - "*.sqlite3"
41
+ - "*.db"
42
+ - "*.db-wal"
43
+ - "*.db-shm"
44
+ - logs
45
+ - "*.log"
46
+ - caches
47
+ - "*.cache"
48
+ - local
49
+ - local_overrides
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://docs.openclaw.ai/plugins/manifest.schema.json",
3
+ "id": "language-tutor",
4
+ "name": "Language Tutor",
5
+ "displayName": "Language Tutor",
6
+ "version": "0.1.0",
7
+ "description": "Text-only language tutor adapter for OpenClaw. Builds boot context on the first tutor message and exposes core text-modality tutor tools.",
8
+ "entry": "dist/index.js",
9
+ "activation": {
10
+ "onStartup": true
11
+ },
12
+ "contracts": {
13
+ "tools": ["language_tutor"]
14
+ },
15
+ "configSchema": {
16
+ "type": "object",
17
+ "properties": {},
18
+ "additionalProperties": false
19
+ },
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "capabilities": {
24
+ "text": "supported",
25
+ "audio": "unsupported",
26
+ "image": "unsupported"
27
+ },
28
+ "lifecycle": {
29
+ "start": "first_message",
30
+ "end": "not_available",
31
+ "bootContextTrigger": "first_tutor_message"
32
+ },
33
+ "tools": [
34
+ {
35
+ "name": "language_tutor.boot_context",
36
+ "optional": false
37
+ },
38
+ {
39
+ "name": "language_tutor.text_exercise",
40
+ "optional": false
41
+ },
42
+ {
43
+ "name": "language_tutor.run_cli",
44
+ "optional": true
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@language-tutor/openclaw-plugin",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw host plugin for the language-tutor text-modality adapter.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=22"
9
+ },
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "openclaw": {
19
+ "extensions": ["./dist/index.js"]
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "openclaw.plugin.json"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "check": "tsc --noEmit -p tsconfig.json"
29
+ },
30
+ "peerDependencies": {
31
+ "openclaw": ">=1.0.0"
32
+ },
33
+ "dependencies": {
34
+ "typebox": "^1.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.5.0"
38
+ }
39
+ }
@@ -0,0 +1,68 @@
1
+ // Language Tutor plugin entry for the OpenClaw host.
2
+ //
3
+ // Uses focused SDK subpath imports (NOT a whole-SDK wildcard import) per the
4
+ // OpenClaw plugin model: https://docs.openclaw.ai/plugins
5
+ //
6
+ // Capability stance (mirrors src/language_tutor/adapters/registry.py OpenClaw
7
+ // defaults): text supported; lifecycle start = first_message; lifecycle end =
8
+ // not_available; boot context is built on the first tutor message. Any
9
+ // side-effectful / binary-dependent tool is registered opt-in only.
10
+
11
+ import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
12
+ import { Type } from "typebox";
13
+
14
+ export default defineToolPlugin({
15
+ id: "language-tutor",
16
+ name: "Language Tutor",
17
+ description:
18
+ "Text-only language tutor adapter for OpenClaw. Builds boot context on the first tutor message and exposes core text-modality tutor tools.",
19
+ tools: (tool) => [
20
+ // First-message boot trigger: OpenClaw has no SessionStart-style hook, so
21
+ // the tutor assembles boot context the first time the learner messages
22
+ // the tutor.
23
+ tool({
24
+ name: "language_tutor.boot_context",
25
+ description:
26
+ "Assemble learner boot context (profile, preferences, focus) on the first tutor message.",
27
+ parameters: Type.Object({
28
+ sessionId: Type.Optional(Type.String()),
29
+ }),
30
+ async execute(_params, _config, _context) {
31
+ // Pure text assembly only. No persistence or host-specific behavior
32
+ // here; the core tutor owns boot-context generation.
33
+ return { sections: [] as string[] };
34
+ },
35
+ }),
36
+ // Text-modality tutor tool (reading / lesson / transcript / vocab /
37
+ // writing / progress are all text-only flows). No side effects beyond
38
+ // returning text.
39
+ tool({
40
+ name: "language_tutor.text_exercise",
41
+ description:
42
+ "Present and evaluate a text-only tutor exercise (reading, lesson, transcript, vocab, writing, progress).",
43
+ parameters: Type.Object({
44
+ modality: Type.String(),
45
+ payload: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
46
+ }),
47
+ async execute(params, _config, _context) {
48
+ return { modality: params.modality, text: "" };
49
+ },
50
+ }),
51
+ // Side-effectful / binary-dependent tool. Shells out to the local tutor
52
+ // CLI, so it is OPT-IN ONLY and gated behind a user allowlist.
53
+ tool({
54
+ name: "language_tutor.run_cli",
55
+ description:
56
+ "Invoke the local language-tutor CLI. Binary-dependent and side-effectful; opt-in only.",
57
+ optional: true,
58
+ parameters: Type.Object({
59
+ command: Type.Array(Type.String()),
60
+ }),
61
+ async execute(params, _config, _context) {
62
+ // Execution is performed by the host only after the user allowlists
63
+ // it.
64
+ return { command: params.command };
65
+ },
66
+ }),
67
+ ],
68
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "declaration": true,
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "verbatimModuleSyntax": true,
14
+ "forceConsistentCasingInFileNames": true
15
+ },
16
+ "include": ["src"]
17
+ }
@@ -0,0 +1 @@
1
+ """Host adapter boundaries."""
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from language_tutor.schemas import (
6
+ APPROVED_HOST_SOURCES,
7
+ AdapterCapabilityProfile,
8
+ BootContextTrigger,
9
+ ConformanceRun,
10
+ HostId,
11
+ HostSetupProfileContract,
12
+ HostSetupTarget,
13
+ ManualProviderInstallReport,
14
+ SetupModel,
15
+ SetupPackage,
16
+ TargetStatus,
17
+ )
18
+
19
+
20
+ class JsonCommandRunner(Protocol):
21
+ def run_json(
22
+ self, args: list[str], payload: dict[str, object] | None = None
23
+ ) -> dict[str, object]:
24
+ """Run a host-facing JSON command."""
25
+ ...
26
+
27
+
28
+ class HookPayloadAdapter(Protocol):
29
+ def normalize(self, payload: dict[str, object]) -> dict[str, object]:
30
+ """Normalize host hook payload into core JSON."""
31
+ ...
32
+
33
+
34
+ class HostCapabilityProvider(Protocol):
35
+ def capability_profile(self) -> AdapterCapabilityProfile:
36
+ """Return the host's declared capability profile."""
37
+ ...
38
+
39
+
40
+ class LifecycleTriggerProvider(Protocol):
41
+ def boot_trigger(self) -> BootContextTrigger:
42
+ """Return the host's boot-context lifecycle trigger."""
43
+ ...
44
+
45
+
46
+ class SetupPackageProvider(Protocol):
47
+ def setup_package(self) -> SetupPackage:
48
+ """Describe the host-owned distribution package."""
49
+ ...
50
+
51
+
52
+ class ConformanceRunner(Protocol):
53
+ def run_conformance(self) -> ConformanceRun:
54
+ """Run shared host-portability conformance checks."""
55
+ ...
56
+
57
+
58
+ class ManualReportProvider(Protocol):
59
+ def manual_report(self) -> ManualProviderInstallReport:
60
+ """Return the host's manual provider install report."""
61
+ ...
62
+
63
+
64
+ # Supported host target registry (US1). Single source of truth for the four
65
+ # approved hosts, their official setup-doc sources, setup models, and contract
66
+ # paths. Antigravity is intentionally absent and rejected at the schema layer.
67
+ _REGISTRY: dict[HostId, HostSetupTarget] = {
68
+ HostId.HERMES: HostSetupTarget(
69
+ id=HostId.HERMES,
70
+ display_name="Hermes",
71
+ official_source_url=APPROVED_HOST_SOURCES[HostId.HERMES.value],
72
+ setup_model=SetupModel.PROFILE_DISTRIBUTION,
73
+ primary_subagent="hermes-adapter-subagent",
74
+ contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/hermes.md",
75
+ status=TargetStatus.PLANNED,
76
+ ),
77
+ HostId.OPENCLAW: HostSetupTarget(
78
+ id=HostId.OPENCLAW,
79
+ display_name="OpenClaw",
80
+ official_source_url=APPROVED_HOST_SOURCES[HostId.OPENCLAW.value],
81
+ setup_model=SetupModel.PLUGIN_PACKAGE,
82
+ primary_subagent="openclaw-adapter-subagent",
83
+ contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/openclaw.md",
84
+ status=TargetStatus.PLANNED,
85
+ ),
86
+ HostId.CLAUDE: HostSetupTarget(
87
+ id=HostId.CLAUDE,
88
+ display_name="Claude",
89
+ official_source_url=APPROVED_HOST_SOURCES[HostId.CLAUDE.value],
90
+ setup_model=SetupModel.PLUGIN_PACKAGE,
91
+ primary_subagent="claude-adapter-subagent",
92
+ contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/claude.md",
93
+ status=TargetStatus.PLANNED,
94
+ ),
95
+ HostId.CODEX: HostSetupTarget(
96
+ id=HostId.CODEX,
97
+ display_name="Codex",
98
+ official_source_url=APPROVED_HOST_SOURCES[HostId.CODEX.value],
99
+ setup_model=SetupModel.LOCAL_MARKETPLACE_PLUGIN,
100
+ primary_subagent="codex-adapter-subagent",
101
+ contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/codex.md",
102
+ status=TargetStatus.PLANNED,
103
+ ),
104
+ }
105
+
106
+
107
+ def supported_host_targets() -> dict[HostId, HostSetupTarget]:
108
+ """Return the registry of approved host setup targets."""
109
+ return dict(_REGISTRY)
110
+
111
+
112
+ def is_supported_host(host_id: str) -> bool:
113
+ """True only for hermes, openclaw, claude, codex. Antigravity is rejected."""
114
+ return host_id in {h.value for h in HostId}
115
+
116
+
117
+ def get_host_target(host_id: str) -> HostSetupTarget:
118
+ return _REGISTRY[HostId(host_id)]
119
+
120
+
121
+ def normalize_hook_payload(payload: dict[str, object]) -> dict[str, object]:
122
+ return dict(payload)
123
+
124
+
125
+ def run_conformance(profile: AdapterCapabilityProfile) -> ConformanceRun:
126
+ """Build a deterministic conformance result from a host capability profile.
127
+
128
+ The runner is host-blind: it derives the six representative-flow outcomes
129
+ from the declared capability profile (a gated flow is ``skipped``, every
130
+ other flow ``pass``) and reports the shared boot/feedback/progress/error/
131
+ data-ownership contract results. It owns no pedagogy or learner state.
132
+ """
133
+ from language_tutor.schemas import (
134
+ ConformanceRun,
135
+ Decision,
136
+ FlowResult,
137
+ RepresentativeFlow,
138
+ )
139
+
140
+ gated = {str(g) for g in profile.flow_gates}
141
+ flows: dict[RepresentativeFlow, FlowResult] = {}
142
+ for flow in RepresentativeFlow:
143
+ flows[flow] = FlowResult.SKIPPED if flow.value in gated else FlowResult.PASS
144
+
145
+ skipped = [f for f in RepresentativeFlow if f.value in gated]
146
+ has_fail = any(r == FlowResult.FAIL for r in flows.values())
147
+ decision = Decision.FAIL if has_fail else Decision.PASS
148
+
149
+ return ConformanceRun(
150
+ host=HostId(str(profile.host)),
151
+ capability_profile="schemas/host_capability_profile.schema.json",
152
+ flows=flows,
153
+ boot_context_result=FlowResult.PASS,
154
+ feedback_contract_result=FlowResult.PASS,
155
+ progress_contract_result=FlowResult.PASS,
156
+ error_behavior_result=FlowResult.PASS,
157
+ data_ownership_result=FlowResult.PASS,
158
+ skipped_flows=skipped,
159
+ decision=decision,
160
+ )
161
+
162
+
163
+ def load_host_setup_profile(path: str) -> HostSetupProfileContract:
164
+ """Load and validate a host setup profile markdown contract.
165
+
166
+ The markdown contract embeds a fenced ```json block describing the
167
+ HostSetupProfileContract fields. This loader extracts and validates it.
168
+ """
169
+ import json
170
+ import re
171
+ from pathlib import Path
172
+
173
+ text = Path(path).read_text(encoding="utf-8")
174
+ match = re.search(r"```json\s*(\{.*?\})\s*```", text, re.DOTALL)
175
+ if not match:
176
+ raise ValueError(f"host setup profile {path} has no json contract block")
177
+ data = json.loads(match.group(1))
178
+ return HostSetupProfileContract.model_validate(data)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from language_tutor.adapters.registry import capability_profile_for
4
+ from language_tutor.schemas import AdapterCapabilityProfile, HostId
5
+
6
+
7
+ def capability_profile() -> AdapterCapabilityProfile:
8
+ """Claude capability profile. No-hook lifecycle (spec 007)."""
9
+ return capability_profile_for(HostId.CLAUDE.value)
10
+
11
+
12
+ def plugin_root_components() -> dict[str, str]:
13
+ return {
14
+ "manifest": ".claude-plugin/plugin.json",
15
+ "setup_skill": "skills/tutor-setup/SKILL.md",
16
+ "vocab_skill": "skills/tutor-vocab/SKILL.md",
17
+ "writing_skill": "skills/tutor-writing/SKILL.md",
18
+ "progress_skill": "skills/tutor-progress/SKILL.md",
19
+ "judge_agent": "agents/tutor-judge.md",
20
+ "cli": "bin/tutor",
21
+ }
@@ -0,0 +1,16 @@
1
+ """Codex host adapter.
2
+
3
+ Codex distributes as a local-marketplace plugin and boots the tutor on the
4
+ first tutor-skill invocation (no plugin hook required). Setup is package-only.
5
+ This module exposes the host capability profile and leaves pedagogy, feedback,
6
+ progress, and learner state to the tutor core.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from language_tutor.adapters.registry import capability_profile_for
12
+ from language_tutor.schemas import AdapterCapabilityProfile, HostId
13
+
14
+
15
+ def capability_profile() -> AdapterCapabilityProfile:
16
+ return capability_profile_for(HostId.CODEX.value)
@@ -0,0 +1,16 @@
1
+ """Hermes host adapter.
2
+
3
+ Hermes uses a git-backed profile distribution and has no Claude-style startup
4
+ hook, so it boots the tutor through an explicit command trigger. Setup is
5
+ package-only; this module exposes the host capability profile and leaves all
6
+ pedagogy, feedback, progress, and learner state to the tutor core.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from language_tutor.adapters.registry import capability_profile_for
12
+ from language_tutor.schemas import AdapterCapabilityProfile, HostId
13
+
14
+
15
+ def capability_profile() -> AdapterCapabilityProfile:
16
+ return capability_profile_for(HostId.HERMES.value)
@@ -0,0 +1,16 @@
1
+ """OpenClaw host adapter.
2
+
3
+ OpenClaw distributes as a Node/TypeScript ESM plugin package and has no
4
+ Claude-style startup hook, so it boots the tutor on the first tutor message.
5
+ Setup is package-only; this module exposes the host capability profile and
6
+ leaves pedagogy, feedback, progress, and learner state to the tutor core.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from language_tutor.adapters.registry import capability_profile_for
12
+ from language_tutor.schemas import AdapterCapabilityProfile, HostId
13
+
14
+
15
+ def capability_profile() -> AdapterCapabilityProfile:
16
+ return capability_profile_for(HostId.OPENCLAW.value)
@@ -0,0 +1,81 @@
1
+ """Host capability profile defaults (single source of truth).
2
+
3
+ Each supported host declares its capability profile here. Host-specific adapter
4
+ modules (claude.py, codex.py, hermes.py, openclaw.py) may translate runtime
5
+ behavior, but capability declarations stay centralized to keep flow names,
6
+ lifecycle values, and boot triggers DRY.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from language_tutor.schemas import (
12
+ AdapterCapabilityProfile,
13
+ BootTrigger,
14
+ CapabilitySupport,
15
+ HostId,
16
+ LifecycleEnd,
17
+ LifecycleStart,
18
+ )
19
+
20
+ _DEFAULTS: dict[HostId, AdapterCapabilityProfile] = {
21
+ HostId.CLAUDE: AdapterCapabilityProfile(
22
+ host=HostId.CLAUDE,
23
+ display_name="Claude",
24
+ text_support=CapabilitySupport.SUPPORTED,
25
+ lifecycle_start=LifecycleStart.FIRST_MESSAGE,
26
+ lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
27
+ boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
28
+ setup_entry_point="claude --plugin-dir <plugin-root>",
29
+ update_behavior="/reload-plugins",
30
+ side_effectful_capabilities=[],
31
+ unsupported_capabilities=["audio", "image"],
32
+ flow_gates=[],
33
+ ),
34
+ HostId.CODEX: AdapterCapabilityProfile(
35
+ host=HostId.CODEX,
36
+ display_name="Codex",
37
+ text_support=CapabilitySupport.SUPPORTED,
38
+ lifecycle_start=LifecycleStart.FIRST_MESSAGE,
39
+ lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
40
+ boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
41
+ setup_entry_point="local marketplace install via .codex-plugin/plugin.json",
42
+ update_behavior="Codex restart/reload after marketplace update",
43
+ side_effectful_capabilities=[],
44
+ unsupported_capabilities=["audio", "image"],
45
+ flow_gates=[],
46
+ ),
47
+ HostId.HERMES: AdapterCapabilityProfile(
48
+ host=HostId.HERMES,
49
+ display_name="Hermes",
50
+ text_support=CapabilitySupport.SUPPORTED,
51
+ lifecycle_start=LifecycleStart.FIRST_MESSAGE,
52
+ lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
53
+ boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
54
+ setup_entry_point="hermes profile install",
55
+ update_behavior="hermes profile update",
56
+ side_effectful_capabilities=[],
57
+ unsupported_capabilities=["audio", "image"],
58
+ flow_gates=[],
59
+ ),
60
+ HostId.OPENCLAW: AdapterCapabilityProfile(
61
+ host=HostId.OPENCLAW,
62
+ display_name="OpenClaw",
63
+ text_support=CapabilitySupport.SUPPORTED,
64
+ lifecycle_start=LifecycleStart.FIRST_MESSAGE,
65
+ lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
66
+ boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
67
+ setup_entry_point="openclaw plugins install <package-name>",
68
+ update_behavior="reinstall/upgrade plugin package",
69
+ side_effectful_capabilities=["binary-dependent tools (opt-in via user allowlist)"],
70
+ unsupported_capabilities=["audio", "image"],
71
+ flow_gates=[],
72
+ ),
73
+ }
74
+
75
+
76
+ def default_capability_profiles() -> dict[HostId, AdapterCapabilityProfile]:
77
+ return dict(_DEFAULTS)
78
+
79
+
80
+ def capability_profile_for(host_id: str) -> AdapterCapabilityProfile:
81
+ return _DEFAULTS[HostId(host_id)]