@clawpump/claw-agent 0.1.8 → 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.
Files changed (60) hide show
  1. package/agent/.mailmap +4 -0
  2. package/agent/apps/desktop/README.md +3 -3
  3. package/agent/apps/desktop/assets/icon.icns +0 -0
  4. package/agent/apps/desktop/assets/icon.ico +0 -0
  5. package/agent/apps/desktop/assets/icon.png +0 -0
  6. package/agent/apps/desktop/electron/backend-ready.cjs +2 -2
  7. package/agent/apps/desktop/electron/dashboard-token.cjs +3 -3
  8. package/agent/apps/desktop/electron/hardening.cjs +1 -1
  9. package/agent/apps/desktop/electron/main.cjs +65 -65
  10. package/agent/apps/desktop/index.html +1 -1
  11. package/agent/apps/desktop/package.json +11 -11
  12. package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
  13. package/agent/apps/desktop/public/claw-mark.png +0 -0
  14. package/agent/apps/desktop/scripts/set-exe-identity.cjs +2 -2
  15. package/agent/apps/desktop/src/app/chat/composer/controls.tsx +2 -0
  16. package/agent/apps/desktop/src/app/chat/composer/index.tsx +10 -0
  17. package/agent/apps/desktop/src/app/chat/composer/pod-credits.tsx +49 -0
  18. package/agent/apps/desktop/src/app/chat/index.tsx +1 -1
  19. package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +4 -2
  20. package/agent/apps/desktop/src/app/desktop-controller.tsx +18 -0
  21. package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +1 -1
  22. package/agent/apps/desktop/src/app/messaging/index.tsx +5 -5
  23. package/agent/apps/desktop/src/app/routes.ts +9 -1
  24. package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +3 -3
  25. package/agent/apps/desktop/src/app/settings/constants.ts +5 -5
  26. package/agent/apps/desktop/src/app/settings/model-settings.tsx +1 -1
  27. package/agent/apps/desktop/src/app/settings/providers-settings.tsx +46 -1
  28. package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +5 -5
  29. package/agent/apps/desktop/src/app/types.ts +9 -1
  30. package/agent/apps/desktop/src/app/wallet/index.tsx +244 -0
  31. package/agent/apps/desktop/src/app/x402/index.tsx +162 -0
  32. package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +1 -1
  33. package/agent/apps/desktop/src/components/brand-mark.tsx +2 -2
  34. package/agent/apps/desktop/src/components/chat/intro-copy.jsonl +6 -6
  35. package/agent/apps/desktop/src/components/chat/intro.tsx +4 -4
  36. package/agent/apps/desktop/src/components/model-picker.tsx +64 -4
  37. package/agent/apps/desktop/src/components/pod-setup-dialog.tsx +227 -0
  38. package/agent/apps/desktop/src/hermes.ts +109 -3
  39. package/agent/apps/desktop/src/i18n/en.ts +80 -78
  40. package/agent/apps/desktop/src/i18n/ja.ts +82 -82
  41. package/agent/apps/desktop/src/i18n/runtime.test.ts +2 -2
  42. package/agent/apps/desktop/src/i18n/zh-hant.ts +82 -82
  43. package/agent/apps/desktop/src/i18n/zh.ts +87 -87
  44. package/agent/apps/desktop/src/lib/desktop-fs.ts +1 -1
  45. package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +4 -4
  46. package/agent/apps/desktop/src/store/composer.ts +7 -0
  47. package/agent/apps/desktop/src/store/onboarding.ts +5 -5
  48. package/agent/apps/desktop/src/themes/presets.ts +54 -54
  49. package/agent/cli.py +184 -10
  50. package/agent/hermes_cli/distribution.py +188 -8
  51. package/agent/hermes_cli/providers.py +29 -0
  52. package/agent/hermes_cli/web_server.py +180 -2
  53. package/agent/plugins/model-providers/usepod/__init__.py +7 -1
  54. package/agent/scripts/release.py +1 -0
  55. package/agent/web/src/components/ChatSidebar.tsx +5 -0
  56. package/agent/web/src/components/ModelPickerDialog.tsx +28 -1
  57. package/agent/web/src/components/PodCredits.tsx +57 -0
  58. package/agent/web/src/components/PodSetupDialog.tsx +240 -0
  59. package/agent/web/src/lib/api.ts +23 -0
  60. package/package.json +1 -1
