everythingiscontext 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.
core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # empty package marker
core/kind_specs.py ADDED
@@ -0,0 +1,135 @@
1
+ """Per-kind module structure: required files, default growth folders, purpose.
2
+
3
+ Single source of truth for how an `integration`, `task`, or `workflow`
4
+ module is laid out on disk.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class KindSpec:
13
+ kind: str
14
+ purpose: str
15
+ required_files: list[str]
16
+ starter_file: str
17
+ starter_outline: str
18
+ default_growth_folders: list[dict] # [{"name": "notes", "path": "notes/<date-slug>.md"}]
19
+
20
+
21
+ KIND_SPECS: dict[str, KindSpec] = {
22
+ "integration": KindSpec(
23
+ kind="integration",
24
+ purpose="Reusable access to an external service, API, database, or local tool.",
25
+ required_files=["module.yaml", "llms.txt", "info.md"],
26
+ starter_file="info.md",
27
+ starter_outline="Purpose, Access, Operations, Example usage.",
28
+ default_growth_folders=[
29
+ {"name": "notes", "path": "notes/<date-slug>.md"},
30
+ ],
31
+ ),
32
+ "task": KindSpec(
33
+ kind="task",
34
+ purpose="A bounded outcome needing progress tracking, subtasks, findings.",
35
+ required_files=["module.yaml", "llms.txt", "brief.md", "status.md"],
36
+ starter_file="brief.md",
37
+ starter_outline="Goal and initial request; status.md tracks subtasks.",
38
+ default_growth_folders=[
39
+ {"name": "progress", "path": "progress/<date-slug>.md"},
40
+ ],
41
+ ),
42
+ "workflow": KindSpec(
43
+ kind="workflow",
44
+ purpose="A repeatable procedure or playbook that should improve across runs.",
45
+ required_files=["module.yaml", "llms.txt", "steps.md"],
46
+ starter_file="steps.md",
47
+ starter_outline="The repeatable steps, numbered.",
48
+ default_growth_folders=[
49
+ {"name": "runs", "path": "runs/<date-slug>.md"},
50
+ {"name": "lessons", "path": "lessons/<date-slug>.md"},
51
+ ],
52
+ ),
53
+ }
54
+
55
+
56
+ def render_kind_specs_md() -> str:
57
+ """Render KIND_SPECS as markdown for structure.md."""
58
+ lines: list[str] = []
59
+ for spec in KIND_SPECS.values():
60
+ lines.append(f"### `{spec.kind}`")
61
+ lines.append("")
62
+ lines.append(spec.purpose)
63
+ lines.append("")
64
+ lines.append("**Files written at creation:**")
65
+ for f in spec.required_files:
66
+ note = f" - {spec.starter_outline}" if f == spec.starter_file else ""
67
+ lines.append(f"- `{f}`{note}")
68
+ if spec.default_growth_folders:
69
+ lines.append("")
70
+ lines.append(
71
+ "**Default growth folders** (lazy: created on first entry; "
72
+ "seed these into `llms.txt` `## Where to write` at module creation):"
73
+ )
74
+ for ga in spec.default_growth_folders:
75
+ lines.append(f"- `{ga['name']}` -> `{ga['path']}`")
76
+ lines.append("")
77
+ return "\n".join(lines)
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class InfoSection:
82
+ name: str
83
+ purpose: str
84
+ format: str # "freeform" or description of strict format
85
+
86
+
87
+ # Core version: 6 sections (no "Python packages" — that's added by Context Agora)
88
+ INTEGRATION_INFO_SECTIONS: list[InfoSection] = [
89
+ InfoSection(
90
+ "Purpose",
91
+ "One-line summary of what this integration gives the agent access to.",
92
+ "freeform",
93
+ ),
94
+ InfoSection(
95
+ "Where it lives",
96
+ "Upstream system: URL, host, endpoint, or local path.",
97
+ "freeform",
98
+ ),
99
+ InfoSection(
100
+ "Auth & access",
101
+ "How the agent authenticates. Required secret names are written as `UPPER_CASE` in backticks.",
102
+ "strict: secret names as `UPPER_CASE` backtick tokens",
103
+ ),
104
+ InfoSection(
105
+ "Key entities",
106
+ "Domain objects the agent can read or write.",
107
+ "freeform",
108
+ ),
109
+ InfoSection(
110
+ "Operations",
111
+ "Common calls or queries with short examples.",
112
+ "freeform",
113
+ ),
114
+ InfoSection(
115
+ "Examples",
116
+ "Runnable snippets.",
117
+ "freeform",
118
+ ),
119
+ ]
120
+
121
+
122
+ def render_info_md_sections_md(sections: list[InfoSection] | None = None) -> str:
123
+ """Render the integration kind's expected info.md sections for structure.md.
124
+
125
+ Accepts an optional sections list so the platform can pass an extended
126
+ list (e.g., with "Python packages" section). Defaults to core's 6-section list.
127
+ """
128
+ sections = sections or INTEGRATION_INFO_SECTIONS
129
+ lines: list[str] = ["### `info.md` sections (integration kind)", ""]
130
+ for s in sections:
131
+ lines.append(f"- `## {s.name}` -- {s.purpose}")
132
+ if s.format != "freeform":
133
+ lines.append(f" - Format: {s.format}")
134
+ lines.append("")
135
+ return "\n".join(lines)
core/llms_gen.py ADDED
@@ -0,0 +1,109 @@
1
+ """Generate workspace-level files: llms.txt, system.md, structure.md.
2
+
3
+ These files are regenerated on every load/unload operation.
4
+ """
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+
10
+ from core.kind_specs import render_kind_specs_md, render_info_md_sections_md
11
+ from core.manifest import render_schema_md
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def extract_module_summary(llms_txt_content: str) -> str:
17
+ """Extract the > blockquote description from a module's llms.txt."""
18
+ for line in llms_txt_content.splitlines():
19
+ if line.startswith("> "):
20
+ return line[2:].strip()
21
+ return ""
22
+
23
+
24
+ def generate_root_llms_txt(context_dir: Path) -> None:
25
+ """Generate root llms.txt from loaded modules' llms.txt files.
26
+
27
+ Skips dot-prefixed directories (e.g. .claude).
28
+ """
29
+ entries = []
30
+ for mod_dir in sorted(context_dir.iterdir()):
31
+ if not mod_dir.is_dir() or mod_dir.name.startswith("."):
32
+ continue
33
+ llms_file = mod_dir / "llms.txt"
34
+ if llms_file.exists():
35
+ summary = extract_module_summary(llms_file.read_text())
36
+ else:
37
+ summary = "Context module"
38
+ entries.append(f"- [{mod_dir.name}]({mod_dir.name}/llms.txt): {summary}")
39
+
40
+ lines = ["# Context", ""]
41
+ lines.extend(entries)
42
+ (context_dir / "llms.txt").write_text("\n".join(lines) + "\n")
43
+
44
+
45
+ def generate_structure_md(context_dir: Path, info_sections: list | None = None) -> None:
46
+ """Write context/structure.md from manifest schema and kind specs.
47
+
48
+ Auto-generated reference for module.yaml fields, per-kind file
49
+ requirements, and info.md section structure.
50
+
51
+ Accepts optional info_sections so the platform can pass its extended
52
+ list (with "Python packages" section). Defaults to core's 6-section list.
53
+ """
54
+ body = "\n".join([
55
+ "# Module Structure",
56
+ "",
57
+ "Auto-generated from `manifest.py` and `kind_specs.py`. Do not edit by hand.",
58
+ "",
59
+ render_schema_md(),
60
+ "",
61
+ "## Module kinds",
62
+ "",
63
+ render_kind_specs_md(),
64
+ "## Starter file structure",
65
+ "",
66
+ render_info_md_sections_md(info_sections),
67
+ ])
68
+ (context_dir / "structure.md").write_text(body)
69
+
70
+
71
+ def generate_system_md(context_dir: Path, template_content: str) -> None:
72
+ """Write context/system.md from a template string + loaded modules table.
73
+
74
+ The caller is responsible for reading/assembling the template content.
75
+ This function appends the loaded modules table at the end.
76
+ Skips dot-prefixed directories.
77
+ """
78
+ rows: list[tuple[str, str, str]] = []
79
+ for mod_dir in sorted(context_dir.iterdir()):
80
+ if not mod_dir.is_dir() or mod_dir.name.startswith("."):
81
+ continue
82
+ kind = "unknown"
83
+ manifest_file = mod_dir / "module.yaml"
84
+ if manifest_file.exists():
85
+ try:
86
+ data = yaml.safe_load(manifest_file.read_text())
87
+ kind = data.get("kind", "integration") if data else "unknown"
88
+ except Exception:
89
+ log.warning("Failed to parse %s", manifest_file)
90
+ llms_file = mod_dir / "llms.txt"
91
+ summary = ""
92
+ if llms_file.exists():
93
+ summary = extract_module_summary(llms_file.read_text())
94
+ rows.append((mod_dir.name, kind, summary))
95
+
96
+ lines = [
97
+ "",
98
+ "## Loaded modules",
99
+ "",
100
+ ]
101
+ if rows:
102
+ lines.append("| Module | Kind | Summary |")
103
+ lines.append("|--------|------|---------|")
104
+ for name, kind, summary in rows:
105
+ lines.append(f"| {name} | {kind} | {summary} |")
106
+ else:
107
+ lines.append("No modules loaded.")
108
+
109
+ (context_dir / "system.md").write_text(template_content + "\n".join(lines) + "\n")
core/manifest.py ADDED
@@ -0,0 +1,225 @@
1
+ """Module manifest (module.yaml) read/write service.
2
+
3
+ Each module declares its name, kind, secrets, dependencies, and optional
4
+ jobs in module.yaml. Replaces per-module .env.schema and requirements.txt.
5
+ """
6
+ import re
7
+ import typing
8
+ from pathlib import Path
9
+ from typing import Literal
10
+
11
+ import yaml
12
+ from pydantic import BaseModel, Field, field_validator
13
+
14
+ ModuleKind = Literal["integration", "task", "workflow"]
15
+
16
+
17
+ _EVERY_RE = re.compile(r"^(\d+)([smh])$")
18
+ _EVERY_UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600}
19
+
20
+ # Must match jobs.TICK_SECONDS — keep as a constant here to avoid
21
+ # importing jobs.py from manifest.py (circular).
22
+ _MIN_EVERY_SECONDS = 30
23
+
24
+
25
+ def parse_every(value: str) -> int:
26
+ """Convert '30s' / '5m' / '1h' to a number of seconds.
27
+
28
+ Raises ValueError on any malformed input or values below the
29
+ scheduler tick (30 s) — sub-tick intervals would round up anyway
30
+ and surprise the user.
31
+ """
32
+ if not isinstance(value, str):
33
+ raise ValueError(f"every must be a string, got {type(value).__name__}")
34
+ match = _EVERY_RE.fullmatch(value)
35
+ if not match:
36
+ raise ValueError(
37
+ f"invalid every '{value}': expected <digits><s|m|h>, e.g. '30s', '5m', '1h'"
38
+ )
39
+ n = int(match.group(1))
40
+ if n == 0:
41
+ raise ValueError(f"invalid every '{value}': must be > 0")
42
+ seconds = n * _EVERY_UNIT_SECONDS[match.group(2)]
43
+ if seconds < _MIN_EVERY_SECONDS:
44
+ raise ValueError(
45
+ f"invalid every '{value}': minimum is {_MIN_EVERY_SECONDS}s"
46
+ )
47
+ return seconds
48
+
49
+
50
+ class JobSpec(BaseModel):
51
+ name: str = Field(..., description="Job identifier; unique within the module.")
52
+ script: str = Field(
53
+ ...,
54
+ description="Relative path to the script inside the module directory. Absolute paths and `..` traversal are rejected.",
55
+ )
56
+ every: str = Field(
57
+ ...,
58
+ description="Cron-like cadence as `<digits><s|m|h>` (e.g. `30s`, `5m`, `1h`). Minimum 30s.",
59
+ )
60
+
61
+ @field_validator("script")
62
+ @classmethod
63
+ def _no_absolute_or_traversal(cls, v: str) -> str:
64
+ if v.startswith("/") or ".." in Path(v).parts:
65
+ raise ValueError(f"invalid script path '{v}': must be relative inside the module")
66
+ return v
67
+
68
+ @field_validator("every")
69
+ @classmethod
70
+ def _validate_every(cls, v: str) -> str:
71
+ parse_every(v) # raises on bad values
72
+ return v
73
+
74
+ @property
75
+ def every_seconds(self) -> int:
76
+ return parse_every(self.every)
77
+
78
+
79
+ class ModuleManifest(BaseModel):
80
+ name: str = Field(
81
+ ...,
82
+ description="Folder slug; must match the module directory name.",
83
+ )
84
+ kind: ModuleKind = Field(
85
+ default="integration",
86
+ description='One of: "integration", "task", "workflow". Drives how the module is rendered in the sidebar.',
87
+ )
88
+ archived: bool = Field(
89
+ default=False,
90
+ description="Hide from the active sidebar without deleting. Archived modules appear under a collapsed Archived section and cannot be loaded into the workspace.",
91
+ )
92
+ secrets: list[str] = Field(
93
+ default_factory=list,
94
+ description="Environment variable names this module needs at runtime. Resolved via varlock from the workspace .env.schema.",
95
+ )
96
+ dependencies: list[str] = Field(
97
+ default_factory=list,
98
+ description="Python packages required at runtime. Installed into the platform venv at boot.",
99
+ )
100
+ icon: str = Field(
101
+ default="",
102
+ description="Icon name for the module's app card (e.g. 'globe', 'server'). Empty means auto-derived.",
103
+ )
104
+ jobs: list[JobSpec] = Field(
105
+ default_factory=list,
106
+ description="Cron-like scheduled scripts owned by this module.",
107
+ )
108
+
109
+
110
+ def read_manifest(module_dir: Path) -> ModuleManifest:
111
+ """Read module.yaml from a module directory.
112
+
113
+ Returns a manifest with defaults (name inferred from dir) if the
114
+ file doesn't exist.
115
+ """
116
+ manifest_path = module_dir / "module.yaml"
117
+ if not manifest_path.exists():
118
+ return ModuleManifest(name=module_dir.name)
119
+ raw = yaml.safe_load(manifest_path.read_text()) or {}
120
+ raw.setdefault("name", module_dir.name)
121
+ return ModuleManifest(**raw)
122
+
123
+
124
+ def write_manifest(module_dir: Path, manifest: ModuleManifest) -> None:
125
+ """Write a ModuleManifest to module.yaml, omitting empty optional fields."""
126
+ data: dict[str, str | bool | list[str]] = {"name": manifest.name}
127
+ if manifest.kind != "integration":
128
+ data["kind"] = manifest.kind
129
+ if manifest.archived:
130
+ data["archived"] = manifest.archived
131
+ if manifest.icon:
132
+ data["icon"] = manifest.icon
133
+ if manifest.secrets:
134
+ data["secrets"] = manifest.secrets
135
+ if manifest.dependencies:
136
+ data["dependencies"] = manifest.dependencies
137
+ if manifest.jobs:
138
+ data["jobs"] = [
139
+ {"name": j.name, "script": j.script, "every": j.every}
140
+ for j in manifest.jobs
141
+ ]
142
+ (module_dir / "module.yaml").write_text(
143
+ yaml.dump(data, default_flow_style=False, sort_keys=False)
144
+ )
145
+
146
+
147
+ _SLUG_NON_ALPHANUM = re.compile(r"[^a-z0-9-]")
148
+ _SLUG_DASH_RUN = re.compile(r"-+")
149
+
150
+
151
+ def slugify_task_name(name: str) -> str:
152
+ """Convert a human task name to a folder-safe slug."""
153
+ slug = name.strip().lower().replace("_", "-")
154
+ slug = _SLUG_NON_ALPHANUM.sub("-", slug)
155
+ slug = _SLUG_DASH_RUN.sub("-", slug)
156
+ return slug.strip("-")
157
+
158
+
159
+ _PRIMITIVE_NAMES = {
160
+ str: "str",
161
+ int: "int",
162
+ float: "float",
163
+ bool: "bool",
164
+ }
165
+
166
+
167
+ def _format_default(field) -> str:
168
+ if field.is_required():
169
+ return "required"
170
+ factory = field.default_factory
171
+ if factory is not None:
172
+ try:
173
+ default = factory()
174
+ except TypeError:
175
+ default = None
176
+ else:
177
+ default = field.default
178
+ if isinstance(default, bool):
179
+ return "default false" if default is False else "default true"
180
+ if isinstance(default, str):
181
+ return f'default "{default}"'
182
+ if isinstance(default, list) and not default:
183
+ return "default []"
184
+ return f"default {default!r}"
185
+
186
+
187
+ def _format_type(annotation) -> str:
188
+ if annotation in _PRIMITIVE_NAMES:
189
+ return _PRIMITIVE_NAMES[annotation]
190
+ origin = getattr(annotation, "__origin__", None)
191
+ if origin is list:
192
+ (inner,) = annotation.__args__
193
+ if inner is JobSpec:
194
+ return "list[JobSpec]"
195
+ return f"list[{_PRIMITIVE_NAMES.get(inner, inner.__name__)}]"
196
+ if typing.get_origin(annotation) is Literal:
197
+ return " | ".join(repr(v) for v in typing.get_args(annotation))
198
+ return getattr(annotation, "__name__", str(annotation))
199
+
200
+
201
+ def render_schema_md() -> str:
202
+ """Render ModuleManifest as a markdown schema block for prompt injection.
203
+
204
+ Walks the model's fields and emits one bullet per field with type,
205
+ default, and description. Single source of truth for what the AI
206
+ should write into module.yaml — adding a new field here makes it
207
+ visible to the chat agent automatically.
208
+ """
209
+ lines = ["## module.yaml schema", ""]
210
+ for field_name, field in ModuleManifest.model_fields.items():
211
+ type_str = _format_type(field.annotation)
212
+ default_str = _format_default(field)
213
+ desc = field.description or ""
214
+ lines.append(f"- `{field_name}` ({type_str}, {default_str}) — {desc}")
215
+ if type_str == "list[JobSpec]":
216
+ for sub_name, sub in JobSpec.model_fields.items():
217
+ sub_type = _format_type(sub.annotation)
218
+ sub_default = _format_default(sub)
219
+ sub_desc = sub.description or ""
220
+ lines.append(f" - `{sub_name}` ({sub_type}, {sub_default}) — {sub_desc}")
221
+ lines.append("")
222
+ lines.append(
223
+ "Omit fields that match their default. Unknown fields are rejected."
224
+ )
225
+ return "\n".join(lines)
core/schemas.py ADDED
@@ -0,0 +1,22 @@
1
+ import re
2
+
3
+ _VALID_MODULE_NAME = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
4
+
5
+
6
+ def validate_module_name(name: str) -> str:
7
+ name = name.strip()
8
+ if not name or not _VALID_MODULE_NAME.match(name):
9
+ raise ValueError(
10
+ f"Invalid module name: '{name}'. "
11
+ "Must start with a letter or number, then letters, numbers, hyphens, underscores."
12
+ )
13
+ return name
14
+
15
+
16
+ def validate_module_file_path(file_path: str) -> str:
17
+ file_path = file_path.strip().strip("/")
18
+ if not file_path:
19
+ raise ValueError("File path cannot be empty")
20
+ if ".." in file_path:
21
+ raise ValueError("File path cannot contain '..'")
22
+ return file_path
@@ -0,0 +1,9 @@
1
+ # Module Features — Optional Capabilities
2
+
3
+ A module is just a folder. Beyond `module.yaml` + `llms.txt` + the kind's starter file (info.md / brief.md / steps.md), a module can optionally carry:
4
+
5
+ ## Scripts
6
+
7
+ Plain Python files inside the module that the agent can run on demand. The canonical example is `verify.py` — a quick sanity check that the integration's credentials and network access work.
8
+
9
+ When to add one: when the module benefits from a repeatable check, fetch, or transformation that the user (or another turn) will want to invoke later.
@@ -0,0 +1,29 @@
1
+ # Principles — Modifying Context
2
+
3
+ These are the rules for creating, editing, or extending modules.
4
+
5
+ ## Module locations
6
+
7
+ Modules live at `modules-repo/<slug>/`. The `context/` directory contains symlinks for the subset of modules currently loaded into the active workspace — **it is a view, not a storage location.**
8
+
9
+ Two operations that look similar but are NOT the same:
10
+
11
+ - **Create / edit a module.** You write files at `modules-repo/<slug>/...`. This is the only kind of write you perform.
12
+ - **Load a module into the workspace.** This creates a symlink inside `context/`. It is managed by `eic.py`, not by you.
13
+
14
+ Each module is a folder with at least `module.yaml` and `llms.txt`.
15
+
16
+ ## Reading context before modifying
17
+
18
+ Before answering anything that references or implies a topic likely covered by an existing module, read that module's `llms.txt` first. If `llms.txt` declares a `## Where to write` section, that section governs where any subsequent appends go (path, naming pattern, template). Do NOT invent a new location.
19
+
20
+ If no module matches, you may answer from general knowledge — but if the conversation produces durable content (a finding, a note worth keeping, a new procedure), propose a new module rather than silently dropping it.
21
+
22
+ ## Module schema and kinds
23
+
24
+ See [structure.md](structure.md) for the authoritative module.yaml schema and the per-kind file layout (integration / task / workflow). That file is auto-generated from code — read it; do not invent fields.
25
+
26
+ ## When to propose a new module vs append to an existing one
27
+
28
+ - **Append** when the content is one more instance of a pattern the module already tracks (another note, another run, another finding).
29
+ - **New module** when the content concerns a different external service, a different task with its own goal, or a different repeatable procedure. New modules cost almost nothing — favor a small new module over stretching an existing one.
@@ -0,0 +1,40 @@
1
+ # System
2
+
3
+ You are an AI agent powered by loaded context modules.
4
+
5
+ You are direct, efficient, and familiar with the loaded context. No hedging, no filler. Lead with the answer.
6
+
7
+ ## Two responsibilities
8
+
9
+ 1. **Operate modules** — read context, run scripts, answer questions. This file covers that.
10
+ 2. **Modify context** — create or edit modules. See [principles.md](principles.md) before doing this.
11
+
12
+ ## How to operate a module
13
+
14
+ **CRITICAL: Assume every question is potentially answerable through your modules. Always navigate the `llms.txt` tree before claiming you can't help. Never dismiss a question as out of scope without checking first.**
15
+
16
+ When asked anything, start by asking: **"Which module do I need?"**
17
+
18
+ 1. Read `llms.txt` — see all loaded modules with one-line descriptions
19
+ 2. Pick the relevant module(s) based on the question
20
+ 3. Read that module's `llms.txt` to find the specific file you need
21
+ 4. Read the actual content, write a script if needed, and get the answer
22
+
23
+ ## Setup per turn
24
+
25
+ 1. Read [llms.txt](llms.txt) — orient yourself in the module hierarchy
26
+ 2. For any module you need, read its `module.yaml` — declares required secrets and dependencies
27
+
28
+ ## Secrets
29
+
30
+ See [secrets.md](secrets.md) for how secrets work in this environment.
31
+
32
+ A module needs secrets if and only if its `module.yaml` declares a `secrets:` list (variable names only, no values).
33
+
34
+ ## Module features (optional capabilities)
35
+
36
+ Modules can expose scripts and other capabilities. See [module_features.md](module_features.md) for the catalog.
37
+
38
+ ## Modifying context
39
+
40
+ If you are asked to create a new module, edit a module's files, or change anything in `modules-repo/<slug>/`, read [principles.md](principles.md) first. It owns the rules for where things go and how writes are gated.
eic/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
eic/cli.py ADDED
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env python3
2
+ """eic — Everything Is Context module manager.
3
+
4
+ Usage:
5
+ eic init
6
+ eic new <kind> <name>
7
+ eic load <name> [name2 ...]
8
+ eic unload <name>
9
+ eic ls
10
+ eic env
11
+ eic validate [name]
12
+ """
13
+ import argparse
14
+ import importlib.resources
15
+ import os
16
+ import re
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Paths — all relative to CWD (the user's project directory)
22
+ # ---------------------------------------------------------------------------
23
+ MODULES_DIR = Path.cwd() / "modules-repo"
24
+ CONTEXT_DIR = Path.cwd() / "context"
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Package-data helpers
28
+ # ---------------------------------------------------------------------------
29
+
30
+ def _read_pkg_text(package: str, *path_parts: str) -> str:
31
+ """Read a text file bundled inside a Python package."""
32
+ return importlib.resources.files(package).joinpath(*path_parts).read_text()
33
+
34
+
35
+ def _copy_pkg_file(package: str, path_parts: tuple[str, ...], dest: Path) -> None:
36
+ """Copy a text file from package data to a filesystem path."""
37
+ src = importlib.resources.files(package).joinpath(*path_parts)
38
+ dest.parent.mkdir(parents=True, exist_ok=True)
39
+ dest.write_text(src.read_text())
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Imports from the core package (installed alongside eic)
44
+ # ---------------------------------------------------------------------------
45
+ from core.manifest import ModuleManifest, read_manifest, write_manifest
46
+ from core.kind_specs import KIND_SPECS, INTEGRATION_INFO_SECTIONS
47
+ from core.schemas import validate_module_name
48
+ from core.llms_gen import generate_root_llms_txt, generate_structure_md, generate_system_md
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Internal helpers
53
+ # ---------------------------------------------------------------------------
54
+
55
+ def _copy_static_files():
56
+ """Copy static template files into context/."""
57
+ for name in ["principles.md", "module_features.md"]:
58
+ _copy_pkg_file("core", ("templates", name), CONTEXT_DIR / name)
59
+ _copy_pkg_file("eic", ("data", "secrets.md"), CONTEXT_DIR / "secrets.md")
60
+
61
+
62
+ def _regenerate():
63
+ """Regenerate all auto-generated files in context/."""
64
+ template_content = _read_pkg_text("core", "templates", "system.md")
65
+ generate_root_llms_txt(CONTEXT_DIR)
66
+ generate_structure_md(CONTEXT_DIR)
67
+ generate_system_md(CONTEXT_DIR, template_content)
68
+ _copy_static_files()
69
+ _generate_env_example()
70
+
71
+
72
+ def _generate_env_example():
73
+ """Generate .env.example from all loaded modules' secrets lists."""
74
+ lines = []
75
+ for mod_dir in sorted(CONTEXT_DIR.iterdir()):
76
+ if not mod_dir.is_dir() or mod_dir.name.startswith("."):
77
+ continue
78
+ manifest = read_manifest(mod_dir)
79
+ if manifest.secrets:
80
+ lines.append(f"# {manifest.name}")
81
+ for secret in manifest.secrets:
82
+ lines.append(f"{secret}=")
83
+ lines.append("")
84
+ env_example = Path.cwd() / ".env.example"
85
+ if lines:
86
+ env_example.write_text("\n".join(lines) + "\n")
87
+ elif env_example.exists():
88
+ env_example.unlink()
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Commands
93
+ # ---------------------------------------------------------------------------
94
+
95
+ _EXAMPLE_FILES = ["module.yaml", "llms.txt", "info.md"]
96
+
97
+
98
+ def cmd_init(args):
99
+ """Initialize the workspace."""
100
+ MODULES_DIR.mkdir(exist_ok=True)
101
+ CONTEXT_DIR.mkdir(exist_ok=True)
102
+
103
+ # Seed the example module when modules-repo is empty
104
+ if not any(MODULES_DIR.iterdir()):
105
+ example_dest = MODULES_DIR / "example"
106
+ example_dest.mkdir(parents=True, exist_ok=True)
107
+ for fname in _EXAMPLE_FILES:
108
+ _copy_pkg_file("eic", ("data", "example", fname), example_dest / fname)
109
+
110
+ # Create .gitignore if it doesn't exist
111
+ gitignore = Path.cwd() / ".gitignore"
112
+ if not gitignore.exists():
113
+ gitignore.write_text(".env\n")
114
+ else:
115
+ content = gitignore.read_text()
116
+ if ".env" not in content.splitlines():
117
+ gitignore.write_text(content.rstrip("\n") + "\n.env\n")
118
+
119
+ _regenerate()
120
+ print("Created modules-repo/")
121
+ print("Created context/")
122
+ print("Ready. Create your first module with: eic new integration <name>")
123
+
124
+
125
+ def cmd_new(args):
126
+ """Scaffold a new module."""
127
+ kind = args.kind
128
+ name = args.name
129
+
130
+ if kind not in KIND_SPECS:
131
+ print(f"Error: unknown kind '{kind}'. Must be one of: {', '.join(KIND_SPECS.keys())}")
132
+ sys.exit(1)
133
+
134
+ try:
135
+ name = validate_module_name(name)
136
+ except ValueError as e:
137
+ print(f"Error: {e}")
138
+ sys.exit(1)
139
+
140
+ mod_dir = MODULES_DIR / name
141
+ if mod_dir.exists():
142
+ print(f"Error: module '{name}' already exists at {mod_dir}")
143
+ sys.exit(1)
144
+
145
+ mod_dir.mkdir(parents=True)
146
+ spec = KIND_SPECS[kind]
147
+
148
+ # Write module.yaml
149
+ manifest = ModuleManifest(name=name, kind=kind)
150
+ write_manifest(mod_dir, manifest)
151
+
152
+ # Write llms.txt
153
+ starter_desc = spec.starter_outline
154
+ llms_content = f"# {name}\n\n> One-line summary of this module\n\n"
155
+ llms_content += f"- [{spec.starter_file}]({spec.starter_file}): {starter_desc}\n"
156
+ llms_content += "- [module.yaml](module.yaml): Module configuration\n"
157
+ (mod_dir / "llms.txt").write_text(llms_content)
158
+
159
+ # Write starter file(s)
160
+ if kind == "integration":
161
+ sections = []
162
+ for s in INTEGRATION_INFO_SECTIONS:
163
+ sections.append(f"## {s.name}\n{s.purpose}\n")
164
+ (mod_dir / "info.md").write_text(f"# {name}\n\n" + "\n".join(sections))
165
+
166
+ elif kind == "task":
167
+ (mod_dir / "brief.md").write_text(
168
+ f"# {name}\n\n## Goal\nWhat this task should accomplish.\n\n## Context\nBackground and constraints.\n"
169
+ )
170
+ (mod_dir / "status.md").write_text(
171
+ "# Status\n\n## Subtasks\n- [ ] First subtask\n"
172
+ )
173
+
174
+ elif kind == "workflow":
175
+ (mod_dir / "steps.md").write_text(
176
+ f"# {name}\n\n## Steps\n1. First step\n2. Second step\n"
177
+ )
178
+
179
+ for f in spec.required_files:
180
+ print(f"Created modules-repo/{name}/{f}")
181
+ print(f'Module "{name}" created. Edit the files, then load it with: eic load {name}')
182
+
183
+
184
+ def cmd_load(args):
185
+ """Load modules into the workspace via symlinks."""
186
+ if not CONTEXT_DIR.exists():
187
+ print("Error: workspace not initialized. Run: eic init")
188
+ sys.exit(1)
189
+
190
+ for name in args.names:
191
+ mod_source = MODULES_DIR / name
192
+ if not mod_source.is_dir():
193
+ print(f"Error: module '{name}' not found in modules-repo/")
194
+ sys.exit(1)
195
+
196
+ link_path = CONTEXT_DIR / name
197
+ if link_path.is_symlink() or link_path.exists():
198
+ print(f"Already loaded: {name}")
199
+ continue
200
+
201
+ # Relative symlink for portability
202
+ link_path.symlink_to(Path("..") / "modules-repo" / name)
203
+ print(f"Loaded {name}")
204
+
205
+ _regenerate()
206
+ loaded_count = sum(1 for p in CONTEXT_DIR.iterdir() if p.is_symlink())
207
+ print(f"Regenerated context/ ({loaded_count} modules)")
208
+
209
+
210
+ def cmd_unload(args):
211
+ """Remove a module from the workspace."""
212
+ name = args.name
213
+ link_path = CONTEXT_DIR / name
214
+
215
+ if not link_path.is_symlink():
216
+ print(f"Error: '{name}' is not loaded")
217
+ sys.exit(1)
218
+
219
+ link_path.unlink()
220
+ print(f"Unloaded {name}")
221
+
222
+ _regenerate()
223
+ loaded_count = sum(1 for p in CONTEXT_DIR.iterdir() if p.is_symlink())
224
+ print(f"Regenerated context/ ({loaded_count} modules)")
225
+
226
+
227
+ def cmd_ls(args):
228
+ """List all modules and their status."""
229
+ if not MODULES_DIR.exists():
230
+ print("No modules-repo/ directory. Run: eic init")
231
+ return
232
+
233
+ loaded_names = set()
234
+ if CONTEXT_DIR.exists():
235
+ loaded_names = {p.name for p in CONTEXT_DIR.iterdir() if p.is_symlink()}
236
+
237
+ all_modules = sorted(p.name for p in MODULES_DIR.iterdir() if p.is_dir())
238
+
239
+ if not all_modules:
240
+ print("No modules found. Create one with: eic new integration <name>")
241
+ return
242
+
243
+ loaded = [(n, read_manifest(MODULES_DIR / n)) for n in all_modules if n in loaded_names]
244
+ available = [(n, read_manifest(MODULES_DIR / n)) for n in all_modules if n not in loaded_names]
245
+
246
+ from core.llms_gen import extract_module_summary
247
+
248
+ def _summary(name):
249
+ llms_file = MODULES_DIR / name / "llms.txt"
250
+ if llms_file.exists():
251
+ return extract_module_summary(llms_file.read_text())
252
+ return ""
253
+
254
+ if loaded:
255
+ print("LOADED")
256
+ for name, m in loaded:
257
+ summary = _summary(name)
258
+ print(f" {name:<20s} {m.kind:<14s} {summary}")
259
+ print()
260
+
261
+ if available:
262
+ print("AVAILABLE")
263
+ for name, m in available:
264
+ summary = _summary(name)
265
+ print(f" {name:<20s} {m.kind:<14s} {summary}")
266
+
267
+
268
+ def cmd_env(args):
269
+ """Check which secret variables are set or missing."""
270
+ if not CONTEXT_DIR.exists():
271
+ print("Error: workspace not initialized. Run: eic init")
272
+ sys.exit(1)
273
+
274
+ # Try to load .env file into a dict (don't inject into os.environ)
275
+ env_vars = dict(os.environ)
276
+ env_file = Path.cwd() / ".env"
277
+ if env_file.exists():
278
+ for line in env_file.read_text().splitlines():
279
+ line = line.strip()
280
+ if not line or line.startswith("#"):
281
+ continue
282
+ if "=" in line:
283
+ key, _, value = line.partition("=")
284
+ env_vars[key.strip()] = value.strip()
285
+
286
+ any_missing = False
287
+ any_secrets = False
288
+
289
+ for mod_dir in sorted(CONTEXT_DIR.iterdir()):
290
+ if not mod_dir.is_dir() or mod_dir.name.startswith("."):
291
+ continue
292
+ manifest = read_manifest(mod_dir)
293
+ if not manifest.secrets:
294
+ continue
295
+ any_secrets = True
296
+ print(f"\n{manifest.name}")
297
+ for secret in manifest.secrets:
298
+ if secret in env_vars and env_vars[secret]:
299
+ print(f" {secret:<30s} set")
300
+ else:
301
+ print(f" {secret:<30s} missing")
302
+ any_missing = True
303
+
304
+ if not any_secrets:
305
+ print("No loaded modules declare secrets.")
306
+ return
307
+
308
+ if any_missing:
309
+ print("\nMissing variables. Add them to .env")
310
+ sys.exit(1)
311
+ else:
312
+ print("\nAll variables set.")
313
+
314
+
315
+ def cmd_validate(args):
316
+ """Validate module structure."""
317
+ if args.name:
318
+ names = [args.name]
319
+ else:
320
+ if not MODULES_DIR.exists():
321
+ print("No modules-repo/ directory.")
322
+ sys.exit(1)
323
+ names = sorted(p.name for p in MODULES_DIR.iterdir() if p.is_dir())
324
+
325
+ if not names:
326
+ print("No modules to validate.")
327
+ return
328
+
329
+ all_pass = True
330
+ for name in names:
331
+ mod_dir = MODULES_DIR / name
332
+ if not mod_dir.is_dir():
333
+ print(f"{name} FAIL module directory not found")
334
+ all_pass = False
335
+ continue
336
+
337
+ errors = []
338
+ manifest = None
339
+
340
+ # Check module.yaml
341
+ manifest_path = mod_dir / "module.yaml"
342
+ if not manifest_path.exists():
343
+ errors.append("missing module.yaml")
344
+ else:
345
+ try:
346
+ manifest = read_manifest(mod_dir)
347
+ except Exception as e:
348
+ errors.append(f"invalid module.yaml: {e}")
349
+
350
+ if manifest and manifest.kind not in KIND_SPECS:
351
+ errors.append(f"unknown kind '{manifest.kind}'")
352
+
353
+ # Check required files per kind
354
+ if manifest_path.exists() and manifest:
355
+ spec = KIND_SPECS.get(manifest.kind)
356
+ if spec:
357
+ for req_file in spec.required_files:
358
+ if not (mod_dir / req_file).exists():
359
+ errors.append(f"missing {req_file} (required for {manifest.kind} kind)")
360
+
361
+ # Check llms.txt links
362
+ llms_path = mod_dir / "llms.txt"
363
+ if llms_path.exists():
364
+ for line in llms_path.read_text().splitlines():
365
+ # Parse markdown links: [text](path)
366
+ for match in re.finditer(r'\[.*?\]\((.+?)\)', line):
367
+ link_target = match.group(1)
368
+ if link_target.startswith("http"):
369
+ continue
370
+ if not (mod_dir / link_target).exists():
371
+ errors.append(f"llms.txt links to '{link_target}' which does not exist")
372
+
373
+ if errors:
374
+ print(f"{name} FAIL")
375
+ for e in errors:
376
+ print(f" - {e}")
377
+ all_pass = False
378
+ else:
379
+ print(f"{name} PASS")
380
+
381
+ if not all_pass:
382
+ sys.exit(1)
383
+
384
+
385
+ # ---------------------------------------------------------------------------
386
+ # Entry point
387
+ # ---------------------------------------------------------------------------
388
+
389
+ def main():
390
+ parser = argparse.ArgumentParser(
391
+ prog="eic",
392
+ description="Everything Is Context — module manager",
393
+ )
394
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
395
+
396
+ # init
397
+ subparsers.add_parser("init", help="Initialize the workspace")
398
+
399
+ # new
400
+ new_parser = subparsers.add_parser("new", help="Scaffold a new module")
401
+ new_parser.add_argument("kind", choices=list(KIND_SPECS.keys()), help="Module kind")
402
+ new_parser.add_argument("name", help="Module name")
403
+
404
+ # load
405
+ load_parser = subparsers.add_parser("load", help="Load modules into workspace")
406
+ load_parser.add_argument("names", nargs="+", help="Module names to load")
407
+
408
+ # unload
409
+ unload_parser = subparsers.add_parser("unload", help="Unload a module from workspace")
410
+ unload_parser.add_argument("name", help="Module name to unload")
411
+
412
+ # ls
413
+ subparsers.add_parser("ls", help="List all modules")
414
+
415
+ # env
416
+ subparsers.add_parser("env", help="Check secret variable status")
417
+
418
+ # validate
419
+ validate_parser = subparsers.add_parser("validate", help="Validate module structure")
420
+ validate_parser.add_argument("name", nargs="?", help="Module name (validates all if omitted)")
421
+
422
+ args = parser.parse_args()
423
+ if args.command == "init":
424
+ cmd_init(args)
425
+ elif args.command == "new":
426
+ cmd_new(args)
427
+ elif args.command == "load":
428
+ cmd_load(args)
429
+ elif args.command == "unload":
430
+ cmd_unload(args)
431
+ elif args.command == "ls":
432
+ cmd_ls(args)
433
+ elif args.command == "env":
434
+ cmd_env(args)
435
+ elif args.command == "validate":
436
+ cmd_validate(args)
437
+ elif args.command is None:
438
+ parser.print_help()
439
+ sys.exit(1)
440
+ else:
441
+ print(f"Command '{args.command}' not yet implemented")
442
+ sys.exit(1)
443
+
444
+
445
+ if __name__ == "__main__":
446
+ main()
@@ -0,0 +1,30 @@
1
+ # example
2
+
3
+ ## Purpose
4
+ This is a sample module showing the structure of a context module. Use it as a reference when creating your own modules.
5
+
6
+ ## Where it lives
7
+ This module exists locally in your workspace.
8
+
9
+ ## Auth & access
10
+ No authentication needed — this is a documentation-only module.
11
+
12
+ ## Key entities
13
+ - **Modules** — folders with a `module.yaml` and `llms.txt`
14
+ - **Kinds** — integration, task, or workflow
15
+
16
+ ## Operations
17
+ - Read this module's files to understand the structure
18
+ - Create your own module with `eic new integration <name>`
19
+
20
+ ## Examples
21
+ ```bash
22
+ # Create a new integration module
23
+ eic new integration stripe
24
+
25
+ # Load it into the workspace
26
+ eic load stripe
27
+
28
+ # Check module structure
29
+ eic validate stripe
30
+ ```
@@ -0,0 +1,6 @@
1
+ # example
2
+
3
+ > A sample module demonstrating the context module structure
4
+
5
+ - [info.md](info.md): What this module contains and how to use it
6
+ - [module.yaml](module.yaml): Module configuration
@@ -0,0 +1 @@
1
+ name: example
eic/data/secrets.md ADDED
@@ -0,0 +1,16 @@
1
+ # Secrets
2
+
3
+ Secrets are environment variables. Modules declare which variables they need in `module.yaml` under the `secrets:` list — variable names only, never values.
4
+
5
+ ## How it works
6
+
7
+ 1. Check a module's `module.yaml` for its `secrets:` list
8
+ 2. Values live in `.env` at the project root (gitignored, never committed)
9
+ 3. `.env.example` is auto-generated with all required variable names
10
+ 4. Run `eic env` to check which variables are set or missing
11
+
12
+ ## Rules
13
+
14
+ - Never hardcode secret values in module files
15
+ - Never commit `.env` to version control
16
+ - Access secrets via `os.environ` in scripts
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: everythingiscontext
3
+ Version: 0.1.0
4
+ Summary: Agent-agnostic context management system
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://everythingiscontext.com
7
+ Project-URL: Repository, https://github.com/bleak-ai/everythingiscontext
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: pydantic
10
+ Requires-Dist: pyyaml
@@ -0,0 +1,19 @@
1
+ core/__init__.py,sha256=VJvE1rQXG4BRwvFK0ezbG4fv97Dd4EYgS21N5Ep4PkY,23
2
+ core/kind_specs.py,sha256=1dqJpj1UoMwm2WtpBISkqcyjx52-BhBcPXs93RR4kmA,4603
3
+ core/llms_gen.py,sha256=ME8a37y44E5i39Wj7axHWyn7VzyBnPQKhTh_HunylqQ,3741
4
+ core/manifest.py,sha256=NkSW1a2cru_gV_Dy4RNsB79qPwBJBvD09ezhiEPuYQA,7839
5
+ core/schemas.py,sha256=LxCOrAnqG49MADz42ulYJHVzIESoN9l9F4G2eZgiX8g,676
6
+ core/templates/module_features.md,sha256=F_vc-Wd0LdImwbxscHE7BYU9eEaYEM5Rvno85obcYyY,554
7
+ core/templates/principles.md,sha256=vTqJPiGopKTM2UFiDfIqT40_Z-c4fKz2rsnbzV40XRo,1931
8
+ core/templates/system.md,sha256=xOpAcblWbm4_RjmQAahm0pvh4kWR6Bh7PoV8f6o1ilg,1775
9
+ eic/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
10
+ eic/cli.py,sha256=edYtj7B7N3-lSd-HUnFtR6VnwIBwatahw9mjkqJbNaw,14805
11
+ eic/data/secrets.md,sha256=9euH9JaUyDd7DpxYvOsNHa00kHsHipemKzrp0VDbwAg,595
12
+ eic/data/example/info.md,sha256=zd3LJhOzWIinHh_gnoLBcgivM6m66to5jAWvLaKp3pY,725
13
+ eic/data/example/llms.txt,sha256=MM1JRSkZqfCOSW2nOQg2osekmV9BPMKHFid2h1ixzaM,190
14
+ eic/data/example/module.yaml,sha256=FfzDhwYlmAv1jxW6kEc2tP-hqESVqPT1HXgeIRAW50M,14
15
+ everythingiscontext-0.1.0.dist-info/METADATA,sha256=qaTkjcXWW_LxW-7psnnjFnmTfqfniU-iAr33S8HKHhU,335
16
+ everythingiscontext-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ everythingiscontext-0.1.0.dist-info/entry_points.txt,sha256=3aE6sudwiW6thin6LpM5vLsx7l00qIpuukmV4FPM-ds,37
18
+ everythingiscontext-0.1.0.dist-info/top_level.txt,sha256=h0-X8BoIp5zgnMAZo8_oshowZPUwb6YTCaapXy18Bwc,9
19
+ everythingiscontext-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eic = eic.cli:main
@@ -0,0 +1,2 @@
1
+ core
2
+ eic