elidia-agent-cli 0.2.0__tar.gz → 0.3.0__tar.gz

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 (129) hide show
  1. elidia_agent_cli-0.3.0/CHANGELOG.md +56 -0
  2. {elidia_agent_cli-0.2.0/elidia_agent_cli.egg-info → elidia_agent_cli-0.3.0}/PKG-INFO +1 -1
  3. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/ARCHITECTURE_FLOWCHARTS.md +40 -0
  4. elidia_agent_cli-0.3.0/docs/guide/tools-mcp.md +113 -0
  5. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/agent/loop.py +2 -0
  6. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/commands.py +5 -0
  7. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/main.py +196 -6
  8. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/repl.py +69 -0
  9. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/permissions/manager.py +1 -0
  10. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/rag/chunker.py +92 -36
  11. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/rag/engine.py +22 -9
  12. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/__init__.py +3 -0
  13. elidia_agent_cli-0.3.0/elidia/tools/rag.py +147 -0
  14. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0/elidia_agent_cli.egg-info}/PKG-INFO +1 -1
  15. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia_agent_cli.egg-info/SOURCES.txt +1 -0
  16. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/pyproject.toml +1 -1
  17. elidia_agent_cli-0.2.0/CHANGELOG.md +0 -16
  18. elidia_agent_cli-0.2.0/docs/guide/tools-mcp.md +0 -86
  19. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/LICENSE +0 -0
  20. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/MANIFEST.in +0 -0
  21. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/README.md +0 -0
  22. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/AUDIT_REPORT_v0.1.0.md +0 -0
  23. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/api/commands.md +0 -0
  24. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/architecture/modules.md +0 -0
  25. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/architecture/overview.md +0 -0
  26. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/getting-started/configuration.md +0 -0
  27. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/getting-started/installation.md +0 -0
  28. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/getting-started/quickstart.md +0 -0
  29. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/guide/chat-modes.md +0 -0
  30. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/guide/creative.md +0 -0
  31. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/guide/models.md +0 -0
  32. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/guide/research.md +0 -0
  33. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/guide/themes.md +0 -0
  34. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/guide/workflows.md +0 -0
  35. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/docs/index.md +0 -0
  36. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/__init__.py +0 -0
  37. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/__main__.py +0 -0
  38. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/agent/__init__.py +0 -0
  39. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/agent/personas.py +0 -0
  40. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/agent/portal.py +0 -0
  41. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/api/__init__.py +0 -0
  42. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/api/client.py +0 -0
  43. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/api/streaming.py +0 -0
  44. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/auth/__init__.py +0 -0
  45. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/auth/keychain.py +0 -0
  46. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cache/__init__.py +0 -0
  47. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cache/lru.py +0 -0
  48. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/__init__.py +0 -0
  49. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/pager.py +0 -0
  50. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/progress.py +0 -0
  51. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/renderer.py +0 -0
  52. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/cli/themes.py +0 -0
  53. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/config/__init__.py +0 -0
  54. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/config/defaults.py +0 -0
  55. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/config/rules.py +0 -0
  56. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/config/settings.py +0 -0
  57. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/creative/__init__.py +0 -0
  58. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/creative/audio.py +0 -0
  59. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/creative/display.py +0 -0
  60. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/creative/image.py +0 -0
  61. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/creative/local.py +0 -0
  62. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/creative/video.py +0 -0
  63. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/daemon/__init__.py +0 -0
  64. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/daemon/config.py +0 -0
  65. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/daemon/ipc.py +0 -0
  66. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/daemon/manager.py +0 -0
  67. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/daemon/process.py +0 -0
  68. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/daemon/worker.py +0 -0
  69. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/db/__init__.py +0 -0
  70. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/db/database.py +0 -0
  71. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/mcp/__init__.py +0 -0
  72. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/mcp/client.py +0 -0
  73. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/mcp/config.py +0 -0
  74. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/mcp/registry.py +0 -0
  75. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/mcp/types.py +0 -0
  76. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/__init__.py +0 -0
  77. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/auto.py +0 -0
  78. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/compaction.py +0 -0
  79. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/embeddings.py +0 -0
  80. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/outcomes.py +0 -0
  81. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/patterns.py +0 -0
  82. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/memory/store.py +0 -0
  83. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/models/__init__.py +0 -0
  84. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/models/adaptive.py +0 -0
  85. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/models/router.py +0 -0
  86. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/__init__.py +0 -0
  87. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/autonomous.py +0 -0
  88. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/budget.py +0 -0
  89. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/classifier.py +0 -0
  90. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/consensus.py +0 -0
  91. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/deep_think.py +0 -0
  92. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/swarm.py +0 -0
  93. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/modes/thinking.py +0 -0
  94. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/permissions/__init__.py +0 -0
  95. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/permissions/audit.py +0 -0
  96. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/permissions/trust.py +0 -0
  97. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/rag/__init__.py +0 -0
  98. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/rag/ingest.py +0 -0
  99. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/rag/portal_bridge.py +0 -0
  100. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/rag/watcher.py +0 -0
  101. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/research/__init__.py +0 -0
  102. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/research/export.py +0 -0
  103. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/research/orchestrator.py +0 -0
  104. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/research/sources.py +0 -0
  105. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/session/__init__.py +0 -0
  106. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/session/history.py +0 -0
  107. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/session/manager.py +0 -0
  108. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/base.py +0 -0
  109. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/browser.py +0 -0
  110. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/calendar.py +0 -0
  111. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/database.py +0 -0
  112. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/email.py +0 -0
  113. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/fetch.py +0 -0
  114. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/filesystem.py +0 -0
  115. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/git.py +0 -0
  116. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/office.py +0 -0
  117. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/search.py +0 -0
  118. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/terminal.py +0 -0
  119. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/tools/tool_router.py +0 -0
  120. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/widgets/__init__.py +0 -0
  121. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/widgets/protocol.py +0 -0
  122. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/widgets/renderer.py +0 -0
  123. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/workflow/__init__.py +0 -0
  124. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia/workflow/engine.py +0 -0
  125. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia_agent_cli.egg-info/dependency_links.txt +0 -0
  126. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia_agent_cli.egg-info/entry_points.txt +0 -0
  127. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia_agent_cli.egg-info/requires.txt +0 -0
  128. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/elidia_agent_cli.egg-info/top_level.txt +0 -0
  129. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,56 @@