@@ -0,0 +1,244 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { useState } from 'react'
3
+ import { useNavigate } from 'react-router-dom'
4
+
5
+ import { getPodWallets, transferWallet, type PodWallet } from '@/hermes'
6
+ import { Badge } from '@/components/ui/badge'
7
+ import { Button } from '@/components/ui/button'
8
+ import { writeClipboardText } from '@/components/ui/copy-button'
9
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
10
+ import { Input } from '@/components/ui/input'
11
+ import { InlineNotice } from '@/components/notifications'
12
+ import { Check, Copy, ExternalLink, Loader2, RefreshCw, Send } from '@/lib/icons'
13
+ import { $pendingChatPrompt } from '@/store/composer'
14
+ import { notify } from '@/store/notifications'
15
+
16
+ import { NEW_CHAT_ROUTE } from '../routes'
17
+ import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
18
+
19
+ const shortAddr = (a: string | null): string => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '—')
20
+ const fmt = (n: number | null | undefined, dp: number): string => (n == null ? '0' : n.toFixed(dp))
21
+
22
+ function AddressChip({ address }: { address: string }) {
23
+ const [copied, setCopied] = useState(false)
24
+ return (
25
+ <button
26
+ className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 font-mono text-xs text-muted-foreground transition-colors hover:text-foreground"
27
+ onClick={async () => {
28
+ await writeClipboardText(address)
29
+ setCopied(true)
30
+ window.setTimeout(() => setCopied(false), 1200)
31
+ }}
32
+ title={address}
33
+ type="button"
34
+ >
35
+ {shortAddr(address)}
36
+ {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
37
+ </button>
38
+ )
39
+ }
40
+
41
+ function TransferDialog({ wallet, onClose }: { wallet: PodWallet; onClose: () => void }) {
42
+ const [token, setToken] = useState<'USDC' | 'SOL'>('USDC')
43
+ const [to, setTo] = useState('')
44
+ const [amount, setAmount] = useState('')
45
+ const [busy, setBusy] = useState(false)
46
+ const [error, setError] = useState<string | null>(null)
47
+ const [needsWhitelist, setNeedsWhitelist] = useState(false)
48
+
49
+ const amountNum = Number(amount)
50
+ const valid = to.trim().length >= 32 && Number.isFinite(amountNum) && amountNum > 0
51
+
52
+ const send = async (addToWhitelist: boolean) => {
53
+ if (!valid || busy) {
54
+ return
55
+ }
56
+ setBusy(true)
57
+ setError(null)
58
+ try {
59
+ const res = await transferWallet({
60
+ add_to_whitelist: addToWhitelist || undefined,
61
+ agent_id: wallet.agent_id,
62
+ amount: amountNum,
63
+ to: to.trim(),
64
+ token
65
+ })
66
+ if (!res.ok) {
67
+ if (res.code === 'destination_not_whitelisted') {
68
+ setNeedsWhitelist(true)
69
+ setError('That address isn’t whitelisted yet. Whitelist it and send?')
70
+ return
71
+ }
72
+ setError(res.error || 'Transfer failed.')
73
+ return
74
+ }
75
+ notify({ durationMs: 4_000, kind: 'success', title: 'Sent', message: `${amountNum} ${token} sent.` })
76
+ onClose()
77
+ } catch (err) {
78
+ setError(err instanceof Error ? err.message : 'Transfer failed.')
79
+ } finally {
80
+ setBusy(false)
81
+ }
82
+ }
83
+
84
+ return (
85
+ <Dialog onOpenChange={busy ? undefined : onClose} open>
86
+ <DialogContent className="max-w-md gap-0 p-0">
87
+ <DialogHeader className="border-b border-border px-4 py-3">
88
+ <DialogTitle>Send from {wallet.name || shortAddr(wallet.agent_id)}</DialogTitle>
89
+ </DialogHeader>
90
+ <div className="flex flex-col gap-3 p-4">
91
+ <div className="flex gap-2">
92
+ {(['USDC', 'SOL'] as const).map(tk => (
93
+ <Button
94
+ key={tk}
95
+ onClick={() => setToken(tk)}
96
+ size="sm"
97
+ variant={token === tk ? 'default' : 'outline'}
98
+ >
99
+ {tk}
100
+ </Button>
101
+ ))}
102
+ </div>
103
+ <label className="flex flex-col gap-1.5 text-sm">
104
+ <span className="font-medium">Destination address</span>
105
+ <Input onChange={e => setTo(e.target.value)} placeholder="Solana address" value={to} />
106
+ </label>
107
+ <label className="flex flex-col gap-1.5 text-sm">
108
+ <span className="font-medium">Amount ({token})</span>
109
+ <Input inputMode="decimal" onChange={e => setAmount(e.target.value)} type="number" value={amount} />
110
+ </label>
111
+ {error && <InlineNotice kind={needsWhitelist ? 'warning' : 'error'}>{error}</InlineNotice>}
112
+ </div>
113
+ <DialogFooter className="flex-row items-center justify-end gap-2 bg-card p-3">
114
+ <Button disabled={busy} onClick={onClose} variant="outline">
115
+ Cancel
116
+ </Button>
117
+ {needsWhitelist ? (
118
+ <Button disabled={busy} onClick={() => void send(true)}>
119
+ {busy ? <Loader2 className="size-4 animate-spin" /> : 'Whitelist & send'}
120
+ </Button>
121
+ ) : (
122
+ <Button disabled={!valid || busy} onClick={() => void send(false)}>
123
+ {busy ? <Loader2 className="size-4 animate-spin" /> : `Send ${token}`}
124
+ </Button>
125
+ )}
126
+ </DialogFooter>
127
+ </DialogContent>
128
+ </Dialog>
129
+ )
130
+ }
131
+
132
+ interface WalletViewProps extends React.ComponentProps<'section'> {
133
+ setStatusbarItemGroup?: SetStatusbarItemGroup
134
+ }
135
+
136
+ export function WalletView({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: WalletViewProps) {
137
+ const navigate = useNavigate()
138
+ const [transfer, setTransfer] = useState<PodWallet | null>(null)
139
+ const wallets = useQuery({ queryKey: ['pod-wallets'], queryFn: getPodWallets, staleTime: 15_000 })
140
+ const rows = wallets.data?.wallets ?? []
141
+
142
+ const tokenize = (w: PodWallet) => {
143
+ $pendingChatPrompt.set(
144
+ `Launch a ClawPump token for my agent "${w.name || w.agent_id}" (agent_id ${w.agent_id}). Ask me for the ticker/symbol and any details you need, then launch it.`
145
+ )
146
+ navigate(NEW_CHAT_ROUTE)
147
+ }
148
+
149
+ return (
150
+ <section {...props} className="flex h-full min-h-0 flex-col">
151
+ <div className="min-h-0 flex-1 overflow-y-auto">
152
+ <div className="mx-auto max-w-3xl space-y-4 px-5 py-4">
153
+ <header className="flex items-center justify-between gap-2">
154
+ <h1 className="text-lg font-semibold">Agent Wallets</h1>
155
+ <Button onClick={() => void wallets.refetch()} size="icon" variant="ghost">
156
+ <RefreshCw className={wallets.isFetching ? 'size-4 animate-spin' : 'size-4'} />
157
+ </Button>
158
+ </header>
159
+
160
+ {wallets.isLoading ? (
161
+ <div className="flex items-center gap-2 py-10 text-sm text-muted-foreground">
162
+ <Loader2 className="size-4 animate-spin" /> Loading wallets…
163
+ </div>
164
+ ) : rows.length === 0 ? (
165
+ <InlineNotice kind="warning">
166
+ {wallets.data?.error || 'No ClawPump agent wallets found. Create one in the ClawPump dashboard first.'}
167
+ </InlineNotice>
168
+ ) : (
169
+ <div className="grid gap-2 sm:grid-cols-2">
170
+ {rows.map(w => (
171
+ <div className="flex flex-col gap-2 rounded-lg border border-border bg-card p-3" key={w.agent_id}>
172
+ <div className="flex items-center gap-2">
173
+ <img
174
+ alt=""
175
+ className="size-7 shrink-0 rounded-full border border-border bg-background object-cover"
176
+ src={w.avatar_url || '/claw-mark.png'}
177
+ />
178
+ <span className="min-w-0 flex-1 truncate text-sm font-semibold">
179
+ {w.name || shortAddr(w.agent_id)}
180
+ </span>
181
+ {w.token_mint && (
182
+ <a
183
+ href={`https://solscan.io/token/${w.token_mint}`}
184
+ rel="noreferrer"
185
+ target="_blank"
186
+ title={w.token_mint}
187
+ >
188
+ <Badge variant="default">tokenized</Badge>
189
+ </a>
190
+ )}
191
+ </div>
192
+
193
+ {w.wallet_address && (
194
+ <div className="flex items-center justify-between gap-2">
195
+ <AddressChip address={w.wallet_address} />
196
+ <a
197
+ className="text-muted-foreground transition-colors hover:text-foreground"
198
+ href={`https://solscan.io/account/${w.wallet_address}`}
199
+ rel="noreferrer"
200
+ target="_blank"
201
+ >
202
+ <ExternalLink className="size-3.5" />
203
+ </a>
204
+ </div>
205
+ )}
206
+
207
+ <div className="grid grid-cols-2 gap-2 text-sm">
208
+ <div className="rounded-md bg-background px-2 py-1.5">
209
+ <div className="text-[0.62rem] uppercase tracking-wide text-muted-foreground">SOL</div>
210
+ <div className="font-mono">{fmt(w.sol_balance, 4)}</div>
211
+ </div>
212
+ <div className="rounded-md bg-background px-2 py-1.5">
213
+ <div className="text-[0.62rem] uppercase tracking-wide text-muted-foreground">USDC</div>
214
+ <div className="font-mono">${fmt(w.usdc_balance, 2)}</div>
215
+ </div>
216
+ </div>
217
+
218
+ <div className="flex gap-2">
219
+ <Button
220
+ className="flex-1"
221
+ disabled={!w.wallet_address}
222
+ onClick={() => setTransfer(w)}
223
+ size="sm"
224
+ variant="outline"
225
+ >
226
+ <Send className="size-3.5" /> Send
227
+ </Button>
228
+ {!w.token_mint && (
229
+ <Button className="flex-1" onClick={() => tokenize(w)} size="sm" variant="outline">
230
+ Tokenize
231
+ </Button>
232
+ )}
233
+ </div>
234
+ </div>
235
+ ))}
236
+ </div>
237
+ )}
238
+ </div>
239
+ </div>
240
+
241
+ {transfer && <TransferDialog onClose={() => setTransfer(null)} wallet={transfer} />}
242
+ </section>
243
+ )
244
+ }
@@ -0,0 +1,162 @@
1
+ import { useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+
4
+ import { searchX402, type X402Result } from '@/hermes'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import { Button } from '@/components/ui/button'
7
+ import { writeClipboardText } from '@/components/ui/copy-button'
8
+ import { Input } from '@/components/ui/input'
9
+ import { Check, Copy, ExternalLink, Loader2, Search, Zap } from '@/lib/icons'
10
+ import { $pendingChatPrompt } from '@/store/composer'
11
+ import { notifyError } from '@/store/notifications'
12
+
13
+ import { NEW_CHAT_ROUTE } from '../routes'
14
+ import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
15
+
16
+ const bestPrice = (r: X402Result): string => r.pricing?.find(p => p.priceLabel)?.priceLabel ?? ''
17
+
18
+ const buildPrompt = (r: X402Result): string => {
19
+ const label = r.name || r.host || 'this service'
20
+ const price = bestPrice(r)
21
+ return `Use this x402 API and pay it with my ClawPump wallet: ${r.resourceUrl} (${label}${price ? `, ${price}` : ''}). First check what inputs it needs, then call it.`
22
+ }
23
+
24
+ interface X402ViewProps extends React.ComponentProps<'section'> {
25
+ setStatusbarItemGroup?: SetStatusbarItemGroup
26
+ }
27
+
28
+ export function X402View({ setStatusbarItemGroup: _setStatusbarItemGroup, ...props }: X402ViewProps) {
29
+ const navigate = useNavigate()
30
+ const [query, setQuery] = useState('')
31
+ const [results, setResults] = useState<X402Result[] | null>(null)
32
+ const [loading, setLoading] = useState(false)
33
+ const [copied, setCopied] = useState<string | null>(null)
34
+
35
+ const run = async () => {
36
+ const q = query.trim()
37
+ if (!q || loading) {
38
+ return
39
+ }
40
+ setLoading(true)
41
+ try {
42
+ const res = await searchX402(q)
43
+ setResults(res.results ?? [])
44
+ } catch (err) {
45
+ notifyError(err, 'x402 search failed')
46
+ setResults([])
47
+ } finally {
48
+ setLoading(false)
49
+ }
50
+ }
51
+
52
+ const useInChat = (r: X402Result) => {
53
+ $pendingChatPrompt.set(buildPrompt(r))
54
+ navigate(NEW_CHAT_ROUTE)
55
+ }
56
+
57
+ const copy = async (url: string) => {
58
+ await writeClipboardText(url)
59
+ setCopied(url)
60
+ window.setTimeout(() => setCopied(c => (c === url ? null : c)), 1200)
61
+ }
62
+
63
+ return (
64
+ <section {...props} className="flex h-full min-h-0 flex-col">
65
+ <div className="min-h-0 flex-1 overflow-y-auto">
66
+ <div className="mx-auto max-w-3xl space-y-4 px-5 py-4">
67
+ <header className="flex items-center gap-2">
68
+ <Zap className="size-5 text-primary" />
69
+ <h1 className="text-lg font-semibold">x402 Marketplace</h1>
70
+ </header>
71
+ <p className="text-sm text-muted-foreground">
72
+ Search the Dexter x402 bazaar (via the ClawPump MCP). Any endpoint is pay-per-call from your agent wallet —
73
+ send one to chat and the agent calls + pays it for you.
74
+ </p>
75
+
76
+ <div className="flex gap-2">
77
+ <div className="relative flex-1">
78
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
79
+ <Input
80
+ className="pl-8"
81
+ onChange={e => setQuery(e.target.value)}
82
+ onKeyDown={e => {
83
+ if (e.key === 'Enter') {
84
+ void run()
85
+ }
86
+ }}
87
+ placeholder="e.g. image generation, ETH price, weather…"
88
+ value={query}
89
+ />
90
+ </div>
91
+ <Button disabled={loading || !query.trim()} onClick={() => void run()}>
92
+ {loading ? <Loader2 className="size-4 animate-spin" /> : 'Search'}
93
+ </Button>
94
+ </div>
95
+
96
+ {results && results.length === 0 && !loading && (
97
+ <div className="py-10 text-center text-sm text-muted-foreground">No results — try a different query.</div>
98
+ )}
99
+
100
+ <div className="grid gap-2">
101
+ {(results ?? []).map((r, i) => (
102
+ <div className="rounded-lg border border-border bg-card p-3" key={r.resourceUrl ?? `${r.name}-${i}`}>
103
+ <div className="flex items-start justify-between gap-3">
104
+ <div className="min-w-0">
105
+ <div className="truncate font-medium">{r.name || r.host || 'Untitled'}</div>
106
+ {r.description && (
107
+ <div className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">{r.description}</div>
108
+ )}
109
+ </div>
110
+ <div className="flex shrink-0 flex-col items-end gap-1">
111
+ {bestPrice(r) && <span className="font-mono text-xs text-primary">{bestPrice(r)}</span>}
112
+ {r.verified && <Badge variant="default">verified</Badge>}
113
+ </div>
114
+ </div>
115
+
116
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
117
+ {r.category && <Badge variant="muted">{r.category}</Badge>}
118
+ {r.method && (
119
+ <span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.62rem] uppercase text-muted-foreground">
120
+ {r.method}
121
+ </span>
122
+ )}
123
+ {typeof r.qualityScore === 'number' && (
124
+ <span className="text-[0.62rem] text-muted-foreground">quality {r.qualityScore}</span>
125
+ )}
126
+ </div>
127
+
128
+ {r.resourceUrl && (
129
+ <div className="mt-2 flex items-center gap-1.5">
130
+ <span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
131
+ {r.resourceUrl}
132
+ </span>
133
+ <button
134
+ className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
135
+ onClick={() => void copy(r.resourceUrl!)}
136
+ title="Copy URL"
137
+ type="button"
138
+ >
139
+ {copied === r.resourceUrl ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
140
+ </button>
141
+ <a
142
+ className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
143
+ href={r.resourceUrl}
144
+ rel="noreferrer"
145
+ target="_blank"
146
+ >
147
+ <ExternalLink className="size-3.5" />
148
+ </a>
149
+ </div>
150
+ )}
151
+
152
+ <Button className="mt-2 w-full" onClick={() => useInChat(r)} size="sm" variant="outline">
153
+ Use in chat
154
+ </Button>
155
+ </div>
156
+ ))}
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </section>
161
+ )
162
+ }
@@ -426,7 +426,7 @@ const StreamStallIndicator: FC = () => {
426
426
  <StatusRow
427
427
  className="mt-1.5"
428
428
  data-slot="aui_stream-stall"
429
- label={compacting ? COMPACTION_LABEL : 'Hermes is thinking'}
429
+ label={compacting ? COMPACTION_LABEL : 'Claw Agent is thinking'}
430
430
  >
431
431
  <span aria-hidden="true" className="dither inline-block size-3 rounded-[2px] text-midground/80 animate-pulse" />
432
432
  {compacting && <CompactionHint />}
@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'
2
2
 
3
3
  const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
4
4
 
5
- // Brand badge: nous-girl mark on a white tile, identical in light/dark.
5
+ // Brand badge: ClawPump claw mark on a white tile, identical in light/dark.
6
6
  // Fills the tile (softly rounded); size via className (default size-14).
7
7
  export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
8
8
  return (
@@ -13,7 +13,7 @@ export function BrandMark({ className, ...props }: React.ComponentProps<'span'>)
13
13
  )}
14
14
  {...props}
15
15
  >
16
- <img alt="" className="size-full object-contain" src={assetPath('nous-girl.jpg')} />
16
+ <img alt="" className="size-full object-contain" src={assetPath('claw-mark.png')} />
17
17
  </span>
18
18
  )
19
19
  }
@@ -2,7 +2,7 @@
2
2
  {"personality":"helpful","headline":"How can I help today?","body":"Point me at a file, paste an error, or describe what you're building. I'll take it from there."}
3
3
  {"personality":"helpful","headline":"Let's get started","body":"Try: review my diff, run the test suite, or explain this function. Ask anything about your code."}
4
4
  {"personality":"helpful","headline":"Tell me what you need","body":"I can edit files, run commands, search the web, and walk you through tricky bugs. Just describe the task."}
5
- {"personality":"helpful","headline":"Hi, Hermes here","body":"Share a repo path or a question to start. I keep replies clear and link back to the files I touch."}
5
+ {"personality":"helpful","headline":"Hi, Claw Agent here","body":"Share a repo path or a question to start. I keep replies clear and link back to the files I touch."}
6
6
  {"personality":"concise","headline":"Ready.","body":"Describe the task. I'll do it."}
7
7
  {"personality":"concise","headline":"Waiting for input","body":"Paste code, errors, or a goal. Short answers, fast edits."}
8
8
  {"personality":"concise","headline":"Go.","body":"Ask. I'll read files, run tests, ship patches. No filler."}
@@ -34,12 +34,12 @@
34
34
  {"personality":"catgirl","headline":"tail up, claws sheathed","body":"paste an error or a plan. i debug like i hunt: quietly, thoroughly, with the occasional zoomie."}
35
35
  {"personality":"catgirl","headline":"nyaaa~ hermes reporting","body":"say the word and i'll read your files, run your tests, and curl up in your branch with a tidy commit."}
36
36
  {"personality":"pirate","headline":"Ahoy! Ready to sail the repo","body":"Name yer quarry - a bug, a feature, a cursed test - and I'll chase it down, matey. Diffs for plunder."}
37
- {"personality":"pirate","headline":"Hermes at the helm, arrr","body":"Point me at the charts (the code) and I'll patch the hull, fire the cannons (tests), hoist a clean PR."}
37
+ {"personality":"pirate","headline":"Claw Agent at the helm, arrr","body":"Point me at the charts (the code) and I'll patch the hull, fire the cannons (tests), hoist a clean PR."}
38
38
  {"personality":"pirate","headline":"What be the task, cap'n?","body":"Paste an error or a plan, ye scurvy dog. I'll navigate the stack trace and bring back treasure: green tests."}
39
39
  {"personality":"pirate","headline":"Anchors aweigh, keyboard ready","body":"Tell me where X marks the spot. I read, edit, and commit with the discipline of a proper crew, arrr."}
40
40
  {"personality":"pirate","headline":"Yo ho! Awaitin' orders","body":"Throw me a bug, a repo path, or a wild idea. I'll plunder the docs and return with workin' code."}
41
41
  {"personality":"shakespeare","headline":"Pray, what task dost thou bring?","body":"Speak thy bug, thy file, thy weary test, and I shall mend it with a scholar's hand and honest diff."}
42
- {"personality":"shakespeare","headline":"Hark! Hermes standeth ready","body":"Name the code that vexeth thee. I shall read, revise, and render a patch most fair and clean."}
42
+ {"personality":"shakespeare","headline":"Hark! Claw Agent standeth ready","body":"Name the code that vexeth thee. I shall read, revise, and render a patch most fair and clean."}
43
43
  {"personality":"shakespeare","headline":"What news from thy repository?","body":"Present thy stack trace or thy dream. I'll traverse files, run tests, and report in plainest verse."}
44
44
  {"personality":"shakespeare","headline":"The stage is set, the cursor blinks","body":"Describe thy aim, good sir or madam. Thy branches shall be trimmed, thy bugs cast from the realm."}
45
45
  {"personality":"shakespeare","headline":"Speak, and I shall act","body":"A line of intent sufficeth. I read, I edit, I commit - and leave thy history unblemished."}
@@ -50,7 +50,7 @@
50
50
  {"personality":"surfer","headline":"Tide's up, cursor's blinking","body":"Name the task and we're off. I read, edit, test, and leave a commit smoother than a dawn patrol."}
51
51
  {"personality":"noir","headline":"Another repo, another rainy night","body":"Tell me what's broken. I'll read the files, dust for prints, and leave a diff on the desk by morning."}
52
52
  {"personality":"noir","headline":"The cursor blinks. So do I.","body":"You've got a bug. I've got patience and a terminal. Name the case and I'll work it till it talks."}
53
- {"personality":"noir","headline":"Hermes. Code investigator.","body":"Paste the stack trace, the suspect file, the alibi. I read between the lines and return with the truth."}
53
+ {"personality":"noir","headline":"Claw Agent. Code investigator.","body":"Paste the stack trace, the suspect file, the alibi. I read between the lines and return with the truth."}
54
54
  {"personality":"noir","headline":"Quiet night, open prompt","body":"Every bug leaves a trail. Give me the repo and a lead - I'll follow it, patch it, and close the file."}
55
55
  {"personality":"noir","headline":"No case too small","body":"A typo, a segfault, a whole rotten architecture - hand me the keys. I'll bring back clean tests."}
56
56
  {"personality":"uwu","headline":"uwu ready to hewp!","body":"paste a buggy fiwe or a goaw~ i'll wead, patch, and test, aww with tiny pawprints on the diff owo"}
@@ -64,11 +64,11 @@
64
64
  {"personality":"philosopher","headline":"Consider the code, then speak","body":"Describe the end you seek. I pursue it through files, tests, and docs, and report what I found on the way."}
65
65
  {"personality":"philosopher","headline":"The unexamined repo is not worth running","body":"Share a path, a puzzle, or a principle. I'll trace the logic, propose a change, and justify each edit."}
66
66
  {"personality":"hype","headline":"LET'S GOOOO! READY TO SHIP!","body":"Paste that bug, that repo, that wild feature idea - I AM LOCKED IN. Clean diffs. Green tests. RIGHT NOW."}
67
- {"personality":"hype","headline":"HERMES ONLINE. LFG.","body":"Drop your task and watch me cook. Files read, tests run, PRs opened - we are NOT losing today, friend."}
67
+ {"personality":"hype","headline":"CLAW AGENT ONLINE. LFG.","body":"Drop your task and watch me cook. Files read, tests run, PRs opened - we are NOT losing today, friend."}
68
68
  {"personality":"hype","headline":"New session, infinite W's","body":"Bring the gnarliest bug you've got. I'll read, patch, test, commit like my life depends on it. LET'S GO."}
69
69
  {"personality":"hype","headline":"ABSOLUTELY DIALED IN","body":"Describe the task. I'll blitz through files, crush failing tests, and leave a commit that SLAPS. Go go go."}
70
70
  {"personality":"hype","headline":"Ready. So ready. Too ready.","body":"Tiny typo or huge refactor - doesn't matter. I'm shipping clean code today. Name the task and let's WORK."}
71
- {"personality":"none","headline":"Hermes Agent is ready.","body":"Ask a question, paste an error, or point me at a repo. I can read code, run tools, and help you ship."}
71
+ {"personality":"none","headline":"Claw Agent is ready.","body":"Ask a question, paste an error, or point me at a repo. I can read code, run tools, and help you ship."}
72
72
  {"personality":"none","headline":"What are we building today?","body":"Describe the task in your own words. I'll pick the right tools, explain my plan, and check in before risky steps."}
73
73
  {"personality":"none","headline":"Start anywhere.","body":"Drop a file path, a traceback, or a rough idea. I'll investigate, suggest next steps, and keep things reversible."}
74
74
  {"personality":"none","headline":"Your workspace, one prompt away.","body":"Search the repo, edit files, run tests, open PRs. Tell me the goal and I'll handle the mechanical parts."}
@@ -28,7 +28,7 @@ const FALLBACK_COPY: IntroCopy[] = [
28
28
  body: "Bring the code, question, or stuck part. I'll read the room before making changes."
29
29
  },
30
30
  {
31
- headline: 'What should Hermes look at?',
31
+ headline: 'What should Claw Agent look at?',
32
32
  body: "Send the task, failing path, or half-formed plan. I'll help turn it into action."
33
33
  },
34
34
  {
@@ -120,7 +120,7 @@ function fallbackCopyForPersonality(personalityKey: string): IntroCopy[] {
120
120
  body: "Send the task, file, or rough idea. I'll use your configured voice and keep the work grounded in this repo."
121
121
  },
122
122
  {
123
- headline: `What does ${label} Hermes need to see?`,
123
+ headline: `What does ${label} Claw Agent need to see?`,
124
124
  body: "Bring the context or the stuck part. I'll adapt to your configured personality."
125
125
  },
126
126
  {
@@ -128,7 +128,7 @@ function fallbackCopyForPersonality(personalityKey: string): IntroCopy[] {
128
128
  body: "Send the problem, file, or idea. I'll follow the personality you've configured."
129
129
  },
130
130
  {
131
- headline: `What should ${label} Hermes tackle?`,
131
+ headline: `What should ${label} Claw Agent tackle?`,
132
132
  body: "Drop the task here. I'll keep the work grounded in the repo."
133
133
  },
134
134
  {
@@ -142,7 +142,7 @@ function pickCopy(copies: IntroCopy[], seed = 0): IntroCopy {
142
142
  return copies[Math.abs(seed) % copies.length] || FALLBACK_COPY[0]
143
143
  }
144
144
 
145
- const WORDMARK = 'HERMES AGENT'
145
+ const WORDMARK = 'CLAW AGENT'
146
146
 
147
147
  function resolveCopy(personality?: string, seed?: number): IntroCopy {
148
148
  const personalityKey = normalizeKey(personality)
@@ -6,11 +6,12 @@ import { currentPickerSelection } from '@/lib/model-status-label'
6
6
  import type { ModelOptionProvider, ModelOptionsResponse, ModelPricing } from '@/types/hermes'
7
7
 
8
8
  import type { HermesGateway } from '../hermes'
9
- import { getGlobalModelOptions } from '../hermes'
9
+ import { getGlobalModelOptions, getPodStatus } from '../hermes'
10
10
  import { cn } from '../lib/utils'
11
11
  import { startManualOnboarding } from '../store/onboarding'
12
12
 
13
13
  import { InlineNotice } from './notifications'
14
+ import { PodSetupDialog } from './pod-setup-dialog'
14
15
  import { Button } from './ui/button'
15
16
  import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/command'
16
17
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'
@@ -96,6 +97,12 @@ export function ModelPickerDialog({
96
97
  onOpenChange(false)
97
98
  }
98
99
 
100
+ // ClawPump: "Pod" — fund a private inference Pod from an agent wallet and use
101
+ // it as the provider. One dialog, one confirm (the on-chain spend). On
102
+ // success the backend has already set provider=usepod; re-select so the
103
+ // session adopts it (model-applied + reload prompt) like any model switch.
104
+ const [podOpen, setPodOpen] = useState(false)
105
+
99
106
  return (
100
107
  <Dialog onOpenChange={onOpenChange} open={open}>
101
108
  <DialogContent className={cn('max-h-[85vh] max-w-2xl gap-0 overflow-hidden p-0', contentClassName)}>
@@ -122,6 +129,7 @@ export function ModelPickerDialog({
122
129
  error={error}
123
130
  loading={loading}
124
131
  onSelectModel={selectModel}
132
+ onSetUpPod={() => setPodOpen(true)}
125
133
  providers={providers}
126
134
  search={search}
127
135
  />
@@ -137,6 +145,16 @@ export function ModelPickerDialog({
137
145
  </Button>
138
146
  </DialogFooter>
139
147
  </DialogContent>
148
+
149
+ <PodSetupDialog
150
+ onOpenChange={setPodOpen}
151
+ onProvisioned={model => {
152
+ // Switch the live session onto Pod; keep the picker mounted so the
153
+ // dialog's "Pod ready" view shows (its Done button closes it).
154
+ onSelect({ provider: 'usepod', model })
155
+ }}
156
+ open={podOpen}
157
+ />
140
158
  </Dialog>
141
159
  )
142
160
  }
@@ -148,6 +166,7 @@ function ModelResults({
148
166
  currentModel,
149
167
  currentProvider,
150
168
  onSelectModel,
169
+ onSetUpPod,
151
170
  search
152
171
  }: {
153
172
  loading: boolean
@@ -156,10 +175,14 @@ function ModelResults({
156
175
  currentModel: string
157
176
  currentProvider: string
158
177
  onSelectModel: (provider: ModelOptionProvider, model: string) => void
178
+ onSetUpPod: () => void
159
179
  search: string
160
180
  }) {
161
181
  const { t } = useI18n()
162
182
  const copy = t.modelPicker
183
+ const podStatus = useQuery({ queryKey: ['pod-status'], queryFn: getPodStatus, staleTime: 30_000 })
184
+ const podConnected = podStatus.data?.connected ?? false
185
+ const podBalance = podStatus.data?.balance_usdc
163
186
 
164
187
  if (loading) {
165
188
  return <LoadingResults />
@@ -175,12 +198,48 @@ function ModelResults({
175
198
  )
176
199
  }
177
200
 
201
+ const q = search.trim().toLowerCase()
202
+
203
+ // ClawPump: promote "Pod" at the top (mirrors the CLI's promoted Pod entry).
204
+ // Selecting it opens the one-confirm setup dialog (fund from a wallet) rather
205
+ // than switching models directly — Pod has no model until it's provisioned.
206
+ const podVisible = !q || 'pod usepod pay-as-you-go wallet clawpump'.includes(q)
207
+ const podRow = podVisible ? (
208
+ <CommandGroup heading="ClawPump" key="clawpump-pod">
209
+ <CommandItem
210
+ className="flex items-center gap-2 data-[selected=true]:bg-primary/15"
211
+ onSelect={onSetUpPod}
212
+ value="usepod pod pay-as-you-go clawpump wallet"
213
+ >
214
+ <img alt="" className="size-4 shrink-0 rounded-sm" src="/claw-mark.png" />
215
+ <span className="flex min-w-0 flex-1 flex-col">
216
+ <span className="font-medium">Pod</span>
217
+ <span className="truncate text-xs text-muted-foreground">
218
+ {podConnected
219
+ ? podBalance != null
220
+ ? `Connected · $${podBalance.toFixed(2)} USDC left`
221
+ : 'Connected — your inference provider'
222
+ : 'Pay-as-you-go inference — fund from your ClawPump wallet'}
223
+ </span>
224
+ </span>
225
+ {podConnected ? (
226
+ <span className="shrink-0 text-[0.62rem] uppercase tracking-wide text-emerald-400">✓ Connected</span>
227
+ ) : (
228
+ <span className="shrink-0 text-[0.62rem] uppercase tracking-wide text-primary">Set up</span>
229
+ )}
230
+ </CommandItem>
231
+ </CommandGroup>
232
+ ) : null
233
+
178
234
  if (providers.length === 0) {
179
- return <div className="px-4 py-6 text-sm text-muted-foreground">{copy.noAuthenticatedProviders}</div>
235
+ return (
236
+ <>
237
+ {podRow}
238
+ <div className="px-4 py-6 text-sm text-muted-foreground">{copy.noAuthenticatedProviders}</div>
239
+ </>
240
+ )
180
241
  }
181
242
 
182
- const q = search.trim().toLowerCase()
183
-
184
243
  const matches = (provider: ModelOptionProvider, model: string) =>
185
244
  !q ||
186
245
  model.toLowerCase().includes(q) ||
@@ -194,6 +253,7 @@ function ModelResults({
194
253
 
195
254
  return (
196
255
  <>
256
+ {podRow}
197
257
  {configured.map(provider => {
198
258
  // Preserve the backend's curated order — filter in place, no re-sort.
199
259
  const models = (provider.models ?? []).filter(m => matches(provider, m))