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.
- elidia_agent_cli-0.4.0/CHANGELOG.md +56 -0
- {elidia_agent_cli-0.2.0/elidia_agent_cli.egg-info → elidia_agent_cli-0.4.0}/PKG-INFO +17 -2
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/README.md +16 -1
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/ARCHITECTURE_FLOWCHARTS.md +40 -0
- elidia_agent_cli-0.4.0/docs/guide/tools-mcp.md +113 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/loop.py +3 -1
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/commands.py +5 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/main.py +297 -6
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/repl.py +69 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/defaults.py +1 -1
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/config.py +34 -3
- elidia_agent_cli-0.4.0/elidia/daemon/ipc.py +186 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/manager.py +37 -0
- elidia_agent_cli-0.4.0/elidia/daemon/worker.py +888 -0
- elidia_agent_cli-0.4.0/elidia/models/local.py +94 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/manager.py +12 -6
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/chunker.py +92 -36
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/engine.py +22 -9
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/__init__.py +3 -0
- elidia_agent_cli-0.4.0/elidia/tools/rag.py +147 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0/elidia_agent_cli.egg-info}/PKG-INFO +17 -2
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/SOURCES.txt +2 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/pyproject.toml +1 -1
- elidia_agent_cli-0.2.0/CHANGELOG.md +0 -16
- elidia_agent_cli-0.2.0/docs/guide/tools-mcp.md +0 -86
- elidia_agent_cli-0.2.0/elidia/daemon/ipc.py +0 -97
- elidia_agent_cli-0.2.0/elidia/daemon/worker.py +0 -129
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/LICENSE +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/MANIFEST.in +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/AUDIT_REPORT_v0.1.0.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/api/commands.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/architecture/modules.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/architecture/overview.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/getting-started/configuration.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/getting-started/installation.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/getting-started/quickstart.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/chat-modes.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/creative.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/models.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/research.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/themes.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/guide/workflows.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/docs/index.md +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/__main__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/personas.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/agent/portal.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/api/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/api/client.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/api/streaming.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/auth/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/auth/keychain.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cache/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cache/lru.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/pager.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/progress.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/renderer.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/cli/themes.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/rules.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/config/settings.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/audio.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/display.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/image.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/local.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/creative/video.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/daemon/process.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/db/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/db/database.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/client.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/config.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/registry.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/mcp/types.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/auto.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/compaction.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/embeddings.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/outcomes.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/patterns.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/memory/store.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/models/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/models/adaptive.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/models/router.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/autonomous.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/budget.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/classifier.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/consensus.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/deep_think.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/swarm.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/modes/thinking.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/audit.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/permissions/trust.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/ingest.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/portal_bridge.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/rag/watcher.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/export.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/orchestrator.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/research/sources.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/session/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/session/history.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/session/manager.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/base.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/browser.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/calendar.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/database.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/email.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/fetch.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/filesystem.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/git.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/office.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/search.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/terminal.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/tools/tool_router.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/widgets/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/widgets/protocol.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/widgets/renderer.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/workflow/__init__.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia/workflow/engine.py +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/dependency_links.txt +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/entry_points.txt +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/requires.txt +0 -0
- {elidia_agent_cli-0.2.0 → elidia_agent_cli-0.4.0}/elidia_agent_cli.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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())
|