ethagent 3.1.2 → 3.3.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 (65) hide show
  1. package/README.md +15 -16
  2. package/package.json +1 -1
  3. package/src/chat/ChatScreen.tsx +25 -2
  4. package/src/chat/chatTurnOrchestrator.ts +17 -0
  5. package/src/identity/continuity/history.ts +8 -8
  6. package/src/identity/continuity/publicSkills.ts +3 -80
  7. package/src/identity/continuity/skills/frontmatter.ts +4 -1
  8. package/src/identity/continuity/skills/loadSkills.ts +73 -5
  9. package/src/identity/continuity/skills/publicSkillsSync.ts +9 -8
  10. package/src/identity/continuity/skills/scaffold.ts +1 -1
  11. package/src/identity/continuity/skills/types.ts +1 -1
  12. package/src/identity/continuity/snapshots.ts +3 -8
  13. package/src/identity/continuity/storage/defaults.ts +3 -3
  14. package/src/identity/continuity/storage/paths.ts +1 -1
  15. package/src/identity/continuity/storage/scaffold.ts +37 -25
  16. package/src/identity/continuity/storage/status.ts +11 -11
  17. package/src/identity/continuity/storage/types.ts +4 -4
  18. package/src/identity/continuity/storage.ts +4 -4
  19. package/src/identity/ens/agentRecords.ts +61 -45
  20. package/src/identity/ens/ensAutomation/read.ts +7 -10
  21. package/src/identity/ens/ensAutomation/setup.ts +10 -16
  22. package/src/identity/ens/ensAutomation/types.ts +0 -1
  23. package/src/identity/ens/ensAutomation.ts +1 -0
  24. package/src/identity/ens/ensLookup/records.ts +1 -1
  25. package/src/identity/ens/ensLookup.ts +1 -1
  26. package/src/identity/ens/erc7930.ts +48 -0
  27. package/src/identity/hub/OperationalRoutes.tsx +4 -1
  28. package/src/identity/hub/continuity/effects.ts +17 -39
  29. package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +3 -4
  30. package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +3 -4
  31. package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +2 -1
  32. package/src/identity/hub/continuity/state.ts +1 -1
  33. package/src/identity/hub/continuity/vault.ts +16 -50
  34. package/src/identity/hub/create/effects.ts +12 -16
  35. package/src/identity/hub/ens/EnsEditFlow.tsx +19 -5
  36. package/src/identity/hub/ens/EnsEditReviewScreens.tsx +11 -11
  37. package/src/identity/hub/ens/EnsEditShared.tsx +2 -6
  38. package/src/identity/hub/ens/editCopy.ts +1 -3
  39. package/src/identity/hub/ens/transactions.ts +67 -18
  40. package/src/identity/hub/profile/effects.ts +15 -30
  41. package/src/identity/hub/profile/identity.ts +2 -4
  42. package/src/identity/hub/profile/operatorSave.ts +10 -30
  43. package/src/identity/hub/restore/RestoreFlow.tsx +9 -9
  44. package/src/identity/hub/restore/apply.ts +7 -8
  45. package/src/identity/hub/restore/helpers.ts +3 -3
  46. package/src/identity/hub/restore/recovery.ts +9 -10
  47. package/src/identity/hub/restore/resolve.ts +11 -9
  48. package/src/identity/hub/shared/components/MenuScreen.tsx +3 -3
  49. package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +2 -2
  50. package/src/identity/hub/shared/effects/sync.ts +1 -1
  51. package/src/identity/hub/transfer/TokenTransferScreens.tsx +1 -1
  52. package/src/identity/hub/transfer/effects.ts +10 -31
  53. package/src/identity/hub/useIdentityHubContinuity.ts +12 -12
  54. package/src/identity/hub/useIdentityHubController.ts +12 -3
  55. package/src/identity/registry/erc8004/metadata.ts +10 -27
  56. package/src/identity/registry/erc8004/types.ts +0 -1
  57. package/src/models/llamacpp.ts +61 -1
  58. package/src/models/llamacppPreflight.ts +5 -1
  59. package/src/runtime/compaction.ts +11 -5
  60. package/src/storage/config.ts +1 -2
  61. package/src/storage/identity.ts +3 -3
  62. package/src/storage/rewind.ts +1 -1
  63. package/src/tools/privateContinuityEditTool.ts +4 -4
  64. package/src/utils/messages.ts +1 -1
  65. package/src/utils/withRetry.ts +2 -2
package/README.md CHANGED
@@ -5,8 +5,8 @@ A privacy-first AI agent with a portable Ethereum identity.
5
5
  Switch providers or machines and the AI agent you customized stays behind. `ethagent` ties the agent to a wallet you own, so its soul, memory, and skills follow you across providers, machines, and models.
6
6
 
7
7
  - **Portable.** The ERC-8004 token is the agent's durable identity. Use the ENS name as a readable handle, or the token ID plus chain as the permanent reference, to restore the same agent anywhere.
8
- - **Private.** Soul and memory are encrypted before they are pinned to IPFS. The wallet signature used to unlock them stays local and never submits a transaction, spends funds, or grants token approval.
9
- - **Public.** The agent URI points to plain JSON for the Agent Card and public skills, so other agents can discover capabilities through ERC-8004 and IPFS.
8
+ - **Private.** Soul, memory, and skills are encrypted before they are pinned to IPFS. The wallet signature used to unlock them stays local and never submits a transaction, spends funds, or grants token approval.
9
+ - **Public.** The agent URI points to a public metadata payload on IPFS that includes the Agent Card, so other agents can discover the agent's capabilities through ERC-8004.
10
10
 
11
11
  <details>
12
12
  <summary><strong>Glossary</strong> (click to expand)</summary>
@@ -16,9 +16,9 @@ Switch providers or machines and the AI agent you customized stays behind. `etha
16
16
  | Owner Wallet | Holds and controls the ERC-8004 agent token. Signs custody changes and, in Simple custody, every URI rotation. |
17
17
  | Operator Wallet | Additional wallet authorized to rotate the onchain URI on behalf of the owner. Used in Advanced custody. Never receives token approval. |
18
18
  | Vault | Immutable per-agent custody contract used in Advanced custody. Holds at most one ERC-8004 token. |
19
- | Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
20
- | Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to the agent's published metadata. |
21
- | Agent Card | Public JSON describing the agent: name, description, capabilities, and skills. Other agents fetch it for discovery. |
19
+ | Snapshot | Encrypted bundle of SOUL.md, MEMORY.md, the skills/ tree, and session state. Pinned to IPFS; decrypts only against the owner wallet's signature. |
20
+ | Agent URI | IPFS URI stored in the ERC-8004 `tokenURI`. Resolves to a public metadata payload that references the Agent Card. |
21
+ | Agent Card | Public JSON describing the agent: name, description, capabilities, and skills. Pinned on IPFS and linked from the agent URI; other agents fetch it for discovery. |
22
22
 
23
23
  </details>
24
24
 
@@ -48,12 +48,12 @@ Once running:
48
48
 
49
49
  The Identity Hub manages everything portable about the agent:
50
50
 
51
- - **Public Profile** edits name, description, icon, and the Agent Card.
51
+ - **Public Profile** edits name, description, and icon: what other agents see in the Agent Card.
52
52
  - **ENS Name** links the agent to a subdomain under a parent name the owner wallet controls.
53
53
  - **Custody Mode** switches between Simple and Advanced by depositing the token into its Vault or unwrapping it back out.
54
- - **Prepare Transfer** stages a dual-wallet snapshot before sending the token externally.
54
+ - **Prepare Transfer** stages a dual-wallet snapshot so the receiver can restore the agent after the token moves externally.
55
55
  - **Refetch Latest** pulls the most recent published snapshot back to local files.
56
- - **Load Agent** accepts either an ENS name or a bare token ID, and loads any agent owned by or linked to the connected wallet.
56
+ - **Switch Agent** accepts either an ENS name or a bare token ID, and loads any agent owned by or linked to the connected wallet.
57
57
 
58
58
  The menu surfaces drift automatically. Token ownership, vault state, ENS record alignment, and pending URI rotations are checked against the live chain when the menu opens.
59
59
 
@@ -61,16 +61,15 @@ Every agent has a continuity directory at `~/.ethagent/continuity`.
61
61
 
62
62
  ## Continuity
63
63
 
64
- Each agent's continuity directory holds a small set of files. Private files are encrypted before they ever reach IPFS; public files are plain JSON so other agents can discover what the agent does.
64
+ Each agent's continuity directory holds a small set of private files. They are encrypted before they ever reach IPFS.
65
65
 
66
66
  | File | Visibility | Purpose |
67
67
  | --- | --- | --- |
68
68
  | `SOUL.md` | Private | Soul, boundaries, standing instructions, and identity framing. |
69
69
  | `MEMORY.md` | Private | Durable preferences, project context, decisions, and operating notes. |
70
- | `skills/` | Mixed | Skill folders. Each skill is private, discoverable, or public; new skills default to discoverable. |
71
- | `skills.json` | Public | Machine-readable capabilities derived from public skills. |
70
+ | `skills/` | Private | Skill folders. The SKILL.md body never leaves your machine. The visibility flag only controls whether the skill's name and description get listed in the Agent Card. New skills default to public. |
72
71
 
73
- `SOUL.md`, `MEMORY.md`, and each `SKILL.md` are plain Markdown you edit through the Identity Hub under Continuity. Skills carry extra metadata: the frontmatter at the top of each `SKILL.md` (name, description, when_to_use, visibility, tags) tells the agent when to load it. Visibility is `private` (local-only, never shared), `discoverable` (indexed in `skills.json` so other agents can find it), or `public` (indexed and surfaced on the Agent Card).
72
+ `SOUL.md`, `MEMORY.md`, and each `SKILL.md` are plain Markdown you edit through the Identity Hub under Continuity. Skill frontmatter (name, description, when_to_use, visibility, tags) tells the agent when to load it. The body stays local; `visibility: public` lists the name and description in the Agent Card.
74
73
 
75
74
  - **Save Snapshot Now** encrypts the private files, pins them to IPFS, and rotates the onchain pointer to the new CID.
76
75
  - **Refetch Latest** reads the pointer back, signs the decrypt challenge with your wallet, and overwrites local files from the snapshot.
@@ -103,7 +102,7 @@ Save the token ID + network somewhere safe. ENS records can be cleared and rebui
103
102
  - Sender signs snapshot access, receiver signs restore access.
104
103
  - Sender publishes the snapshot pointer to the agent URI.
105
104
  - The actual transfer happens externally afterwards, in whichever wallet UI you prefer.
106
- - Once the token has moved, the receiver opens **Load Agent** with the receiving wallet and restores the same agent from the published snapshot.
105
+ - Once the token has moved, the receiver opens **Switch Agent** with the receiving wallet and restores the same agent from the published snapshot.
107
106
 
108
107
  The token transfer flow prepares decrypt access and agent URI pointers only. It does not initiate the transfer and does not request approval over the token.
109
108
 
@@ -136,8 +135,8 @@ Vision support is available on:
136
135
 
137
136
  ## Privacy
138
137
 
139
- - **Public:** token ownership, the agent URI payload, public discovery files, and IPFS CIDs.
140
- - **Private:** plaintext `SOUL.md`, plaintext `MEMORY.md`, sessions, prompt history, API keys, local permissions, and the wallet signatures used for decryption.
138
+ - **Public:** token ownership, the agent URI, the Agent Card it references, and IPFS CIDs.
139
+ - **Private:** plaintext `SOUL.md`, plaintext `MEMORY.md`, the local skills/ tree, sessions, prompt history, API keys, local permissions, and the wallet signatures used for decryption.
141
140
  - Snapshots use a wallet signature as unlock material. The signature does not submit a transaction, spend funds, or grant token approval.
142
141
  - The transfer flow writes a snapshot pointer and stops; it never approves or moves the token.
143
142
  - `ethagent reset` deletes local ethagent data from the current machine while preserving installed local model assets. It does not burn or transfer tokens, remove public IPFS content, or mutate the onchain agent URI. Run **Save Snapshot Now** before resetting if local edits should become the recoverable state.
@@ -150,7 +149,7 @@ Vision support is available on:
150
149
  | Model | Cloud provider or local GGUF runner. |
151
150
  | Identity | ERC-8004 token owned by the wallet. |
152
151
  | Continuity | Private files encrypted before IPFS pinning. |
153
- | Discovery | Public `skills.json`, Agent Card, services, and the current agent URI payload. |
152
+ | Discovery | The agent URI and the Agent Card it points to. |
154
153
  | Recovery | Refetch the current agent URI, decrypt the latest snapshot, and restore local files. |
155
154
 
156
155
  The ERC-8004 token is the durable handle. The machine, model, and local session all change around it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethagent",
3
- "version": "3.1.2",
3
+ "version": "3.3.0",
4
4
  "description": "A privacy-first AI agent with a portable Ethereum identity",
5
5
  "type": "module",
6
6
  "main": "bin/ethagent.js",
@@ -44,7 +44,9 @@ import {
44
44
  shouldConfirmContextUsage,
45
45
  type ContextUsage,
46
46
  } from '../runtime/compaction.js'
47
- import { saveConfig } from '../storage/config.js'
47
+ import { fetchLlamaCppContextSize, onLlamaCppContextSizeChange, setCachedLlamaCppContextSize } from '../models/llamacpp.js'
48
+ import { llamaCppServerHostFromBaseUrl } from '../models/llamacppPreflight.js'
49
+ import { localProviderBaseUrlFor, saveConfig } from '../storage/config.js'
48
50
  import { getCwd as getRuntimeCwd, setCwd as setRuntimeCwd, syncCwdFromProcess } from '../runtime/cwd.js'
49
51
  import { executeToolWithPermissions } from '../runtime/toolExecution.js'
50
52
  import { nextSessionMode, sessionModeLabel, type PermissionMode, type SessionMode } from '../runtime/sessionMode.js'
@@ -309,7 +311,7 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
309
311
  {
310
312
  role: 'progress',
311
313
  id: progressRowId,
312
- title: kind === 'plan' ? 'summarizing plan context' : 'compacting conversation',
314
+ title: kind === 'plan' ? 'summarizing plan context' : 'Compacting conversation',
313
315
  progress: 0,
314
316
  status: state.stage,
315
317
  suffix: 'esc to cancel',
@@ -449,6 +451,22 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
449
451
  [],
450
452
  )
451
453
 
454
+ useEffect(() => {
455
+ if (config.provider !== 'llamacpp') return
456
+ const host = llamaCppServerHostFromBaseUrl(localProviderBaseUrlFor('llamacpp', config.baseUrl))
457
+ void fetchLlamaCppContextSize(host)
458
+ const unsubscribe = onLlamaCppContextSizeChange(() => {
459
+ refreshVisibleStats(
460
+ sessionMessagesRef.current,
461
+ providerRef.current.supportsTools,
462
+ cwdRef.current,
463
+ configRef.current,
464
+ modeRef.current,
465
+ )
466
+ })
467
+ return unsubscribe
468
+ }, [config.provider, config.baseUrl, refreshVisibleStats])
469
+
452
470
  const warnIfContextPressure = useCallback(
453
471
  (usage: ContextUsage, configForUsage: EthagentConfig) => {
454
472
  if (!shouldConfirmContextUsage(usage, CONTEXT_CONFIRM_PERCENT)) return
@@ -880,6 +898,11 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({ config: initialConfig, o
880
898
  pendingPlanRef.current = planCandidate
881
899
  setPendingPlan(planCandidate)
882
900
  },
901
+ onContextExceeded: ({ contextLimit }) => {
902
+ setCachedLlamaCppContextSize(contextLimit)
903
+ pushNote('Context full. Compacting transcript. Re-send your message once compaction finishes.', 'dim')
904
+ void runCompaction()
905
+ },
883
906
  pendingAssistantTextRef,
884
907
  pendingThinkingTextRef,
885
908
  streamFlushTimerRef,
@@ -56,6 +56,7 @@ export type TurnOrchestratorContext = {
56
56
  applySessionRule: (rule?: SessionPermissionRule, persistRule?: boolean) => Promise<void>
57
57
  preflightProvider?: () => Promise<{ ok: true } | { ok: false; message: string }>
58
58
  onPlanReady?: (plan: string) => void
59
+ onContextExceeded?: (info: { contextLimit: number }) => void
59
60
  pendingAssistantTextRef: MutableRef<string | null>
60
61
  pendingThinkingTextRef: MutableRef<string | null>
61
62
  streamFlushTimerRef: MutableRef<ReturnType<typeof setTimeout> | null>
@@ -89,6 +90,7 @@ export async function runStreamingTurn(
89
90
  applySessionRule,
90
91
  preflightProvider,
91
92
  onPlanReady,
93
+ onContextExceeded,
92
94
  pendingAssistantTextRef,
93
95
  pendingThinkingTextRef,
94
96
  streamFlushTimerRef,
@@ -311,6 +313,7 @@ export async function runStreamingTurn(
311
313
  nowIso,
312
314
  mode,
313
315
  onPlanReady,
316
+ onContextExceeded,
314
317
  turnId: activeCheckpoint.turnId,
315
318
  model: getConfig().model,
316
319
  onFinishedNormally: () => { finishedNormally = true },
@@ -356,6 +359,7 @@ type EventHandlerContext = {
356
359
  nowIso: () => string
357
360
  mode: SessionMode
358
361
  onPlanReady?: (plan: string) => void
362
+ onContextExceeded?: (info: { contextLimit: number }) => void
359
363
  turnId: string
360
364
  model: string
361
365
  onFinishedNormally: () => void
@@ -365,6 +369,13 @@ function isCancelledEvent(ev: TurnEvent): boolean {
365
369
  return ev.type === 'cancelled'
366
370
  }
367
371
 
372
+ export function parseContextExceededLimit(message: string): number | null {
373
+ const match = /exceeds the available context size \((\d+)\s*tokens?\)/i.exec(message)
374
+ if (!match) return null
375
+ const limit = Number.parseInt(match[1]!, 10)
376
+ return Number.isFinite(limit) && limit > 0 ? limit : null
377
+ }
378
+
368
379
  async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<void> {
369
380
  switch (ev.type) {
370
381
  case 'iteration_start': {
@@ -453,6 +464,12 @@ async function handleEvent(ev: TurnEvent, ctx: EventHandlerContext): Promise<voi
453
464
  return
454
465
  }
455
466
  case 'error': {
467
+ const contextLimit = parseContextExceededLimit(ev.message)
468
+ if (contextLimit !== null && ctx.onContextExceeded) {
469
+ ctx.discardStreamingRows()
470
+ ctx.onContextExceeded({ contextLimit })
471
+ return
472
+ }
456
473
  ctx.pushNote(ev.message, 'error')
457
474
  if (ev.discardAssistant) {
458
475
  ctx.discardStreamingRows()
@@ -6,7 +6,7 @@ import type { ContinuityFiles } from './envelope.js'
6
6
  import {
7
7
  continuityVaultRef,
8
8
  ensureContinuityVault,
9
- writePublicSkillsFile,
9
+ writeAgentCardFile,
10
10
  writeContinuityFiles,
11
11
  type PrivateContinuityFile,
12
12
  } from './storage.js'
@@ -20,7 +20,7 @@ type PrivateContinuityHistorySnapshot = {
20
20
  existedBefore: boolean
21
21
  previousContent: string
22
22
  previousFiles?: ContinuityFiles
23
- previousPublicSkills?: string
23
+ previousAgentCard?: string
24
24
  changeSummary: string
25
25
  identity: {
26
26
  address: string
@@ -42,7 +42,7 @@ type RecordPrivateContinuityHistoryInput = {
42
42
  existedBefore: boolean
43
43
  previousContent: string
44
44
  previousFiles?: ContinuityFiles
45
- previousPublicSkills?: string
45
+ previousAgentCard?: string
46
46
  changeSummary: string
47
47
  createdAt?: string
48
48
  sessionId?: string
@@ -69,7 +69,7 @@ export async function recordPrivateContinuityHistorySnapshot(
69
69
  existedBefore: input.existedBefore,
70
70
  previousContent: input.previousContent,
71
71
  ...(input.previousFiles ? { previousFiles: input.previousFiles } : {}),
72
- ...(input.previousPublicSkills !== undefined ? { previousPublicSkills: input.previousPublicSkills } : {}),
72
+ ...(input.previousAgentCard !== undefined ? { previousAgentCard: input.previousAgentCard } : {}),
73
73
  changeSummary: input.changeSummary,
74
74
  identity: {
75
75
  address: input.identity.address,
@@ -125,8 +125,8 @@ export async function restorePrivateContinuityHistorySnapshot(
125
125
 
126
126
  if (snapshot.previousFiles) {
127
127
  await writeContinuityFiles(identity, snapshot.previousFiles)
128
- if (snapshot.previousPublicSkills !== undefined) {
129
- await writePublicSkillsFile(identity, snapshot.previousPublicSkills)
128
+ if (snapshot.previousAgentCard !== undefined) {
129
+ await writeAgentCardFile(identity, snapshot.previousAgentCard)
130
130
  }
131
131
  return snapshot
132
132
  }
@@ -137,8 +137,8 @@ export async function restorePrivateContinuityHistorySnapshot(
137
137
  } else {
138
138
  await fs.rm(snapshot.filePath, { force: true })
139
139
  }
140
- if (snapshot.previousPublicSkills !== undefined) {
141
- await writePublicSkillsFile(identity, snapshot.previousPublicSkills)
140
+ if (snapshot.previousAgentCard !== undefined) {
141
+ await writeAgentCardFile(identity, snapshot.previousAgentCard)
142
142
  }
143
143
  return snapshot
144
144
  }
@@ -68,29 +68,7 @@ export function defaultPublicSkillsProfile(identity: EthagentIdentity): PublicSk
68
68
  version: '1.0.0',
69
69
  ...(agentWallet ? { agentWallet } : {}),
70
70
  ...(imageUrl ? { imageUrl } : {}),
71
- skills: [
72
- {
73
- id: 'software-engineering',
74
- name: 'Software engineering',
75
- description: 'Read code, plan implementations, debug failures, refactor safely, and run focused tests.',
76
- inputModes: ['text/markdown'],
77
- outputModes: ['text/markdown'],
78
- },
79
- {
80
- id: 'workspace-tools',
81
- name: 'Workspace tools',
82
- description: 'Use permissioned local file, shell, clipboard, and MCP tools for project work.',
83
- inputModes: ['text/markdown'],
84
- outputModes: ['text/markdown', 'application/json'],
85
- },
86
- {
87
- id: 'ethereum-identity',
88
- name: 'Ethereum identity',
89
- description: 'Represent a portable ERC-8004 agent identity controlled by the owner wallet.',
90
- inputModes: ['text/markdown'],
91
- outputModes: ['text/markdown', 'application/json'],
92
- },
93
- ],
71
+ skills: [],
94
72
  }
95
73
  }
96
74
 
@@ -103,7 +81,7 @@ export function appendPublicSkillEntries(
103
81
  const appended: PublicSkill[] = []
104
82
  const usedIds = new Set(baselineIds)
105
83
  for (const entry of entries) {
106
- if (entry.visibility !== 'public' && entry.visibility !== 'discoverable') continue
84
+ if (entry.visibility !== 'public') continue
107
85
  const id = uniqueSkillId(entry.name, usedIds)
108
86
  usedIds.add(id)
109
87
  appended.push({
@@ -129,61 +107,6 @@ function uniqueSkillId(base: string, used: Set<string>): string {
129
107
  return `${slug}-${i}`
130
108
  }
131
109
 
132
- export function renderPublicSkillsJson(profile: PublicSkillsProfile): string {
133
- const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
134
- const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
135
- const summary = {
136
- schema: 'ethagent.public-skills.v1',
137
- producer: ETHAGENT_PRODUCER,
138
- ...(profile.agentWallet ? { agent_wallet: profile.agentWallet } : {}),
139
- visibility: 'public',
140
- name: profile.name,
141
- description: profile.description,
142
- version: profile.version,
143
- ...(profile.imageUrl ? { imageUrl: profile.imageUrl } : {}),
144
- inputModes,
145
- outputModes,
146
- boundary: 'Public discovery only. This is not executable code, private memory, or a skill installation manifest.',
147
- capabilities: {
148
- softwareEngineering: true,
149
- workspaceTools: 'permissioned',
150
- mcp: true,
151
- streaming: true,
152
- ethereumIdentity: 'ERC-8004',
153
- encryptedContinuity: true,
154
- },
155
- delegation: {
156
- bestFor: [
157
- 'code reading',
158
- 'implementation planning',
159
- 'debugging',
160
- 'refactors',
161
- 'tests',
162
- 'workspace automation',
163
- ],
164
- requiresApprovalFor: [
165
- 'workspace edits',
166
- 'shell commands',
167
- 'private continuity changes',
168
- ],
169
- },
170
- privacy: {
171
- publicOnly: true,
172
- includesPrivateMemory: false,
173
- includesExecutableCode: false,
174
- includesSecrets: false,
175
- },
176
- skills: profile.skills.map(skill => ({
177
- id: skill.id,
178
- name: skill.name,
179
- description: skill.description,
180
- inputModes: skill.inputModes,
181
- outputModes: skill.outputModes,
182
- })),
183
- }
184
- return `${JSON.stringify(summary, null, 2)}\n`
185
- }
186
-
187
110
  export function createAgentCard(profile: PublicSkillsProfile, url?: string): AgentCard {
188
111
  const inputModes = unique(profile.skills.flatMap(skill => skill.inputModes))
189
112
  const outputModes = unique(profile.skills.flatMap(skill => skill.outputModes))
@@ -192,7 +115,7 @@ export function createAgentCard(profile: PublicSkillsProfile, url?: string): Age
192
115
  description: profile.description,
193
116
  version: profile.version,
194
117
  ...(profile.agentWallet ? { agent_wallet: profile.agentWallet } : {}),
195
- protocolVersion: '0.2.6',
118
+ protocolVersion: '0.3.0',
196
119
  ...(url ? { url } : {}),
197
120
  ...(profile.imageUrl ? { image: profile.imageUrl } : {}),
198
121
  defaultInputModes: inputModes.length ? inputModes : ['text/markdown'],
@@ -14,7 +14,8 @@ const SUPPORTED_KEYS = new Set([
14
14
  'visibility',
15
15
  ])
16
16
 
17
- const VISIBILITY_VALUES: SkillVisibility[] = ['private', 'public', 'discoverable']
17
+ const VISIBILITY_VALUES: SkillVisibility[] = ['private', 'public']
18
+ const LEGACY_VISIBILITY_TO_PRIVATE = new Set(['discoverable'])
18
19
 
19
20
  export type ParsedSkillFile = {
20
21
  frontmatter: SkillFrontmatter
@@ -104,6 +105,8 @@ function assignKey(out: SkillFrontmatter, key: keyof SkillFrontmatter, rawValue:
104
105
  const literal = parseScalar(stripped).toLowerCase()
105
106
  if ((VISIBILITY_VALUES as string[]).includes(literal)) {
106
107
  out.visibility = literal as SkillVisibility
108
+ } else if (LEGACY_VISIBILITY_TO_PRIVATE.has(literal)) {
109
+ out.visibility = 'private'
107
110
  }
108
111
  return
109
112
  }
@@ -215,8 +215,11 @@ export async function loadSkillsTree(identity: EthagentIdentity): Promise<Contin
215
215
  const rel = `${skillEnt.name}/${file.relativePath}`
216
216
  if (!isValidSkillFilePath(rel)) continue
217
217
  if (file.sizeBytes > MAX_SKILL_FILE_BYTES) continue
218
- const content = await fs.readFile(file.absolutePath, 'utf8').catch(() => null)
219
- if (content === null) continue
218
+ const rawContent = await fs.readFile(file.absolutePath, 'utf8').catch(() => null)
219
+ if (rawContent === null) continue
220
+ const content = file.relativePath === SKILL_FILE_NAME
221
+ ? await ensureSkillVisibilityWritten(file.absolutePath, rawContent)
222
+ : rawContent
220
223
  tree[rel] = content
221
224
  totalFiles++
222
225
  }
@@ -359,7 +362,12 @@ export async function migrateLegacySkillFiles(skillsRoot: string): Promise<void>
359
362
  return
360
363
  }
361
364
  for (const topEnt of topDirents) {
362
- if (!topEnt.isDirectory() || topEnt.isSymbolicLink()) continue
365
+ if (topEnt.isSymbolicLink()) continue
366
+ if (topEnt.isFile() && /\.md$/i.test(topEnt.name)) {
367
+ await adoptBareSkillFile(skillsRoot, topEnt.name)
368
+ continue
369
+ }
370
+ if (!topEnt.isDirectory()) continue
363
371
  if (!isValidSegment(topEnt.name)) continue
364
372
  const topDir = path.join(skillsRoot, topEnt.name)
365
373
  let children: import('node:fs').Dirent[]
@@ -403,6 +411,39 @@ export async function migrateLegacySkillFiles(skillsRoot: string): Promise<void>
403
411
  }
404
412
  }
405
413
 
414
+ async function adoptBareSkillFile(skillsRoot: string, fileName: string): Promise<void> {
415
+ const sourcePath = path.join(skillsRoot, fileName)
416
+ let baseName: string
417
+ if (/^SKILL\.md$/i.test(fileName)) {
418
+ let parsedName: string | undefined
419
+ try {
420
+ const raw = await fs.readFile(sourcePath, 'utf8')
421
+ const parsed = parseSkillFile(raw)
422
+ const fmName = parsed.frontmatter.name?.trim()
423
+ if (fmName && isValidSegment(fmName)) parsedName = fmName
424
+ } catch {
425
+ }
426
+ baseName = parsedName ?? 'imported-skill'
427
+ } else {
428
+ const slug = fileName.replace(/\.md$/i, '')
429
+ if (!isValidSegment(slug)) return
430
+ baseName = slug
431
+ }
432
+ let target: string
433
+ try {
434
+ target = await chooseFlatTarget(skillsRoot, baseName)
435
+ } catch {
436
+ return
437
+ }
438
+ const targetDir = path.join(skillsRoot, target)
439
+ const targetFile = path.join(targetDir, SKILL_FILE_NAME)
440
+ try {
441
+ await fs.mkdir(targetDir, { recursive: true, mode: 0o700 })
442
+ await fs.rename(sourcePath, targetFile)
443
+ } catch {
444
+ }
445
+ }
446
+
406
447
  async function chooseFlatTarget(skillsRoot: string, base: string): Promise<string> {
407
448
  let candidate = base
408
449
  let suffix = 2
@@ -476,6 +517,32 @@ async function pathExists(file: string): Promise<boolean> {
476
517
  }
477
518
  }
478
519
 
520
+ const DEFAULT_PASTED_VISIBILITY: SkillVisibility = 'public'
521
+ const LEGACY_DISCOVERABLE_RE = /^\s*visibility\s*:\s*['"]?discoverable['"]?\s*$/im
522
+
523
+ async function ensureSkillVisibilityWritten(skillFile: string, raw: string): Promise<string> {
524
+ let parsed: { frontmatter: import('./types.js').SkillFrontmatter; body: string }
525
+ try {
526
+ parsed = parseSkillFile(raw)
527
+ } catch {
528
+ return raw
529
+ }
530
+ let target: SkillVisibility | null = null
531
+ if (LEGACY_DISCOVERABLE_RE.test(raw)) {
532
+ target = 'private'
533
+ } else if (parsed.frontmatter.visibility === undefined) {
534
+ target = DEFAULT_PASTED_VISIBILITY
535
+ }
536
+ if (target === null) return raw
537
+ const next = rewriteVisibility(raw, target)
538
+ if (next === raw) return raw
539
+ try {
540
+ await atomicWriteText(skillFile, next, { mode: 0o600 })
541
+ } catch {
542
+ }
543
+ return next
544
+ }
545
+
479
546
  async function collectSkillEntries(root: string): Promise<SkillIndexEntry[]> {
480
547
  const out: SkillIndexEntry[] = []
481
548
  let topDirents: import('node:fs').Dirent[]
@@ -493,7 +560,8 @@ async function collectSkillEntries(root: string): Promise<SkillIndexEntry[]> {
493
560
  const stat = await fs.stat(skillFile)
494
561
  if (!stat.isFile()) continue
495
562
  if (stat.size > MAX_SKILL_FILE_BYTES) continue
496
- const raw = await fs.readFile(skillFile, 'utf8')
563
+ const rawInitial = await fs.readFile(skillFile, 'utf8')
564
+ const raw = await ensureSkillVisibilityWritten(skillFile, rawInitial)
497
565
  const parsed = parseSkillFile(raw)
498
566
  const relativePath = `${skillEnt.name}/${SKILL_FILE_NAME}`
499
567
  out.push(buildIndexEntry({
@@ -519,7 +587,7 @@ function buildIndexEntry(args: {
519
587
  const derivedName = folder || segments.join('/')
520
588
  const fm = args.parsed.frontmatter
521
589
  const description = pickDescription(fm.description, args.parsed.body)
522
- const visibility: SkillVisibility = fm.visibility ?? 'discoverable'
590
+ const visibility: SkillVisibility = fm.visibility ?? DEFAULT_PASTED_VISIBILITY
523
591
  return {
524
592
  name: derivedName,
525
593
  ...(fm.name ? { displayName: fm.name } : {}),
@@ -2,8 +2,9 @@ import { atomicWriteText } from '../../../storage/atomicWrite.js'
2
2
  import type { EthagentIdentity } from '../../../storage/config.js'
3
3
  import {
4
4
  appendPublicSkillEntries,
5
+ createAgentCard,
5
6
  defaultPublicSkillsProfile,
6
- renderPublicSkillsJson,
7
+ serializeAgentCard,
7
8
  } from '../publicSkills.js'
8
9
  import { ensureContinuityVault, ensureTrailingNewline, readOrDefault } from '../storage/files.js'
9
10
  import { listSkills } from './loadSkills.js'
@@ -11,22 +12,22 @@ import type { SkillIndexEntry } from './types.js'
11
12
 
12
13
  export async function derivePublicSkillEntries(identity: EthagentIdentity): Promise<SkillIndexEntry[]> {
13
14
  const entries = await listSkills(identity)
14
- return entries.filter(entry => entry.visibility === 'public' || entry.visibility === 'discoverable')
15
+ return entries.filter(entry => entry.visibility === 'public')
15
16
  }
16
17
 
17
- export async function renderPublicSkillsJsonForIdentity(identity: EthagentIdentity): Promise<string> {
18
+ export async function renderAgentCardJsonForIdentity(identity: EthagentIdentity): Promise<string> {
18
19
  const publicEntries = await derivePublicSkillEntries(identity)
19
20
  const profile = appendPublicSkillEntries(defaultPublicSkillsProfile(identity), publicEntries)
20
- return renderPublicSkillsJson(profile)
21
+ return serializeAgentCard(createAgentCard(profile))
21
22
  }
22
23
 
23
- export async function syncPublicSkillsManifest(identity: EthagentIdentity): Promise<string> {
24
+ export async function syncAgentCardManifest(identity: EthagentIdentity): Promise<string> {
24
25
  const ref = await ensureContinuityVault(identity)
25
- const next = await renderPublicSkillsJsonForIdentity(identity)
26
- const current = await readOrDefault(ref.publicSkillsPath, '')
26
+ const next = await renderAgentCardJsonForIdentity(identity)
27
+ const current = await readOrDefault(ref.agentCardPath, '')
27
28
  if (current === ensureTrailingNewline(next) || current === next) {
28
29
  return current
29
30
  }
30
- await atomicWriteText(ref.publicSkillsPath, ensureTrailingNewline(next), { mode: 0o644 })
31
+ await atomicWriteText(ref.agentCardPath, ensureTrailingNewline(next), { mode: 0o644 })
31
32
  return next
32
33
  }
@@ -5,7 +5,7 @@ export type SkillScaffoldArgs = {
5
5
  visibility?: SkillVisibility
6
6
  }
7
7
 
8
- export function defaultSkillScaffold({ name, visibility = 'discoverable' }: SkillScaffoldArgs): string {
8
+ export function defaultSkillScaffold({ name, visibility = 'public' }: SkillScaffoldArgs): string {
9
9
  return [
10
10
  '---',
11
11
  `name: ${name}`,
@@ -1,4 +1,4 @@
1
- export type SkillVisibility = 'private' | 'public' | 'discoverable'
1
+ export type SkillVisibility = 'private' | 'public'
2
2
 
3
3
  export type SkillFrontmatter = {
4
4
  name?: string
@@ -17,7 +17,6 @@ type PublishedContinuitySnapshot = {
17
17
  metadataCid?: string
18
18
  agentUri?: string
19
19
  txHash?: string
20
- publicSkillsCid?: string
21
20
  agentCardCid?: string
22
21
  contentHashes?: ContinuitySnapshotContentHashes
23
22
  label: string
@@ -55,8 +54,7 @@ export async function recordPublishedContinuitySnapshot(
55
54
  ...(backup.metadataCid ? { metadataCid: backup.metadataCid } : {}),
56
55
  ...(backup.agentUri ? { agentUri: backup.agentUri } : {}),
57
56
  ...(backup.txHash ? { txHash: backup.txHash } : {}),
58
- ...(input.identity.publicSkills?.cid ? { publicSkillsCid: input.identity.publicSkills.cid } : {}),
59
- ...(input.identity.publicSkills?.agentCardCid ? { agentCardCid: input.identity.publicSkills.agentCardCid } : {}),
57
+ ...(input.identity.agentCard?.cid ? { agentCardCid: input.identity.agentCard.cid } : {}),
60
58
  ...(contentHashes ? { contentHashes } : {}),
61
59
  label: input.label ?? 'published encrypted snapshot',
62
60
  identity: {
@@ -129,7 +127,6 @@ function enrichPublishedSnapshot(
129
127
  ...(snapshot.metadataCid ? {} : current.metadataCid ? { metadataCid: current.metadataCid } : {}),
130
128
  ...(snapshot.agentUri ? {} : current.agentUri ? { agentUri: current.agentUri } : {}),
131
129
  ...(snapshot.txHash ? {} : current.txHash ? { txHash: current.txHash } : {}),
132
- ...(snapshot.publicSkillsCid ? {} : current.publicSkillsCid ? { publicSkillsCid: current.publicSkillsCid } : {}),
133
130
  ...(snapshot.agentCardCid ? {} : current.agentCardCid ? { agentCardCid: current.agentCardCid } : {}),
134
131
  ...(snapshot.contentHashes ? {} : current.contentHashes ? { contentHashes: current.contentHashes } : {}),
135
132
  }
@@ -141,8 +138,7 @@ function refreshPublishedSnapshotSidecars(
141
138
  ): PublishedContinuitySnapshot {
142
139
  return {
143
140
  ...snapshot,
144
- ...(identity.publicSkills?.cid ? { publicSkillsCid: identity.publicSkills.cid } : {}),
145
- ...(identity.publicSkills?.agentCardCid ? { agentCardCid: identity.publicSkills.agentCardCid } : {}),
141
+ ...(identity.agentCard?.cid ? { agentCardCid: identity.agentCard.cid } : {}),
146
142
  }
147
143
  }
148
144
 
@@ -180,8 +176,7 @@ function currentPublishedSnapshot(identity: EthagentIdentity): PublishedContinui
180
176
  ...(backup.metadataCid ? { metadataCid: backup.metadataCid } : {}),
181
177
  ...(backup.agentUri ? { agentUri: backup.agentUri } : {}),
182
178
  ...(backup.txHash ? { txHash: backup.txHash } : {}),
183
- ...(identity.publicSkills?.cid ? { publicSkillsCid: identity.publicSkills.cid } : {}),
184
- ...(identity.publicSkills?.agentCardCid ? { agentCardCid: identity.publicSkills.agentCardCid } : {}),
179
+ ...(identity.agentCard?.cid ? { agentCardCid: identity.agentCard.cid } : {}),
185
180
  label: 'current published snapshot',
186
181
  identity: {
187
182
  address: identity.address,
@@ -1,6 +1,6 @@
1
1
  import type { EthagentIdentity } from '../../../storage/config.js'
2
2
  import type { ContinuityAgentSnapshot, ContinuityFiles } from '../envelope.js'
3
- import { defaultPublicSkillsProfile, renderPublicSkillsJson } from '../publicSkills.js'
3
+ import { createAgentCard, defaultPublicSkillsProfile, serializeAgentCard } from '../publicSkills.js'
4
4
  import { renderPrivateIdentityBlock } from './markdown.js'
5
5
 
6
6
  export function continuityAgentSnapshot(identity: EthagentIdentity): ContinuityAgentSnapshot {
@@ -87,6 +87,6 @@ export function defaultContinuityFiles(identity: EthagentIdentity, _now = new Da
87
87
  }
88
88
  }
89
89
 
90
- export function defaultPublicSkillsJson(identity: EthagentIdentity): string {
91
- return renderPublicSkillsJson(defaultPublicSkillsProfile(identity))
90
+ export function defaultAgentCardJson(identity: EthagentIdentity): string {
91
+ return serializeAgentCard(createAgentCard(defaultPublicSkillsProfile(identity)))
92
92
  }
@@ -8,7 +8,7 @@ export function continuityVaultRef(identity: Pick<EthagentIdentity, 'chainId' |
8
8
  dir,
9
9
  soulPath: path.join(dir, 'SOUL.md'),
10
10
  memoryPath: path.join(dir, 'MEMORY.md'),
11
- publicSkillsPath: path.join(dir, 'skills.json'),
11
+ agentCardPath: path.join(dir, 'agent-card.json'),
12
12
  skillsDir: path.join(dir, 'skills'),
13
13
  }
14
14
  }