ignite-ember 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. ember_code/__init__.py +3 -0
  2. ember_code/__main__.py +11 -0
  3. ember_code/backend/__init__.py +5 -0
  4. ember_code/backend/__main__.py +361 -0
  5. ember_code/backend/command_handler.py +662 -0
  6. ember_code/backend/server.py +668 -0
  7. ember_code/cli.py +279 -0
  8. ember_code/core/__init__.py +0 -0
  9. ember_code/core/auth/__init__.py +21 -0
  10. ember_code/core/auth/client.py +193 -0
  11. ember_code/core/auth/credentials.py +174 -0
  12. ember_code/core/config/__init__.py +13 -0
  13. ember_code/core/config/api_keys.py +33 -0
  14. ember_code/core/config/defaults.py +145 -0
  15. ember_code/core/config/models.py +308 -0
  16. ember_code/core/config/permissions.py +165 -0
  17. ember_code/core/config/settings.py +271 -0
  18. ember_code/core/config/tool_permissions.py +303 -0
  19. ember_code/core/evals/__init__.py +11 -0
  20. ember_code/core/evals/assertions.py +81 -0
  21. ember_code/core/evals/loader.py +83 -0
  22. ember_code/core/evals/reporter.py +76 -0
  23. ember_code/core/evals/runner.py +279 -0
  24. ember_code/core/guardrails/__init__.py +16 -0
  25. ember_code/core/guardrails/base.py +33 -0
  26. ember_code/core/guardrails/injection.py +52 -0
  27. ember_code/core/guardrails/moderation.py +23 -0
  28. ember_code/core/guardrails/pii.py +56 -0
  29. ember_code/core/guardrails/runner.py +54 -0
  30. ember_code/core/hooks/__init__.py +14 -0
  31. ember_code/core/hooks/events.py +18 -0
  32. ember_code/core/hooks/executor.py +178 -0
  33. ember_code/core/hooks/loader.py +77 -0
  34. ember_code/core/hooks/schemas.py +22 -0
  35. ember_code/core/hooks/tool_hook.py +171 -0
  36. ember_code/core/init.py +439 -0
  37. ember_code/core/knowledge/__init__.py +27 -0
  38. ember_code/core/knowledge/embedder.py +108 -0
  39. ember_code/core/knowledge/embedder_registry.py +85 -0
  40. ember_code/core/knowledge/manager.py +107 -0
  41. ember_code/core/knowledge/models.py +83 -0
  42. ember_code/core/knowledge/sync.py +179 -0
  43. ember_code/core/knowledge/vector_store.py +120 -0
  44. ember_code/core/learn.py +51 -0
  45. ember_code/core/mcp/__init__.py +15 -0
  46. ember_code/core/mcp/approval.py +110 -0
  47. ember_code/core/mcp/client.py +217 -0
  48. ember_code/core/mcp/config.py +134 -0
  49. ember_code/core/mcp/tools.py +28 -0
  50. ember_code/core/mcp/transport.py +52 -0
  51. ember_code/core/memory/__init__.py +5 -0
  52. ember_code/core/memory/manager.py +88 -0
  53. ember_code/core/pool.py +531 -0
  54. ember_code/core/prompts/__init__.py +14 -0
  55. ember_code/core/prompts/main_agent.md +250 -0
  56. ember_code/core/queue_hook.py +140 -0
  57. ember_code/core/scheduler/__init__.py +6 -0
  58. ember_code/core/scheduler/models.py +32 -0
  59. ember_code/core/scheduler/parser.py +145 -0
  60. ember_code/core/scheduler/runner.py +179 -0
  61. ember_code/core/scheduler/store.py +124 -0
  62. ember_code/core/session/__init__.py +17 -0
  63. ember_code/core/session/commands.py +103 -0
  64. ember_code/core/session/core.py +778 -0
  65. ember_code/core/session/ide_context.py +161 -0
  66. ember_code/core/session/interactive.py +177 -0
  67. ember_code/core/session/knowledge_ops.py +281 -0
  68. ember_code/core/session/memory_ops.py +90 -0
  69. ember_code/core/session/persistence.py +106 -0
  70. ember_code/core/session/runner.py +81 -0
  71. ember_code/core/skills/__init__.py +13 -0
  72. ember_code/core/skills/executor.py +64 -0
  73. ember_code/core/skills/loader.py +131 -0
  74. ember_code/core/skills/parser.py +85 -0
  75. ember_code/core/tools/__init__.py +5 -0
  76. ember_code/core/tools/codeindex.py +353 -0
  77. ember_code/core/tools/custom_loader.py +101 -0
  78. ember_code/core/tools/edit.py +104 -0
  79. ember_code/core/tools/knowledge.py +122 -0
  80. ember_code/core/tools/notebook.py +260 -0
  81. ember_code/core/tools/orchestrate.py +421 -0
  82. ember_code/core/tools/registry.py +256 -0
  83. ember_code/core/tools/schedule.py +103 -0
  84. ember_code/core/tools/search.py +155 -0
  85. ember_code/core/tools/web.py +78 -0
  86. ember_code/core/utils/__init__.py +1 -0
  87. ember_code/core/utils/audit.py +69 -0
  88. ember_code/core/utils/context.py +146 -0
  89. ember_code/core/utils/display.py +125 -0
  90. ember_code/core/utils/media.py +141 -0
  91. ember_code/core/utils/mentions.py +30 -0
  92. ember_code/core/utils/response.py +23 -0
  93. ember_code/core/utils/tips.py +142 -0
  94. ember_code/core/utils/update_checker.py +163 -0
  95. ember_code/core/workspace.py +66 -0
  96. ember_code/core/worktree.py +171 -0
  97. ember_code/frontend/__init__.py +0 -0
  98. ember_code/frontend/tui/__init__.py +5 -0
  99. ember_code/frontend/tui/app.py +1075 -0
  100. ember_code/frontend/tui/backend_client.py +408 -0
  101. ember_code/frontend/tui/conversation_view.py +87 -0
  102. ember_code/frontend/tui/file_index.py +162 -0
  103. ember_code/frontend/tui/format_helpers.py +3 -0
  104. ember_code/frontend/tui/hitl_handler.py +152 -0
  105. ember_code/frontend/tui/input_handler.py +202 -0
  106. ember_code/frontend/tui/process_manager.py +117 -0
  107. ember_code/frontend/tui/run_controller.py +818 -0
  108. ember_code/frontend/tui/session_manager.py +90 -0
  109. ember_code/frontend/tui/status_tracker.py +91 -0
  110. ember_code/frontend/tui/widgets/__init__.py +67 -0
  111. ember_code/frontend/tui/widgets/_activity.py +239 -0
  112. ember_code/frontend/tui/widgets/_agent_run.py +70 -0
  113. ember_code/frontend/tui/widgets/_chrome.py +439 -0
  114. ember_code/frontend/tui/widgets/_constants.py +5 -0
  115. ember_code/frontend/tui/widgets/_dialogs.py +609 -0
  116. ember_code/frontend/tui/widgets/_file_picker.py +103 -0
  117. ember_code/frontend/tui/widgets/_formatting.py +31 -0
  118. ember_code/frontend/tui/widgets/_help_panel.py +309 -0
  119. ember_code/frontend/tui/widgets/_input.py +123 -0
  120. ember_code/frontend/tui/widgets/_mcp_panel.py +292 -0
  121. ember_code/frontend/tui/widgets/_messages.py +620 -0
  122. ember_code/frontend/tui/widgets/_task_progress.py +142 -0
  123. ember_code/frontend/tui/widgets/_tasks.py +267 -0
  124. ember_code/frontend/tui/widgets/_tokens.py +120 -0
  125. ember_code/protocol/__init__.py +8 -0
  126. ember_code/protocol/agno_events.py +317 -0
  127. ember_code/protocol/messages.py +325 -0
  128. ember_code/protocol/serializer.py +197 -0
  129. ember_code/transport/__init__.py +11 -0
  130. ember_code/transport/base.py +37 -0
  131. ember_code/transport/in_process.py +69 -0
  132. ember_code/transport/unix_socket.py +186 -0
  133. ignite_ember-0.1.0.dist-info/METADATA +184 -0
  134. ignite_ember-0.1.0.dist-info/RECORD +137 -0
  135. ignite_ember-0.1.0.dist-info/WHEEL +4 -0
  136. ignite_ember-0.1.0.dist-info/entry_points.txt +2 -0
  137. ignite_ember-0.1.0.dist-info/licenses/LICENSE +21 -0
