ethagent 3.0.2 → 3.1.0

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 (69) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +15 -116
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/identity/continuity/challenges.ts +123 -0
  23. package/src/identity/continuity/envelope.ts +49 -1484
  24. package/src/identity/continuity/envelopeCreate.ts +322 -0
  25. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  26. package/src/identity/continuity/envelopeParse.ts +441 -0
  27. package/src/identity/continuity/envelopeTypes.ts +204 -0
  28. package/src/identity/continuity/envelopeVersion.ts +1 -0
  29. package/src/identity/continuity/payloadNormalization.ts +183 -0
  30. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  31. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  32. package/src/identity/continuity/skillsNormalization.ts +119 -0
  33. package/src/identity/continuity/snapshotToken.ts +28 -0
  34. package/src/identity/hub/continuity/completion.ts +67 -0
  35. package/src/identity/hub/continuity/effects.ts +5 -62
  36. package/src/identity/hub/profile/effects.ts +6 -170
  37. package/src/identity/hub/profile/operatorSave.ts +202 -0
  38. package/src/identity/wallet/browserWallet/html.ts +1 -57
  39. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  40. package/src/identity/wallet/page/controller.ts +1 -1
  41. package/src/identity/wallet/page/errorView.ts +122 -0
  42. package/src/identity/wallet/page/view.ts +3 -114
  43. package/src/mcp/manager.ts +8 -66
  44. package/src/mcp/managerHelpers.ts +70 -0
  45. package/src/models/ModelPicker.tsx +69 -889
  46. package/src/models/huggingface.ts +20 -137
  47. package/src/models/huggingfaceStorage.ts +136 -0
  48. package/src/models/llamacpp.ts +37 -303
  49. package/src/models/llamacppCommands.ts +44 -0
  50. package/src/models/llamacppConfig.ts +34 -0
  51. package/src/models/llamacppDiscovery.ts +176 -0
  52. package/src/models/llamacppOutput.ts +65 -0
  53. package/src/models/modelPickerCatalogFlow.ts +56 -0
  54. package/src/models/modelPickerCredentials.ts +166 -0
  55. package/src/models/modelPickerData.ts +41 -0
  56. package/src/models/modelPickerDisplay.tsx +132 -0
  57. package/src/models/modelPickerHfFlow.ts +192 -0
  58. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  59. package/src/models/modelPickerTypes.ts +69 -0
  60. package/src/models/modelPickerUninstallFlow.ts +48 -0
  61. package/src/models/modelPickerViewHelpers.ts +174 -0
  62. package/src/providers/openai-chat.ts +5 -124
  63. package/src/providers/openaiChatWire.ts +124 -0
  64. package/src/runtime/providerTurn.ts +38 -0
  65. package/src/runtime/textToolParser.ts +161 -0
  66. package/src/runtime/toolIntent.ts +1 -1
  67. package/src/runtime/turn.ts +43 -499
  68. package/src/runtime/turnNudges.ts +223 -0
  69. package/src/runtime/turnTypes.ts +86 -0
@@ -0,0 +1,120 @@
1
+ import type { EthagentConfig, ProviderId } from '../storage/config.js'
2
+ import { getConfigPath, localProviderBaseUrlFor } from '../storage/config.js'
3
+ import { detectLlamaCpp } from '../models/llamacpp.js'
4
+ import { detectSpec } from '../models/runtimeDetection.js'
5
+ import { getIdentityStatus } from '../storage/identity.js'
6
+ import { getLocalHfCacheDir } from '../models/huggingface.js'
7
+ import { formatModelDisplayName } from '../models/modelDisplay.js'
8
+ import { providerDisplayName } from '../models/modelPickerOptions.js'
9
+ import type { ModelCatalogResult } from '../models/catalog.js'
10
+ import type { SlashContext } from './commands.js'
11
+
12
+ export function renderStatus(ctx: SlashContext): string {
13
+ const elapsedMs = Date.now() - ctx.startedAt
14
+ const minutes = Math.floor(elapsedMs / 60000)
15
+ const seconds = Math.floor((elapsedMs % 60000) / 1000)
16
+ const elapsed = minutes > 0 ? `${minutes}m${seconds.toString().padStart(2, '0')}s` : `${seconds}s`
17
+ const displayModel = formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })
18
+ return [
19
+ `provider ${providerDisplayName(ctx.config.provider)}`,
20
+ `model ${displayModel}`,
21
+ `cwd ${ctx.cwd}`,
22
+ `session ${ctx.sessionId.slice(0, 8)}`,
23
+ 'state active',
24
+ `turns ${ctx.turns}`,
25
+ `tokens ~${ctx.approxTokens}`,
26
+ `context ${ctx.contextUsage.percent}% (~${ctx.contextUsage.usedTokens}/${ctx.contextUsage.windowTokens}, ${ctx.contextUsage.source})`,
27
+ `elapsed ${elapsed}`,
28
+ ].join('\n')
29
+ }
30
+
31
+ export function renderContext(ctx: SlashContext): string {
32
+ const usage = ctx.contextUsage
33
+ const free = Math.max(0, usage.windowTokens - usage.usedTokens)
34
+ const action =
35
+ usage.percent >= 90
36
+ ? 'Context is near the model limit. New requests will ask you to summarize into a new conversation, switch models, ignore and send, or cancel.'
37
+ : usage.percent >= 75
38
+ ? 'Context is getting full. Consider /compact before a new task boundary.'
39
+ : 'Context has comfortable room.'
40
+ return [
41
+ 'context usage:',
42
+ ` model ${providerDisplayName(ctx.config.provider)} · ${formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })}`,
43
+ ` used ~${usage.usedTokens} / ${usage.windowTokens} tokens (${usage.percent}%)`,
44
+ ` free ~${free} tokens`,
45
+ ` estimate ${usage.confidence} (${usage.source})`,
46
+ ' session active',
47
+ '',
48
+ action,
49
+ ].join('\n')
50
+ }
51
+
52
+ export function renderDoctor(
53
+ spec: Awaited<ReturnType<typeof detectSpec>>,
54
+ keys: ReadonlyArray<readonly [ProviderId, boolean]>,
55
+ identity: Awaited<ReturnType<typeof getIdentityStatus>>,
56
+ ctx: SlashContext,
57
+ llamaCpp: Awaited<ReturnType<typeof detectLlamaCpp>>,
58
+ hfModelCount: number,
59
+ ): string {
60
+ const lines: string[] = ['diagnostics:']
61
+ lines.push(` platform ${spec.platform}/${spec.arch}${spec.isAppleSilicon ? ' (apple silicon)' : ''}`)
62
+ lines.push(` ram ${formatGB(spec.effectiveRamBytes)}${spec.gpuVramBytes ? ` · vram ${formatGB(spec.gpuVramBytes)}` : ''}`)
63
+ lines.push(` local run ${llamaCpp.binaryPresent ? 'installed' : 'not installed'} · server ${llamaCpp.serverUp ? 'up' : 'down'}`)
64
+ lines.push(` hf models ${hfModelCount} downloaded`)
65
+ lines.push('')
66
+ lines.push('config:')
67
+ lines.push(` provider ${providerDisplayName(ctx.config.provider)}`)
68
+ lines.push(` model ${formatModelDisplayName(ctx.config.provider, ctx.config.model, { maxLength: 72 })}`)
69
+ if (ctx.config.baseUrl) lines.push(` baseUrl ${ctx.config.baseUrl}`)
70
+ if (ctx.config.provider === 'llamacpp') lines.push(` hf cache ${getLocalHfCacheDir()}`)
71
+ lines.push(` path ${getConfigPath()}`)
72
+ lines.push('')
73
+ lines.push('keys:')
74
+ for (const [provider, present] of keys) {
75
+ lines.push(` ${providerDisplayName(provider).padEnd(9)} ${present ? 'set' : 'not set'}`)
76
+ }
77
+ lines.push('')
78
+ lines.push('identity:')
79
+ if (identity) {
80
+ lines.push(` address ${identity.address}`)
81
+ lines.push(` backend ${identity.backend}`)
82
+ if (identity.source) lines.push(` source ${identity.source}`)
83
+ if (identity.agentId) lines.push(` token #${identity.agentId}`)
84
+ } else {
85
+ lines.push(' address not set')
86
+ }
87
+ return lines.join('\n')
88
+ }
89
+
90
+ export function renderModelCatalog(catalog: ModelCatalogResult, currentModel: string): string {
91
+ const title = catalog.status === 'fallback'
92
+ ? `${providerDisplayName(catalog.provider)} models (fallback${catalog.error ? `: ${catalog.error}` : ''}):`
93
+ : `${providerDisplayName(catalog.provider)} models:`
94
+ const lines = catalog.entries.map(entry => {
95
+ const marker = entry.id === currentModel ? '*' : ' '
96
+ const suffix = entry.source === 'fallback' ? ' fallback' : ''
97
+ return `${marker} ${formatModelDisplayName(catalog.provider, entry.id, { maxLength: 72 })}${suffix}`
98
+ })
99
+ return [title, ...lines].join('\n')
100
+ }
101
+
102
+ export function baseUrlForModelSwitch(config: EthagentConfig): string | undefined {
103
+ if (config.provider === 'llamacpp') return localProviderBaseUrlFor('llamacpp', config.baseUrl)
104
+ if (config.provider === 'openai') return config.baseUrl
105
+ return undefined
106
+ }
107
+
108
+ export function formatBytes(bytes: number): string {
109
+ if (bytes <= 0) return '—'
110
+ const gb = bytes / (1024 * 1024 * 1024)
111
+ if (gb >= 1) return `${gb.toFixed(1)}GB`
112
+ const mb = bytes / (1024 * 1024)
113
+ return `${mb.toFixed(0)}MB`
114
+ }
115
+
116
+ function formatGB(bytes: number): string {
117
+ const gb = bytes / (1024 * 1024 * 1024)
118
+ if (gb >= 10) return `${Math.round(gb)}GB`
119
+ return `${gb.toFixed(1)}GB`
120
+ }
@@ -0,0 +1,123 @@
1
+ import { toChecksumAddress } from '../crypto/eth.js'
2
+ import { normalizeContinuitySnapshotToken, type ContinuitySnapshotToken } from './snapshotToken.js'
3
+
4
+ const CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES = [
5
+ 'Save or Restore Identity Files',
6
+ 'Action: encrypt or decrypt local identity files',
7
+ 'Private: SOUL.md, MEMORY.md, skills',
8
+ 'Public: public skills and profile',
9
+ 'Safety: no transaction, spending, or approvals',
10
+ 'Version: 2',
11
+ ] as const
12
+
13
+ export function createContinuitySnapshotChallenge(ownerAddress: string): string {
14
+ const checksum = toChecksumAddress(ownerAddress)
15
+ return [
16
+ CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES[0],
17
+ `Owner: ${checksum}`,
18
+ ...CONTINUITY_SNAPSHOT_CHALLENGE_MESSAGES.slice(1),
19
+ ].join('\n')
20
+ }
21
+
22
+ export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY = 'Prepare Transfer Restore Snapshot'
23
+ export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER = 'Prepare Transfer Snapshot · Sender Restore Slot'
24
+ export const TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER = 'Prepare Transfer Snapshot · Receiver Restore Slot'
25
+
26
+ export function createTransferContinuitySnapshotChallenge(args: {
27
+ token: ContinuitySnapshotToken
28
+ ownerAddress: string
29
+ targetAddress: string
30
+ role?: 'sender' | 'receiver'
31
+ }): string {
32
+ const token = normalizeContinuitySnapshotToken(args.token)
33
+ const ownerAddress = toChecksumAddress(args.ownerAddress)
34
+ const targetAddress = toChecksumAddress(args.targetAddress)
35
+ const header = args.role === 'sender'
36
+ ? TRANSFER_SNAPSHOT_CHALLENGE_HEADER_SENDER
37
+ : args.role === 'receiver'
38
+ ? TRANSFER_SNAPSHOT_CHALLENGE_HEADER_RECEIVER
39
+ : TRANSFER_SNAPSHOT_CHALLENGE_HEADER_LEGACY
40
+ return [
41
+ header,
42
+ `ERC-8004 Chain ID: ${token.chainId}`,
43
+ `ERC-8004 Registry: ${token.identityRegistryAddress}`,
44
+ `ERC-8004 Token ID: ${token.agentId}`,
45
+ `Sender Owner: ${ownerAddress}`,
46
+ `Receiver Owner: ${targetAddress}`,
47
+ 'Action: encrypt or decrypt local identity files for this token transfer',
48
+ 'Private: SOUL.md, MEMORY.md, skills',
49
+ 'Public: public skills and profile',
50
+ 'Safety: no transaction, spending, or approvals',
51
+ 'Version: 2',
52
+ ].join('\n')
53
+ }
54
+
55
+ export type WalletChallengePurpose =
56
+ | 'create-agent'
57
+ | 'update-snapshot'
58
+ | 'update-ens-snapshot'
59
+ | 'clear-ens-snapshot'
60
+ | 'update-profile-snapshot'
61
+ | 'update-operators-snapshot'
62
+ | 'refetch-snapshot'
63
+ | 'operator-proof'
64
+ | 'restore-owner'
65
+ | 'restore-operator'
66
+ | 'transfer-prepare-sender'
67
+ | 'transfer-prepare-receiver'
68
+
69
+ const WALLET_CHALLENGE_V2_COPY: Record<WalletChallengePurpose, { title: string; action: string }> = {
70
+ 'create-agent': { title: 'Create Agent Snapshot Key', action: 'Action: encrypt the new agent snapshot for owner restore' },
71
+ 'update-snapshot': { title: 'Save Snapshot Encryption Key', action: 'Action: encrypt the updated agent snapshot' },
72
+ 'update-ens-snapshot': { title: 'Update ENS in Agent Snapshot', action: 'Action: encrypt the snapshot with the new ENS name. No onchain ENS records change.' },
73
+ 'clear-ens-snapshot': { title: 'Unlink ENS from Agent', action: 'Action: encrypt the snapshot with no ENS name. No onchain ENS records change.' },
74
+ 'update-profile-snapshot': { title: 'Update Public Profile Snapshot Key', action: 'Action: encrypt the snapshot with the updated profile' },
75
+ 'update-operators-snapshot': { title: 'Update Operator Wallets Snapshot Key', action: 'Action: encrypt the snapshot with the updated operator list' },
76
+ 'refetch-snapshot': { title: 'Refetch Latest Snapshot', action: 'Action: decrypt the latest published snapshot' },
77
+ 'operator-proof': { title: 'Authorize Operator Wallet Restore Access', action: 'Action: prove this operator wallet can decrypt future snapshots' },
78
+ 'restore-owner': { title: 'Restore Agent with Owner Wallet', action: 'Action: decrypt the snapshot for the owner wallet' },
79
+ 'restore-operator': { title: 'Restore Agent with Operator Wallet', action: 'Action: decrypt the snapshot for the authorized operator wallet' },
80
+ 'transfer-prepare-sender': { title: 'Prepare Token Transfer (Sender)', action: 'Action: encrypt the transfer snapshot for the receiver' },
81
+ 'transfer-prepare-receiver': { title: 'Receive Token Transfer (Receiver)', action: 'Action: prepare receiver decryption for the transfer snapshot' },
82
+ }
83
+
84
+ export function createWalletRestoreAccessChallenge(args: {
85
+ token: ContinuitySnapshotToken
86
+ ownerAddress: string
87
+ walletAddress: string
88
+ accessEpoch?: number
89
+ purpose?: WalletChallengePurpose
90
+ }): string {
91
+ const token = normalizeContinuitySnapshotToken(args.token)
92
+ const ownerAddress = toChecksumAddress(args.ownerAddress)
93
+ const walletAddress = toChecksumAddress(args.walletAddress)
94
+ if (args.purpose) {
95
+ const copy = WALLET_CHALLENGE_V2_COPY[args.purpose]
96
+ return [
97
+ copy.title,
98
+ `ERC-8004 Chain ID: ${token.chainId}`,
99
+ `ERC-8004 Registry: ${token.identityRegistryAddress}`,
100
+ `ERC-8004 Token ID: ${token.agentId}`,
101
+ `Owner: ${ownerAddress}`,
102
+ `Wallet: ${walletAddress}`,
103
+ `Access Epoch: ${args.accessEpoch ?? 1}`,
104
+ copy.action,
105
+ 'Private: SOUL.md, MEMORY.md, skills',
106
+ 'Safety: no transaction, spending, or approvals',
107
+ 'Version: 3',
108
+ ].join('\n')
109
+ }
110
+ return [
111
+ 'Authorize Wallet Restore Access',
112
+ `ERC-8004 Chain ID: ${token.chainId}`,
113
+ `ERC-8004 Registry: ${token.identityRegistryAddress}`,
114
+ `ERC-8004 Token ID: ${token.agentId}`,
115
+ `Owner: ${ownerAddress}`,
116
+ `Wallet: ${walletAddress}`,
117
+ `Access Epoch: ${args.accessEpoch ?? 1}`,
118
+ 'Action: create a restore key for encrypted identity snapshots',
119
+ 'Private: SOUL.md, MEMORY.md, skills',
120
+ 'Safety: no transaction, spending, or approvals',
121
+ 'Version: 2',
122
+ ].join('\n')
123
+ }