grok-assistant 1.0__py3-none-any.whl

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 (92) hide show
  1. grok_assistant/__init__.py +4 -0
  2. grok_assistant/__main__.py +110 -0
  3. grok_assistant/_bundle/LICENSE.md +39 -0
  4. grok_assistant/_bundle/docs/img/grok-mark.png +0 -0
  5. grok_assistant/_bundle/docs/img/grok.ico +0 -0
  6. grok_assistant/_bundle/listeners/dictation.ps1 +80 -0
  7. grok_assistant/_bundle/listeners/install_speech.ps1 +19 -0
  8. grok_assistant/_bundle/scripts/build_exe.ps1 +21 -0
  9. grok_assistant/_bundle/scripts/speak.ps1 +14 -0
  10. grok_assistant/_bundle/scripts/voices.ps1 +6 -0
  11. grok_assistant/build_stamp.txt +2 -0
  12. grok_assistant/buildinfo.py +69 -0
  13. grok_assistant/cloud/__init__.py +1 -0
  14. grok_assistant/cloud/account_agents.py +92 -0
  15. grok_assistant/cloud/account_usage.py +49 -0
  16. grok_assistant/cloud/grok_cli.py +166 -0
  17. grok_assistant/cloud/prompts.py +61 -0
  18. grok_assistant/cloud/setup_grok.py +167 -0
  19. grok_assistant/console.py +26 -0
  20. grok_assistant/house/__init__.py +1 -0
  21. grok_assistant/house/helptext.py +155 -0
  22. grok_assistant/house/marketplace.py +531 -0
  23. grok_assistant/house/music.py +166 -0
  24. grok_assistant/house/personality.py +430 -0
  25. grok_assistant/house/updates.py +165 -0
  26. grok_assistant/i18n.py +172 -0
  27. grok_assistant/lang/de.json +393 -0
  28. grok_assistant/lang/en.json +393 -0
  29. grok_assistant/lang/es.json +366 -0
  30. grok_assistant/lang/fr.json +393 -0
  31. grok_assistant/lines/banter.json +638 -0
  32. grok_assistant/lines/hellos-es.txt +188 -0
  33. grok_assistant/lines/waits-es.txt +170 -0
  34. grok_assistant/listening/__init__.py +1 -0
  35. grok_assistant/listening/ear_score.py +87 -0
  36. grok_assistant/listening/enroll_audio.py +259 -0
  37. grok_assistant/listening/kroko_ear.py +324 -0
  38. grok_assistant/listening/listen.py +217 -0
  39. grok_assistant/listening/offline_ear.py +243 -0
  40. grok_assistant/listening/refine.py +131 -0
  41. grok_assistant/listening/voiceprint.py +75 -0
  42. grok_assistant/mind/__init__.py +1 -0
  43. grok_assistant/mind/local_llm.py +152 -0
  44. grok_assistant/notebook/__init__.py +1 -0
  45. grok_assistant/notebook/auth.py +48 -0
  46. grok_assistant/notebook/settings.py +100 -0
  47. grok_assistant/notebook/store.py +669 -0
  48. grok_assistant/paths.py +81 -0
  49. grok_assistant/quiet.py +15 -0
  50. grok_assistant/rules/__init__.py +1 -0
  51. grok_assistant/rules/brain.py +72 -0
  52. grok_assistant/rules/common.py +100 -0
  53. grok_assistant/rules/hub.py +338 -0
  54. grok_assistant/rules/match.py +523 -0
  55. grok_assistant/rules/orders.py +235 -0
  56. grok_assistant/rules/prints.py +173 -0
  57. grok_assistant/rules/status.py +159 -0
  58. grok_assistant/rules/talk.py +451 -0
  59. grok_assistant/rules/trace.py +78 -0
  60. grok_assistant/speaking/__init__.py +1 -0
  61. grok_assistant/speaking/banter.py +81 -0
  62. grok_assistant/speaking/speech.py +222 -0
  63. grok_assistant/startup.py +50 -0
  64. grok_assistant/textutil.py +55 -0
  65. grok_assistant/ui/__init__.py +1 -0
  66. grok_assistant/ui/actions.py +624 -0
  67. grok_assistant/ui/app.py +36 -0
  68. grok_assistant/ui/chrome.py +59 -0
  69. grok_assistant/ui/deps.py +39 -0
  70. grok_assistant/ui/ears.py +156 -0
  71. grok_assistant/ui/flow.py +187 -0
  72. grok_assistant/ui/market.py +413 -0
  73. grok_assistant/ui/menus.py +649 -0
  74. grok_assistant/ui/prints_ui.py +379 -0
  75. grok_assistant/ui/round.py +244 -0
  76. grok_assistant/ui/theme.py +164 -0
  77. grok_assistant/ui/themes/aurora.json +30 -0
  78. grok_assistant/ui/themes/cobre.json +30 -0
  79. grok_assistant/ui/themes/dia.json +30 -0
  80. grok_assistant/ui/themes/lino.json +30 -0
  81. grok_assistant/ui/themes/mar.json +30 -0
  82. grok_assistant/ui/themes/noche.json +30 -0
  83. grok_assistant/ui/themes/oliva.json +30 -0
  84. grok_assistant/ui/win_tray.py +649 -0
  85. grok_assistant/ui/window.py +538 -0
  86. grok_assistant/ui/worker.py +253 -0
  87. grok_assistant-1.0.dist-info/METADATA +58 -0
  88. grok_assistant-1.0.dist-info/RECORD +92 -0
  89. grok_assistant-1.0.dist-info/WHEEL +5 -0
  90. grok_assistant-1.0.dist-info/entry_points.txt +2 -0
  91. grok_assistant-1.0.dist-info/licenses/LICENSE.md +39 -0
  92. grok_assistant-1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ """A voice assistant that listens at home and bothers the cloud on purpose."""
2
+
3
+ __version__ = "1.0"
4
+ __release__ = "v1.0-rc.2"
@@ -0,0 +1,110 @@
1
+ """Grok Assistant, the PC body of the experiment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import faulthandler
6
+ import sys
7
+ import tempfile
8
+ import traceback
9
+ from pathlib import Path
10
+
11
+ from grok_assistant.paths import bundle_root, default_data_dir, load_lines
12
+
13
+ _FAULT_LOG = None
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> None:
17
+ try:
18
+ folder = default_data_dir()
19
+ folder.mkdir(parents=True, exist_ok=True)
20
+ crash = folder / "crash.log"
21
+ # The file has to stay referenced or a later native fault has nowhere to land.
22
+ global _FAULT_LOG
23
+ _FAULT_LOG = crash.open("a", encoding="utf-8")
24
+ faulthandler.enable(_FAULT_LOG, all_threads=True)
25
+ except OSError:
26
+ pass
27
+ try:
28
+ _main(argv)
29
+ except Exception:
30
+ path = _crash_log()
31
+ _tell_crash(path)
32
+ raise SystemExit(1)
33
+
34
+
35
+ def _main(argv: list[str] | None) -> None:
36
+ args = list(sys.argv[1:] if argv is None else argv)
37
+ if "--check" in args:
38
+ _write_check()
39
+ return
40
+ if "--console" in args:
41
+ from grok_assistant.console import run
42
+ run()
43
+ return
44
+ from grok_assistant.cloud.setup_grok import ensure_grok
45
+ from grok_assistant.ui.app import run
46
+
47
+ ensure_grok()
48
+ run()
49
+
50
+
51
+ def _crash_log() -> Path:
52
+ folder = default_data_dir()
53
+ folder.mkdir(parents=True, exist_ok=True)
54
+ path = folder / "crash.log"
55
+ with path.open("a", encoding="utf-8") as handle:
56
+ handle.write(traceback.format_exc())
57
+ handle.write("\n")
58
+ return path
59
+
60
+
61
+ def _tell_crash(path: Path) -> None:
62
+ try:
63
+ import tkinter as tk
64
+ from tkinter import messagebox
65
+
66
+ root = tk.Tk()
67
+ root.withdraw()
68
+ messagebox.showerror("Grok Assistant", f"Se cerró por un error.\n{path}")
69
+ root.destroy()
70
+ except Exception:
71
+ return
72
+
73
+
74
+ def _importable(name: str) -> bool:
75
+ try:
76
+ __import__(name)
77
+ except Exception:
78
+ return False
79
+ return True
80
+
81
+
82
+ def _kroko_ready() -> bool:
83
+ from grok_assistant.listening.kroko_ear import kroko_dir
84
+
85
+ return kroko_dir() is not None
86
+
87
+
88
+ def _write_check() -> None:
89
+ from grok_assistant import __version__
90
+ from grok_assistant.cloud.grok_cli import GrokCLI
91
+ from grok_assistant.cloud.setup_grok import probe
92
+
93
+ status = probe()
94
+ lines = [
95
+ f"version={__version__}",
96
+ "channel=release-candidate",
97
+ f"hellos={len(load_lines('hellos-es.txt'))}",
98
+ f"scripts={(bundle_root() / 'scripts' / 'speak.ps1').exists()}",
99
+ f"listeners={(bundle_root() / 'listeners' / 'dictation.ps1').exists()}",
100
+ f"grok={GrokCLI.find() or ''}",
101
+ f"state={status.state}",
102
+ f"kroko_model={_kroko_ready()}",
103
+ f"sherpa={_importable('sherpa_onnx')}",
104
+ f"mic={_importable('sounddevice')}",
105
+ ]
106
+ Path(tempfile.gettempdir(), "GrokAssistant-check.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
@@ -0,0 +1,39 @@
1
+ # Licencia y límites — Grok Assistant
2
+
3
+ Versión candidata 1.0. Aplicación experimental.
4
+
5
+ Autor: Antonio Castellon, Castellon.CH.
6
+
7
+ ## Qué es
8
+
9
+ Grok Assistant es un programa experimental para hablar con un asistente en este equipo. El audio se queda aquí. Cuando hace falta, el programa envía texto a Grok. Grok y sus herramientas son un servicio de un tercero, no de este programa.
10
+
11
+ ## Responsabilidad
12
+
13
+ Antonio Castellon no se hace responsable de cualquier fallo de seguridad o brecha que pueda darse por el uso de Grok y de sus herramientas. Eso incluye el acceso a la cuenta, lo que Grok busque o escriba, un archivo que cambie cuando esa opción esté permitida, y cualquier daño o pérdida que venga de ese uso.
14
+
15
+ El programa se ofrece tal cual, sin garantía. Quien lo ejecuta lo hace por su cuenta.
16
+
17
+ Si un administrador permite que Grok cambie archivos, esa elección se hace en este equipo. La shell sigue apagada. Aun así, el autor no responde de lo que Grok haga con las herramientas que se hayan dejado activas.
18
+
19
+ ## Texto que manda
20
+
21
+ Este archivo está en español. La traducción al inglés es una ayuda para leerlo. Si los dos textos no dicen lo mismo, manda el español.
22
+
23
+ ---
24
+
25
+ # License and limits — Grok Assistant
26
+
27
+ Release candidate 1.0. Experimental application.
28
+
29
+ Author: Antonio Castellon, Castellon.CH.
30
+
31
+ Grok Assistant is an experimental program for talking with an assistant on this computer. The audio stays here. When needed, the program sends text to Grok. Grok and its tools are a third-party service, not part of this program.
32
+
33
+ Antonio Castellon is not responsible for any security failure or breach that may come from using Grok and its tools. That includes access to the account, what Grok looks up or writes, a file it changes when that option is allowed, and any damage or loss that comes from that use.
34
+
35
+ The program is offered as it is, without warranty. Anyone who runs it does so on their own account.
36
+
37
+ If an administrator allows Grok to change files, that choice is made on this computer. The shell stays off. The author is still not answerable for what Grok does with the tools that were left on.
38
+
39
+ If the Spanish text and this English text differ, the Spanish text prevails.
Binary file
@@ -0,0 +1,80 @@
1
+ param(
2
+ [string]$Culture = "es-ES",
3
+ [string]$PauseFile = "",
4
+ [string]$ScoreDir = "",
5
+ [switch]$Probe
6
+ )
7
+ $ErrorActionPreference = "Stop"
8
+ Add-Type -AssemblyName System.Speech
9
+ # The string constructor wants a recognizer id such as MS-3082-80-DESK, not a culture name.
10
+ try {
11
+ $cultureInfo = [System.Globalization.CultureInfo]::new($Culture)
12
+ $engine = New-Object System.Speech.Recognition.SpeechRecognitionEngine($cultureInfo)
13
+ } catch {
14
+ $info = [System.Speech.Recognition.SpeechRecognitionEngine]::InstalledRecognizers() |
15
+ Where-Object { $_.Culture.Name -eq $Culture } |
16
+ Select-Object -First 1
17
+ if (-not $info) {
18
+ [Console]::Out.WriteLine("ERR:no-recognizer")
19
+ [Console]::Out.Flush()
20
+ exit 2
21
+ }
22
+ try {
23
+ $engine = New-Object System.Speech.Recognition.SpeechRecognitionEngine($info)
24
+ } catch {
25
+ [Console]::Out.WriteLine("ERR:no-recognizer")
26
+ [Console]::Out.Flush()
27
+ exit 2
28
+ }
29
+ }
30
+ if ($Probe) {
31
+ [Console]::Out.WriteLine("READY")
32
+ [Console]::Out.Flush()
33
+ exit 0
34
+ }
35
+ if ($ScoreDir) {
36
+ try {
37
+ $engine.LoadGrammar((New-Object System.Speech.Recognition.DictationGrammar))
38
+ } catch {
39
+ [Console]::Out.WriteLine("ERR:no-recognizer")
40
+ [Console]::Out.Flush()
41
+ exit 2
42
+ }
43
+ Get-ChildItem -LiteralPath $ScoreDir -Filter *.wav | Sort-Object Name | ForEach-Object {
44
+ $text = ""
45
+ try {
46
+ $engine.SetInputToWaveFile($_.FullName)
47
+ $result = $engine.Recognize()
48
+ if ($result -and $result.Text) { $text = $result.Text }
49
+ } catch {
50
+ $text = ""
51
+ }
52
+ [Console]::Out.WriteLine("LINE:" + $_.Name + "|" + $text)
53
+ [Console]::Out.Flush()
54
+ }
55
+ exit 0
56
+ }
57
+ try {
58
+ $engine.LoadGrammar((New-Object System.Speech.Recognition.DictationGrammar))
59
+ $engine.SetInputToDefaultAudioDevice()
60
+ } catch {
61
+ [Console]::Out.WriteLine("ERR:no-microphone")
62
+ [Console]::Out.Flush()
63
+ exit 3
64
+ }
65
+ $engine.EndSilenceTimeout = [TimeSpan]::FromMilliseconds(700)
66
+ $engine.EndSilenceTimeoutAmbiguous = [TimeSpan]::FromMilliseconds(700)
67
+ $engine.InitialSilenceTimeout = [TimeSpan]::FromSeconds(2)
68
+ [Console]::Out.WriteLine("READY")
69
+ [Console]::Out.Flush()
70
+ while ($true) {
71
+ if ($PauseFile -and (Test-Path -LiteralPath $PauseFile)) {
72
+ Start-Sleep -Milliseconds 200
73
+ continue
74
+ }
75
+ $result = $engine.Recognize()
76
+ if ($result -and $result.Text) {
77
+ [Console]::Out.WriteLine("LINE:" + $result.Text)
78
+ [Console]::Out.Flush()
79
+ }
80
+ }
@@ -0,0 +1,19 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [string]$Log
4
+ )
5
+ $ErrorActionPreference = "Stop"
6
+ try {
7
+ $name = "Language.Speech~~~es-ES~0.0.1.0"
8
+ $cap = Get-WindowsCapability -Online | Where-Object { $_.Name -eq $name } | Select-Object -First 1
9
+ if (-not $cap) {
10
+ throw "Windows no ofrece el idioma de voz es-ES en este equipo."
11
+ }
12
+ if ($cap.State -ne "Installed") {
13
+ Add-WindowsCapability -Online -Name $name | Out-Null
14
+ }
15
+ Set-Content -LiteralPath $Log -Value "OK" -Encoding utf8
16
+ } catch {
17
+ Set-Content -LiteralPath $Log -Value ("ERR:" + $_.Exception.Message) -Encoding utf8
18
+ exit 1
19
+ }
@@ -0,0 +1,21 @@
1
+ # Build the single GrokAssistant.exe. This is for making the program, not for installing it.
2
+ $ErrorActionPreference = "Stop"
3
+ $root = Resolve-Path (Join-Path $PSScriptRoot "..")
4
+ Set-Location $root
5
+ $python = $null
6
+ foreach ($candidate in @("python", "py")) {
7
+ if (Get-Command $candidate -ErrorAction SilentlyContinue) {
8
+ if ($candidate -eq "py") {
9
+ $python = & py -3 -c "import sys; print(sys.executable)"
10
+ } else {
11
+ $python = & python -c "import sys; print(sys.executable)"
12
+ }
13
+ break
14
+ }
15
+ }
16
+ if (-not $python) { throw "Python 3.11 or newer is required to build the executable." }
17
+ & $python -m pip install --upgrade pip pyinstaller pystray Pillow "sherpa-onnx==1.13.8" "sounddevice==0.5.6" "numpy>=2.2,<2.5"
18
+ & $python -m PyInstaller --noconfirm --clean (Join-Path $root "GrokAssistant.spec")
19
+ $exe = Join-Path $root "dist\GrokAssistant.exe"
20
+ if (-not (Test-Path $exe)) { throw "The executable was not created." }
21
+ Write-Host "Built $exe"
@@ -0,0 +1,14 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)][string]$Path,
3
+ [int]$Volume = 70,
4
+ [string]$Voice = ""
5
+ )
6
+ $ErrorActionPreference = "Stop"
7
+ Add-Type -AssemblyName System.Speech
8
+ $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
9
+ $synth.Volume = [Math]::Max(0, [Math]::Min(100, $Volume))
10
+ if ($Voice) {
11
+ try { $synth.SelectVoice($Voice) } catch { }
12
+ }
13
+ $text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
14
+ if ($text) { $synth.Speak($text) }
@@ -0,0 +1,6 @@
1
+ $ErrorActionPreference = "Stop"
2
+ Add-Type -AssemblyName System.Speech
3
+ $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
4
+ $synth.GetInstalledVoices() | ForEach-Object {
5
+ $_.VoiceInfo.Name + "|" + $_.VoiceInfo.Culture.Name
6
+ }
@@ -0,0 +1,2 @@
1
+ 73c54b29e221
2
+ 2026-10-02T07:32:53Z
@@ -0,0 +1,69 @@
1
+ """The build baked into this copy. About shows it. The updater compares it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+
11
+ def _git(cwd: Path, *args: str) -> str:
12
+ try:
13
+ from grok_assistant.quiet import no_window
14
+
15
+ out = subprocess.check_output(
16
+ ["git", *args],
17
+ cwd=cwd,
18
+ text=True,
19
+ encoding="utf-8",
20
+ errors="replace",
21
+ stderr=subprocess.DEVNULL,
22
+ timeout=5,
23
+ **no_window(),
24
+ )
25
+ except (OSError, subprocess.SubprocessError):
26
+ return ""
27
+ return out.strip()
28
+
29
+
30
+ def _read_file(path: Path) -> tuple[str, str]:
31
+ try:
32
+ lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
33
+ except OSError:
34
+ return "", ""
35
+ ident = lines[0] if lines else ""
36
+ when = lines[1] if len(lines) > 1 else ""
37
+ if not ident or ident == "unknown":
38
+ return "", when
39
+ return ident, when
40
+
41
+
42
+ def read_stamp() -> tuple[str, str]:
43
+ """Return the build id and the UTC time it was made. Empty strings when unknown."""
44
+ if getattr(sys, "frozen", False):
45
+ return _read_file(Path(getattr(sys, "_MEIPASS")) / "grok_assistant" / "build_stamp.txt")
46
+ here = Path(__file__).resolve()
47
+ checkout = here.parents[2]
48
+ if (checkout / "scripts" / "speak.ps1").is_file():
49
+ ident = _git(checkout, "rev-parse", "--short=12", "HEAD")
50
+ if ident:
51
+ return ident, _git(checkout, "log", "-1", "--format=%cI")
52
+ return _read_file(here.parent / "build_stamp.txt")
53
+
54
+
55
+ def build_line() -> str:
56
+ ident, _when = read_stamp()
57
+ if not ident:
58
+ return "build —"
59
+ return f"build {ident}"
60
+
61
+
62
+ def write_stamp(path: Path | None = None) -> tuple[str, str]:
63
+ """Write the source commit and the current UTC time. The executable and the wheel both call this."""
64
+ dest = Path(path) if path else Path(__file__).resolve().parent / "build_stamp.txt"
65
+ checkout = Path(__file__).resolve().parents[2]
66
+ ident = _git(checkout, "rev-parse", "--short=12", "HEAD")
67
+ when = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
68
+ dest.write_text(f"{ident}\n{when}\n", encoding="utf-8")
69
+ return ident, when
@@ -0,0 +1 @@
1
+ """The door to the Grok executable."""
@@ -0,0 +1,92 @@
1
+ """Agents the signed-in Grok account already publishes. No prompt is sent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from pathlib import Path
9
+
10
+ BUNDLE = "https://cli-chat-proxy.grok.com/v1/subagents/bundle"
11
+ SETTINGS = "https://grok.com/rest/user-settings"
12
+
13
+
14
+ def agents_from_bundle(payload: dict) -> dict[str, str]:
15
+ """Name to markdown, from the account bundle. Empty when the shape is not that list."""
16
+ agents = payload.get("agents") if isinstance(payload, dict) else None
17
+ if not isinstance(agents, dict):
18
+ return {}
19
+ found = {}
20
+ for name, body in agents.items():
21
+ title = " ".join(str(name).split())
22
+ if not title or not isinstance(body, str) or not body.strip():
23
+ continue
24
+ found[title] = body if body.endswith("\n") else body + "\n"
25
+ return found
26
+
27
+
28
+ def agents_from_customizations(payload: dict) -> dict[str, str]:
29
+ """Name to markdown, from grok.com agent customizations. Empty when there are none."""
30
+ items = payload.get("agentCustomizations") if isinstance(payload, dict) else None
31
+ if not isinstance(items, list):
32
+ return {}
33
+ found = {}
34
+ for item in items:
35
+ if not isinstance(item, dict):
36
+ continue
37
+ raw_name = item.get("name") or item.get("title") or item.get("displayName") or item.get("id")
38
+ title = " ".join(str(raw_name or "").split())
39
+ if not title:
40
+ continue
41
+ prompt = item.get("instructions") or item.get("systemPrompt") or item.get("prompt") or item.get("description") or ""
42
+ found[title] = (
43
+ f"---\nname: {title}\n"
44
+ "description: Agente de la cuenta de Grok.\n"
45
+ "prompt_mode: full\nmodel: grok-4.7\n---\n\n"
46
+ f"{prompt}\n"
47
+ )
48
+ return found
49
+
50
+
51
+ def fetch_account_agents(auth_path: Path | None = None) -> dict[str, str]:
52
+ """Agents linked to the signed-in account. {} when the account cannot be read."""
53
+ creds = _creds(auth_path or Path.home() / ".grok" / "auth.json")
54
+ if creds is None:
55
+ return {}
56
+ key, user_id = creds
57
+ found = agents_from_bundle(_get(BUNDLE, key, user_id))
58
+ custom = agents_from_customizations(_get(SETTINGS, key, user_id))
59
+ found.update(custom)
60
+ return found
61
+
62
+
63
+ def _creds(path: Path) -> tuple[str, str] | None:
64
+ try:
65
+ raw = json.loads(path.read_text(encoding="utf-8"))
66
+ except (OSError, json.JSONDecodeError):
67
+ return None
68
+ if not isinstance(raw, dict):
69
+ return None
70
+ for item in raw.values():
71
+ if isinstance(item, dict) and item.get("key"):
72
+ return str(item["key"]), str(item.get("user_id") or "")
73
+ return None
74
+
75
+
76
+ def _get(url: str, key: str, user_id: str) -> dict:
77
+ request = urllib.request.Request(
78
+ url,
79
+ headers={
80
+ "Authorization": f"Bearer {key}",
81
+ "Accept": "application/json",
82
+ "User-Agent": "GrokAssistant",
83
+ "X-XAI-Token-Auth": "xai-grok-cli",
84
+ "x-userid": user_id,
85
+ },
86
+ )
87
+ try:
88
+ with urllib.request.urlopen(request, timeout=12) as response:
89
+ payload = json.load(response)
90
+ except (OSError, urllib.error.URLError, json.JSONDecodeError, TimeoutError):
91
+ return {}
92
+ return payload if isinstance(payload, dict) else {}
@@ -0,0 +1,49 @@
1
+ """How much of the linked Grok account allowance is already used. No audio, no prompt."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from pathlib import Path
9
+
10
+ BILLING = "https://cli-chat-proxy.grok.com/v1/billing?format=credits"
11
+
12
+
13
+ def percent_used(payload: dict) -> int | None:
14
+ """Weekly allowance already spent, 0–100. None when the account did not say."""
15
+ config = payload.get("config") if isinstance(payload, dict) else None
16
+ source = config if isinstance(config, dict) else payload
17
+ if not isinstance(source, dict) or source.get("creditUsagePercent") is None:
18
+ return None
19
+ try:
20
+ value = int(round(float(source["creditUsagePercent"])))
21
+ except (TypeError, ValueError):
22
+ return None
23
+ return max(0, min(100, value))
24
+
25
+
26
+ def fetch_account_percent(auth_path: Path | None = None) -> int | None:
27
+ path = auth_path or Path.home() / ".grok" / "auth.json"
28
+ try:
29
+ raw = json.loads(path.read_text(encoding="utf-8"))
30
+ except (OSError, json.JSONDecodeError):
31
+ return None
32
+ key = ""
33
+ if isinstance(raw, dict):
34
+ for item in raw.values():
35
+ if isinstance(item, dict) and item.get("key"):
36
+ key = str(item["key"])
37
+ break
38
+ if not key:
39
+ return None
40
+ request = urllib.request.Request(
41
+ BILLING,
42
+ headers={"Authorization": f"Bearer {key}", "Accept": "application/json", "User-Agent": "GrokAssistant"},
43
+ )
44
+ try:
45
+ with urllib.request.urlopen(request, timeout=15) as response:
46
+ payload = json.load(response)
47
+ except (OSError, urllib.error.URLError, json.JSONDecodeError, TimeoutError):
48
+ return None
49
+ return percent_used(payload)