@drico2008/fincli 0.1.3 → 0.1.9
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.
- package/README.md +41 -7
- package/fincli/__init__.py +1 -1
- package/fincli/app/analysis/assistant_context.py +27 -1
- package/fincli/app/analysis/indicators.py +1 -1
- package/fincli/app/analysis/market_structure.py +1 -1
- package/fincli/app/cli/commands.py +10 -4
- package/fincli/app/cli/router.py +211 -18
- package/fincli/app/modules/session_history.py +113 -0
- package/fincli/app/providers/ai/anthropic_provider.py +8 -7
- package/fincli/app/providers/ai/gemini_provider.py +8 -7
- package/fincli/app/providers/ai/groq_provider.py +8 -7
- package/fincli/app/providers/ai/huggingface_provider.py +8 -7
- package/fincli/app/providers/ai/openai_provider.py +8 -7
- package/fincli/app/providers/ai/openrouter_provider.py +8 -7
- package/fincli/app/providers/ai/together_provider.py +8 -7
- package/fincli/app/providers/market/manager.py +1 -1
- package/fincli/app/providers/market/news_provider.py +4 -4
- package/fincli/app/providers/market/yfinance_provider.py +1 -1
- package/fincli/app/services/web_research.py +267 -0
- package/fincli/app/storage/cache.py +2 -2
- package/fincli/app/storage/database.py +17 -0
- package/fincli/app/storage/secrets.py +5 -2
- package/fincli/app/tui/components.py +1 -1
- package/fincli/app/tui/layout.py +8 -7
- package/fincli/app/tui/market_provider_selector.py +3 -0
- package/fincli/app/tui/model_selector.py +11 -3
- package/fincli/app/utils/formatting.py +50 -0
- package/npm/bin/fincli.js +9 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/fincli/app/tui/layout.py
CHANGED
|
@@ -7,6 +7,7 @@ from threading import Lock
|
|
|
7
7
|
|
|
8
8
|
from textual.app import App, ComposeResult, SystemCommand
|
|
9
9
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
10
|
+
from textual.css.query import NoMatches
|
|
10
11
|
from textual.screen import Screen
|
|
11
12
|
from textual.worker import Worker, WorkerState
|
|
12
13
|
from textual.widgets import Header, Input, RichLog, Static
|
|
@@ -16,7 +17,7 @@ from fincli.app.cli.autocomplete import SlashAutocomplete
|
|
|
16
17
|
from fincli.app.cli.commands import CommandRegistry
|
|
17
18
|
from fincli.app.cli.router import CommandResult, CommandRouter
|
|
18
19
|
from fincli.app.providers.ai.manager import AIProviderManager
|
|
19
|
-
from fincli.app.tui.components import CommandPalette,
|
|
20
|
+
from fincli.app.tui.components import CommandPalette, format_thinking_message, format_user_message
|
|
20
21
|
from fincli.app.tui.market_provider_selector import MarketProviderSelectorScreen
|
|
21
22
|
from fincli.app.tui.model_selector import AIModelSelectorScreen
|
|
22
23
|
from fincli.app.tui.theme import APP_CSS
|
|
@@ -230,8 +231,11 @@ class FinCLIApp(App[None]):
|
|
|
230
231
|
sequence = int(str(meta.get("sequence", "0")))
|
|
231
232
|
if sequence < self._latest_worker_sequence:
|
|
232
233
|
return
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
try:
|
|
235
|
+
output = self.query_one("#output", RichLog)
|
|
236
|
+
status = self.query_one("#status_bar", Static)
|
|
237
|
+
except NoMatches:
|
|
238
|
+
return
|
|
235
239
|
display_raw = str(meta["display_raw"])
|
|
236
240
|
|
|
237
241
|
if event.state == WorkerState.CANCELLED:
|
|
@@ -248,10 +252,7 @@ class FinCLIApp(App[None]):
|
|
|
248
252
|
if result.clear:
|
|
249
253
|
output.clear()
|
|
250
254
|
elif result.renderable:
|
|
251
|
-
|
|
252
|
-
output.write(format_ai_message(str(result.renderable)))
|
|
253
|
-
else:
|
|
254
|
-
output.write(result.renderable)
|
|
255
|
+
output.write(result.renderable)
|
|
255
256
|
|
|
256
257
|
if bool(meta.get("chat")):
|
|
257
258
|
status.update(f"{result.status} | ai chat")
|
|
@@ -291,6 +291,9 @@ class MarketProviderSelectorScreen(ModalScreen[tuple[str, ...] | None]):
|
|
|
291
291
|
if self.pending_env_keys:
|
|
292
292
|
self._set_mode("key")
|
|
293
293
|
else:
|
|
294
|
+
providers = recommended_provider_priority(self.selected_provider)
|
|
295
|
+
self.config.set_market_provider_priority(list(providers))
|
|
296
|
+
self.on_selected(providers)
|
|
294
297
|
self._set_mode("priority")
|
|
295
298
|
|
|
296
299
|
|
|
@@ -425,6 +425,9 @@ class AIModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
|
425
425
|
if choice is None or not value.strip():
|
|
426
426
|
return
|
|
427
427
|
save_secret(choice.env_key, value)
|
|
428
|
+
model = self.config.settings.ai_model if self.config.settings.ai_provider == choice.provider else _default_model(choice.provider)
|
|
429
|
+
self.config.set_ai_model(choice.provider, model)
|
|
430
|
+
self.on_selected(choice.provider, model)
|
|
428
431
|
self._set_mode("model")
|
|
429
432
|
|
|
430
433
|
|
|
@@ -438,9 +441,14 @@ def _has_key(provider: str) -> bool:
|
|
|
438
441
|
return bool(choice and os.getenv(choice.env_key))
|
|
439
442
|
|
|
440
443
|
|
|
441
|
-
def _masked_key(provider: str) -> str:
|
|
444
|
+
def _masked_key(provider: str) -> str:
|
|
442
445
|
choice = _provider_choice(provider)
|
|
443
446
|
if choice is None:
|
|
444
447
|
return "not configured"
|
|
445
|
-
masked = mask_secret(os.getenv(choice.env_key))
|
|
446
|
-
return "not configured" if masked == "not set" else f"configured {masked}"
|
|
448
|
+
masked = mask_secret(os.getenv(choice.env_key))
|
|
449
|
+
return "not configured" if masked == "not set" else f"configured {masked}"
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _default_model(provider: str) -> str:
|
|
453
|
+
info = AIProviderManager().get(provider)
|
|
454
|
+
return info.default_model if info else provider
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
6
|
+
from rich.markdown import Markdown
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
|
|
9
|
+
from fincli.app.providers.ai.base import AIResponse
|
|
10
|
+
|
|
5
11
|
|
|
6
12
|
def mask_secret(value: str | None) -> str:
|
|
7
13
|
"""Mask API keys and tokens before displaying them."""
|
|
@@ -15,3 +21,47 @@ def mask_secret(value: str | None) -> str:
|
|
|
15
21
|
def normalize_symbol(symbol: str) -> str:
|
|
16
22
|
"""Normalize user-entered market symbols."""
|
|
17
23
|
return symbol.strip().upper()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AIResponseView:
|
|
27
|
+
"""Renderable AI response that preserves Markdown formatting in Rich/Textual."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, response: AIResponse) -> None:
|
|
30
|
+
self.response = response
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return (
|
|
34
|
+
f"Provider: {self.response.provider}\n"
|
|
35
|
+
f"Model: {self.response.model}\n"
|
|
36
|
+
f"Response:\n{self.response.content}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
40
|
+
header = Text()
|
|
41
|
+
header.append("Provider: ", style="bold cyan")
|
|
42
|
+
header.append(self.response.provider, style="white")
|
|
43
|
+
header.append(" Model: ", style="bold cyan")
|
|
44
|
+
header.append(self.response.model, style="white")
|
|
45
|
+
yield header
|
|
46
|
+
yield Markdown(self.response.content)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MarkdownBlock:
|
|
50
|
+
"""Small renderable block for titled Markdown content."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, title: str, body: object, footer: str | None = None) -> None:
|
|
53
|
+
self.title = title
|
|
54
|
+
self.body = body
|
|
55
|
+
self.footer = footer
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
parts = [self.title, str(self.body)]
|
|
59
|
+
if self.footer:
|
|
60
|
+
parts.append(self.footer)
|
|
61
|
+
return "\n".join(parts)
|
|
62
|
+
|
|
63
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
64
|
+
yield Text(self.title, style="bold cyan")
|
|
65
|
+
yield self.body
|
|
66
|
+
if self.footer:
|
|
67
|
+
yield Text(self.footer, style="dim")
|
package/npm/bin/fincli.js
CHANGED
|
@@ -5,20 +5,27 @@ const path = require("path");
|
|
|
5
5
|
const { spawn } = require("child_process");
|
|
6
6
|
|
|
7
7
|
const packageRoot = path.resolve(__dirname, "..", "..");
|
|
8
|
+
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
8
9
|
const venvDir = path.join(packageRoot, ".npm-python");
|
|
9
10
|
const pythonBin = process.platform === "win32"
|
|
10
11
|
? path.join(venvDir, "Scripts", "python.exe")
|
|
11
12
|
: path.join(venvDir, "bin", "python");
|
|
12
13
|
|
|
13
14
|
function run() {
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
17
|
+
console.log(packageJson.version);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
if (!fs.existsSync(pythonBin)) {
|
|
15
22
|
console.error("FinCLI Python runtime is missing.");
|
|
16
|
-
console.error("Try reinstalling with: npm install -g fincli");
|
|
23
|
+
console.error("Try reinstalling with: npm install -g @drico2008/fincli");
|
|
17
24
|
console.error("Python 3.11+ must be available during npm install.");
|
|
18
25
|
process.exit(1);
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
const child = spawn(pythonBin, ["-m", "fincli.app.main", ...
|
|
28
|
+
const child = spawn(pythonBin, ["-m", "fincli.app.main", ...args], {
|
|
22
29
|
cwd: packageRoot,
|
|
23
30
|
stdio: "inherit"
|
|
24
31
|
});
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED