namel3ss 0.1.0a0__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 (157) hide show
  1. namel3ss/__init__.py +4 -0
  2. namel3ss/ast/__init__.py +5 -0
  3. namel3ss/ast/agents.py +13 -0
  4. namel3ss/ast/ai.py +23 -0
  5. namel3ss/ast/base.py +10 -0
  6. namel3ss/ast/expressions.py +55 -0
  7. namel3ss/ast/nodes.py +86 -0
  8. namel3ss/ast/pages.py +43 -0
  9. namel3ss/ast/program.py +22 -0
  10. namel3ss/ast/records.py +27 -0
  11. namel3ss/ast/statements.py +107 -0
  12. namel3ss/ast/tool.py +11 -0
  13. namel3ss/cli/__init__.py +2 -0
  14. namel3ss/cli/actions_mode.py +39 -0
  15. namel3ss/cli/app_loader.py +22 -0
  16. namel3ss/cli/commands/action.py +27 -0
  17. namel3ss/cli/commands/run.py +43 -0
  18. namel3ss/cli/commands/ui.py +26 -0
  19. namel3ss/cli/commands/validate.py +23 -0
  20. namel3ss/cli/format_mode.py +30 -0
  21. namel3ss/cli/io/json_io.py +19 -0
  22. namel3ss/cli/io/read_source.py +16 -0
  23. namel3ss/cli/json_io.py +21 -0
  24. namel3ss/cli/lint_mode.py +29 -0
  25. namel3ss/cli/main.py +135 -0
  26. namel3ss/cli/new_mode.py +146 -0
  27. namel3ss/cli/runner.py +28 -0
  28. namel3ss/cli/studio_mode.py +22 -0
  29. namel3ss/cli/ui_mode.py +14 -0
  30. namel3ss/config/__init__.py +4 -0
  31. namel3ss/config/dotenv.py +33 -0
  32. namel3ss/config/loader.py +83 -0
  33. namel3ss/config/model.py +49 -0
  34. namel3ss/errors/__init__.py +2 -0
  35. namel3ss/errors/base.py +34 -0
  36. namel3ss/errors/render.py +22 -0
  37. namel3ss/format/__init__.py +3 -0
  38. namel3ss/format/formatter.py +18 -0
  39. namel3ss/format/rules.py +97 -0
  40. namel3ss/ir/__init__.py +3 -0
  41. namel3ss/ir/lowering/__init__.py +4 -0
  42. namel3ss/ir/lowering/agents.py +42 -0
  43. namel3ss/ir/lowering/ai.py +45 -0
  44. namel3ss/ir/lowering/expressions.py +49 -0
  45. namel3ss/ir/lowering/flow.py +21 -0
  46. namel3ss/ir/lowering/pages.py +48 -0
  47. namel3ss/ir/lowering/program.py +34 -0
  48. namel3ss/ir/lowering/records.py +25 -0
  49. namel3ss/ir/lowering/statements.py +122 -0
  50. namel3ss/ir/lowering/tools.py +16 -0
  51. namel3ss/ir/model/__init__.py +50 -0
  52. namel3ss/ir/model/agents.py +33 -0
  53. namel3ss/ir/model/ai.py +31 -0
  54. namel3ss/ir/model/base.py +20 -0
  55. namel3ss/ir/model/expressions.py +50 -0
  56. namel3ss/ir/model/pages.py +43 -0
  57. namel3ss/ir/model/program.py +28 -0
  58. namel3ss/ir/model/statements.py +76 -0
  59. namel3ss/ir/model/tools.py +11 -0
  60. namel3ss/ir/nodes.py +88 -0
  61. namel3ss/lexer/__init__.py +2 -0
  62. namel3ss/lexer/lexer.py +152 -0
  63. namel3ss/lexer/tokens.py +98 -0
  64. namel3ss/lint/__init__.py +4 -0
  65. namel3ss/lint/engine.py +125 -0
  66. namel3ss/lint/semantic.py +45 -0
  67. namel3ss/lint/text_scan.py +70 -0
  68. namel3ss/lint/types.py +22 -0
  69. namel3ss/parser/__init__.py +3 -0
  70. namel3ss/parser/agent.py +78 -0
  71. namel3ss/parser/ai.py +113 -0
  72. namel3ss/parser/constraints.py +37 -0
  73. namel3ss/parser/core.py +166 -0
  74. namel3ss/parser/expressions.py +105 -0
  75. namel3ss/parser/flow.py +37 -0
  76. namel3ss/parser/pages.py +76 -0
  77. namel3ss/parser/program.py +45 -0
  78. namel3ss/parser/records.py +66 -0
  79. namel3ss/parser/statements/__init__.py +27 -0
  80. namel3ss/parser/statements/control_flow.py +116 -0
  81. namel3ss/parser/statements/core.py +66 -0
  82. namel3ss/parser/statements/data.py +17 -0
  83. namel3ss/parser/statements/letset.py +22 -0
  84. namel3ss/parser/statements.py +1 -0
  85. namel3ss/parser/tokens.py +35 -0
  86. namel3ss/parser/tool.py +29 -0
  87. namel3ss/runtime/__init__.py +3 -0
  88. namel3ss/runtime/ai/http/client.py +24 -0
  89. namel3ss/runtime/ai/mock_provider.py +5 -0
  90. namel3ss/runtime/ai/provider.py +29 -0
  91. namel3ss/runtime/ai/providers/__init__.py +18 -0
  92. namel3ss/runtime/ai/providers/_shared/errors.py +20 -0
  93. namel3ss/runtime/ai/providers/_shared/parse.py +18 -0
  94. namel3ss/runtime/ai/providers/anthropic.py +55 -0
  95. namel3ss/runtime/ai/providers/gemini.py +50 -0
  96. namel3ss/runtime/ai/providers/mistral.py +51 -0
  97. namel3ss/runtime/ai/providers/mock.py +23 -0
  98. namel3ss/runtime/ai/providers/ollama.py +39 -0
  99. namel3ss/runtime/ai/providers/openai.py +55 -0
  100. namel3ss/runtime/ai/providers/registry.py +38 -0
  101. namel3ss/runtime/ai/trace.py +18 -0
  102. namel3ss/runtime/executor/__init__.py +3 -0
  103. namel3ss/runtime/executor/agents.py +91 -0
  104. namel3ss/runtime/executor/ai_runner.py +90 -0
  105. namel3ss/runtime/executor/api.py +54 -0
  106. namel3ss/runtime/executor/assign.py +40 -0
  107. namel3ss/runtime/executor/context.py +31 -0
  108. namel3ss/runtime/executor/executor.py +77 -0
  109. namel3ss/runtime/executor/expr_eval.py +110 -0
  110. namel3ss/runtime/executor/records_ops.py +64 -0
  111. namel3ss/runtime/executor/result.py +13 -0
  112. namel3ss/runtime/executor/signals.py +6 -0
  113. namel3ss/runtime/executor/statements.py +99 -0
  114. namel3ss/runtime/memory/manager.py +52 -0
  115. namel3ss/runtime/memory/profile.py +17 -0
  116. namel3ss/runtime/memory/semantic.py +20 -0
  117. namel3ss/runtime/memory/short_term.py +18 -0
  118. namel3ss/runtime/records/service.py +105 -0
  119. namel3ss/runtime/store/__init__.py +2 -0
  120. namel3ss/runtime/store/memory_store.py +62 -0
  121. namel3ss/runtime/tools/registry.py +13 -0
  122. namel3ss/runtime/ui/__init__.py +2 -0
  123. namel3ss/runtime/ui/actions.py +124 -0
  124. namel3ss/runtime/validators/__init__.py +2 -0
  125. namel3ss/runtime/validators/constraints.py +126 -0
  126. namel3ss/schema/__init__.py +2 -0
  127. namel3ss/schema/records.py +52 -0
  128. namel3ss/studio/__init__.py +4 -0
  129. namel3ss/studio/api.py +115 -0
  130. namel3ss/studio/edit/__init__.py +3 -0
  131. namel3ss/studio/edit/ops.py +80 -0
  132. namel3ss/studio/edit/selectors.py +74 -0
  133. namel3ss/studio/edit/transform.py +39 -0
  134. namel3ss/studio/server.py +175 -0
  135. namel3ss/studio/session.py +11 -0
  136. namel3ss/studio/web/app.js +248 -0
  137. namel3ss/studio/web/index.html +44 -0
  138. namel3ss/studio/web/styles.css +42 -0
  139. namel3ss/templates/__init__.py +3 -0
  140. namel3ss/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  141. namel3ss/templates/ai_assistant/.gitignore +1 -0
  142. namel3ss/templates/ai_assistant/README.md +10 -0
  143. namel3ss/templates/ai_assistant/app.ai +30 -0
  144. namel3ss/templates/crud/.gitignore +1 -0
  145. namel3ss/templates/crud/README.md +10 -0
  146. namel3ss/templates/crud/app.ai +26 -0
  147. namel3ss/templates/multi_agent/.gitignore +1 -0
  148. namel3ss/templates/multi_agent/README.md +10 -0
  149. namel3ss/templates/multi_agent/app.ai +43 -0
  150. namel3ss/ui/__init__.py +2 -0
  151. namel3ss/ui/manifest.py +220 -0
  152. namel3ss/utils/__init__.py +2 -0
  153. namel3ss-0.1.0a0.dist-info/METADATA +123 -0
  154. namel3ss-0.1.0a0.dist-info/RECORD +157 -0
  155. namel3ss-0.1.0a0.dist-info/WHEEL +5 -0
  156. namel3ss-0.1.0a0.dist-info/entry_points.txt +2 -0
  157. namel3ss-0.1.0a0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from namel3ss.errors.base import Namel3ssError
6
+
7
+
8
+ def parse_payload(text: str | None) -> dict:
9
+ if text is None or text == "":
10
+ return {}
11
+ try:
12
+ data = json.loads(text)
13
+ except json.JSONDecodeError as exc:
14
+ raise Namel3ssError(f"Invalid JSON payload: {exc.msg}") from exc
15
+ if not isinstance(data, dict):
16
+ raise Namel3ssError("Payload must be a JSON object")
17
+ return data
18
+
19
+
20
+ def dumps_pretty(obj: object) -> str:
21
+ return json.dumps(obj, indent=2, ensure_ascii=False)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from namel3ss.errors.base import Namel3ssError
6
+ from namel3ss.errors.render import format_error
7
+ from namel3ss.lint.engine import lint_source
8
+
9
+
10
+ def run_lint(path_str: str, check_only: bool) -> int:
11
+ path = Path(path_str)
12
+ if path.suffix != ".ai":
13
+ raise Namel3ssError("Input file must have .ai extension")
14
+ try:
15
+ source = path.read_text(encoding="utf-8")
16
+ except FileNotFoundError as err:
17
+ raise Namel3ssError(f"File not found: {path}") from err
18
+ findings = lint_source(source)
19
+ output = {
20
+ "ok": len(findings) == 0,
21
+ "count": len(findings),
22
+ "findings": [f.to_dict() for f in findings],
23
+ }
24
+ import json
25
+
26
+ print(json.dumps(output, indent=2, ensure_ascii=False))
27
+ if check_only and findings:
28
+ return 1
29
+ return 0
namel3ss/cli/main.py ADDED
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from namel3ss.cli.actions_mode import list_actions
6
+ from namel3ss.cli.app_loader import load_program
7
+ from namel3ss.cli.format_mode import run_format
8
+ from namel3ss.cli.new_mode import run_new
9
+ from namel3ss.cli.lint_mode import run_lint
10
+ from namel3ss.cli.json_io import dumps_pretty, parse_payload
11
+ from namel3ss.cli.runner import run_flow
12
+ from namel3ss.cli.ui_mode import render_manifest, run_action
13
+ from namel3ss.cli.studio_mode import run_studio
14
+ from namel3ss.errors.base import Namel3ssError
15
+ from namel3ss.errors.render import format_error
16
+
17
+ RESERVED = {"check", "ui", "flow", "help", "format", "lint", "actions", "studio"}
18
+
19
+
20
+ def main(argv: list[str] | None = None) -> int:
21
+ args = sys.argv[1:] if argv is None else argv
22
+ try:
23
+ if not args:
24
+ _print_usage()
25
+ return 1
26
+
27
+ if args[0] == "help":
28
+ _print_usage()
29
+ return 0
30
+ if args[0] == "new":
31
+ return run_new(args[1:])
32
+
33
+ path = args[0]
34
+ remainder = args[1:]
35
+
36
+ if remainder and remainder[0] == "format":
37
+ check_only = len(remainder) > 1 and remainder[1] == "check"
38
+ return run_format(path, check_only)
39
+ if remainder and remainder[0] == "lint":
40
+ check_only = len(remainder) > 1 and remainder[1] == "check"
41
+ return run_lint(path, check_only)
42
+ if remainder and remainder[0] == "actions":
43
+ json_mode = len(remainder) > 1 and remainder[1] == "json"
44
+ program_ir, source = load_program(path)
45
+ json_payload, text_output = list_actions(program_ir, json_mode)
46
+ if json_mode:
47
+ print(dumps_pretty(json_payload))
48
+ else:
49
+ print(text_output or "")
50
+ return 0
51
+ if remainder and remainder[0] == "studio":
52
+ port = 7333
53
+ dry = False
54
+ tail = remainder[1:]
55
+ i = 0
56
+ while i < len(tail):
57
+ if tail[i] == "--port" and i + 1 < len(tail):
58
+ try:
59
+ port = int(tail[i + 1])
60
+ except ValueError:
61
+ raise Namel3ssError("Port must be an integer")
62
+ i += 2
63
+ continue
64
+ if tail[i] == "--dry":
65
+ dry = True
66
+ i += 1
67
+ continue
68
+ i += 1
69
+ return run_studio(path, port, dry)
70
+
71
+ program_ir, source = load_program(path)
72
+ if not remainder:
73
+ return _run_default(program_ir)
74
+ cmd = remainder[0]
75
+ tail = remainder[1:]
76
+ if cmd == "check":
77
+ print("OK")
78
+ return 0
79
+ if cmd == "ui":
80
+ manifest = render_manifest(program_ir)
81
+ print(dumps_pretty(manifest))
82
+ return 0
83
+ if cmd == "flow":
84
+ if not tail:
85
+ raise Namel3ssError('Missing flow name. Use: n3 <app.ai> flow "<name>"')
86
+ flow_name = tail[0]
87
+ output = run_flow(program_ir, flow_name)
88
+ print(dumps_pretty(output))
89
+ return 0
90
+ if cmd == "help":
91
+ _print_usage()
92
+ return 0
93
+ # action mode
94
+ if cmd in RESERVED:
95
+ raise Namel3ssError("Unexpected reserved command position")
96
+ action_id = cmd
97
+ payload_text = tail[0] if tail else "{}"
98
+ payload = parse_payload(payload_text)
99
+ response = run_action(program_ir, action_id, payload)
100
+ print(dumps_pretty(response))
101
+ return 0
102
+ except Namel3ssError as err:
103
+ print(format_error(err, locals().get("source", "")), file=sys.stderr)
104
+ return 1
105
+
106
+
107
+ def _run_default(program_ir) -> int:
108
+ output = run_flow(program_ir, None)
109
+ print(dumps_pretty(output))
110
+ return 0
111
+
112
+
113
+ def _print_usage() -> None:
114
+ usage = """Usage:
115
+ n3 new [template] [name] # scaffold from a template (omit args to list)
116
+ n3 <app.ai> # run default flow
117
+ n3 <app.ai> check # validate only
118
+ n3 <app.ai> ui # print UI manifest
119
+ n3 <app.ai> flow "<name>" # run specific flow
120
+ n3 <app.ai> format # format in place
121
+ n3 <app.ai> format check # check formatting only
122
+ n3 <app.ai> lint # lint and print findings
123
+ n3 <app.ai> lint check # lint, fail on findings
124
+ n3 <app.ai> studio [--port N] # start Studio viewer (use --dry to skip server in tests)
125
+ n3 <app.ai> studio --dry # dry run (prints URL)
126
+ n3 <app.ai> actions # list actions (plain text)
127
+ n3 <app.ai> actions json # list actions (JSON)
128
+ n3 <app.ai> <action_id> [json] # execute UI action (payload optional)
129
+ n3 <app.ai> help # this help
130
+ """
131
+ print(usage.strip())
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from namel3ss.errors.base import Namel3ssError
9
+ from namel3ss.format import format_source
10
+ from namel3ss.lint.engine import lint_source
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TemplateSpec:
15
+ name: str
16
+ directory: str
17
+ description: str
18
+ aliases: tuple[str, ...] = ()
19
+
20
+ def matches(self, candidate: str) -> bool:
21
+ normalized = candidate.lower().replace("_", "-")
22
+ if normalized == self.name:
23
+ return True
24
+ normalized_aliases = {alias.lower().replace("_", "-") for alias in self.aliases}
25
+ normalized_aliases.add(self.directory.lower().replace("_", "-"))
26
+ return normalized in normalized_aliases
27
+
28
+
29
+ TEMPLATES: tuple[TemplateSpec, ...] = (
30
+ TemplateSpec(name="crud", directory="crud", description="CRUD dashboard with form and table."),
31
+ TemplateSpec(
32
+ name="ai-assistant",
33
+ directory="ai_assistant",
34
+ description="AI assistant over records with memory and tooling.",
35
+ aliases=("ai_assistant",),
36
+ ),
37
+ TemplateSpec(
38
+ name="multi-agent",
39
+ directory="multi_agent",
40
+ description="Planner, critic, and researcher agents sharing one assistant.",
41
+ aliases=("multi_agent",),
42
+ ),
43
+ )
44
+
45
+
46
+ def run_new(args: list[str]) -> int:
47
+ if not args:
48
+ print(render_templates_list())
49
+ return 0
50
+ if len(args) > 2:
51
+ raise Namel3ssError("Usage: n3 new <template> [project_name]")
52
+ template_name = args[0]
53
+ template = _resolve_template(template_name)
54
+ project_input = args[1] if len(args) == 2 else template.name
55
+ project_name = _normalize_project_name(project_input)
56
+
57
+ template_dir = _templates_root() / template.directory
58
+ if not template_dir.exists():
59
+ raise Namel3ssError(f"Template '{template.name}' is not installed (missing {template_dir}).")
60
+
61
+ target_dir = Path.cwd() / project_name
62
+ if target_dir.exists():
63
+ raise Namel3ssError(f"Directory already exists: {target_dir}")
64
+
65
+ try:
66
+ shutil.copytree(template_dir, target_dir)
67
+ _prepare_readme(target_dir, project_name)
68
+ formatted_source = _prepare_app_file(target_dir, project_name)
69
+ except Exception:
70
+ shutil.rmtree(target_dir, ignore_errors=True)
71
+ raise
72
+
73
+ findings = lint_source(formatted_source)
74
+ if findings:
75
+ print("Lint findings:")
76
+ for finding in findings:
77
+ location = f"[line {finding.line}, col {finding.column}] " if finding.line else ""
78
+ print(f" - {location}{finding.code}: {finding.message}")
79
+
80
+ _print_success_message(project_name, target_dir)
81
+ return 0
82
+
83
+
84
+ def render_templates_list() -> str:
85
+ longest = max(len(t.name) for t in TEMPLATES)
86
+ lines = ["Available templates:"]
87
+ for template in TEMPLATES:
88
+ padded = template.name.ljust(longest)
89
+ lines.append(f" {padded} - {template.description}")
90
+ return "\n".join(lines)
91
+
92
+
93
+ def _templates_root() -> Path:
94
+ return Path(__file__).resolve().parent.parent / "templates"
95
+
96
+
97
+ def _resolve_template(name: str) -> TemplateSpec:
98
+ for template in TEMPLATES:
99
+ if template.matches(name):
100
+ return template
101
+ available = ", ".join(t.name for t in TEMPLATES)
102
+ raise Namel3ssError(f"Unknown template '{name}'. Available templates: {available}")
103
+
104
+
105
+ def _normalize_project_name(name: str) -> str:
106
+ normalized = name.replace("-", "_")
107
+ normalized = re.sub(r"[^A-Za-z0-9_]+", "_", normalized).strip("_")
108
+ if not normalized:
109
+ raise Namel3ssError("Project name cannot be empty after normalization")
110
+ return normalized
111
+
112
+
113
+ def _prepare_readme(target_dir: Path, project_name: str) -> None:
114
+ readme_path = target_dir / "README.md"
115
+ if not readme_path.exists():
116
+ raise Namel3ssError(f"Template is missing README.md at {readme_path}")
117
+ _rewrite_with_project_name(readme_path, project_name)
118
+
119
+
120
+ def _prepare_app_file(target_dir: Path, project_name: str) -> str:
121
+ app_path = target_dir / "app.ai"
122
+ if not app_path.exists():
123
+ raise Namel3ssError(f"Template is missing app.ai at {app_path}")
124
+ raw = _rewrite_with_project_name(app_path, project_name)
125
+ formatted = format_source(raw)
126
+ app_mode = app_path.stat().st_mode
127
+ app_path.write_text(formatted, encoding="utf-8")
128
+ app_path.chmod(app_mode)
129
+ return formatted
130
+
131
+
132
+ def _rewrite_with_project_name(path: Path, project_name: str) -> str:
133
+ original_mode = path.stat().st_mode
134
+ contents = path.read_text(encoding="utf-8")
135
+ updated = contents.replace("{{PROJECT_NAME}}", project_name)
136
+ path.write_text(updated, encoding="utf-8")
137
+ path.chmod(original_mode)
138
+ return updated
139
+
140
+
141
+ def _print_success_message(project_name: str, target_dir: Path) -> None:
142
+ print(f"Created project at {target_dir}")
143
+ print("Next steps:")
144
+ print(f" cd {project_name}")
145
+ print(" n3 app.ai studio")
146
+ print(" n3 app.ai actions")
namel3ss/cli/runner.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.errors.base import Namel3ssError
4
+ from namel3ss.runtime.executor import execute_program_flow
5
+ from namel3ss.runtime.store.memory_store import MemoryStore
6
+
7
+
8
+ def run_flow(program_ir, flow_name: str | None = None) -> dict:
9
+ selected = _select_flow(program_ir, flow_name)
10
+ result = execute_program_flow(program_ir, selected, state={}, input={}, store=MemoryStore())
11
+ traces = [_trace_to_dict(t) for t in result.traces]
12
+ return {"ok": True, "state": result.state, "result": result.last_value, "traces": traces}
13
+
14
+
15
+ def _select_flow(program_ir, flow_name: str | None) -> str:
16
+ if flow_name:
17
+ return flow_name
18
+ if len(program_ir.flows) == 1:
19
+ return program_ir.flows[0].name
20
+ raise Namel3ssError('Multiple flows found; use: n3 <app.ai> flow "<name>"')
21
+
22
+
23
+ def _trace_to_dict(trace) -> dict:
24
+ if hasattr(trace, "__dict__"):
25
+ return trace.__dict__
26
+ if isinstance(trace, dict):
27
+ return trace
28
+ return {"trace": trace}
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from namel3ss.cli.app_loader import load_program
6
+ from namel3ss.errors.base import Namel3ssError
7
+ from namel3ss.errors.render import format_error
8
+ from namel3ss.studio.server import start_server
9
+
10
+
11
+ def run_studio(path: str, port: int, dry: bool) -> int:
12
+ source = ""
13
+ try:
14
+ program_ir, source = load_program(path)
15
+ if dry:
16
+ print(f"Studio: http://127.0.0.1:{port}/")
17
+ return 0
18
+ start_server(path, port)
19
+ return 0
20
+ except Namel3ssError as err:
21
+ print(format_error(err, source), file=sys.stderr)
22
+ return 1
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.errors.base import Namel3ssError
4
+ from namel3ss.runtime.store.memory_store import MemoryStore
5
+ from namel3ss.runtime.ui.actions import handle_action
6
+ from namel3ss.ui.manifest import build_manifest
7
+
8
+
9
+ def render_manifest(program_ir) -> dict:
10
+ return build_manifest(program_ir, state={}, store=MemoryStore())
11
+
12
+
13
+ def run_action(program_ir, action_id: str, payload: dict) -> dict:
14
+ return handle_action(program_ir, action_id=action_id, payload=payload, state={}, store=MemoryStore())
@@ -0,0 +1,4 @@
1
+ from namel3ss.config.loader import load_config
2
+ from namel3ss.config.model import AppConfig, OllamaConfig
3
+
4
+ __all__ = ["load_config", "AppConfig", "OllamaConfig"]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def load_dotenv_for_path(ai_path: str) -> dict[str, str]:
8
+ path = Path(ai_path).resolve()
9
+ env_path = path.parent / ".env"
10
+ if not env_path.exists():
11
+ return {}
12
+ values: dict[str, str] = {}
13
+ for line in env_path.read_text(encoding="utf-8").splitlines():
14
+ stripped = line.strip()
15
+ if not stripped or stripped.startswith("#"):
16
+ continue
17
+ if "=" not in stripped:
18
+ continue
19
+ key, raw_value = stripped.split("=", 1)
20
+ key = key.strip()
21
+ value = raw_value.strip().strip('"').strip("'")
22
+ if key:
23
+ values[key] = value
24
+ return values
25
+
26
+
27
+ def apply_dotenv(values: dict[str, str]) -> None:
28
+ for key, value in values.items():
29
+ if key not in os.environ:
30
+ os.environ[key] = value
31
+
32
+
33
+ __all__ = ["load_dotenv_for_path", "apply_dotenv"]
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from namel3ss.config.model import AppConfig
8
+ from namel3ss.errors.base import Namel3ssError
9
+
10
+
11
+ CONFIG_PATH = Path.home() / ".namel3ss" / "config.json"
12
+
13
+
14
+ def load_config(config_path: Path | None = None) -> AppConfig:
15
+ config = AppConfig()
16
+ path = config_path or CONFIG_PATH
17
+ if path.exists():
18
+ _apply_file_config(config, path)
19
+ _apply_env_overrides(config)
20
+ return config
21
+
22
+
23
+ def _apply_file_config(config: AppConfig, path: Path) -> None:
24
+ try:
25
+ data = json.loads(path.read_text(encoding="utf-8"))
26
+ except json.JSONDecodeError as err:
27
+ raise Namel3ssError(f"Invalid config file at {path}: {err}") from err
28
+ if not isinstance(data, dict):
29
+ raise Namel3ssError(f"Config file must contain an object at {path}")
30
+ ollama_cfg = data.get("ollama", {})
31
+ if isinstance(ollama_cfg, dict):
32
+ if "host" in ollama_cfg:
33
+ config.ollama.host = str(ollama_cfg["host"])
34
+ if "timeout_seconds" in ollama_cfg:
35
+ try:
36
+ config.ollama.timeout_seconds = int(ollama_cfg["timeout_seconds"])
37
+ except (TypeError, ValueError) as err:
38
+ raise Namel3ssError("ollama.timeout_seconds must be an integer") from err
39
+ openai_cfg = data.get("openai", {})
40
+ if isinstance(openai_cfg, dict):
41
+ if "api_key" in openai_cfg:
42
+ config.openai.api_key = str(openai_cfg["api_key"])
43
+ if "base_url" in openai_cfg:
44
+ config.openai.base_url = str(openai_cfg["base_url"])
45
+ anthropic_cfg = data.get("anthropic", {})
46
+ if isinstance(anthropic_cfg, dict) and "api_key" in anthropic_cfg:
47
+ config.anthropic.api_key = str(anthropic_cfg["api_key"])
48
+ gemini_cfg = data.get("gemini", {})
49
+ if isinstance(gemini_cfg, dict) and "api_key" in gemini_cfg:
50
+ config.gemini.api_key = str(gemini_cfg["api_key"])
51
+ mistral_cfg = data.get("mistral", {})
52
+ if isinstance(mistral_cfg, dict) and "api_key" in mistral_cfg:
53
+ config.mistral.api_key = str(mistral_cfg["api_key"])
54
+
55
+
56
+ def _apply_env_overrides(config: AppConfig) -> None:
57
+ host = os.getenv("NAMEL3SS_OLLAMA_HOST")
58
+ if host:
59
+ config.ollama.host = host
60
+ timeout = os.getenv("NAMEL3SS_OLLAMA_TIMEOUT_SECONDS")
61
+ if timeout:
62
+ try:
63
+ config.ollama.timeout_seconds = int(timeout)
64
+ except ValueError as err:
65
+ raise Namel3ssError("NAMEL3SS_OLLAMA_TIMEOUT_SECONDS must be an integer") from err
66
+ api_key = os.getenv("NAMEL3SS_OPENAI_API_KEY")
67
+ if api_key:
68
+ config.openai.api_key = api_key
69
+ base_url = os.getenv("NAMEL3SS_OPENAI_BASE_URL")
70
+ if base_url:
71
+ config.openai.base_url = base_url
72
+ anthropic_key = os.getenv("NAMEL3SS_ANTHROPIC_API_KEY")
73
+ if anthropic_key:
74
+ config.anthropic.api_key = anthropic_key
75
+ gemini_key = os.getenv("NAMEL3SS_GEMINI_API_KEY")
76
+ if gemini_key:
77
+ config.gemini.api_key = gemini_key
78
+ mistral_key = os.getenv("NAMEL3SS_MISTRAL_API_KEY")
79
+ if mistral_key:
80
+ config.mistral.api_key = mistral_key
81
+
82
+
83
+ __all__ = ["load_config", "CONFIG_PATH"]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class OllamaConfig:
8
+ host: str = "http://127.0.0.1:11434"
9
+ timeout_seconds: int = 30
10
+
11
+
12
+ @dataclass
13
+ class OpenAIConfig:
14
+ api_key: str | None = None
15
+ base_url: str = "https://api.openai.com"
16
+
17
+
18
+ @dataclass
19
+ class AnthropicConfig:
20
+ api_key: str | None = None
21
+
22
+
23
+ @dataclass
24
+ class GeminiConfig:
25
+ api_key: str | None = None
26
+
27
+
28
+ @dataclass
29
+ class MistralConfig:
30
+ api_key: str | None = None
31
+
32
+
33
+ @dataclass
34
+ class AppConfig:
35
+ ollama: OllamaConfig = field(default_factory=OllamaConfig)
36
+ openai: OpenAIConfig = field(default_factory=OpenAIConfig)
37
+ anthropic: AnthropicConfig = field(default_factory=AnthropicConfig)
38
+ gemini: GeminiConfig = field(default_factory=GeminiConfig)
39
+ mistral: MistralConfig = field(default_factory=MistralConfig)
40
+
41
+
42
+ __all__ = [
43
+ "AppConfig",
44
+ "OllamaConfig",
45
+ "OpenAIConfig",
46
+ "AnthropicConfig",
47
+ "GeminiConfig",
48
+ "MistralConfig",
49
+ ]
@@ -0,0 +1,2 @@
1
+ """Shared error types and helpers."""
2
+
@@ -0,0 +1,34 @@
1
+ """
2
+ Shared error types for Namel3ss.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+
11
+ @dataclass
12
+ class Namel3ssError(Exception):
13
+ """Base error with optional source location."""
14
+
15
+ message: str
16
+ line: Optional[int] = None
17
+ column: Optional[int] = None
18
+ end_line: Optional[int] = None
19
+ end_column: Optional[int] = None
20
+
21
+ def __str__(self) -> str:
22
+ location = self._format_location()
23
+ return f"{location}{self.message}" if location else self.message
24
+
25
+ def _format_location(self) -> str:
26
+ if self.line is None:
27
+ return ""
28
+ if self.end_line is None or self.end_column is None:
29
+ return f"[line {self.line}, col {self.column or 1}] "
30
+ return (
31
+ f"[line {self.line}, col {self.column or 1} - "
32
+ f"line {self.end_line}, col {self.end_column}] "
33
+ )
34
+
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from namel3ss.errors.base import Namel3ssError
6
+
7
+
8
+ def format_error(err: Namel3ssError, source: Optional[str] = None) -> str:
9
+ base = str(err)
10
+ if source is None or err.line is None:
11
+ return base
12
+
13
+ lines = source.splitlines()
14
+ line_index = err.line - 1
15
+ if line_index < 0 or line_index >= len(lines):
16
+ return base
17
+
18
+ line_text = lines[line_index]
19
+ column = err.column if err.column is not None else 1
20
+ caret_pos = max(1, min(column, len(line_text) + 1))
21
+ caret_line = " " * (caret_pos - 1) + "^"
22
+ return f"{base}\n{line_text}\n{caret_line}"
@@ -0,0 +1,3 @@
1
+ from namel3ss.format.formatter import format_source
2
+
3
+ __all__ = ["format_source"]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from namel3ss.format.rules import collapse_blank_lines, migrate_buttons, normalize_indentation, normalize_spacing
4
+
5
+
6
+ def format_source(source: str) -> str:
7
+ lines = source.splitlines()
8
+ lines = [line.rstrip() for line in lines]
9
+ lines = migrate_buttons(lines)
10
+ lines = [normalize_spacing(line) for line in lines]
11
+ lines = normalize_indentation(lines)
12
+ lines = collapse_blank_lines(lines)
13
+ formatted = "\n".join(lines)
14
+ if formatted and not formatted.endswith("\n"):
15
+ formatted += "\n"
16
+ if not formatted and source.endswith("\n"):
17
+ formatted = "\n"
18
+ return formatted