ember_code/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Ember Code — Terminal-based AI coding assistant built on Agno."""
2
+
3
+ __version__ = "0.1.0"
ember_code/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Entry point for `ignite-ember` and `python -m ember_code`."""
2
+
3
+ from ember_code.cli import cli
4
+
5
+
6
+ def main():
7
+ cli()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,5 @@
1
+ """Backend server — owns Session and all AI logic.
2
+
3
+ The TUI (or any future frontend) communicates with the backend
4
+ exclusively through protocol messages. No Agno types cross this boundary.
5
+ """
@@ -0,0 +1,361 @@
1
+ """Backend process entry point.
2
+
3
+ Usage: python -m ember_code.backend --socket /tmp/ember-code/<uuid>.sock
4
+
5
+ Starts a BackendServer, listens on the given Unix socket, and
6
+ processes FE messages until shutdown.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import logging
13
+ import signal
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import click
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @click.command()
23
+ @click.option("--socket", "socket_path", required=True, help="Unix socket path")
24
+ @click.option("--project-dir", type=click.Path(exists=True), default=".")
25
+ @click.option("--resume-session", "resume_session_id", default=None)
26
+ @click.option("--additional-dirs", multiple=True, default=())
27
+ @click.option("--debug", is_flag=True, default=False)
28
+ def main(
29
+ socket_path: str,
30
+ project_dir: str,
31
+ resume_session_id: str | None,
32
+ additional_dirs: tuple[str, ...],
33
+ debug: bool,
34
+ ) -> None:
35
+ """Start the Ember Code backend server."""
36
+ if debug:
37
+ log_path = Path.home() / ".ember" / "debug.log"
38
+ log_path.parent.mkdir(parents=True, exist_ok=True)
39
+ logging.basicConfig(
40
+ filename=str(log_path),
41
+ level=logging.DEBUG,
42
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
43
+ force=True,
44
+ )
45
+ logging.getLogger("ember_code").setLevel(logging.DEBUG)
46
+
47
+ extra_dirs = [Path(d) for d in additional_dirs] if additional_dirs else None
48
+ asyncio.run(_run(socket_path, Path(project_dir), resume_session_id, extra_dirs))
49
+
50
+
51
+ async def _check_update() -> dict | None:
52
+ try:
53
+ from ember_code.core.utils.update_checker import check_for_update
54
+
55
+ info = await check_for_update()
56
+ if info and info.available:
57
+ return {"available": True, "version": info.latest_version, "message": info.message}
58
+ except Exception:
59
+ pass
60
+ return None
61
+
62
+
63
+ # ── RPC dispatch table ──────────────────────────────────────────────
64
+
65
+
66
+ def _build_rpc_table(backend: Any, transport: Any) -> dict[str, Any]:
67
+ """Build method dispatch for RPCRequest messages."""
68
+
69
+ async def _login(args: dict) -> dict:
70
+ # Wrap on_status callback to push notifications
71
+ async def _on_status(text: str) -> None:
72
+ from ember_code.protocol import messages as msg
73
+
74
+ await transport.send(
75
+ msg.PushNotification(channel="login_status", payload={"text": text})
76
+ )
77
+
78
+ success, result = await backend.login(on_status=_on_status)
79
+ return {"success": success, "result": result}
80
+
81
+ async def _get_skill_definitions(args: dict) -> list[dict]:
82
+ pool = backend.get_skill_pool()
83
+ return [
84
+ {"name": s.name, "description": s.description, "prompt": getattr(s, "prompt", "")}
85
+ for s in pool.list_skills()
86
+ ]
87
+
88
+ return {
89
+ # Async methods
90
+ "ensure_mcp": lambda args: backend.ensure_mcp(),
91
+ "mcp_connect": lambda args: backend.mcp_connect(args["server_name"]),
92
+ "mcp_disconnect": lambda args: backend.mcp_disconnect(args["server_name"]),
93
+ "compact_if_needed": lambda args: backend.compact_if_needed(
94
+ args["ctx_tokens"], args["max_ctx"]
95
+ ),
96
+ "extract_learnings": lambda args: backend.extract_learnings(
97
+ args["user_msg"], args["assistant_msg"]
98
+ ),
99
+ "login": _login,
100
+ "fire_session_start_hook": lambda args: backend.fire_session_start_hook(),
101
+ "auto_sync_knowledge": lambda args: backend.auto_sync_knowledge(),
102
+ "shutdown": lambda args: backend.shutdown(),
103
+ "get_chat_history": lambda args: backend.get_chat_history(args["session_id"]),
104
+ "execute_scheduled_task": lambda args: backend.execute_scheduled_task(args["description"]),
105
+ "cancel_scheduled_task": lambda args: backend.cancel_scheduled_task(args["task_id"]),
106
+ "get_scheduled_tasks": lambda args: backend.get_scheduled_tasks(
107
+ args.get("include_done", True)
108
+ ),
109
+ "list_sessions": lambda args: backend.list_sessions(),
110
+ "switch_session": lambda args: backend.switch_session(args["session_id"]),
111
+ # Sync accessors
112
+ "get_processing": lambda args: backend.processing,
113
+ "get_session_id": lambda args: backend.session_id,
114
+ "get_run_timeout": lambda args: backend.run_timeout,
115
+ "get_skill_names": lambda args: backend.skill_names,
116
+ "get_mcp_status": lambda args: backend.get_mcp_status(),
117
+ "get_mcp_server_details": lambda args: backend.get_mcp_server_details(),
118
+ "get_mcp_servers": lambda args: backend.get_mcp_servers(),
119
+ "get_status": lambda args: backend.get_status(),
120
+ "switch_model": lambda args: backend.switch_model(args["model_name"]),
121
+ "reload_cloud_credentials": lambda args: backend.reload_cloud_credentials(),
122
+ "clear_cloud_credentials": lambda args: backend.clear_cloud_credentials(),
123
+ "toggle_verbose": lambda args: backend.toggle_verbose(),
124
+ "cancel_run": lambda args: backend.cancel_run(),
125
+ "check_permission": lambda args: backend.check_permission(
126
+ args["tool_name"], args["func_name"], args["tool_args"]
127
+ ),
128
+ "save_permission_rule": lambda args: backend.save_permission_rule(
129
+ args["rule"], args["level"]
130
+ ),
131
+ "get_display_config": lambda args: (
132
+ backend.settings.display.model_dump()
133
+ if hasattr(backend.settings.display, "model_dump")
134
+ else {}
135
+ ),
136
+ "get_model_registry": lambda args: {
137
+ "default": backend.settings.models.default,
138
+ "max_context_window": backend.settings.models.max_context_window,
139
+ "registry": {k: v for k, v in backend.settings.models.registry.items()},
140
+ },
141
+ "check_for_update": lambda args: _check_update(),
142
+ "get_skill_definitions": _get_skill_definitions,
143
+ "start_scheduler": lambda args: _start_scheduler_with_push(backend, transport),
144
+ }
145
+
146
+
147
+ def _start_scheduler_with_push(backend: Any, transport: Any) -> None:
148
+ """Start the scheduler with push notification callbacks."""
149
+ from ember_code.protocol import messages as msg
150
+
151
+ def on_started(task_id: str, description: str) -> None:
152
+ asyncio.ensure_future(
153
+ transport.send(
154
+ msg.PushNotification(
155
+ channel="scheduler_started",
156
+ payload={"task_id": task_id, "description": description},
157
+ )
158
+ )
159
+ )
160
+
161
+ def on_completed(task_id: str, description: str, result: str) -> None:
162
+ asyncio.ensure_future(
163
+ transport.send(
164
+ msg.PushNotification(
165
+ channel="scheduler_completed",
166
+ payload={"task_id": task_id, "description": description, "result": result},
167
+ )
168
+ )
169
+ )
170
+
171
+ backend.start_scheduler(on_task_started=on_started, on_task_completed=on_completed)
172
+
173
+
174
+ # ── Main loop ──────────────────────────────────────────────────────
175
+
176
+
177
+ async def _run(
178
+ socket_path: str,
179
+ project_dir: Path,
180
+ resume_session_id: str | None,
181
+ additional_dirs: list[Path] | None = None,
182
+ ) -> None:
183
+ from ember_code.backend.server import BackendServer
184
+ from ember_code.core.config.settings import load_settings
185
+ from ember_code.protocol import messages as msg
186
+ from ember_code.transport.unix_socket import UnixSocketServerTransport
187
+
188
+ settings = load_settings(project_dir=project_dir)
189
+
190
+ backend = BackendServer(
191
+ settings,
192
+ project_dir=project_dir,
193
+ resume_session_id=resume_session_id,
194
+ additional_dirs=additional_dirs,
195
+ )
196
+
197
+ transport = UnixSocketServerTransport(socket_path)
198
+ await transport.start()
199
+
200
+ # Signal ready to FE
201
+ print(f"READY {socket_path}", flush=True)
202
+
203
+ # Handle SIGTERM/SIGINT
204
+ shutdown_event = asyncio.Event()
205
+
206
+ def _signal_handler():
207
+ shutdown_event.set()
208
+
209
+ loop = asyncio.get_event_loop()
210
+ for sig in (signal.SIGTERM, signal.SIGINT):
211
+ loop.add_signal_handler(sig, _signal_handler)
212
+
213
+ # Queue for message injection (replaces wire_queue_hook)
214
+ _queue: list[str] = []
215
+ backend.wire_queue_hook(_queue)
216
+
217
+ # Orchestrate progress → push notification
218
+ def _on_progress(line: str) -> None:
219
+ asyncio.ensure_future(
220
+ transport.send(
221
+ msg.PushNotification(channel="orchestrate_progress", payload={"line": line})
222
+ )
223
+ )
224
+
225
+ backend.wire_orchestrate_progress(_on_progress)
226
+
227
+ rpc_table = _build_rpc_table(backend, transport)
228
+
229
+ try:
230
+ await transport.wait_for_connection()
231
+ logger.info("FE connected, processing messages")
232
+
233
+ async for message in transport.receive():
234
+ if isinstance(message, msg.Shutdown):
235
+ break
236
+
237
+ if shutdown_event.is_set():
238
+ break
239
+
240
+ await _handle_message(message, backend, transport, rpc_table, _queue)
241
+
242
+ except Exception as exc:
243
+ logger.error("Backend error: %s", exc, exc_info=True)
244
+ finally:
245
+ await backend.shutdown()
246
+ await transport.close()
247
+ logger.info("Backend shut down")
248
+
249
+
250
+ async def _handle_message(
251
+ message: Any,
252
+ backend: Any,
253
+ transport: Any,
254
+ rpc_table: dict,
255
+ queue: list[str],
256
+ ) -> None:
257
+ from ember_code.protocol import messages as msg
258
+ from ember_code.protocol.messages import Message
259
+
260
+ req_id = message.id or ""
261
+
262
+ # ── Streaming: run_message ──
263
+ if isinstance(message, msg.UserMessage):
264
+ async for proto in backend.run_message(message.text, media=message.file_contents):
265
+ if req_id:
266
+ proto = proto.model_copy(update={"id": req_id})
267
+ await transport.send(proto)
268
+ await transport.send(msg.StreamEnd(id=req_id))
269
+
270
+ # ── Streaming: resolve_hitl ──
271
+ elif isinstance(message, msg.HITLResponse):
272
+ async for proto in backend.resolve_hitl(
273
+ message.requirement_id, message.action, message.choice
274
+ ):
275
+ if req_id:
276
+ proto = proto.model_copy(update={"id": req_id})
277
+ await transport.send(proto)
278
+ await transport.send(msg.StreamEnd(id=req_id))
279
+
280
+ # ── Command ──
281
+ elif isinstance(message, msg.Command):
282
+ result = await backend.handle_command(message.text)
283
+ result = result.model_copy(update={"id": req_id})
284
+ await transport.send(result)
285
+
286
+ # ── Session management (typed messages) ──
287
+ elif isinstance(message, msg.SessionList):
288
+ result = await backend.list_sessions()
289
+ result = result.model_copy(update={"id": req_id})
290
+ await transport.send(result)
291
+
292
+ elif isinstance(message, msg.SessionSwitch):
293
+ result = await backend.switch_session(message.session_id)
294
+ result = result.model_copy(update={"id": req_id})
295
+ await transport.send(result)
296
+
297
+ elif isinstance(message, msg.ModelSwitch):
298
+ result = backend.switch_model(message.model_name)
299
+ result = result.model_copy(update={"id": req_id})
300
+ await transport.send(result)
301
+
302
+ elif isinstance(message, msg.MCPToggle):
303
+ result = await backend.toggle_mcp(message.server_name, message.connect)
304
+ result = result.model_copy(update={"id": req_id})
305
+ await transport.send(result)
306
+
307
+ # ── Queue injection ──
308
+ elif isinstance(message, msg.QueueMessage):
309
+ queue.append(message.text)
310
+
311
+ # ── Cancel ──
312
+ elif isinstance(message, msg.Cancel):
313
+ backend.cancel_run()
314
+
315
+ # ── Generic RPC ──
316
+ elif isinstance(message, msg.RPCRequest):
317
+ handler = rpc_table.get(message.method)
318
+ if handler is None:
319
+ await transport.send(
320
+ msg.RPCResponse(id=req_id, error=f"Unknown RPC method: {message.method}")
321
+ )
322
+ return
323
+
324
+ try:
325
+ result = handler(message.args)
326
+ if asyncio.iscoroutine(result) or asyncio.isfuture(result):
327
+ result = await result
328
+
329
+ # If result is a Message, send it directly with correlation ID
330
+ if isinstance(result, Message):
331
+ result = result.model_copy(update={"id": req_id})
332
+ await transport.send(result)
333
+ else:
334
+ # Wrap in RPCResponse for plain values
335
+ await transport.send(msg.RPCResponse(id=req_id, result=_serialize(result)))
336
+ except Exception as exc:
337
+ logger.error("RPC %s failed: %s", message.method, exc, exc_info=True)
338
+ await transport.send(msg.RPCResponse(id=req_id, error=str(exc)))
339
+
340
+ else:
341
+ logger.warning("Unknown FE message type: %s", type(message).__name__)
342
+
343
+
344
+ def _serialize(value: Any) -> Any:
345
+ """Make a value JSON-serializable."""
346
+ if value is None:
347
+ return None
348
+ if isinstance(value, (str, int, float, bool)):
349
+ return value
350
+ if isinstance(value, (list, tuple)):
351
+ return [_serialize(v) for v in value]
352
+ if isinstance(value, dict):
353
+ return {str(k): _serialize(v) for k, v in value.items()}
354
+ # Pydantic models
355
+ if hasattr(value, "model_dump"):
356
+ return value.model_dump()
357
+ return str(value)
358
+
359
+
360
+ if __name__ == "__main__":
361
+ main()