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 +1 -0
- core/kind_specs.py +135 -0
- core/llms_gen.py +109 -0
- core/manifest.py +225 -0
- core/schemas.py +22 -0
- core/templates/module_features.md +9 -0
- core/templates/principles.md +29 -0
- core/templates/system.md +40 -0
- eic/__init__.py +1 -0
- eic/cli.py +446 -0
- eic/data/example/info.md +30 -0
- eic/data/example/llms.txt +6 -0
- eic/data/example/module.yaml +1 -0
- eic/data/secrets.md +16 -0
- everythingiscontext-0.1.0.dist-info/METADATA +10 -0
- everythingiscontext-0.1.0.dist-info/RECORD +19 -0
- everythingiscontext-0.1.0.dist-info/WHEEL +5 -0
- everythingiscontext-0.1.0.dist-info/entry_points.txt +2 -0
- everythingiscontext-0.1.0.dist-info/top_level.txt +2 -0
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.
|
core/templates/system.md
ADDED
|
@@ -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()
|
eic/data/example/info.md
ADDED
|
@@ -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 @@
|
|
|
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,,
|