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.
- grok_assistant/__init__.py +4 -0
- grok_assistant/__main__.py +110 -0
- grok_assistant/_bundle/LICENSE.md +39 -0
- grok_assistant/_bundle/docs/img/grok-mark.png +0 -0
- grok_assistant/_bundle/docs/img/grok.ico +0 -0
- grok_assistant/_bundle/listeners/dictation.ps1 +80 -0
- grok_assistant/_bundle/listeners/install_speech.ps1 +19 -0
- grok_assistant/_bundle/scripts/build_exe.ps1 +21 -0
- grok_assistant/_bundle/scripts/speak.ps1 +14 -0
- grok_assistant/_bundle/scripts/voices.ps1 +6 -0
- grok_assistant/build_stamp.txt +2 -0
- grok_assistant/buildinfo.py +69 -0
- grok_assistant/cloud/__init__.py +1 -0
- grok_assistant/cloud/account_agents.py +92 -0
- grok_assistant/cloud/account_usage.py +49 -0
- grok_assistant/cloud/grok_cli.py +166 -0
- grok_assistant/cloud/prompts.py +61 -0
- grok_assistant/cloud/setup_grok.py +167 -0
- grok_assistant/console.py +26 -0
- grok_assistant/house/__init__.py +1 -0
- grok_assistant/house/helptext.py +155 -0
- grok_assistant/house/marketplace.py +531 -0
- grok_assistant/house/music.py +166 -0
- grok_assistant/house/personality.py +430 -0
- grok_assistant/house/updates.py +165 -0
- grok_assistant/i18n.py +172 -0
- grok_assistant/lang/de.json +393 -0
- grok_assistant/lang/en.json +393 -0
- grok_assistant/lang/es.json +366 -0
- grok_assistant/lang/fr.json +393 -0
- grok_assistant/lines/banter.json +638 -0
- grok_assistant/lines/hellos-es.txt +188 -0
- grok_assistant/lines/waits-es.txt +170 -0
- grok_assistant/listening/__init__.py +1 -0
- grok_assistant/listening/ear_score.py +87 -0
- grok_assistant/listening/enroll_audio.py +259 -0
- grok_assistant/listening/kroko_ear.py +324 -0
- grok_assistant/listening/listen.py +217 -0
- grok_assistant/listening/offline_ear.py +243 -0
- grok_assistant/listening/refine.py +131 -0
- grok_assistant/listening/voiceprint.py +75 -0
- grok_assistant/mind/__init__.py +1 -0
- grok_assistant/mind/local_llm.py +152 -0
- grok_assistant/notebook/__init__.py +1 -0
- grok_assistant/notebook/auth.py +48 -0
- grok_assistant/notebook/settings.py +100 -0
- grok_assistant/notebook/store.py +669 -0
- grok_assistant/paths.py +81 -0
- grok_assistant/quiet.py +15 -0
- grok_assistant/rules/__init__.py +1 -0
- grok_assistant/rules/brain.py +72 -0
- grok_assistant/rules/common.py +100 -0
- grok_assistant/rules/hub.py +338 -0
- grok_assistant/rules/match.py +523 -0
- grok_assistant/rules/orders.py +235 -0
- grok_assistant/rules/prints.py +173 -0
- grok_assistant/rules/status.py +159 -0
- grok_assistant/rules/talk.py +451 -0
- grok_assistant/rules/trace.py +78 -0
- grok_assistant/speaking/__init__.py +1 -0
- grok_assistant/speaking/banter.py +81 -0
- grok_assistant/speaking/speech.py +222 -0
- grok_assistant/startup.py +50 -0
- grok_assistant/textutil.py +55 -0
- grok_assistant/ui/__init__.py +1 -0
- grok_assistant/ui/actions.py +624 -0
- grok_assistant/ui/app.py +36 -0
- grok_assistant/ui/chrome.py +59 -0
- grok_assistant/ui/deps.py +39 -0
- grok_assistant/ui/ears.py +156 -0
- grok_assistant/ui/flow.py +187 -0
- grok_assistant/ui/market.py +413 -0
- grok_assistant/ui/menus.py +649 -0
- grok_assistant/ui/prints_ui.py +379 -0
- grok_assistant/ui/round.py +244 -0
- grok_assistant/ui/theme.py +164 -0
- grok_assistant/ui/themes/aurora.json +30 -0
- grok_assistant/ui/themes/cobre.json +30 -0
- grok_assistant/ui/themes/dia.json +30 -0
- grok_assistant/ui/themes/lino.json +30 -0
- grok_assistant/ui/themes/mar.json +30 -0
- grok_assistant/ui/themes/noche.json +30 -0
- grok_assistant/ui/themes/oliva.json +30 -0
- grok_assistant/ui/win_tray.py +649 -0
- grok_assistant/ui/window.py +538 -0
- grok_assistant/ui/worker.py +253 -0
- grok_assistant-1.0.dist-info/METADATA +58 -0
- grok_assistant-1.0.dist-info/RECORD +92 -0
- grok_assistant-1.0.dist-info/WHEEL +5 -0
- grok_assistant-1.0.dist-info/entry_points.txt +2 -0
- grok_assistant-1.0.dist-info/licenses/LICENSE.md +39 -0
- grok_assistant-1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
|
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,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)
|