@clawpump/claw-agent 0.1.7 → 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/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 +65 -65
- package/agent/apps/desktop/index.html +1 -1
- package/agent/apps/desktop/package.json +11 -11
- 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/set-exe-identity.cjs +2 -2
- 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/providers.py +29 -0
- package/agent/hermes_cli/web_server.py +403 -34
- package/agent/plugins/model-providers/usepod/__init__.py +7 -1
- package/agent/scripts/release.py +1 -0
- package/agent/web/public/claw-logo.png +0 -0
- package/agent/web/src/App.tsx +6 -4
- 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 +135 -0
- package/agent/web/src/pages/AgentMailPage.tsx +684 -0
- package/agent/web/src/pages/WalletPage.tsx +53 -5
- package/package.json +1 -1
|
@@ -189,7 +189,7 @@ async function checkRuntime(ctx: OnboardingContext): Promise<RuntimeReadinessRes
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
function notifyReady(provider: string) {
|
|
192
|
-
notify({ kind: 'success', title: '
|
|
192
|
+
notify({ kind: 'success', title: 'Claw Agent is ready', message: `${provider} connected.` })
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Human-friendly labels for tools auto-routed through the Nous Tool Gateway,
|
|
@@ -360,8 +360,8 @@ function providerResolutionFailure(reason: null | string) {
|
|
|
360
360
|
const detail = reason?.trim()
|
|
361
361
|
|
|
362
362
|
return detail
|
|
363
|
-
? `Connected, but
|
|
364
|
-
: 'Connected, but
|
|
363
|
+
? `Connected, but Claw Agent still cannot resolve a usable provider. ${detail}`
|
|
364
|
+
: 'Connected, but Claw Agent still cannot resolve a usable provider.'
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
async function refreshProviders() {
|
|
@@ -722,7 +722,7 @@ export async function recheckExternalSignin(ctx: OnboardingContext) {
|
|
|
722
722
|
provider,
|
|
723
723
|
message:
|
|
724
724
|
reason?.trim() ||
|
|
725
|
-
`
|
|
725
|
+
`Claw Agent still cannot reach ${provider.name}. Run \`${provider.cli_command}\` in a terminal first.`
|
|
726
726
|
})
|
|
727
727
|
)
|
|
728
728
|
}
|
|
@@ -837,7 +837,7 @@ export async function saveOnboardingLocalEndpoint(baseUrl: string, apiKey: strin
|
|
|
837
837
|
if (!runtime.ready) {
|
|
838
838
|
const detail = (runtime.reason ?? '').trim()
|
|
839
839
|
|
|
840
|
-
return { ok: false, message: detail || `Saved, but
|
|
840
|
+
return { ok: false, message: detail || `Saved, but Claw Agent still cannot reach ${url}.` }
|
|
841
841
|
}
|
|
842
842
|
|
|
843
843
|
notifyReady('Local / custom endpoint')
|
|
@@ -21,75 +21,75 @@ const SYSTEM_MONO =
|
|
|
21
21
|
|
|
22
22
|
export const DEFAULT_TYPOGRAPHY: DesktopThemeTypography = { fontSans: SYSTEM_SANS, fontMono: SYSTEM_MONO }
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
24
|
+
const CLAW_GREEN = '#22C55E'
|
|
25
|
+
const CLAW_GREEN_LIGHT = '#4ADE80'
|
|
26
|
+
const CLAW_GREEN_DEEP = '#16A34A'
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const clawTint = (pct: number) => `color-mix(in srgb, ${CLAW_GREEN} ${pct}%, #FFFFFF)`
|
|
29
|
+
const clawTintTransparent = (pct: number) => `color-mix(in srgb, ${CLAW_GREEN} ${pct}%, transparent)`
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Claw Agent — canonical ClawPump desktop identity (built on Hermes by Nous
|
|
33
|
+
* Research). Solana-green accents on glass neutrals in light mode, deep
|
|
34
|
+
* ClawPump green in dark mode.
|
|
35
35
|
*/
|
|
36
36
|
export const nousTheme: DesktopTheme = {
|
|
37
37
|
name: 'nous',
|
|
38
|
-
label: '
|
|
39
|
-
description: '
|
|
38
|
+
label: 'Claw Agent',
|
|
39
|
+
description: 'ClawPump green on glass neutrals',
|
|
40
40
|
colors: {
|
|
41
|
-
background: '#
|
|
42
|
-
foreground: '#
|
|
41
|
+
background: '#F6FBF8',
|
|
42
|
+
foreground: '#13201A',
|
|
43
43
|
card: '#FFFFFF',
|
|
44
|
-
cardForeground: '#
|
|
45
|
-
muted:
|
|
46
|
-
mutedForeground: '#
|
|
44
|
+
cardForeground: '#13201A',
|
|
45
|
+
muted: clawTint(5),
|
|
46
|
+
mutedForeground: '#5B6B62',
|
|
47
47
|
popover: '#FFFFFF',
|
|
48
|
-
popoverForeground: '#
|
|
49
|
-
primary:
|
|
50
|
-
primaryForeground: '#
|
|
51
|
-
secondary:
|
|
52
|
-
secondaryForeground: '#
|
|
53
|
-
accent:
|
|
54
|
-
accentForeground: '#
|
|
55
|
-
border:
|
|
56
|
-
input:
|
|
57
|
-
ring:
|
|
58
|
-
midground:
|
|
59
|
-
composerRing:
|
|
48
|
+
popoverForeground: '#13201A',
|
|
49
|
+
primary: CLAW_GREEN,
|
|
50
|
+
primaryForeground: '#FCFFFD',
|
|
51
|
+
secondary: clawTint(7),
|
|
52
|
+
secondaryForeground: '#1E2A24',
|
|
53
|
+
accent: clawTint(10),
|
|
54
|
+
accentForeground: '#1B2620',
|
|
55
|
+
border: clawTintTransparent(22),
|
|
56
|
+
input: clawTintTransparent(30),
|
|
57
|
+
ring: CLAW_GREEN,
|
|
58
|
+
midground: CLAW_GREEN,
|
|
59
|
+
composerRing: CLAW_GREEN,
|
|
60
60
|
destructive: '#C72E4D',
|
|
61
61
|
destructiveForeground: '#FFFFFF',
|
|
62
|
-
sidebarBackground: '#
|
|
63
|
-
sidebarBorder:
|
|
64
|
-
userBubble:
|
|
65
|
-
userBubbleBorder:
|
|
62
|
+
sidebarBackground: '#F1FAF4',
|
|
63
|
+
sidebarBorder: clawTintTransparent(18),
|
|
64
|
+
userBubble: clawTint(6),
|
|
65
|
+
userBubbleBorder: clawTintTransparent(24)
|
|
66
66
|
},
|
|
67
67
|
darkColors: {
|
|
68
|
-
background: '#
|
|
69
|
-
foreground:
|
|
70
|
-
card: '#
|
|
71
|
-
cardForeground:
|
|
72
|
-
muted: '#
|
|
73
|
-
mutedForeground: '#
|
|
74
|
-
popover: '#
|
|
75
|
-
popoverForeground:
|
|
76
|
-
primary:
|
|
77
|
-
primaryForeground: '#
|
|
78
|
-
secondary: '#
|
|
79
|
-
secondaryForeground: '#
|
|
80
|
-
accent:
|
|
81
|
-
accentForeground: '#
|
|
82
|
-
border: '#
|
|
83
|
-
input: '#
|
|
84
|
-
ring:
|
|
85
|
-
midground:
|
|
86
|
-
composerRing:
|
|
68
|
+
background: '#0B1F14',
|
|
69
|
+
foreground: '#DCFCE7',
|
|
70
|
+
card: '#0F2A1B',
|
|
71
|
+
cardForeground: '#DCFCE7',
|
|
72
|
+
muted: '#15301F',
|
|
73
|
+
mutedForeground: '#86B89C',
|
|
74
|
+
popover: '#0F2A1B',
|
|
75
|
+
popoverForeground: '#DCFCE7',
|
|
76
|
+
primary: CLAW_GREEN_LIGHT,
|
|
77
|
+
primaryForeground: '#06140C',
|
|
78
|
+
secondary: '#173B26',
|
|
79
|
+
secondaryForeground: '#D6F5E1',
|
|
80
|
+
accent: CLAW_GREEN_DEEP,
|
|
81
|
+
accentForeground: '#ECFDF3',
|
|
82
|
+
border: '#1E5236',
|
|
83
|
+
input: '#0A1B11',
|
|
84
|
+
ring: CLAW_GREEN_LIGHT,
|
|
85
|
+
midground: CLAW_GREEN,
|
|
86
|
+
composerRing: CLAW_GREEN_LIGHT,
|
|
87
87
|
destructive: '#C0473A',
|
|
88
88
|
destructiveForeground: '#FEF2F2',
|
|
89
|
-
sidebarBackground: '#
|
|
90
|
-
sidebarBorder: '#
|
|
91
|
-
userBubble: '#
|
|
92
|
-
userBubbleBorder: '#
|
|
89
|
+
sidebarBackground: '#081A10',
|
|
90
|
+
sidebarBorder: '#1B4A30',
|
|
91
|
+
userBubble: '#143625',
|
|
92
|
+
userBubbleBorder: '#2E6B47'
|
|
93
93
|
},
|
|
94
94
|
typography: {
|
|
95
95
|
fontSans: SYSTEM_SANS,
|
package/agent/cli.py
CHANGED
|
@@ -2247,6 +2247,13 @@ def _replay_output_history() -> None:
|
|
|
2247
2247
|
_OUTPUT_HISTORY_REPLAYING = False
|
|
2248
2248
|
|
|
2249
2249
|
|
|
2250
|
+
# Internal sentinel enqueued by the model picker to run the deterministic Pod
|
|
2251
|
+
# setup on the process_loop worker thread (see process_command). Not a
|
|
2252
|
+
# user-facing command; the leading slash makes process_loop route it to
|
|
2253
|
+
# process_command rather than treating it as agent chat input.
|
|
2254
|
+
_POD_SETUP_COMMAND = "/__clawpump_pod_setup__"
|
|
2255
|
+
|
|
2256
|
+
|
|
2250
2257
|
def _cprint(text: str):
|
|
2251
2258
|
"""Print ANSI-colored text through prompt_toolkit's native renderer.
|
|
2252
2259
|
|
|
@@ -7009,6 +7016,147 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|
|
7009
7016
|
else:
|
|
7010
7017
|
_cprint(" (session only — add --global to persist)")
|
|
7011
7018
|
|
|
7019
|
+
def _run_pod_setup_flow(self) -> bool:
|
|
7020
|
+
"""Deterministic Pod setup (mirrors the desktop modal): pick wallet →
|
|
7021
|
+
amount → confirm → provision + fund on-chain → switch onto Pod.
|
|
7022
|
+
|
|
7023
|
+
MUST be called on the process_loop worker thread (it is — via the
|
|
7024
|
+
_POD_SETUP_COMMAND sentinel in process_command), NOT the prompt_toolkit
|
|
7025
|
+
event-loop thread: it uses ``_prompt_text_input_modal`` (the app-native
|
|
7026
|
+
modal billing uses) for every step, and the ~30-90s blocking provision
|
|
7027
|
+
runs here without freezing the TUI (the event loop is the main thread).
|
|
7028
|
+
|
|
7029
|
+
Returns True when handled (success OR user cancel); False to fall back to
|
|
7030
|
+
the chat-guided flow (no interactive app, MCP unavailable, no wallets).
|
|
7031
|
+
"""
|
|
7032
|
+
if not getattr(self, "_app", None):
|
|
7033
|
+
return False # non-interactive — let the chat-guided flow handle it
|
|
7034
|
+
try:
|
|
7035
|
+
from hermes_cli import distribution as _dist
|
|
7036
|
+
except Exception:
|
|
7037
|
+
return False
|
|
7038
|
+
|
|
7039
|
+
try:
|
|
7040
|
+
wallets = _dist.usepod_fetch_wallets()
|
|
7041
|
+
except Exception:
|
|
7042
|
+
logger.debug("Pod setup: wallet fetch failed", exc_info=True)
|
|
7043
|
+
return False
|
|
7044
|
+
if not wallets:
|
|
7045
|
+
return False
|
|
7046
|
+
|
|
7047
|
+
# 1. Pick the funding wallet (default = most-funded, listed first).
|
|
7048
|
+
wallet_choices = [
|
|
7049
|
+
(
|
|
7050
|
+
w["agent_id"],
|
|
7051
|
+
f"{w.get('name') or (w.get('agent_id') or '')[:8]} — ${(w.get('usdc_balance') or 0):.2f} USDC",
|
|
7052
|
+
"fund the pod from this wallet",
|
|
7053
|
+
)
|
|
7054
|
+
for w in wallets
|
|
7055
|
+
]
|
|
7056
|
+
wallet_choices.append(("cancel", "Cancel", "do nothing"))
|
|
7057
|
+
raw = self._prompt_text_input_modal(
|
|
7058
|
+
title="⚡ Set up Pod — pay from which agent wallet?",
|
|
7059
|
+
detail="Pod is pay-as-you-go inference funded from a ClawPump wallet.",
|
|
7060
|
+
choices=wallet_choices,
|
|
7061
|
+
)
|
|
7062
|
+
agent_id = self._normalize_slash_confirm_choice(raw, wallet_choices)
|
|
7063
|
+
if not agent_id or agent_id == "cancel":
|
|
7064
|
+
_cprint(" 🟡 Pod setup cancelled.")
|
|
7065
|
+
return True
|
|
7066
|
+
wallet = next((w for w in wallets if w["agent_id"] == agent_id), None)
|
|
7067
|
+
if not wallet:
|
|
7068
|
+
_cprint(" 🟡 Pod setup cancelled.")
|
|
7069
|
+
return True
|
|
7070
|
+
|
|
7071
|
+
# 2. Amount — preset choices (a text modal off the main thread can't
|
|
7072
|
+
# reliably read free text, so offer presets bounded by the balance).
|
|
7073
|
+
bal = wallet.get("usdc_balance") or 0
|
|
7074
|
+
amount_choices = [
|
|
7075
|
+
(str(a), f"${a:.0f} USDC", f"fund ${a:.0f}")
|
|
7076
|
+
for a in (5, 10, 25, 50)
|
|
7077
|
+
if a <= bal
|
|
7078
|
+
]
|
|
7079
|
+
if not amount_choices:
|
|
7080
|
+
# Balance under $5 — offer the whole balance (min 0.5) or cancel.
|
|
7081
|
+
usable = round(max(bal, 0), 2)
|
|
7082
|
+
if usable < 0.5:
|
|
7083
|
+
_cprint(f" 🔴 Wallet only holds ${bal:.2f} USDC — fund it first.")
|
|
7084
|
+
return True
|
|
7085
|
+
amount_choices = [(str(usable), f"${usable:.2f} USDC (full balance)", "fund the balance")]
|
|
7086
|
+
amount_choices.append(("cancel", "Cancel", "do nothing"))
|
|
7087
|
+
raw = self._prompt_text_input_modal(
|
|
7088
|
+
title=f"⚡ Fund amount (wallet has ${bal:.2f} USDC)",
|
|
7089
|
+
detail="The pod holds a prepaid USDC balance you draw down per request.",
|
|
7090
|
+
choices=amount_choices,
|
|
7091
|
+
)
|
|
7092
|
+
amt_choice = self._normalize_slash_confirm_choice(raw, amount_choices)
|
|
7093
|
+
if not amt_choice or amt_choice == "cancel":
|
|
7094
|
+
_cprint(" 🟡 Pod setup cancelled.")
|
|
7095
|
+
return True
|
|
7096
|
+
try:
|
|
7097
|
+
amount = float(amt_choice)
|
|
7098
|
+
if amount <= 0:
|
|
7099
|
+
raise ValueError
|
|
7100
|
+
except ValueError:
|
|
7101
|
+
_cprint(" 🟡 Pod setup cancelled.")
|
|
7102
|
+
return True
|
|
7103
|
+
|
|
7104
|
+
# 3. Final confirm — the on-chain spend.
|
|
7105
|
+
confirm_choices = [
|
|
7106
|
+
("pay", f"Fund ${amount:.2f} & use Pod", "submit the on-chain deposit"),
|
|
7107
|
+
("cancel", "Cancel", "do not spend"),
|
|
7108
|
+
]
|
|
7109
|
+
raw = self._prompt_text_input_modal(
|
|
7110
|
+
title="⚡ Confirm Pod funding",
|
|
7111
|
+
detail=f"Spend ${amount:.2f} USDC from '{wallet.get('name') or agent_id[:8]}' on-chain. Irreversible.",
|
|
7112
|
+
choices=confirm_choices,
|
|
7113
|
+
)
|
|
7114
|
+
if self._normalize_slash_confirm_choice(raw, confirm_choices) != "pay":
|
|
7115
|
+
_cprint(" 🟡 Pod setup cancelled. No funds moved.")
|
|
7116
|
+
return True
|
|
7117
|
+
|
|
7118
|
+
# 4. Provision + fund (blocking ~30-90s — fine on this worker thread).
|
|
7119
|
+
_cprint(f" ⚡ Provisioning Pod and funding ${amount:.2f} USDC on-chain… (~30-60s)")
|
|
7120
|
+
try:
|
|
7121
|
+
res = _dist.usepod_provision_call(wallet["agent_id"], amount)
|
|
7122
|
+
except Exception as exc:
|
|
7123
|
+
_cprint(f" 🔴 Pod setup failed: {exc}")
|
|
7124
|
+
return True
|
|
7125
|
+
|
|
7126
|
+
api_token = res.get("api_token") or ""
|
|
7127
|
+
if not api_token:
|
|
7128
|
+
_cprint(f" 🔴 Pod setup failed: {res.get('funding_error') or 'no token returned'}")
|
|
7129
|
+
return True
|
|
7130
|
+
|
|
7131
|
+
# 5. Persist + switch. Persist FIRST so a later switch error never loses
|
|
7132
|
+
# a token the user already paid for; guard the switch separately so it
|
|
7133
|
+
# can't bubble to the picker handler and double-handle as chat.
|
|
7134
|
+
_dist.persist_usepod_credentials(api_token, res.get("deposit_code") or "")
|
|
7135
|
+
try:
|
|
7136
|
+
model, _provider, base_url = _dist.usepod_pod_switch_target(
|
|
7137
|
+
api_token, getattr(self, "model", "") or ""
|
|
7138
|
+
)
|
|
7139
|
+
self._pending_pod_activation = {
|
|
7140
|
+
"model": model,
|
|
7141
|
+
"provider": "usepod",
|
|
7142
|
+
"base_url": base_url,
|
|
7143
|
+
"api_key": api_token,
|
|
7144
|
+
}
|
|
7145
|
+
self._activate_pending_pod()
|
|
7146
|
+
except Exception:
|
|
7147
|
+
logger.debug("Pod setup: switch failed (token persisted)", exc_info=True)
|
|
7148
|
+
_cprint(" ✓ Pod funded and saved. Re-open the model picker to select a Pod model.")
|
|
7149
|
+
return True
|
|
7150
|
+
|
|
7151
|
+
if res.get("funding_error"):
|
|
7152
|
+
_cprint(
|
|
7153
|
+
f" ✓ Pod created and selected ({model}). "
|
|
7154
|
+
f"⚠ funding issue: {res['funding_error']} — top it up and retry."
|
|
7155
|
+
)
|
|
7156
|
+
else:
|
|
7157
|
+
_cprint(f" ✓ Pod ready — funded ${amount:.2f} USDC, now using {model}.")
|
|
7158
|
+
return True
|
|
7159
|
+
|
|
7012
7160
|
def _handle_model_picker_selection(self, persist_global: bool = False) -> None:
|
|
7013
7161
|
state = self._model_picker_state
|
|
7014
7162
|
if not state:
|
|
@@ -7027,16 +7175,16 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|
|
7027
7175
|
# failing switch. Hand the agent the configure instruction.
|
|
7028
7176
|
if provider_data.get("clawpump_setup"):
|
|
7029
7177
|
self._close_model_picker()
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7178
|
+
# Run the deterministic Pod setup (pick wallet → amount → confirm
|
|
7179
|
+
# → provision) on the process_loop background thread, NOT here:
|
|
7180
|
+
# this handler runs on the prompt_toolkit main/event-loop thread,
|
|
7181
|
+
# where the interactive curses pickers + the ~30-90s on-chain
|
|
7182
|
+
# provision would freeze the TUI. Enqueuing the internal command
|
|
7183
|
+
# routes it through process_command() on the worker thread — the
|
|
7184
|
+
# same path the /billing interactive flow uses safely. The flow
|
|
7185
|
+
# itself falls back to the chat-guided message if it can't run.
|
|
7038
7186
|
if hasattr(self, "_pending_input"):
|
|
7039
|
-
self._pending_input.put(
|
|
7187
|
+
self._pending_input.put(_POD_SETUP_COMMAND)
|
|
7040
7188
|
return
|
|
7041
7189
|
# Use the curated model list from list_authenticated_providers()
|
|
7042
7190
|
# (same lists as `hermes model` and gateway pickers).
|
|
@@ -7489,6 +7637,29 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|
|
7489
7637
|
cmd_lower = command.lower().strip()
|
|
7490
7638
|
cmd_original = command.strip()
|
|
7491
7639
|
|
|
7640
|
+
# Internal: deterministic Pod setup, enqueued by the model picker so it
|
|
7641
|
+
# runs HERE on the process_loop worker thread (not the keybinding/event
|
|
7642
|
+
# thread). On this thread the curses pickers do direct terminal I/O and
|
|
7643
|
+
# the slow on-chain provision doesn't block the TUI event loop.
|
|
7644
|
+
if cmd_original == _POD_SETUP_COMMAND:
|
|
7645
|
+
try:
|
|
7646
|
+
if self._run_pod_setup_flow():
|
|
7647
|
+
return True
|
|
7648
|
+
except Exception:
|
|
7649
|
+
logger.debug("Pod setup flow failed; falling back to chat", exc_info=True)
|
|
7650
|
+
# Couldn't run deterministically — hand the agent the guided message.
|
|
7651
|
+
try:
|
|
7652
|
+
from hermes_cli.distribution import usepod_setup_request_message
|
|
7653
|
+
_msg = usepod_setup_request_message()
|
|
7654
|
+
except Exception:
|
|
7655
|
+
_msg = (
|
|
7656
|
+
"Help me set up the Pod (UsePod) provider step by step and "
|
|
7657
|
+
"fund it from my ClawPump wallet."
|
|
7658
|
+
)
|
|
7659
|
+
if hasattr(self, "_pending_input"):
|
|
7660
|
+
self._pending_input.put(_msg)
|
|
7661
|
+
return True
|
|
7662
|
+
|
|
7492
7663
|
# Resolve aliases via central registry so adding an alias is a one-line
|
|
7493
7664
|
# change in hermes_cli/commands.py instead of touching every dispatch site.
|
|
7494
7665
|
from hermes_cli.commands import resolve_command as _resolve_cmd
|
|
@@ -14006,7 +14177,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|
|
14006
14177
|
continue
|
|
14007
14178
|
|
|
14008
14179
|
if not _file_drop and isinstance(user_input, str) and _looks_like_slash_command(user_input):
|
|
14009
|
-
|
|
14180
|
+
# Internal sentinels (e.g. the Pod-setup hand-off from the
|
|
14181
|
+
# model picker) aren't user commands — don't echo them.
|
|
14182
|
+
if user_input != _POD_SETUP_COMMAND:
|
|
14183
|
+
_cprint(f"\n⚙️ {user_input}")
|
|
14010
14184
|
try:
|
|
14011
14185
|
if not self.process_command(user_input):
|
|
14012
14186
|
self._should_exit = True
|
|
@@ -406,29 +406,49 @@ def usepod_provision_token(function_name: str, function_result: Any):
|
|
|
406
406
|
def usepod_pod_switch_target(api_token: str, current_model: str = ""):
|
|
407
407
|
"""Return ``(model, provider, base_url)`` to switch onto for a Pod token.
|
|
408
408
|
|
|
409
|
-
Keeps the user's current model when
|
|
410
|
-
otherwise
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
Keeps the user's current model only when the pod's LIVE catalog actually
|
|
410
|
+
serves it; otherwise picks a pod-served model. Using the live catalog (not
|
|
411
|
+
the static ``fallback_models``) matters: the static list includes names like
|
|
412
|
+
``gpt-5.5`` that the pod does NOT serve, so keeping the user's old model
|
|
413
|
+
there left them on a model the pod can't run — the runtime then fell back to
|
|
414
|
+
that model's native provider (e.g. Codex) and errored. The secret-bearing
|
|
415
|
+
base_url is derived from the token (never persisted to config.yaml).
|
|
413
416
|
"""
|
|
414
417
|
provider = "usepod"
|
|
415
|
-
|
|
418
|
+
pp = None
|
|
416
419
|
try:
|
|
417
420
|
from providers import get_provider_profile
|
|
418
421
|
|
|
419
422
|
pp = get_provider_profile(provider)
|
|
420
|
-
|
|
423
|
+
except Exception:
|
|
424
|
+
pp = None
|
|
425
|
+
|
|
426
|
+
# Live catalog first (what the pod really serves), then the static list.
|
|
427
|
+
models = []
|
|
428
|
+
try:
|
|
429
|
+
if pp is not None and api_token:
|
|
430
|
+
live = pp.fetch_models(api_key=api_token)
|
|
431
|
+
if live:
|
|
432
|
+
models = list(live)
|
|
421
433
|
except Exception:
|
|
422
434
|
models = []
|
|
435
|
+
if not models:
|
|
436
|
+
models = list(getattr(pp, "fallback_models", ()) or []) if pp else []
|
|
423
437
|
|
|
424
438
|
model = ""
|
|
425
439
|
cur = (current_model or "").strip()
|
|
426
440
|
if cur and cur in models:
|
|
427
441
|
model = cur
|
|
428
442
|
elif models:
|
|
429
|
-
|
|
443
|
+
# Prefer a sensible default if present, else the first served model.
|
|
444
|
+
for pref in ("claude-opus-4-8", "claude-sonnet-4-6"):
|
|
445
|
+
if pref in models:
|
|
446
|
+
model = pref
|
|
447
|
+
break
|
|
448
|
+
if not model:
|
|
449
|
+
model = models[0]
|
|
430
450
|
else:
|
|
431
|
-
model =
|
|
451
|
+
model = "claude-opus-4-8"
|
|
432
452
|
|
|
433
453
|
base_url = ""
|
|
434
454
|
try:
|
|
@@ -455,3 +475,163 @@ def persist_usepod_credentials(api_token: str, deposit_code: str = "") -> bool:
|
|
|
455
475
|
return True
|
|
456
476
|
except Exception:
|
|
457
477
|
return False
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ── Deterministic Pod setup (terminal flow, mirrors the desktop modal) ─────
|
|
481
|
+
# Lets the TUI run a clean "pick wallet → amount → confirm → provision" flow
|
|
482
|
+
# directly, instead of injecting a chat message and asking the agent to do it.
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _clawpump_mcp_config():
|
|
486
|
+
"""Return (server_name, resolved_config) for the ClawPump MCP, or (None, None)."""
|
|
487
|
+
try:
|
|
488
|
+
from hermes_cli.mcp_config import _get_mcp_servers, _resolve_mcp_server_config
|
|
489
|
+
|
|
490
|
+
servers = _get_mcp_servers()
|
|
491
|
+
name = next((n for n in ("clawpump", "clawpump-stdio") if n in servers), None)
|
|
492
|
+
if not name:
|
|
493
|
+
return (None, None)
|
|
494
|
+
return (name, _resolve_mcp_server_config(servers[name]))
|
|
495
|
+
except Exception:
|
|
496
|
+
return (None, None)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _clawpump_tool_struct(tool: str, arguments=None, *, timeout: float = 120):
|
|
500
|
+
"""Call a ClawPump MCP tool, returning its structuredContent when present.
|
|
501
|
+
|
|
502
|
+
Like ``mcp_config._call_single_tool`` but keeps ``structuredContent`` — the
|
|
503
|
+
machine channel where ``usepod_provision`` puts the REAL api_token (the text
|
|
504
|
+
channel redacts it). Falls back to the parsed text JSON otherwise. Blocking.
|
|
505
|
+
"""
|
|
506
|
+
import json as _json
|
|
507
|
+
|
|
508
|
+
name, config = _clawpump_mcp_config()
|
|
509
|
+
if not name:
|
|
510
|
+
raise RuntimeError("ClawPump MCP is not configured. Run `claw clawpump setup`.")
|
|
511
|
+
|
|
512
|
+
from tools.mcp_tool import (
|
|
513
|
+
_ensure_mcp_loop,
|
|
514
|
+
_run_on_mcp_loop,
|
|
515
|
+
_connect_server,
|
|
516
|
+
_stop_mcp_loop_if_idle,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
_ensure_mcp_loop()
|
|
520
|
+
out = {"structured": None, "text": "", "error": None}
|
|
521
|
+
|
|
522
|
+
async def _call():
|
|
523
|
+
import asyncio as _asyncio
|
|
524
|
+
|
|
525
|
+
server = await _asyncio.wait_for(_connect_server(name, config), timeout=timeout)
|
|
526
|
+
try:
|
|
527
|
+
result = await server.session.call_tool(tool, arguments=arguments or {})
|
|
528
|
+
text = "".join(b.text for b in (result.content or []) if hasattr(b, "text"))
|
|
529
|
+
if getattr(result, "isError", False):
|
|
530
|
+
out["error"] = text or "MCP tool returned an error"
|
|
531
|
+
return
|
|
532
|
+
out["text"] = text
|
|
533
|
+
sc = getattr(result, "structuredContent", None)
|
|
534
|
+
if isinstance(sc, dict) and sc:
|
|
535
|
+
out["structured"] = sc
|
|
536
|
+
finally:
|
|
537
|
+
await server.shutdown()
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
_run_on_mcp_loop(_call(), timeout=timeout + 30)
|
|
541
|
+
finally:
|
|
542
|
+
try:
|
|
543
|
+
_stop_mcp_loop_if_idle()
|
|
544
|
+
except Exception:
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
if out["error"]:
|
|
548
|
+
raise RuntimeError(out["error"])
|
|
549
|
+
# Both channels: structuredContent (machine; real secrets) AND the text
|
|
550
|
+
# (human; may redact secrets). Callers that need a secret check structured
|
|
551
|
+
# first, then fall back to the text — never drop one silently.
|
|
552
|
+
return {"structured": out["structured"], "text": out["text"]}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def usepod_fetch_wallets():
|
|
556
|
+
"""Return [{agent_id, name, usdc_balance}] for the user's ClawPump wallets.
|
|
557
|
+
|
|
558
|
+
Sorted by USDC balance (most-funded first) so the picker can default to it.
|
|
559
|
+
"""
|
|
560
|
+
summaries = _channel_payload(_clawpump_tool_struct("get_wallet_summaries", {}, timeout=30))
|
|
561
|
+
if isinstance(summaries, dict):
|
|
562
|
+
summaries = summaries.get("wallets") or summaries.get("summaries") or []
|
|
563
|
+
rows = [w for w in (summaries or []) if isinstance(w, dict) and w.get("agent_id")]
|
|
564
|
+
|
|
565
|
+
# Enrich with display names (summaries carry only the agent_id UUID).
|
|
566
|
+
try:
|
|
567
|
+
agents = _channel_payload(_clawpump_tool_struct("list_agents", {}, timeout=30))
|
|
568
|
+
if isinstance(agents, dict):
|
|
569
|
+
agents = agents.get("agents") or []
|
|
570
|
+
names = {a.get("id"): a.get("name") for a in (agents or []) if isinstance(a, dict)}
|
|
571
|
+
for w in rows:
|
|
572
|
+
w["name"] = names.get(w.get("agent_id")) or w.get("name")
|
|
573
|
+
except Exception:
|
|
574
|
+
pass
|
|
575
|
+
|
|
576
|
+
rows.sort(key=lambda w: (w.get("usdc_balance") or 0), reverse=True)
|
|
577
|
+
return rows
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _channel_payload(channels):
|
|
581
|
+
"""Pick the usable object from a ``{structured, text}`` channel result:
|
|
582
|
+
structuredContent if present, else the parsed text JSON, else the raw."""
|
|
583
|
+
if not isinstance(channels, dict) or ("structured" not in channels and "text" not in channels):
|
|
584
|
+
return channels # already a plain payload (back-compat)
|
|
585
|
+
sc = channels.get("structured")
|
|
586
|
+
if isinstance(sc, dict) and sc:
|
|
587
|
+
return sc
|
|
588
|
+
text = channels.get("text")
|
|
589
|
+
if isinstance(text, str) and text:
|
|
590
|
+
try:
|
|
591
|
+
import json as _json
|
|
592
|
+
|
|
593
|
+
return _json.loads(text)
|
|
594
|
+
except Exception:
|
|
595
|
+
return text
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def usepod_provision_call(agent_id: str, amount: float) -> dict:
|
|
600
|
+
"""Register + fund a Pod from ``agent_id``'s wallet. Spends on-chain USDC.
|
|
601
|
+
|
|
602
|
+
Returns the parsed result with the REAL api_token. The token lives in
|
|
603
|
+
``structuredContent`` (the text channel redacts it), but we check BOTH —
|
|
604
|
+
structured first, then the text via the regex extractor — so a money-spent
|
|
605
|
+
provision never reports "no token" just because the channels were swapped.
|
|
606
|
+
"""
|
|
607
|
+
channels = _clawpump_tool_struct(
|
|
608
|
+
"usepod_provision",
|
|
609
|
+
{"agent_id": agent_id, "amount": amount, "confirm_deposit": True},
|
|
610
|
+
timeout=120,
|
|
611
|
+
)
|
|
612
|
+
data = _channel_payload(channels)
|
|
613
|
+
if not isinstance(data, dict):
|
|
614
|
+
data = {}
|
|
615
|
+
|
|
616
|
+
def _real(tok: str) -> str:
|
|
617
|
+
# The redacted placeholder ("<applied… hidden>") is truthy but useless —
|
|
618
|
+
# treat it as no-token so we never persist it as a working credential.
|
|
619
|
+
tok = (tok or "").strip()
|
|
620
|
+
return "" if (not tok or tok.startswith("<") or "applied" in tok.lower()) else tok
|
|
621
|
+
|
|
622
|
+
# 1. structuredContent dict, 2. its nested wrappers, 3. the raw text regex.
|
|
623
|
+
api_token = _real(str(data.get("api_token") or data.get("token") or ""))
|
|
624
|
+
if not api_token:
|
|
625
|
+
tok = usepod_provision_token("mcp_clawpump_usepod_provision", data)
|
|
626
|
+
api_token = _real(tok[0] if tok else "")
|
|
627
|
+
if not api_token and isinstance(channels, dict):
|
|
628
|
+
tok = usepod_provision_token("mcp_clawpump_usepod_provision", channels.get("text") or "")
|
|
629
|
+
api_token = _real(tok[0] if tok else "")
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
"api_token": api_token,
|
|
633
|
+
"deposit_code": str(data.get("deposit_code") or "").strip(),
|
|
634
|
+
"signature": str(data.get("signature") or ""),
|
|
635
|
+
"funding_error": str(data.get("funding_error") or ""),
|
|
636
|
+
"amount": data.get("amount"),
|
|
637
|
+
}
|
|
@@ -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
|