@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.
@@ -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, format_ai_message, format_thinking_message, format_user_message
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
- output = self.query_one("#output", RichLog)
234
- status = self.query_one("#status_bar", Static)
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
- if bool(meta.get("chat")) and result.status == "ready":
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", ...process.argv.slice(2)], {
28
+ const child = spawn(pythonBin, ["-m", "fincli.app.main", ...args], {
22
29
  cwd: packageRoot,
23
30
  stdio: "inherit"
24
31
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drico2008/fincli",
3
- "version": "0.1.3",
3
+ "version": "0.1.9",
4
4
  "description": "Modern financial CLI/TUI terminal for market monitoring and analysis.",
5
5
  "license": "MIT",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fincli"
7
- version = "0.1.3"
7
+ version = "0.1.9"
8
8
  description = "Modern financial CLI/TUI terminal for market monitoring and analysis."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"