@clawpump/claw-agent 0.1.8 → 0.1.11
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/agent/.mailmap +4 -0
- package/agent/apps/desktop/README.md +3 -3
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-ready.cjs +2 -2
- package/agent/apps/desktop/electron/dashboard-token.cjs +3 -3
- package/agent/apps/desktop/electron/hardening.cjs +1 -1
- package/agent/apps/desktop/electron/main.cjs +67 -67
- package/agent/apps/desktop/index.html +1 -1
- package/agent/apps/desktop/package.json +16 -16
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/claw-mark.png +0 -0
- package/agent/apps/desktop/scripts/after-pack.cjs +1 -1
- package/agent/apps/desktop/scripts/set-exe-identity.cjs +2 -2
- package/agent/apps/desktop/scripts/test-desktop.mjs +3 -3
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +2 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +10 -0
- package/agent/apps/desktop/src/app/chat/composer/pod-credits.tsx +49 -0
- package/agent/apps/desktop/src/app/chat/index.tsx +1 -1
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +4 -2
- package/agent/apps/desktop/src/app/desktop-controller.tsx +18 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +1 -1
- package/agent/apps/desktop/src/app/messaging/index.tsx +5 -5
- package/agent/apps/desktop/src/app/routes.ts +9 -1
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +3 -3
- package/agent/apps/desktop/src/app/settings/constants.ts +5 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +1 -1
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +46 -1
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +5 -5
- package/agent/apps/desktop/src/app/types.ts +9 -1
- package/agent/apps/desktop/src/app/wallet/index.tsx +244 -0
- package/agent/apps/desktop/src/app/x402/index.tsx +162 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +1 -1
- package/agent/apps/desktop/src/components/brand-mark.tsx +2 -2
- package/agent/apps/desktop/src/components/chat/intro-copy.jsonl +6 -6
- package/agent/apps/desktop/src/components/chat/intro.tsx +4 -4
- package/agent/apps/desktop/src/components/model-picker.tsx +64 -4
- package/agent/apps/desktop/src/components/pod-setup-dialog.tsx +227 -0
- package/agent/apps/desktop/src/hermes.ts +109 -3
- package/agent/apps/desktop/src/i18n/en.ts +80 -78
- package/agent/apps/desktop/src/i18n/ja.ts +82 -82
- package/agent/apps/desktop/src/i18n/runtime.test.ts +2 -2
- package/agent/apps/desktop/src/i18n/zh-hant.ts +82 -82
- package/agent/apps/desktop/src/i18n/zh.ts +87 -87
- package/agent/apps/desktop/src/lib/desktop-fs.ts +1 -1
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +4 -4
- package/agent/apps/desktop/src/store/composer.ts +7 -0
- package/agent/apps/desktop/src/store/onboarding.ts +5 -5
- package/agent/apps/desktop/src/themes/presets.ts +54 -54
- package/agent/cli.py +184 -10
- package/agent/hermes_cli/distribution.py +188 -8
- package/agent/hermes_cli/gui_uninstall.py +11 -6
- package/agent/hermes_cli/main.py +9 -4
- package/agent/hermes_cli/providers.py +29 -0
- package/agent/hermes_cli/web_server.py +180 -2
- package/agent/plugins/model-providers/usepod/__init__.py +7 -1
- package/agent/scripts/install.sh +3 -1
- package/agent/scripts/release.py +1 -0
- package/agent/web/src/components/ChatSidebar.tsx +5 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +28 -1
- package/agent/web/src/components/PodCredits.tsx +57 -0
- package/agent/web/src/components/PodSetupDialog.tsx +240 -0
- package/agent/web/src/lib/api.ts +23 -0
- package/package.json +1 -1
|
@@ -70,21 +70,21 @@ def _agent_root(hermes_home: Path) -> Path:
|
|
|
70
70
|
def desktop_userdata_dir() -> Path:
|
|
71
71
|
"""Return the Electron ``userData`` directory for the desktop app.
|
|
72
72
|
|
|
73
|
-
Mirrors Electron's ``app.getPath('userData')`` for an app named "
|
|
73
|
+
Mirrors Electron's ``app.getPath('userData')`` for an app named "Claw Agent"
|
|
74
74
|
on each platform. This is GUI-only state (connection.json, updates.json,
|
|
75
75
|
Chromium cache) and never holds agent config or sessions.
|
|
76
76
|
"""
|
|
77
77
|
home = Path.home()
|
|
78
78
|
if sys.platform == "darwin":
|
|
79
|
-
return home / "Library" / "Application Support" / "
|
|
79
|
+
return home / "Library" / "Application Support" / "Claw Agent"
|
|
80
80
|
if sys.platform == "win32":
|
|
81
81
|
appdata = os.environ.get("APPDATA")
|
|
82
82
|
base = Path(appdata) if appdata else (home / "AppData" / "Roaming")
|
|
83
|
-
return base / "
|
|
83
|
+
return base / "Claw Agent"
|
|
84
84
|
# Linux / other POSIX — XDG config home.
|
|
85
85
|
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
86
86
|
base = Path(xdg) if xdg else (home / ".config")
|
|
87
|
-
return base / "
|
|
87
|
+
return base / "Claw Agent"
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def source_built_gui_artifacts(hermes_home: Path) -> "list[Path]":
|
|
@@ -119,6 +119,9 @@ def packaged_gui_app_paths() -> "list[Path]":
|
|
|
119
119
|
paths: list[Path] = []
|
|
120
120
|
if sys.platform == "darwin":
|
|
121
121
|
paths += [
|
|
122
|
+
Path("/Applications/Claw Agent.app"),
|
|
123
|
+
home / "Applications" / "Claw Agent.app",
|
|
124
|
+
# Legacy bundle name (older local builds) — clean up if present.
|
|
122
125
|
Path("/Applications/Hermes.app"),
|
|
123
126
|
home / "Applications" / "Hermes.app",
|
|
124
127
|
]
|
|
@@ -126,14 +129,16 @@ def packaged_gui_app_paths() -> "list[Path]":
|
|
|
126
129
|
local = os.environ.get("LOCALAPPDATA")
|
|
127
130
|
local_base = Path(local) if local else (home / "AppData" / "Local")
|
|
128
131
|
paths += [
|
|
129
|
-
# NSIS per-user install (perMachine=false → Programs\
|
|
130
|
-
local_base / "Programs" / "
|
|
132
|
+
# NSIS per-user install (perMachine=false → Programs\Claw Agent).
|
|
133
|
+
local_base / "Programs" / "Claw Agent",
|
|
134
|
+
local_base / "Programs" / "Hermes", # legacy
|
|
131
135
|
# Older / alternate layout some builds used.
|
|
132
136
|
local_base / "hermes-desktop",
|
|
133
137
|
]
|
|
134
138
|
program_files = os.environ.get("ProgramFiles")
|
|
135
139
|
if program_files:
|
|
136
140
|
# NSIS per-machine fallback (needs admin to remove).
|
|
141
|
+
paths.append(Path(program_files) / "Claw Agent")
|
|
137
142
|
paths.append(Path(program_files) / "Hermes")
|
|
138
143
|
else:
|
|
139
144
|
# Linux: AppImage is a single file the user placed somewhere; we can
|
package/agent/hermes_cli/main.py
CHANGED
|
@@ -5003,19 +5003,24 @@ def _desktop_packaged_executable(desktop_dir: Path) -> Optional[Path]:
|
|
|
5003
5003
|
"""Return the current platform's unpacked Electron app executable."""
|
|
5004
5004
|
release_dir = desktop_dir / "release"
|
|
5005
5005
|
if sys.platform == "darwin":
|
|
5006
|
-
|
|
5006
|
+
# Renamed bundle "Claw Agent.app" (binary MacOS/Claw Agent); keep the
|
|
5007
|
+
# legacy "Hermes.app" as a fallback for older local builds.
|
|
5008
|
+
candidates = list(release_dir.glob("mac*/Claw Agent.app/Contents/MacOS/Claw Agent"))
|
|
5009
|
+
candidates += list(release_dir.glob("mac*/Hermes.app/Contents/MacOS/Hermes"))
|
|
5007
5010
|
elif sys.platform == "win32":
|
|
5008
5011
|
candidates = [
|
|
5012
|
+
release_dir / "win-unpacked" / "Claw Agent.exe",
|
|
5013
|
+
release_dir / "win-ia32-unpacked" / "Claw Agent.exe",
|
|
5014
|
+
release_dir / "win-arm64-unpacked" / "Claw Agent.exe",
|
|
5009
5015
|
release_dir / "win-unpacked" / "Hermes.exe",
|
|
5010
|
-
release_dir / "win-ia32-unpacked" / "Hermes.exe",
|
|
5011
|
-
release_dir / "win-arm64-unpacked" / "Hermes.exe",
|
|
5012
5016
|
]
|
|
5013
5017
|
else:
|
|
5014
5018
|
candidates = [
|
|
5019
|
+
release_dir / "linux-unpacked" / "claw-agent",
|
|
5015
5020
|
release_dir / "linux-unpacked" / "hermes",
|
|
5016
5021
|
release_dir / "linux-unpacked" / "Hermes",
|
|
5022
|
+
release_dir / "linux-arm64-unpacked" / "claw-agent",
|
|
5017
5023
|
release_dir / "linux-arm64-unpacked" / "hermes",
|
|
5018
|
-
release_dir / "linux-arm64-unpacked" / "Hermes",
|
|
5019
5024
|
]
|
|
5020
5025
|
|
|
5021
5026
|
existing = [p for p in candidates if p.exists()]
|
|
@@ -731,4 +731,33 @@ def resolve_provider_full(
|
|
|
731
731
|
except Exception:
|
|
732
732
|
pass
|
|
733
733
|
|
|
734
|
+
# 4. Plugin model providers (providers/ registry, e.g. usepod). Not in
|
|
735
|
+
# models.dev or the built-in alias table, so synthesize a ProviderDef
|
|
736
|
+
# from the plugin's ProviderProfile. Without this the live model-switch
|
|
737
|
+
# path ("Unknown provider 'usepod'") rejects any plugin api-key provider
|
|
738
|
+
# even though it's fully configured and serving models.
|
|
739
|
+
try:
|
|
740
|
+
from providers import get_provider_profile
|
|
741
|
+
|
|
742
|
+
prof = get_provider_profile(canonical) or get_provider_profile(raw)
|
|
743
|
+
if prof is not None:
|
|
744
|
+
env_vars = tuple(getattr(prof, "env_vars", ()) or ())
|
|
745
|
+
# First env var is the API key; a trailing *_BASE_URL is the
|
|
746
|
+
# optional custom-endpoint override.
|
|
747
|
+
key_envs = tuple(e for e in env_vars if not e.upper().endswith("_BASE_URL")) or env_vars[:1]
|
|
748
|
+
base_env = next((e for e in env_vars if e.upper().endswith("_BASE_URL")), "")
|
|
749
|
+
transport = "anthropic_messages" if getattr(prof, "api_mode", "") == "anthropic_messages" else "openai_chat"
|
|
750
|
+
return ProviderDef(
|
|
751
|
+
id=canonical,
|
|
752
|
+
name=getattr(prof, "display_name", "") or getattr(prof, "name", canonical),
|
|
753
|
+
transport=transport,
|
|
754
|
+
api_key_env_vars=key_envs,
|
|
755
|
+
base_url=getattr(prof, "base_url", "") or "",
|
|
756
|
+
base_url_env_var=base_env,
|
|
757
|
+
auth_type=getattr(prof, "auth_type", "api_key") or "api_key",
|
|
758
|
+
source="plugin",
|
|
759
|
+
)
|
|
760
|
+
except Exception:
|
|
761
|
+
pass
|
|
762
|
+
|
|
734
763
|
return None
|
|
@@ -12097,13 +12097,21 @@ class _McpToolError(RuntimeError):
|
|
|
12097
12097
|
"""The tool ran but returned an error — the session itself is healthy."""
|
|
12098
12098
|
|
|
12099
12099
|
|
|
12100
|
-
def _clawpump_call(
|
|
12100
|
+
def _clawpump_call(
|
|
12101
|
+
tool: str, arguments: Optional[dict] = None, *, timeout: float = 20, prefer_structured: bool = False
|
|
12102
|
+
) -> Any:
|
|
12101
12103
|
"""Call a ClawPump MCP tool over the dashboard's warm, reused session.
|
|
12102
12104
|
|
|
12103
12105
|
Reuses one long-lived connection and reconnects only if the session has
|
|
12104
12106
|
died, so the dashboard behaves like the agent's persistent connection
|
|
12105
12107
|
instead of reconnecting (and stalling) per request. Returns parsed JSON;
|
|
12106
12108
|
raises ``RuntimeError`` when the MCP is unconfigured or the tool errors.
|
|
12109
|
+
|
|
12110
|
+
``prefer_structured`` returns the tool's ``structuredContent`` (the machine
|
|
12111
|
+
channel) when present, instead of the human-visible text. Needed for tools
|
|
12112
|
+
that REDACT secrets in the text but put the real value in structuredContent
|
|
12113
|
+
(e.g. usepod_provision returns a placeholder api_token in text and the real
|
|
12114
|
+
token in structuredContent). Default off so existing callers are unchanged.
|
|
12107
12115
|
"""
|
|
12108
12116
|
global _warm_mcp_server
|
|
12109
12117
|
|
|
@@ -12123,9 +12131,14 @@ def _clawpump_call(tool: str, arguments: Optional[dict] = None, *, timeout: floa
|
|
|
12123
12131
|
text = "".join(b.text for b in (result.content or []) if hasattr(b, "text"))
|
|
12124
12132
|
if getattr(result, "isError", False):
|
|
12125
12133
|
raise _McpToolError(text or "MCP tool returned an error")
|
|
12134
|
+
if prefer_structured:
|
|
12135
|
+
sc = getattr(result, "structuredContent", None)
|
|
12136
|
+
if isinstance(sc, dict) and sc:
|
|
12137
|
+
return sc
|
|
12126
12138
|
return text
|
|
12127
12139
|
|
|
12128
|
-
|
|
12140
|
+
raw = _run_on_mcp_loop(_coro, timeout=timeout)
|
|
12141
|
+
data = raw if isinstance(raw, dict) else _parse_mcp_json(raw)
|
|
12129
12142
|
if isinstance(data, dict) and isinstance(data.get("error"), str) and data["error"]:
|
|
12130
12143
|
raise _McpToolError(data["error"])
|
|
12131
12144
|
return data
|
|
@@ -12186,6 +12199,171 @@ def get_wallet_balances():
|
|
|
12186
12199
|
return {"ok": True, "wallets": wallets}
|
|
12187
12200
|
|
|
12188
12201
|
|
|
12202
|
+
class PodProvisionBody(BaseModel):
|
|
12203
|
+
agent_id: str
|
|
12204
|
+
amount: float
|
|
12205
|
+
|
|
12206
|
+
|
|
12207
|
+
@app.post("/api/clawpump/pod/provision")
|
|
12208
|
+
async def post_pod_provision(body: PodProvisionBody, profile: Optional[str] = None):
|
|
12209
|
+
"""Provision a UsePod "Pod" and switch the agent onto it as the provider.
|
|
12210
|
+
|
|
12211
|
+
One ClawPump-wallet-funded call: ``usepod_provision`` registers a fresh pod
|
|
12212
|
+
and funds it with ``amount`` USDC from ``agent_id``'s wallet (double-gated by
|
|
12213
|
+
``confirm_deposit``), returning an ``api_token``. We persist the token
|
|
12214
|
+
(USEPOD_API_KEY) and set ``model.provider=usepod`` so the session uses Pod —
|
|
12215
|
+
the desktop's one-click "Set up Pod" flow, mirroring the CLI Pod picker.
|
|
12216
|
+
|
|
12217
|
+
Spends real on-chain USDC; the UI confirms the amount before calling this.
|
|
12218
|
+
Sync body runs in a worker thread (MCP + ``_profile_scope`` both block).
|
|
12219
|
+
"""
|
|
12220
|
+
agent_id = (body.agent_id or "").strip()
|
|
12221
|
+
try:
|
|
12222
|
+
amount = float(body.amount)
|
|
12223
|
+
except (TypeError, ValueError):
|
|
12224
|
+
raise HTTPException(status_code=400, detail="amount must be a number")
|
|
12225
|
+
if not agent_id:
|
|
12226
|
+
raise HTTPException(status_code=400, detail="agent_id is required")
|
|
12227
|
+
if amount <= 0:
|
|
12228
|
+
raise HTTPException(status_code=400, detail="amount must be positive")
|
|
12229
|
+
|
|
12230
|
+
def _unwrap(value: "Any") -> "Any":
|
|
12231
|
+
# ClawPump MCP results can arrive double-wrapped: {"result": "<escaped
|
|
12232
|
+
# json>"} or {"structuredContent": {...}}. Peel those so we see the real
|
|
12233
|
+
# {api_token, deposit_code, signature, funding_error} object.
|
|
12234
|
+
seen = 0
|
|
12235
|
+
while isinstance(value, str) and seen < 3:
|
|
12236
|
+
try:
|
|
12237
|
+
value = json.loads(value)
|
|
12238
|
+
seen += 1
|
|
12239
|
+
except Exception:
|
|
12240
|
+
break
|
|
12241
|
+
if isinstance(value, dict):
|
|
12242
|
+
sc = value.get("structuredContent")
|
|
12243
|
+
if isinstance(sc, dict):
|
|
12244
|
+
return sc
|
|
12245
|
+
if set(value.keys()) <= {"result"} and value.get("result") is not None:
|
|
12246
|
+
return _unwrap(value["result"])
|
|
12247
|
+
return value
|
|
12248
|
+
|
|
12249
|
+
def _provision_and_apply():
|
|
12250
|
+
from hermes_cli import distribution as _dist
|
|
12251
|
+
|
|
12252
|
+
raw = _clawpump_call(
|
|
12253
|
+
"usepod_provision",
|
|
12254
|
+
{"amount": amount, "agent_id": agent_id, "confirm_deposit": True},
|
|
12255
|
+
timeout=120,
|
|
12256
|
+
# The real api_token is only in structuredContent — the text channel
|
|
12257
|
+
# redacts it to a placeholder. Without this we'd persist the
|
|
12258
|
+
# placeholder as USEPOD_API_KEY and Pod would look set but not work.
|
|
12259
|
+
prefer_structured=True,
|
|
12260
|
+
)
|
|
12261
|
+
# Token extraction goes through the battle-tested downstream helper
|
|
12262
|
+
# (tolerates nesting/escaping); signature is best-effort from _unwrap.
|
|
12263
|
+
tok = _dist.usepod_provision_token("mcp_clawpump_usepod_provision", raw)
|
|
12264
|
+
data = _unwrap(raw)
|
|
12265
|
+
data = data if isinstance(data, dict) else {}
|
|
12266
|
+
if tok:
|
|
12267
|
+
api_token, deposit_code = tok
|
|
12268
|
+
else:
|
|
12269
|
+
api_token = str(data.get("api_token") or data.get("token") or "").strip()
|
|
12270
|
+
deposit_code = str(data.get("deposit_code") or "").strip()
|
|
12271
|
+
if not api_token:
|
|
12272
|
+
detail = str(data.get("funding_error") or data.get("error") or "no api_token returned")
|
|
12273
|
+
raise RuntimeError(f"Pod provisioning failed: {detail}")
|
|
12274
|
+
|
|
12275
|
+
_dist.persist_usepod_credentials(api_token, deposit_code)
|
|
12276
|
+
|
|
12277
|
+
current = ""
|
|
12278
|
+
try:
|
|
12279
|
+
from hermes_cli.config import load_config
|
|
12280
|
+
|
|
12281
|
+
current = str((load_config().get("model") or {}).get("default") or "")
|
|
12282
|
+
except Exception:
|
|
12283
|
+
current = ""
|
|
12284
|
+
model, _provider, _base_url = _dist.usepod_pod_switch_target(api_token, current)
|
|
12285
|
+
with _profile_scope(body.profile if hasattr(body, "profile") else profile):
|
|
12286
|
+
_apply_model_assignment_sync("main", "usepod", model, "", "", "")
|
|
12287
|
+
|
|
12288
|
+
return {
|
|
12289
|
+
"ok": True,
|
|
12290
|
+
"provider": "usepod",
|
|
12291
|
+
"model": model,
|
|
12292
|
+
"amount": amount,
|
|
12293
|
+
"signature": str(data.get("signature") or ""),
|
|
12294
|
+
"funding_error": str(data.get("funding_error") or ""),
|
|
12295
|
+
}
|
|
12296
|
+
|
|
12297
|
+
try:
|
|
12298
|
+
return await asyncio.to_thread(_provision_and_apply)
|
|
12299
|
+
except HTTPException:
|
|
12300
|
+
raise
|
|
12301
|
+
except Exception as exc:
|
|
12302
|
+
_log.exception("POST /api/clawpump/pod/provision failed")
|
|
12303
|
+
return {"ok": False, "error": str(exc)}
|
|
12304
|
+
|
|
12305
|
+
|
|
12306
|
+
# Last successfully-fetched Pod balance, keyed by token. A transient probe
|
|
12307
|
+
# failure (Cloudflare hiccup / timeout) then falls back to the last good value
|
|
12308
|
+
# so the UI never flickers from "$0.96" back to a bare "Pod". {token: usdc}.
|
|
12309
|
+
_pod_balance_cache: Dict[str, float] = {}
|
|
12310
|
+
|
|
12311
|
+
|
|
12312
|
+
def _fetch_pod_balance(token: str) -> Optional[float]:
|
|
12313
|
+
"""Fetch the pod's USDC balance (UI units). Retries once; None on failure."""
|
|
12314
|
+
from hermes_cli.auth import USEPOD_API_BASE
|
|
12315
|
+
from providers.base import _profile_user_agent
|
|
12316
|
+
|
|
12317
|
+
url = f"{USEPOD_API_BASE}/proxy/{token}/balance"
|
|
12318
|
+
for attempt in range(2):
|
|
12319
|
+
try:
|
|
12320
|
+
# UsePod sits behind Cloudflare, which 403s ("error 1010") a bare
|
|
12321
|
+
# urllib request — send a browser-ish UA + the bearer token, matching
|
|
12322
|
+
# the provider's own fetch_models. The token also lives in the path.
|
|
12323
|
+
req = urllib.request.Request(url, method="GET")
|
|
12324
|
+
req.add_header("User-Agent", _profile_user_agent())
|
|
12325
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
12326
|
+
req.add_header("Accept", "application/json")
|
|
12327
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
12328
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
12329
|
+
micro = data.get("usdc_balance")
|
|
12330
|
+
if micro is not None:
|
|
12331
|
+
return round(float(micro) / 1_000_000.0, 4)
|
|
12332
|
+
return None
|
|
12333
|
+
except Exception:
|
|
12334
|
+
if attempt == 0:
|
|
12335
|
+
continue
|
|
12336
|
+
return None
|
|
12337
|
+
return None
|
|
12338
|
+
|
|
12339
|
+
|
|
12340
|
+
@app.get("/api/clawpump/pod/status")
|
|
12341
|
+
def get_pod_status():
|
|
12342
|
+
"""Whether a UsePod Pod is configured + its remaining USDC balance.
|
|
12343
|
+
|
|
12344
|
+
Lets the desktop show Pod as "Connected" (with balance) instead of "Set up"
|
|
12345
|
+
once provisioned. Reads USEPOD_API_KEY from ~/.hermes/.env (never returns the
|
|
12346
|
+
token) and best-effort fetches the pod's balance — falling back to the last
|
|
12347
|
+
cached value on a transient failure so the credits readout stays stable.
|
|
12348
|
+
"""
|
|
12349
|
+
from hermes_cli.config import get_env_value
|
|
12350
|
+
|
|
12351
|
+
token = (get_env_value("USEPOD_API_KEY") or "").strip()
|
|
12352
|
+
# Empty, or the redacted placeholder from the old token-handling bug, both
|
|
12353
|
+
# mean "not really connected".
|
|
12354
|
+
if not token or token.startswith("<") or "applied" in token.lower():
|
|
12355
|
+
return {"connected": False}
|
|
12356
|
+
|
|
12357
|
+
balance_usdc = _fetch_pod_balance(token)
|
|
12358
|
+
if balance_usdc is not None:
|
|
12359
|
+
_pod_balance_cache[token] = balance_usdc
|
|
12360
|
+
else:
|
|
12361
|
+
# Probe failed this time — show the last known balance if we have one.
|
|
12362
|
+
balance_usdc = _pod_balance_cache.get(token)
|
|
12363
|
+
|
|
12364
|
+
return {"connected": True, "balance_usdc": balance_usdc}
|
|
12365
|
+
|
|
12366
|
+
|
|
12189
12367
|
class WalletTransferBody(BaseModel):
|
|
12190
12368
|
agent_id: str
|
|
12191
12369
|
to: str
|
|
@@ -39,13 +39,19 @@ USEPOD_API_BASE = "https://api.usepod.ai"
|
|
|
39
39
|
class UsePodProfile(ProviderProfile):
|
|
40
40
|
"""UsePod — OpenAI-compatible proxy with the auth token in the URL path."""
|
|
41
41
|
|
|
42
|
-
def fetch_models(self, *, api_key: str | None = None, timeout: float = 8.0):
|
|
42
|
+
def fetch_models(self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 8.0):
|
|
43
43
|
"""List models from the per-token catalog endpoint.
|
|
44
44
|
|
|
45
45
|
UsePod exposes ``/proxy/<token>/v1/models``; the token lives in the
|
|
46
46
|
path, so the base-class implementation (which builds the URL from the
|
|
47
47
|
static ``base_url``) cannot reach it. Returns None when no token is
|
|
48
48
|
available so callers fall back to ``fallback_models``.
|
|
49
|
+
|
|
50
|
+
``base_url`` is accepted (and ignored — the URL is derived from the
|
|
51
|
+
token) only so the generic picker fetch in ``models.provider_model_ids``
|
|
52
|
+
— which calls ``fetch_models(api_key=…, base_url=…)`` for every api-key
|
|
53
|
+
provider — doesn't raise a TypeError. That TypeError was silently
|
|
54
|
+
swallowed, leaving UsePod with zero models in the picker. (#pod-models)
|
|
49
55
|
"""
|
|
50
56
|
token = (api_key or "").strip()
|
|
51
57
|
if not token:
|
package/agent/scripts/install.sh
CHANGED
|
@@ -172,7 +172,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
172
172
|
echo " --stage NAME Run one desktop bootstrap stage"
|
|
173
173
|
echo " --json Print a JSON result frame for --stage"
|
|
174
174
|
echo " --non-interactive Skip stages that require user input"
|
|
175
|
-
echo " --include-desktop Also build the desktop app (apps/desktop ->
|
|
175
|
+
echo " --include-desktop Also build the desktop app (apps/desktop -> Claw Agent.app)"
|
|
176
176
|
echo " --dir PATH Installation directory"
|
|
177
177
|
echo " default (non-root): ~/.hermes/hermes-agent"
|
|
178
178
|
echo " default (root, Linux): /usr/local/lib/hermes-agent"
|
|
@@ -2579,6 +2579,8 @@ install_desktop() {
|
|
|
2579
2579
|
else
|
|
2580
2580
|
local cand
|
|
2581
2581
|
for cand in \
|
|
2582
|
+
"$desktop_dir/release/mac-arm64/Claw Agent.app" \
|
|
2583
|
+
"$desktop_dir/release/mac/Claw Agent.app" \
|
|
2582
2584
|
"$desktop_dir/release/mac-arm64/Hermes.app" \
|
|
2583
2585
|
"$desktop_dir/release/mac/Hermes.app"; do
|
|
2584
2586
|
if [ -d "$cand" ]; then
|
package/agent/scripts/release.py
CHANGED
|
@@ -46,6 +46,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
|
|
|
46
46
|
# Auto-extracted from noreply emails + manual overrides
|
|
47
47
|
AUTHOR_MAP = {
|
|
48
48
|
"chris@100x.dev": "chris-gilbert",
|
|
49
|
+
"0xtomi204@gmail.com": "tomi204", # ClawPump maintainer (Tomas Oliver)
|
|
49
50
|
"charles@salesondemand.io": "salesondemandio",
|
|
50
51
|
"victor@rocketfueldev.com": "victor-kyriazakos",
|
|
51
52
|
"87440198+JoaoMarcos44@users.noreply.github.com": "JoaoMarcos44",
|
|
@@ -29,6 +29,7 @@ import { Badge } from "@nous-research/ui/ui/components/badge";
|
|
|
29
29
|
import { Card } from "@nous-research/ui/ui/components/card";
|
|
30
30
|
|
|
31
31
|
import { ModelPickerDialog } from "@/components/ModelPickerDialog";
|
|
32
|
+
import PodCredits from "@/components/PodCredits";
|
|
32
33
|
import { ModelReloadConfirm } from "@/components/ModelReloadConfirm";
|
|
33
34
|
import { ReasoningPicker } from "@/components/ReasoningPicker";
|
|
34
35
|
import { ToolCall, type ToolEntry } from "@/components/ToolCall";
|
|
@@ -121,6 +122,7 @@ export function ChatSidebar({
|
|
|
121
122
|
// elsewhere, so the badge would go stale. `/api/model/info` is profile-scoped
|
|
122
123
|
// by `fetchJSON`, so it reads the same profile this sidebar is scoped to.
|
|
123
124
|
const [effectiveModel, setEffectiveModel] = useState("");
|
|
125
|
+
const [effectiveProvider, setEffectiveProvider] = useState("");
|
|
124
126
|
// Whether the effective model supports reasoning effort — gates the
|
|
125
127
|
// ReasoningPicker. Read from the same `/api/model/info` capabilities the
|
|
126
128
|
// (currently unused) ModelInfoCard surfaces, so the dashboard exposes a
|
|
@@ -143,6 +145,7 @@ export function ChatSidebar({
|
|
|
143
145
|
.getModelInfo()
|
|
144
146
|
.then((r) => {
|
|
145
147
|
if (r?.model) setEffectiveModel(String(r.model));
|
|
148
|
+
setEffectiveProvider(r?.provider ? String(r.provider) : "");
|
|
146
149
|
setSupportsReasoning(!!r?.capabilities?.supports_reasoning);
|
|
147
150
|
// Bump so ReasoningPicker re-reads the saved effort for the new model.
|
|
148
151
|
setModelRefreshKey((k) => k + 1);
|
|
@@ -449,6 +452,8 @@ export function ChatSidebar({
|
|
|
449
452
|
</span>
|
|
450
453
|
</Button>
|
|
451
454
|
|
|
455
|
+
<PodCredits provider={effectiveProvider} />
|
|
456
|
+
|
|
452
457
|
<Badge tone={STATE_TONE[state]} className="hidden shrink-0 md:inline-flex">
|
|
453
458
|
{STATE_LABEL[state]}
|
|
454
459
|
</Badge>
|
|
@@ -5,8 +5,9 @@ import { Spinner } from "@nous-research/ui/ui/components/spinner";
|
|
|
5
5
|
import { Input } from "@nous-research/ui/ui/components/input";
|
|
6
6
|
import { Label } from "@nous-research/ui/ui/components/label";
|
|
7
7
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
|
8
|
+
import PodSetupDialog from "@/components/PodSetupDialog";
|
|
8
9
|
import type { GatewayClient } from "@/lib/gatewayClient";
|
|
9
|
-
import { Check, Search, X } from "lucide-react";
|
|
10
|
+
import { Check, Search, X, Zap } from "lucide-react";
|
|
10
11
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
11
12
|
import { createPortal } from "react-dom";
|
|
12
13
|
import { cn, themedBody } from "@/lib/utils";
|
|
@@ -110,6 +111,7 @@ export function ModelPickerDialog(props: Props) {
|
|
|
110
111
|
const [selectedModel, setSelectedModel] = useState("");
|
|
111
112
|
const [query, setQuery] = useState("");
|
|
112
113
|
const [persistGlobal, setPersistGlobal] = useState(alwaysGlobal);
|
|
114
|
+
const [podOpen, setPodOpen] = useState(false);
|
|
113
115
|
const [applying, setApplying] = useState(false);
|
|
114
116
|
const [pendingConfirm, setPendingConfirm] =
|
|
115
117
|
useState<PendingExpensiveConfirm | null>(null);
|
|
@@ -366,6 +368,13 @@ export function ModelPickerDialog(props: Props) {
|
|
|
366
368
|
</div>
|
|
367
369
|
|
|
368
370
|
<footer className="border-t border-border p-3 flex items-center justify-between gap-3 flex-wrap">
|
|
371
|
+
<Button
|
|
372
|
+
outlined
|
|
373
|
+
onClick={() => setPodOpen(true)}
|
|
374
|
+
className="gap-1.5"
|
|
375
|
+
>
|
|
376
|
+
<Zap className="h-3.5 w-3.5 text-primary" /> Set up Pod
|
|
377
|
+
</Button>
|
|
369
378
|
{alwaysGlobal ? (
|
|
370
379
|
<span className="text-xs text-muted-foreground">
|
|
371
380
|
Saves to config.yaml — applies to new sessions.
|
|
@@ -415,6 +424,24 @@ export function ModelPickerDialog(props: Props) {
|
|
|
415
424
|
void applySelection(true, pending);
|
|
416
425
|
}}
|
|
417
426
|
/>
|
|
427
|
+
{podOpen && (
|
|
428
|
+
<PodSetupDialog
|
|
429
|
+
onClose={() => setPodOpen(false)}
|
|
430
|
+
onProvisioned={(model) => {
|
|
431
|
+
// Called from the dialog's "Pod ready" → Done. Backend already set
|
|
432
|
+
// provider=usepod + model in config; switch the live session too
|
|
433
|
+
// (slash in chat mode / onApply in standalone). applySelection calls
|
|
434
|
+
// onClose() → that unmounts this dialog and the whole picker.
|
|
435
|
+
setPodOpen(false);
|
|
436
|
+
void applySelection(false, {
|
|
437
|
+
message: "",
|
|
438
|
+
model,
|
|
439
|
+
persistGlobal: true,
|
|
440
|
+
provider: "usepod",
|
|
441
|
+
});
|
|
442
|
+
}}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
418
445
|
</div>,
|
|
419
446
|
document.body,
|
|
420
447
|
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { api } from "@/lib/api";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compact "Pod $X.XX" credits pill, shown next to the model badge only when the
|
|
6
|
+
* active provider is UsePod (Pod). Polls /api/clawpump/pod/status; keeps the
|
|
7
|
+
* last value so a transient probe failure never blanks it. Mirrors the desktop
|
|
8
|
+
* PodCredits.
|
|
9
|
+
*/
|
|
10
|
+
export default function PodCredits({ provider }: { provider: string }) {
|
|
11
|
+
const isPod = provider === "usepod";
|
|
12
|
+
const [balance, setBalance] = useState<number | null>(null);
|
|
13
|
+
const [connected, setConnected] = useState(false);
|
|
14
|
+
const lastBalance = useRef<number | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!isPod) return;
|
|
18
|
+
let cancelled = false;
|
|
19
|
+
const tick = () => {
|
|
20
|
+
void api
|
|
21
|
+
.getPodStatus()
|
|
22
|
+
.then((r) => {
|
|
23
|
+
if (cancelled) return;
|
|
24
|
+
setConnected(!!r.connected);
|
|
25
|
+
if (r.balance_usdc != null) {
|
|
26
|
+
lastBalance.current = r.balance_usdc;
|
|
27
|
+
setBalance(r.balance_usdc);
|
|
28
|
+
} else if (lastBalance.current != null) {
|
|
29
|
+
setBalance(lastBalance.current);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.catch(() => undefined);
|
|
33
|
+
};
|
|
34
|
+
tick();
|
|
35
|
+
const id = window.setInterval(tick, 60_000);
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true;
|
|
38
|
+
window.clearInterval(id);
|
|
39
|
+
};
|
|
40
|
+
}, [isPod]);
|
|
41
|
+
|
|
42
|
+
if (!isPod || !connected) return null;
|
|
43
|
+
|
|
44
|
+
const low = balance != null && balance < 0.5;
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
title={balance != null ? `Pod credits: $${balance.toFixed(4)} USDC` : "Pod connected"}
|
|
48
|
+
className={`inline-flex shrink-0 items-center gap-1 rounded-md border px-1.5 py-0.5 text-[0.6875rem] font-medium ${
|
|
49
|
+
low
|
|
50
|
+
? "border-amber-500/40 bg-amber-500/10 text-amber-300"
|
|
51
|
+
: "border-emerald-500/30 bg-emerald-500/10 text-emerald-300"
|
|
52
|
+
}`}
|
|
53
|
+
>
|
|
54
|
+
⚡ {balance != null ? `$${balance.toFixed(2)}` : "Pod"}
|
|
55
|
+
</span>
|
|
56
|
+
);
|
|
57
|
+
}
|