1
+ # Changelog
2
+
3
+ All notable changes to Elidia CLI will be documented in this file.
4
+
5
+ ## [0.3.0] - 2026-07-27
6
+
7
+ ### Added
8
+ - RAG reconnected to every user-facing surface: `elidia rag ingest/search/list/clear`
9
+ (CLI), `/rag ingest|search|list|clear` (REPL), and auto-ingest for files over
10
+ ~8,000 chars passed via `-f/--file` (preview + index instead of blind truncation,
11
+ including files >1MB that were previously dropped entirely). New agent-facing
12
+ `rag_search`/`rag_list_sources` tools (AUTO permission tier).
13
+
14
+ ### Fixed
15
+ - `elidia workflow run` required an API key even for pure-shell workflows with no
16
+ `llm` step.
17
+ - Email skill used the same field for the SMTP/IMAP login and the visible `From`
18
+ address, which breaks for transactional relays (Zepto, SendGrid, Mailgun, SES)
19
+ whose AUTH username is a token distinct from the sender address. Added
20
+ `from_address`.
21
+ - RAG's FTS5 query builder crashed on any hyphenated search term (`"no such
22
+ column: call"` on a query containing "on-call").
23
+ - RAG's chunker never split a paragraph/section/line once it exceeded
24
+ `chunk_size` — a 21KB file with no blank lines produced 1 chunk instead of
25
+ ~40, diluting the embedding enough that content buried mid-file became
26
+ unfindable by search.
27
+ - `elidia ask` silently dropped `-f/--file` — the subcommand never declared its
28
+ own option or read the parent group's.
29
+
30
+ ## [0.2.0] - 2026-07-26
31
+
32
+ ### Added
33
+ - Real background daemon (`elidia daemon start/stop/restart/status/init`),
34
+ `elidia mcp list/health`, `elidia workflow run`.
35
+ - Vision support: `--image`/`-i` flag, `/image` REPL command, S3/CDN upload.
36
+ - Browser, Office, Database, Email, and Calendar skills (5 new tool categories).
37
+
38
+ ### Fixed
39
+ - Tool-calling never actually worked — the system prompt (and therefore every
40
+ tool schema) was never sent to the model; the agent could only ever respond
41
+ from training knowledge, never call a real tool.
42
+ - Permission classification for tool calls with an omitted path argument fell
43
+ through to the wrong (over-restrictive) tier.
44
+
45
+ ## [0.1.0] - 2026-07-25
46
+
47
+ ### Added
48
+ - Phase 0: Core CLI framework (Click + Rich + prompt_toolkit)
49
+ - Phase 1: MCP integration, built-in tools, model router, permissions
50
+ - Phase 2: Persistent memory (sqlite-vec), RAG engine, session management
51
+ - Phase 3: Advanced modes (classifier, consensus, deep think, autonomous, swarm)
52
+ - Phase 3: Research pipeline (YOYO 5-stage), creative generation, workflow engine
53
+ - Phase 3: Daemon mode, widget protocol, terminal image display
54
+ - Phase 4: Auto-pager, progress indicators, response cache, configurable themes
55
+ - Phase 4: Connection pooling (httpx HTTP/2), PyPI packaging, CI/CD pipeline
56
+ - Phase 4: Automated test suite, MkDocs documentation site
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elidia-agent-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Elidia Agent CLI — Universal AI Agent for your terminal
5
5
  Author-email: Elidia Technology Pvt Ltd <support@aiutils.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -788,3 +788,43 @@ are now permanently exempt from promotion, proven with a regression test
