elidia-agent-cli 0.2.0__tar.gz → 0.4.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 (132) hide show
  1. elidia_agent_cli-0.4.0/CHANGELOG.md +56 -0
  2. {elidia_agent_cli-0.2.0/elidia_agent_cli.egg-info → elidia_agent_cli-0.4.0}/PKG-INFO +17 -2
  3. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/README.md +16 -1
  4. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/ARCHITECTURE_FLOWCHARTS.md +40 -0
  5. elidia_agent_cli-0.4.0/docs/guide/tools-mcp.md +113 -0
  6. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/loop.py +3 -1
  7. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/commands.py +5 -0
  8. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/main.py +297 -6
  9. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/repl.py +69 -0
  10. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/defaults.py +1 -1
  11. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/config.py +34 -3
  12. elidia_agent_cli-0.4.0/elidia/daemon/ipc.py +186 -0
  13. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/manager.py +37 -0
  14. elidia_agent_cli-0.4.0/elidia/daemon/worker.py +888 -0
  15. elidia_agent_cli-0.4.0/elidia/models/local.py +94 -0
  16. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/manager.py +12 -6
  17. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/chunker.py +92 -36
  18. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/engine.py +22 -9
  19. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/__init__.py +3 -0
  20. elidia_agent_cli-0.4.0/elidia/tools/rag.py +147 -0
  21. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0/elidia_agent_cli.egg-info}/PKG-INFO +17 -2
  22. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/SOURCES.txt +2 -0
  23. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/pyproject.toml +1 -1
  24. elidia_agent_cli-0.2.0/CHANGELOG.md +0 -16
  25. elidia_agent_cli-0.2.0/docs/guide/tools-mcp.md +0 -86
  26. elidia_agent_cli-0.2.0/elidia/daemon/ipc.py +0 -97
  27. elidia_agent_cli-0.2.0/elidia/daemon/worker.py +0 -129
  28. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/LICENSE +0 -0
  29. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/MANIFEST.in +0 -0
  30. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/AUDIT_REPORT_v0.1.0.md +0 -0
  31. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/api/commands.md +0 -0
  32. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/architecture/modules.md +0 -0
  33. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/architecture/overview.md +0 -0
  34. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/getting-started/configuration.md +0 -0
  35. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/getting-started/installation.md +0 -0
  36. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/getting-started/quickstart.md +0 -0
  37. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/chat-modes.md +0 -0
  38. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/creative.md +0 -0
  39. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/models.md +0 -0
  40. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/research.md +0 -0
  41. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/themes.md +0 -0
  42. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/workflows.md +0 -0
  43. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/index.md +0 -0
  44. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/__init__.py +0 -0
  45. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/__main__.py +0 -0
  46. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/__init__.py +0 -0
  47. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/personas.py +0 -0
  48. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/portal.py +0 -0
  49. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/api/__init__.py +0 -0
  50. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/api/client.py +0 -0
  51. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/api/streaming.py +0 -0
  52. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/auth/__init__.py +0 -0
  53. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/auth/keychain.py +0 -0
  54. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cache/__init__.py +0 -0
  55. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cache/lru.py +0 -0
  56. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/__init__.py +0 -0
  57. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/pager.py +0 -0
  58. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/progress.py +0 -0
  59. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/renderer.py +0 -0
  60. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/themes.py +0 -0
  61. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/__init__.py +0 -0
  62. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/rules.py +0 -0
  63. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/settings.py +0 -0
  64. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/__init__.py +0 -0
  65. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/audio.py +0 -0
  66. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/display.py +0 -0
  67. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/image.py +0 -0
  68. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/local.py +0 -0
  69. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/video.py +0 -0
  70. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/__init__.py +0 -0
  71. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/process.py +0 -0
  72. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/db/__init__.py +0 -0
  73. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/db/database.py +0 -0
  74. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/__init__.py +0 -0
  75. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/client.py +0 -0
  76. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/config.py +0 -0
  77. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/registry.py +0 -0
  78. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/types.py +0 -0
  79. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/__init__.py +0 -0
  80. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/auto.py +0 -0
  81. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/compaction.py +0 -0
  82. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/embeddings.py +0 -0
  83. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/outcomes.py +0 -0
  84. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/patterns.py +0 -0
  85. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/store.py +0 -0
  86. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/models/__init__.py +0 -0
  87. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/models/adaptive.py +0 -0
  88. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/models/router.py +0 -0
  89. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/__init__.py +0 -0
  90. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/autonomous.py +0 -0
  91. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/budget.py +0 -0
  92. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/classifier.py +0 -0
  93. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/consensus.py +0 -0
  94. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/deep_think.py +0 -0
  95. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/swarm.py +0 -0
  96. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/thinking.py +0 -0
  97. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/__init__.py +0 -0
  98. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/audit.py +0 -0
  99. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/trust.py +0 -0
  100. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/__init__.py +0 -0
  101. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/ingest.py +0 -0
  102. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/portal_bridge.py +0 -0
  103. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/watcher.py +0 -0
  104. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/__init__.py +0 -0
  105. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/export.py +0 -0
  106. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/orchestrator.py +0 -0
  107. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/sources.py +0 -0
  108. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/session/__init__.py +0 -0
  109. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/session/history.py +0 -0
  110. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/session/manager.py +0 -0
  111. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/base.py +0 -0
  112. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/browser.py +0 -0
  113. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/calendar.py +0 -0
  114. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/database.py +0 -0
  115. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/email.py +0 -0
  116. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/fetch.py +0 -0
  117. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/filesystem.py +0 -0
  118. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/git.py +0 -0
  119. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/office.py +0 -0
  120. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/search.py +0 -0
  121. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/terminal.py +0 -0
  122. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/tool_router.py +0 -0
  123. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/widgets/__init__.py +0 -0
  124. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/widgets/protocol.py +0 -0
  125. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/widgets/renderer.py +0 -0
  126. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/workflow/__init__.py +0 -0
  127. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/workflow/engine.py +0 -0
  128. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/dependency_links.txt +0 -0
  129. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/entry_points.txt +0 -0
  130. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/requires.txt +0 -0
  131. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/top_level.txt +0 -0
  132. {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.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.4.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
@@ -136,6 +136,20 @@ Powered by the [AiUtils Developer API](https://developer.aiutils.io), Elidia Age
136
136
 
137
137
  ```bash
138
138
  pip install elidia-agent-cli
139
+ elidia auth login # enter your AiUtils API key
140
+ elidia ask "Hello!" # send your first message
141
+ ```
142
+
143
+ **If `elidia` command not found after install**, add pip's bin directory to PATH:
144
+
145
+ ```bash
146
+ # Add to ~/.zshrc (macOS/Linux) to make permanent:
147
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
148
+ source ~/.zshrc
149
+
150
+ # Or for bash:
151
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
152
+ source ~/.bashrc
139
153
  ```
140
154
 
141
155
  ### From GitHub Release
@@ -143,13 +157,14 @@ pip install elidia-agent-cli
143
157
  Download the latest `.whl` file from [Releases](https://github.com/Elidia-Technology/elidia-agent-cli/releases), then:
144
158
 
145
159
  ```bash
146
- pip install elidia_agent_cli-0.1.0-py3-none-any.whl
160
+ pip install elidia_agent_cli-0.3.0-py3-none-any.whl
147
161
  ```
148
162
 
149
163
  ### Requirements
150
164
 
151
165
  - Python 3.11 or higher
152
166
  - An AiUtils Developer API key ([get one here](https://developer.aiutils.io))
167
+ - macOS 12+, Ubuntu 22.04+, or Windows 10+
153
168
 
154
169
  ---
155
170
 
@@ -76,6 +76,20 @@ Powered by the [AiUtils Developer API](https://developer.aiutils.io), Elidia Age
76
76
 
77
77
  ```bash
78
78
  pip install elidia-agent-cli
79
+ elidia auth login # enter your AiUtils API key
80
+ elidia ask "Hello!" # send your first message
81
+ ```
82
+
83
+ **If `elidia` command not found after install**, add pip's bin directory to PATH:
84
+
85
+ ```bash
86
+ # Add to ~/.zshrc (macOS/Linux) to make permanent:
87
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
88
+ source ~/.zshrc
89
+
90
+ # Or for bash:
91
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
92
+ source ~/.bashrc
79
93
  ```
80
94
 
81
95
  ### From GitHub Release
@@ -83,13 +97,14 @@ pip install elidia-agent-cli
83
97
  Download the latest `.whl` file from [Releases](https://github.com/Elidia-Technology/elidia-agent-cli/releases), then:
84
98
 
85
99
  ```bash
86
- pip install elidia_agent_cli-0.1.0-py3-none-any.whl
100
+ pip install elidia_agent_cli-0.3.0-py3-none-any.whl
87
101
  ```
88
102
 
89
103
  ### Requirements
90
104
 
91
105
  - Python 3.11 or higher
92
106
  - An AiUtils Developer API key ([get one here](https://developer.aiutils.io))
107
+ - macOS 12+, Ubuntu 22.04+, or Windows 10+
93
108
 
94
109
  ---
95
110
 
@@ -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
+ ```
@@ -210,7 +210,7 @@ class AgentLoop:
210
210
 
211
211
  if self._permissions:
212
212
  action_type = self._classify_tool_action(tool_name, tool_args)
213
- allowed = self._permissions.check(
213
+ allowed = await self._permissions.check(
214
214
  action=action_type,
215
215
  session_id=session_id,
216
216
  path=tool_args.get("path"),
@@ -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)
@@ -364,6 +422,54 @@ def config_show() -> None:
364
422
  console.print(f"\n[dim]Config dir: {ELIDIA_HOME}[/dim]")
365
423
 
366
424
 
425
+ @config.command("set")
426
+ @click.argument("key")
427
+ @click.argument("value")
428
+ def config_set(key: str, value: str) -> None:
429
+ """Set a configuration value. Examples:
430
+ elidia config set models.default gpt-4o
431
+ elidia config set models.code claude-sonnet-4-6
432
+ elidia config set budget.session_limit_dt 100000
433
+ elidia config set permissions.auto_approve_commands true
434
+ """
435
+ logger.debug(f"Entered into config_set: key={key}, value={value}")
436
+ from elidia.config.settings import ELIDIA_HOME, load_config, save_config
437
+
438
+ cfg = load_config()
439
+ parts = key.split(".")
440
+ if len(parts) != 2:
441
+ console.print("[red]Key must be in format: section.field (e.g., models.default)[/red]")
442
+ return
443
+
444
+ section_name, field_name = parts
445
+ section = getattr(cfg, section_name, None)
446
+ if section is None:
447
+ console.print(f"[red]Unknown section: {section_name}[/red] Available: {[f.name for f in dataclasses.fields(cfg)]}")
448
+ return
449
+
450
+ if not hasattr(section, field_name):
451
+ console.print(f"[red]Unknown field: {field_name}[/red] Available: {[f.name for f in dataclasses.fields(section)]}")
452
+ return
453
+
454
+ # Coerce value to the correct type
455
+ current = getattr(section, field_name)
456
+ if isinstance(current, bool):
457
+ val = value.lower() in ("true", "yes", "1", "on")
458
+ elif isinstance(current, int):
459
+ val = int(value)
460
+ elif isinstance(current, float):
461
+ val = float(value)
462
+ elif isinstance(current, list):
463
+ val = [v.strip() for v in value.split(",")]
464
+ else:
465
+ val = value
466
+
467
+ setattr(section, field_name, val)
468
+ save_config(cfg, ELIDIA_HOME / "config.toml")
469
+ console.print(f"[green]v[/green] {key} = {val}")
470
+ console.print(f"[dim]Saved to {ELIDIA_HOME / 'config.toml'}[/dim]")
471
+
472
+
367
473
  # --- Session subcommands ---
368
474
 
369
475
 
@@ -643,3 +749,188 @@ def workflow_run(path: str) -> None:
643
749
  await client.close()
644
750
 
645
751
  asyncio.run(_run())
752
+
753
+
754
+ # --- Models command ---
755
+
756
+ @cli.command("models")
757
+ @click.option("--local", is_flag=True, help="Show only locally available models (Ollama)")
758
+ def models_cmd(local: bool) -> None:
759
+ """List available AI models."""
760
+ logger.debug(f"Entered into models_cmd: local={local}")
761
+ if local:
762
+ from elidia.models.local import list_local_models
763
+ async def _run():
764
+ models = await list_local_models()
765
+ if not models:
766
+ console.print("[yellow]No local models found. Install Ollama and pull a model.[/yellow]")
767
+ return
768
+ console.print(f"\n[bold]Local Models (Ollama)[/bold]\n")
769
+ for m in models:
770
+ console.print(f" [cyan]{m.name}[/cyan] {m.parameter_size} {m.context_length} ctx caps: {', '.join(m.capabilities[:3])}")
771
+ console.print(f"\n[dim]{len(models)} model(s) available locally[/dim]")
772
+ return asyncio.run(_run())
773
+
774
+ from elidia.auth.keychain import get_api_key
775
+ from elidia.config.settings import load_config
776
+ from elidia.api.client import AiUtilsClient
777
+ api_key = get_api_key()
778
+ if not api_key:
779
+ console.print("[red]No API key configured. Run: elidia auth login[/red]")
780
+ return
781
+ config = load_config()
782
+
783
+ async def _run_remote():
784
+ client = AiUtilsClient(api_key=api_key, base_url=config.api.base_url)
785
+ try:
786
+ models = await client.list_models()
787
+ if not models:
788
+ console.print("[yellow]No models returned from API.[/yellow]")
789
+ return
790
+ console.print(f"\n[bold]Available Models ({len(models)} total)[/bold]\n")
791
+ for m in models:
792
+ name = m.get("id", m.get("name", "?"))
793
+ provider = m.get("owned_by", m.get("provider", "?"))
794
+ cost = m.get("pricing", {}).get("input", "?")
795
+ caps = []
796
+ if m.get("supports_vision"): caps.append("vision")
797
+ if m.get("supports_reasoning"): caps.append("reasoning")
798
+ if m.get("supports_tools"): caps.append("tools")
799
+ cap_str = f"[dim]({', '.join(caps)})[/dim]" if caps else ""
800
+ console.print(f" [cyan]{name}[/cyan] @ [dim]{provider}[/dim] {cap_str}")
801
+ console.print(f"\n[dim]Use: elidia --model <name> ask '...' | /model <name> in REPL[/dim]")
802
+ finally:
803
+ await client.close()
804
+ asyncio.run(_run_remote())
805
+
806
+
807
+ # --- RAG subcommands ---
808
+
809
+
810
+ async def _open_rag_engine():
811
+ """Shared setup for the rag CLI subcommands: real API key required
812
+ (embeddings go through the AiUtils API), console error + SystemExit(1)
813
+ if missing so every subcommand fails the same clear way."""
814
+ from elidia.auth.keychain import get_api_key
815
+ from elidia.memory.embeddings import EmbeddingClient
816
+ from elidia.rag.engine import RagEngine
817
+
818
+ api_key = get_api_key()
819
+ if not api_key:
820
+ console.print("[red]No API key configured. Run: elidia auth login[/red]")
821
+ console.print("[dim]RAG ingestion/search needs it for embeddings.[/dim]")
822
+ raise SystemExit(1)
823
+
824
+ engine = RagEngine(EmbeddingClient(api_key=api_key))
825
+ engine.open()
826
+ return engine
827
+
828
+
829
+ @cli.group()
830
+ def rag() -> None:
831
+ """Manage the local RAG (retrieval-augmented search) store."""
832
+
833
+
834
+ @rag.command("ingest")
835
+ @click.argument("path", type=click.Path(exists=True))
836
+ @click.option("--recursive/--no-recursive", default=True, help="Recurse into subdirectories (directories only)")
837
+ def rag_ingest(path: str, recursive: bool) -> None:
838
+ """Ingest a file or directory into the local RAG store for later rag_search."""
839
+ logger.debug(f"Entered into rag_ingest: path={path}, recursive={recursive}")
840
+ from elidia.rag.ingest import FileIngestPipeline
841
+
842
+ target = Path(path)
843
+
844
+ async def _run() -> None:
845
+ engine = await _open_rag_engine()
846
+ pipeline = FileIngestPipeline(engine)
847
+ try:
848
+ if target.is_dir():
849
+ console.print(f"[cyan]Ingesting directory:[/cyan] {target}")
850
+ result = await pipeline.ingest_directory(target, recursive=recursive)
851
+ console.print(
852
+ f"[green]v[/green] Ingested {result['files']} file(s), "
853
+ f"{result['chunks']} chunk(s), skipped {result['skipped']}"
854
+ )
855
+ else:
856
+ console.print(f"[cyan]Ingesting file:[/cyan] {target}")
857
+ ids = await pipeline.ingest_file(target)
858
+ if ids:
859
+ console.print(f"[green]v[/green] Ingested {len(ids)} chunk(s) from {target.name}")
860
+ else:
861
+ console.print(
862
+ f"[yellow]Nothing ingested from {target.name}[/yellow] "
863
+ "(unsupported type, empty, too large, or already ingested — same content hash)"
864
+ )
865
+ finally:
866
+ engine.close()
867
+
868
+ asyncio.run(_run())
869
+
870
+
871
+ @rag.command("search")
872
+ @click.argument("query")
873
+ @click.option("--limit", default=5, type=int, help="Max results")
874
+ def rag_search_cmd(query: str, limit: int) -> None:
875
+ """Search the local RAG store."""
876
+ logger.debug(f"Entered into rag_search_cmd: query={query!r}, limit={limit}")
877
+
878
+ async def _run() -> None:
879
+ engine = await _open_rag_engine()
880
+ try:
881
+ results = await engine.search(query, limit=limit)
882
+ if not results:
883
+ console.print("[yellow]No matching content found.[/yellow]")
884
+ return
885
+ for r in results:
886
+ doc = r.document
887
+ console.print(
888
+ f"\n[cyan]{doc.source}[/cyan] "
889
+ f"[dim](chunk {doc.chunk_index + 1}/{doc.total_chunks}, score={r.score:.3f})[/dim]"
890
+ )
891
+ console.print(doc.content[:500] + ("..." if len(doc.content) > 500 else ""))
892
+ finally:
893
+ engine.close()
894
+
895
+ asyncio.run(_run())
896
+
897
+
898
+ @rag.command("list")
899
+ def rag_list() -> None:
900
+ """Show how much content is currently ingested."""
901
+ logger.debug("Entered into rag_list")
902
+
903
+ async def _run() -> None:
904
+ engine = await _open_rag_engine()
905
+ try:
906
+ counts = engine.count_documents()
907
+ if counts["sources"] == 0:
908
+ console.print("[yellow]Nothing has been ingested yet.[/yellow] Run: elidia rag ingest <path>")
909
+ else:
910
+ console.print(f"{counts['sources']} source(s), {counts['chunks']} chunk(s) ingested.")
911
+ finally:
912
+ engine.close()
913
+
914
+ asyncio.run(_run())
915
+
916
+
917
+ @rag.command("clear")
918
+ @click.option("--source", default=None, help="Only clear entries from this source path (default: clear everything)")
919
+ @click.confirmation_option(prompt="This permanently deletes ingested RAG data. Continue?")
920
+ def rag_clear(source: str | None) -> None:
921
+ """Delete ingested content from the local RAG store."""
922
+ logger.debug(f"Entered into rag_clear: source={source}")
923
+
924
+ async def _run() -> None:
925
+ engine = await _open_rag_engine()
926
+ try:
927
+ if source:
928
+ n = engine.delete_source(source)
929
+ console.print(f"[green]v[/green] Deleted {n} chunk(s) from {source}")
930
+ else:
931
+ n = engine.clear_all()
932
+ console.print(f"[green]v[/green] Deleted {n} chunk(s) — RAG store is now empty")
933
+ finally:
934
+ engine.close()
935
+
936
+ asyncio.run(_run())