788
788
  showing 25 consecutive approvals still prompt on the 26th call. See the
789
789
  master plan (`06_CLI_AND_DESKTOP_MASTER_PLAN.md`, §4.4) for full detail
790
790
  and commit references.
791
+
792
+ ## 22. RAG Subsystem — Reconnected (2026-07-27, AIUT-2141)
793
+
794
+ `RagEngine`/`FileIngestPipeline` existed (~1,135 LOC) but were never
795
+ called from any user-facing surface — confirmed via `grep -rn
796
+ "FileIngestPipeline(\|RagEngine(" elidia/` returning zero hits outside
797
+ `elidia/rag/` itself. Wired up per the requested design (all three, not
798
+ one-or-the-other): `elidia rag ingest/search/list/clear` (CLI), `/rag
799
+ ...` (REPL), and auto-ingest for files over ~8,000 chars passed via
800
+ `-f/--file` (preview + index instead of blind truncation, including the
801
+ >1MB case that previously just dropped the file). The read side is a new
802
+ `rag_search` tool (AUTO tier, agent-invocable) — ingestion stays a
803
+ deliberate user action across all three entry points, never
804
+ agent-triggered, so an embedding-cost operation can't fire mid-conversation
805
+ without the user asking for it.
806
+
807
+ Live end-to-end verification surfaced and fixed three further real bugs
808
+ along the way, not just wiring:
809
+
810
+ 1. **FTS5 query builder crashed on hyphenated terms** — `_build_fts_query`
811
+ escaped special characters individually but not `-`, and FTS5's query
812
+ grammar treats an in-word hyphen as syntax (`on-call*` → `no such
813
+ column: call`), not just a tokenizer boundary. Fixed by quoting every
814
+ term (`"on-call"*`) instead of trying to enumerate escapable characters.
815
+ 2. **Chunker never split an oversized paragraph/section/line** — a 21KB
816
+ file with no blank lines produced *one* chunk containing the entire
817
+ file instead of ~40, diluting the embedding enough that a fact in the
818
+ middle became unfindable by search. Fixed for all three content types
819
+ (`text`, `markdown`, `code`) via a shared `_split_oversized` helper.
820
+ 3. **`elidia ask` silently dropped `-f/--file`** — the subcommand never
821
+ declared its own `--file` option and never read the parent group's
822
+ either, so both `elidia ask -f x.txt "..."` and `elidia --file x.txt
823
+ ask "..."` reached the model with nothing attached. Fixed by mirroring
824
+ how `-i/--image` already worked (own option + parent fallback).
825
+
826
+ 34 new tests added across `test_rag_tool.py` and `test_chunker.py`
827
+ (chunker fix verified with a real FTS5 in-memory table, not just string
828
+ inspection). Full live proof, not just unit tests: real `elidia rag
829
+ ingest` → real `elidia ask` with the agent autonomously calling
830
+ `rag_search` → correct grounded answer citing the ingested content.
@@ -0,0 +1,113 @@
1
+ # Tools & MCP
2
+
3
+ ## Built-in Tools
4
+
5
+ Elidia ships with tools in several categories:
6
+
7
+ | Category | Tools | Description |
8
+ |----------|-------|-------------|
9
+ | `filesystem` | read, write, edit, list, glob, grep | File operations |
10
+ | `terminal` | command_exec | Shell command execution |
11
+ | `git` | status, diff, log, commit, branch | Git workflow |
12
+ | `search` / `fetch` | web_search, http_fetch | Web requests and search |
13
+ | `browser` | navigate, click, type, screenshot, extract_links | Headless browser automation (Playwright) |
14
+ | `office` | read/write docx, read/write xlsx, read pptx | Office document parsing |
15
+ | `database` | connect, query, list_tables, describe_table | Read-only SQL |
16
+ | `email` | send, search, read | SMTP send / IMAP search+read |
17
+ | `calendar` | list_events, add_event, find_conflicts | Local .ics calendar |
18
+ | `rag` | rag_search, rag_list_sources | Search content ingested into the local RAG store |
19
+
20
+ View all tools:
21
+
22
+ ```
23
+ > /tools
24
+ > /tools filesystem # Filter by category
25
+ ```
26
+
27
+ ## RAG (Retrieval-Augmented Search)
28
+
29
+ Large files don't need to fit in the conversation — Elidia can index them
30
+ into a local RAG store (sqlite-vec, hybrid BM25 + semantic search) and
31
+ retrieve relevant chunks on demand via the `rag_search` tool. Three ways
32
+ to get content in:
33
+
34
+ ```
35
+ elidia rag ingest <path> # explicit, works on a file or a directory
36
+ > /rag ingest <path> # same, from inside the REPL
37
+ > /rag search <query> # search without waiting for the agent to do it
38
+ elidia ask -f big_file.md "..." # auto-ingest: files over ~8,000 chars are
39
+ # previewed + indexed instead of truncated
40
+ ```
41
+
42
+ `elidia rag list` / `/rag list` shows how much is currently ingested;
43
+ `elidia rag clear` / `/rag clear` empties the store. Ingestion is always
44
+ a deliberate, user-triggered action (CLI command, slash command, or the
45
+ `-f` auto-ingest path) — the agent can search what's already ingested via
46
+ `rag_search`, but never triggers ingestion on its own mid-conversation.
47
+
48
+ ## Permissions
49
+
50
+ Tools require user approval based on risk level:
51
+
52
+ - **Read** — auto-approved by default
53
+ - **Write** — requires approval
54
+ - **Shell** — requires approval
55
+ - **Web** — configurable
56
+
57
+ The trust engine learns from your decisions:
58
+
59
+ ```
60
+ > /trust # Show trust stats
61
+ ```
62
+
63
+ After consistently approving an action, Elidia auto-promotes it (no more prompts).
64
+
65
+ ## MCP Integration
66
+
67
+ Elidia supports [Model Context Protocol](https://modelcontextprotocol.io/) servers for extending tool capabilities.
68
+
69
+ ### Configuration
70
+
71
+ Add MCP servers to `~/.elidia/mcp.json`:
72
+
73
+ ```json
74
+ {
75
+ "servers": {
76
+ "filesystem": {
77
+ "command": "npx",
78
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
79
+ },
80
+ "github": {
81
+ "command": "npx",
82
+ "args": ["-y", "@modelcontextprotocol/server-github"],
83
+ "env": {
84
+ "GITHUB_TOKEN": "ghp_..."
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Managing MCP Servers
92
+
93
+ ```
94
+ > /mcp # List connected servers
95
+ > /mcp disconnect github # Disconnect a server
96
+ ```
97
+
98
+ MCP tools appear alongside built-in tools and are invoked with the same permission system.
99
+
100
+ ## Tool Execution Flow
101
+
102
+ ```mermaid
103
+ graph TD
104
+ A[User Query] --> B{Agent Loop}
105
+ B --> C[LLM decides tool call]
106
+ C --> D{Permission Check}
107
+ D -->|Approved| E[Execute Tool]
108
+ D -->|Denied| F[Report to LLM]
109
+ E --> G[Return Result to LLM]
110
+ G --> B
111
+ F --> B
112
+ B --> H[Final Response]
113
+ ```
@@ -482,6 +482,8 @@ class AgentLoop:
482
482
  "calendar_list_events": "file_read",
483
483
  "calendar_find_conflicts": "file_read",
484
484
  "calendar_add_event": "file_write",
485
+ "rag_search": "rag_search",
486
+ "rag_list_sources": "rag_search",
485
487
  }
486
488
  return action_map.get(tool_name, "mcp_call_session")
487
489
 
@@ -118,5 +118,10 @@ def build_default_commands() -> CommandRegistry:
118
118
  name="rules", description="Show project rules (.elidia/rules.md)",
119
119
  usage="/rules", category="general",
120
120
  ))
121
+ registry.register(SlashCommand(
122
+ name="rag", description="Ingest files or search the local RAG store",
123
+ usage="/rag [ingest <path>|search <query>|list|clear]",
124
+ category="tools",
125
+ ))
121
126
 
122
127
  return registry
@@ -110,9 +110,11 @@ cli.add_command(_default_message)
110
110
  @click.option("--mode", default=None, type=click.Choice(["chat", "code", "research", "think", "create"]))
111
111
  @click.option("--image", "-i", "images", multiple=True, type=click.Path(exists=True),
112
112
  help="Attach an image for vision analysis (repeatable, jpg/png/webp/gif)")
113
+ @click.option("--file", "-f", "files", multiple=True, type=click.Path(exists=True),
114
+ help="Include file content in context (repeatable)")
113
115
  @click.pass_context
114
116
  def ask(ctx: click.Context, message: tuple[str, ...], model: str | None, mode: str | None,
115
- images: tuple[str, ...]) -> None:
117
+ images: tuple[str, ...], files: tuple[str, ...]) -> None:
116
118
  """Send a one-shot message: elidia ask 'your question here'"""
117
119
  logger.debug("Entered into ask")
118
120
  parent = ctx.parent
@@ -120,8 +122,9 @@ def ask(ctx: click.Context, message: tuple[str, ...], model: str | None, mode: s
120
122
  model = model or parent_obj.get("model")
121
123
  mode = mode or parent_obj.get("mode") or "chat"
122
124
  images = images or parent_obj.get("images", ())
125
+ files = files or parent_obj.get("files", ())
123
126
  user_msg = " ".join(message)
124
- asyncio.run(_one_shot(user_msg, model=model, mode=mode, images=images))
127
+ asyncio.run(_one_shot(user_msg, model=model, mode=mode, images=images, files=files))
125
128
 
126
129
 
127
130
  _IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".heic"}
@@ -153,8 +156,47 @@ def _summarize_ics(path: Path) -> str:
153
156
  return "\n".join(lines) if lines else "[No events in this calendar]"
154
157
 
155
158
 
156
- def _build_file_context(files: tuple[str, ...]) -> str:
157
- """Read file contents and build a context prefix for the message."""
159
+ _AUTO_INGEST_THRESHOLD = 8_000 # chars — beyond this, index into RAG + preview instead of blind truncation
160
+
161
+
162
+ async def _auto_ingest_file(path: Path, content: str) -> bool:
163
+ """Best-effort: index a large file into the local RAG store so it's
164
+ retrievable via rag_search instead of being silently truncated.
165
+ Returns False (never raises) if no API key is configured or ingestion
166
+ fails for any reason — callers fall back to plain truncation."""
167
+ logger.debug(f"Entered into _auto_ingest_file: path={path}")
168
+ from elidia.auth.keychain import get_api_key
169
+
170
+ api_key = get_api_key()
171
+ if not api_key:
172
+ return False
173
+
174
+ try:
175
+ import hashlib
176
+
177
+ from elidia.memory.embeddings import EmbeddingClient
178
+ from elidia.rag.engine import RagEngine
179
+
180
+ engine = RagEngine(EmbeddingClient(api_key=api_key))
181
+ engine.open()
182
+ try:
183
+ file_hash = hashlib.sha256(content.encode()).hexdigest()[:16]
184
+ await engine.ingest(text=content, source=str(path.resolve()), file_hash=file_hash)
185
+ finally:
186
+ engine.close()
187
+ return True
188
+ except Exception as e:
189
+ logger.warning(f"Entered into _auto_ingest_file: auto-ingest failed for {path}: {e}")
190
+ return False
191
+
192
+
193
+ async def _build_file_context(files: tuple[str, ...]) -> str:
194
+ """Read file contents and build a context prefix for the message.
195
+
196
+ Files larger than _AUTO_INGEST_THRESHOLD are indexed into the local
197
+ RAG store (auto-ingest) instead of being blindly truncated — the
198
+ message gets a preview plus a pointer to rag_search for the rest.
199
+ """
158
200
  if not files:
159
201
  return ""
160
202
  parts: list[str] = []
@@ -171,7 +213,14 @@ def _build_file_context(files: tuple[str, ...]) -> str:
171
213
  try:
172
214
  size = path.stat().st_size
173
215
  if size > 1_000_000:
174
- parts.append(f"[File too large to include: {path.name} ({size} bytes)]")
216
+ ingested = await _auto_ingest_file(path, path.read_text(encoding="utf-8", errors="replace"))
217
+ if ingested:
218
+ parts.append(
219
+ f"[{path.name} ({size} bytes) is too large to inline but has been indexed "
220
+ f"into the local RAG store — use rag_search to find relevant sections]"
221
+ )
222
+ else:
223
+ parts.append(f"[File too large to include: {path.name} ({size} bytes)]")
175
224
  continue
176
225
  suffix = path.suffix.lower()
177
226
  if suffix in _OFFICE_PARSERS:
@@ -182,6 +231,15 @@ def _build_file_context(files: tuple[str, ...]) -> str:
182
231
  content = _summarize_ics(path)
183
232
  else:
184
233
  content = path.read_text(encoding="utf-8", errors="replace")
234
+
235
+ if len(content) > _AUTO_INGEST_THRESHOLD:
236
+ ingested = await _auto_ingest_file(path, content)
237
+ if ingested:
238
+ preview = content[:_AUTO_INGEST_THRESHOLD] + "\n... (truncated — indexed into RAG, use rag_search for the rest)"
239
+ parts.append(f"--- {path.name} (preview, {len(content)} chars total, indexed into RAG) ---\n{preview}\n--- end {path.name} preview ---")
240
+ total += _AUTO_INGEST_THRESHOLD
241
+ continue
242
+
185
243
  if total + len(content) > max_total:
186
244
  content = content[:max_total - total] + "\n... (truncated)"
187
245
  parts.append(f"--- {path.name} ---\n{content}\n--- end {path.name} ---")
@@ -200,7 +258,7 @@ async def _one_shot(message: str, model: str | None = None, mode: str = "chat",
200
258
  )
201
259
  from elidia.cli.repl import ElidiaRepl
202
260
 
203
- file_ctx = _build_file_context(files)
261
+ file_ctx = await _build_file_context(files)
204
262
  full_message = f"{file_ctx}{message}" if file_ctx else message
205
263
 
206
264
  repl = ElidiaRepl(forced_model=model, mode=mode)
@@ -643,3 +701,135 @@ def workflow_run(path: str) -> None:
643
701
  await client.close()
644
702
 
645
703
  asyncio.run(_run())
704
+
705
+
706
+ # --- RAG subcommands ---
707
+
708
+
709
+ async def _open_rag_engine():
710
+ """Shared setup for the rag CLI subcommands: real API key required
711
+ (embeddings go through the AiUtils API), console error + SystemExit(1)
712
+ if missing so every subcommand fails the same clear way."""
713
+ from elidia.auth.keychain import get_api_key
714
+ from elidia.memory.embeddings import EmbeddingClient
715
+ from elidia.rag.engine import RagEngine
716
+
717
+ api_key = get_api_key()
718
+ if not api_key:
719
+ console.print("[red]No API key configured. Run: elidia auth login[/red]")
720
+ console.print("[dim]RAG ingestion/search needs it for embeddings.[/dim]")
721
+ raise SystemExit(1)
722
+
723
+ engine = RagEngine(EmbeddingClient(api_key=api_key))
724
+ engine.open()
725
+ return engine
726
+
727
+
728
+ @cli.group()
729
+ def rag() -> None:
730
+ """Manage the local RAG (retrieval-augmented search) store."""
731
+
732
+
733
+ @rag.command("ingest")
734
+ @click.argument("path", type=click.Path(exists=True))
735
+ @click.option("--recursive/--no-recursive", default=True, help="Recurse into subdirectories (directories only)")
736
+ def rag_ingest(path: str, recursive: bool) -> None:
737
+ """Ingest a file or directory into the local RAG store for later rag_search."""
738
+ logger.debug(f"Entered into rag_ingest: path={path}, recursive={recursive}")
739
+ from elidia.rag.ingest import FileIngestPipeline
740
+
741
+ target = Path(path)
742
+
743
+ async def _run() -> None:
744
+ engine = await _open_rag_engine()
745
+ pipeline = FileIngestPipeline(engine)
746
+ try:
747
+ if target.is_dir():
748
+ console.print(f"[cyan]Ingesting directory:[/cyan] {target}")
749
+ result = await pipeline.ingest_directory(target, recursive=recursive)
750
+ console.print(
751
+ f"[green]v[/green] Ingested {result['files']} file(s), "
752
+ f"{result['chunks']} chunk(s), skipped {result['skipped']}"
753
+ )
754
+ else:
755
+ console.print(f"[cyan]Ingesting file:[/cyan] {target}")
756
+ ids = await pipeline.ingest_file(target)
757
+ if ids:
758
+ console.print(f"[green]v[/green] Ingested {len(ids)} chunk(s) from {target.name}")
759
+ else:
760
+ console.print(
761
+ f"[yellow]Nothing ingested from {target.name}[/yellow] "
762
+ "(unsupported type, empty, too large, or already ingested — same content hash)"
763
+ )
764
+ finally:
765
+ engine.close()
766
+
767
+ asyncio.run(_run())
768
+
769
+
770
+ @rag.command("search")
771
+ @click.argument("query")
772
+ @click.option("--limit", default=5, type=int, help="Max results")
773
+ def rag_search_cmd(query: str, limit: int) -> None:
774
+ """Search the local RAG store."""
775
+ logger.debug(f"Entered into rag_search_cmd: query={query!r}, limit={limit}")
776
+
777
+ async def _run() -> None:
778
+ engine = await _open_rag_engine()
779
+ try:
780
+ results = await engine.search(query, limit=limit)
781
+ if not results:
782
+ console.print("[yellow]No matching content found.[/yellow]")
783
+ return
784
+ for r in results:
785
+ doc = r.document
786
+ console.print(
787
+ f"\n[cyan]{doc.source}[/cyan] "
788
+ f"[dim](chunk {doc.chunk_index + 1}/{doc.total_chunks}, score={r.score:.3f})[/dim]"
789
+ )
790
+ console.print(doc.content[:500] + ("..." if len(doc.content) > 500 else ""))
791
+ finally:
792
+ engine.close()
793
+
794
+ asyncio.run(_run())
795
+
796
+
797
+ @rag.command("list")
798
+ def rag_list() -> None:
799
+ """Show how much content is currently ingested."""
800
+ logger.debug("Entered into rag_list")
801
+
802
+ async def _run() -> None:
803
+ engine = await _open_rag_engine()
804
+ try:
805
+ counts = engine.count_documents()
806
+ if counts["sources"] == 0:
807
+ console.print("[yellow]Nothing has been ingested yet.[/yellow] Run: elidia rag ingest <path>")
808
+ else:
809
+ console.print(f"{counts['sources']} source(s), {counts['chunks']} chunk(s) ingested.")
810
+ finally:
811
+ engine.close()
812
+
813
+ asyncio.run(_run())
814
+
815
+
816
+ @rag.command("clear")
817
+ @click.option("--source", default=None, help="Only clear entries from this source path (default: clear everything)")
818
+ @click.confirmation_option(prompt="This permanently deletes ingested RAG data. Continue?")
819
+ def rag_clear(source: str | None) -> None:
820
+ """Delete ingested content from the local RAG store."""
821
+ logger.debug(f"Entered into rag_clear: source={source}")
822
+
823
+ async def _run() -> None:
824
+ engine = await _open_rag_engine()
825
+ try:
826
+ if source:
827
+ n = engine.delete_source(source)
828
+ console.print(f"[green]v[/green] Deleted {n} chunk(s) from {source}")
829
+ else:
830
+ n = engine.clear_all()
831
+ console.print(f"[green]v[/green] Deleted {n} chunk(s) — RAG store is now empty")
832
+ finally:
833
+ engine.close()
834
+
835
+ asyncio.run(_run())
@@ -242,8 +242,10 @@ class ElidiaRepl:
242
242
  logger.debug("Entered into cleanup")
243
243
  from elidia.tools.browser import close_browser_session
244
244
  from elidia.tools.database import close_database_session
245
+ from elidia.tools.rag import close_rag_session
245
246
  await close_browser_session()
246
247
  close_database_session()
248
+ close_rag_session()
247
249
  if self._daemon:
248
250
  await self._daemon.stop()
249
251
  if self._mcp_registry:
@@ -386,6 +388,8 @@ class ElidiaRepl:
386
388
  return self._cmd_pager(arg)
387
389
  if cmd == "image":
388
390
  return await self._cmd_image(arg)
391
+ if cmd == "rag":
392
+ return await self._cmd_rag(arg)
389
393
  if cmd in ("quit", "exit"):
390
394
  raise EOFError()
391
395
 
@@ -471,8 +475,10 @@ class ElidiaRepl:
471
475
  def _cmd_new(self) -> bool:
472
476
  from elidia.tools.browser import close_browser_session
473
477
  from elidia.tools.database import close_database_session
478
+ from elidia.tools.rag import close_rag_session
474
479
  asyncio.ensure_future(close_browser_session())
475
480
  close_database_session()
481
+ close_rag_session()
476
482
 
477
483
  # Compact current session before starting a new one
478
484
  if self._compactor and len(self._messages) >= 4:
@@ -934,6 +940,69 @@ class ElidiaRepl:
934
940
 
935
941
  return True
936
942
 
943
+ async def _cmd_rag(self, arg: str) -> bool:
944
+ logger.debug(f"Entered into _cmd_rag: arg={arg}")
945
+ from elidia.tools.rag import _get_session
946
+
947
+ parts = arg.split(maxsplit=1)
948
+ action = parts[0] if parts else ""
949
+ rest = parts[1] if len(parts) > 1 else ""
950
+
951
+ if action == "ingest" and rest:
952
+ from pathlib import Path as _Path
953
+
954
+ from elidia.rag.ingest import FileIngestPipeline
955
+ from elidia.tools.rag import _ensure_engine
956
+
957
+ target = _Path(rest).expanduser()
958
+ if not target.exists():
959
+ self._console.print(f"[red]Path not found: {rest}[/red]")
960
+ return True
961
+ try:
962
+ engine = await _ensure_engine()
963
+ except RuntimeError as e:
964
+ self._console.print(f"[red]{e}[/red]")
965
+ return True
966
+ pipeline = FileIngestPipeline(engine)
967
+ if target.is_dir():
968
+ self._console.print(f"[cyan]Ingesting directory:[/cyan] {target}")
969
+ result = await pipeline.ingest_directory(target)
970
+ self._console.print(
971
+ f"[green]v[/green] Ingested {result['files']} file(s), "
972
+ f"{result['chunks']} chunk(s), skipped {result['skipped']}"
973
+ )
974
+ else:
975
+ ids = await pipeline.ingest_file(target)
976
+ if ids:
977
+ self._console.print(f"[green]v[/green] Ingested {len(ids)} chunk(s) from {target.name}")
978
+ else:
979
+ self._console.print(f"[yellow]Nothing ingested from {target.name}[/yellow]")
980
+ return True
981
+
982
+ if action == "search" and rest:
983
+ from elidia.tools.rag import _rag_search
984
+ result = await _rag_search(rest, limit=5)
985
+ self._console.print(result.content)
986
+ return True
987
+
988
+ if action == "list" or (not action and not arg):
989
+ from elidia.tools.rag import _rag_list_sources
990
+ result = await _rag_list_sources()
991
+ self._console.print(result.content)
992
+ return True
993
+
994
+ if action == "clear":
995
+ session = _get_session()
996
+ if session.engine is not None:
997
+ n = session.engine.clear_all()
998
+ self._console.print(f"[green]v[/green] Deleted {n} chunk(s) — RAG store is now empty")
999
+ else:
1000
+ self._console.print("[dim]RAG store not open this session.[/dim]")
1001
+ return True
1002
+
1003
+ self._console.print("[yellow]Usage: /rag [ingest <path>|search <query>|list|clear][/yellow]")
1004
+ return True
1005
+
937
1006
  async def _cmd_daemon(self, arg: str) -> bool:
938
1007
  logger.debug(f"Entered into _cmd_daemon: arg={arg}")
939
1008
  if not self._daemon:
@@ -30,6 +30,7 @@ ACTION_TIERS: dict[str, PermissionTier] = {
30
30
  "clipboard_read": PermissionTier.AUTO,
31
31
  "mcp_call_auto": PermissionTier.AUTO,
32
32
  "browser_read": PermissionTier.AUTO,
33
+ "rag_search": PermissionTier.AUTO,
33
34
 
34
35
  "file_write_project": PermissionTier.SESSION,
35
36
  "command_exec": PermissionTier.SESSION,