nebula-ai-core 0.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 (109) hide show
  1. package/README.md +24 -0
  2. package/package.json +69 -0
  3. package/src/brain/compaction.ts +131 -0
  4. package/src/brain/frozen-prefix.ts +320 -0
  5. package/src/brain/history-persist.ts +154 -0
  6. package/src/brain/index.ts +43 -0
  7. package/src/brain/openai-brain.ts +533 -0
  8. package/src/brain/sanitize.ts +23 -0
  9. package/src/brain/stub.ts +20 -0
  10. package/src/brain/types.ts +129 -0
  11. package/src/chain.ts +75 -0
  12. package/src/claude-plugins/discovery.ts +152 -0
  13. package/src/claude-plugins/index.ts +6 -0
  14. package/src/claude-plugins/types.ts +38 -0
  15. package/src/commands/index.ts +16 -0
  16. package/src/commands/registry.ts +255 -0
  17. package/src/config.ts +213 -0
  18. package/src/economy/index.ts +6 -0
  19. package/src/events/index.ts +4 -0
  20. package/src/events/listeners.ts +37 -0
  21. package/src/events/queue.ts +63 -0
  22. package/src/events/router.ts +42 -0
  23. package/src/events/types.ts +28 -0
  24. package/src/format.ts +12 -0
  25. package/src/identity/agent-card.ts +110 -0
  26. package/src/identity/deployments.ts +20 -0
  27. package/src/identity/erc8004.ts +161 -0
  28. package/src/identity/index.ts +29 -0
  29. package/src/identity/keystore-blob.ts +60 -0
  30. package/src/identity/receipt.ts +27 -0
  31. package/src/identity/stub.ts +29 -0
  32. package/src/identity/types.ts +20 -0
  33. package/src/index.ts +372 -0
  34. package/src/locks.ts +233 -0
  35. package/src/mcp/discovery.ts +150 -0
  36. package/src/mcp/index.ts +10 -0
  37. package/src/mcp/manager.ts +110 -0
  38. package/src/mcp/stdio-client.ts +154 -0
  39. package/src/mcp/types.ts +44 -0
  40. package/src/memory/edit.ts +53 -0
  41. package/src/memory/encryption.ts +88 -0
  42. package/src/memory/fs-util.ts +15 -0
  43. package/src/memory/index-file.ts +74 -0
  44. package/src/memory/index-sync.ts +99 -0
  45. package/src/memory/index.ts +58 -0
  46. package/src/memory/list-tool.ts +105 -0
  47. package/src/memory/pack-blob.ts +120 -0
  48. package/src/memory/pack-gather.ts +112 -0
  49. package/src/memory/parser.ts +20 -0
  50. package/src/memory/read-tool.ts +198 -0
  51. package/src/memory/save-tool.ts +189 -0
  52. package/src/memory/scan.ts +63 -0
  53. package/src/memory/topic.ts +32 -0
  54. package/src/memory/types.ts +49 -0
  55. package/src/migration/index.ts +6 -0
  56. package/src/migration/option3-crypto.ts +127 -0
  57. package/src/operator/index.ts +9 -0
  58. package/src/operator/keychain.ts +53 -0
  59. package/src/operator/keystore-file.ts +33 -0
  60. package/src/operator/privkey-base.ts +60 -0
  61. package/src/operator/raw-privkey.ts +39 -0
  62. package/src/operator/signer.ts +46 -0
  63. package/src/operator/walletconnect.ts +454 -0
  64. package/src/pairing.ts +285 -0
  65. package/src/paths.ts +70 -0
  66. package/src/permission/dangerous.ts +108 -0
  67. package/src/permission/env-redact.ts +54 -0
  68. package/src/permission/index.ts +16 -0
  69. package/src/permission/path-guard.ts +114 -0
  70. package/src/permission/service.ts +191 -0
  71. package/src/plugins/context.ts +225 -0
  72. package/src/plugins/hooks.ts +81 -0
  73. package/src/plugins/index.ts +24 -0
  74. package/src/plugins/tool-search.ts +49 -0
  75. package/src/public/card.ts +67 -0
  76. package/src/runtime/activity.ts +29 -0
  77. package/src/runtime/index.ts +2 -0
  78. package/src/runtime/runtime.ts +113 -0
  79. package/src/sandbox/credentials.ts +25 -0
  80. package/src/sandbox/docker.ts +396 -0
  81. package/src/sandbox/factory.ts +99 -0
  82. package/src/sandbox/index.ts +15 -0
  83. package/src/sandbox/linux.ts +141 -0
  84. package/src/sandbox/local.ts +19 -0
  85. package/src/sandbox/macos.ts +71 -0
  86. package/src/sandbox/seatbelt-profile.ts +139 -0
  87. package/src/sandbox/types.ts +129 -0
  88. package/src/skills/index.ts +8 -0
  89. package/src/skills/scanner.ts +257 -0
  90. package/src/skills/triggers.ts +78 -0
  91. package/src/skills/types.ts +37 -0
  92. package/src/storage/encryption.ts +87 -0
  93. package/src/storage/factory.ts +31 -0
  94. package/src/storage/index.ts +11 -0
  95. package/src/storage/local-stub.ts +70 -0
  96. package/src/storage/sqlite.ts +95 -0
  97. package/src/storage/types.ts +21 -0
  98. package/src/tools/escalation.ts +200 -0
  99. package/src/tools/index.ts +11 -0
  100. package/src/tools/registry.ts +152 -0
  101. package/src/tools/types.ts +65 -0
  102. package/src/tools/zod-helpers.ts +36 -0
  103. package/src/tools/zod-schema.ts +99 -0
  104. package/src/wallet/drain.ts +79 -0
  105. package/src/wallet/eoa.ts +51 -0
  106. package/src/wallet/index.ts +47 -0
  107. package/src/wallet/keystore.ts +50 -0
  108. package/src/wallet/operator-keystore-crypto.ts +530 -0
  109. package/src/wallet/operator-session.ts +344 -0
package/src/config.ts ADDED
@@ -0,0 +1,213 @@
1
+ /**
2
+ * User-facing configuration shape for `nebula.config.ts`.
3
+ *
4
+ * Example:
5
+ *
6
+ * import { defineConfig } from 'nebula-ai-core'
7
+ *
8
+ * export default defineConfig({
9
+ * network: 'mantle-mainnet', // or 'mantle-testnet'
10
+ * storage: { network: 'mantle-mainnet' },
11
+ * brain: { model: 'gpt-4o-mini' }, // chosen at `nebula init`
12
+ * plugins: ['onchain', 'system'],
13
+ * tools: { 'defi.*': false, 'shell.run': false },
14
+ * imports: { claudeCode: true },
15
+ * })
16
+ */
17
+
18
+ export type NebulaNetwork = 'mantle-mainnet' | 'mantle-testnet'
19
+
20
+ export type NebulaPlugin = 'onchain' | 'comms' | 'system' | 'telegram'
21
+
22
+ export type OperatorSourceKind = 'walletconnect' | 'keychain' | 'keystore-file' | 'raw-privkey'
23
+
24
+ /**
25
+ * Persisted hint about which operator source to use when commands like
26
+ * `nebula` (chat) and `nebula drain` need to talk to the operator wallet
27
+ * again. Stores enough metadata to reconstruct the signer without re-prompting
28
+ * the user from scratch (passphrases / QR scans still happen per-session).
29
+ */
30
+ export interface OperatorSourceHint {
31
+ source: OperatorSourceKind
32
+ /** Only for `keychain`: the macOS Keychain service name to read. */
33
+ keychainService?: string
34
+ /** Only for `keystore-file`: absolute or `~`-prefixed path to the JSON keystore. */
35
+ keystorePath?: string
36
+ }
37
+
38
+ export interface NebulaConfig {
39
+ identity: {
40
+ /** Operator wallet address that encrypts (and can recover) the agent keystore. */
41
+ operator: string | null
42
+ /** Agent EOA address (separate key, signs + pays gas). */
43
+ agent: string | null
44
+ }
45
+ network: NebulaNetwork
46
+ storage: {
47
+ network: NebulaNetwork
48
+ }
49
+ brain: {
50
+ provider: string | null
51
+ model: string | null
52
+ /** Max assistant output tokens per turn. Default 4096. */
53
+ maxOutputTokens?: number
54
+ /**
55
+ * Model context window. Used for auto-compaction trigger. Default
56
+ * 1_000_000. Override for smaller models.
57
+ */
58
+ contextWindow?: number
59
+ /**
60
+ * Pre-flight summarize-fold of older history when the running estimate
61
+ * breaches `threshold * contextWindow`. Set to `null` to disable.
62
+ * Default: { threshold: 0.5, keepRecent: 8 }.
63
+ */
64
+ compaction?: {
65
+ threshold?: number
66
+ keepRecent?: number
67
+ } | null
68
+ /**
69
+ * Persist channel histories to JSONL under
70
+ * `~/.nebula/agents/<id>/conversations/`. Loaded on boot, appended per
71
+ * turn, atomically rewritten on compaction. Default true.
72
+ */
73
+ persistConversations?: boolean
74
+ }
75
+ plugins: NebulaPlugin[]
76
+ /** Glob-level tool allow/deny. Right-most match wins. */
77
+ tools: Record<string, boolean>
78
+ imports: {
79
+ claudeCode: boolean
80
+ }
81
+ /**
82
+ * Which operator source to use when reconnecting. Optional so legacy configs
83
+ * still parse; commands fall back to the interactive picker when missing.
84
+ */
85
+ operator?: OperatorSourceHint | null
86
+ /**
87
+ * Permission system. `prompt` (default) prompts on dangerous commands;
88
+ * `strict` always denies them; `off` is YOLO (no prompts). The `--yolo` CLI
89
+ * flag and `/yolo` TUI slash both flip the active service to 'off' for the
90
+ * current session without rewriting the file.
91
+ */
92
+ approvals?: {
93
+ mode: 'strict' | 'prompt' | 'off'
94
+ /** Always-approved patterns (regex against `kind|command|path` signature). */
95
+ allowlist?: string[]
96
+ }
97
+ /**
98
+ * Skills system. `disabled` is the persistent list of skill ids that should
99
+ * never auto-load or appear in the index.
100
+ */
101
+ skills?: {
102
+ disabled?: string[]
103
+ }
104
+ /**
105
+ * Operator-supplied additions to the system prompt. `append` is concatenated
106
+ * under a `# Operator instructions` header AFTER nebula's built-in safety +
107
+ * tool-use scaffolding. Can NOT replace the base prompt; use it for personal
108
+ * rules ("always reply in Indonesian", "prefer Bun over npm").
109
+ */
110
+ prompt?: {
111
+ append?: string | null
112
+ }
113
+ /**
114
+ * Multimodal vision routing. Vision limbs (vision.analyze, browser.vision)
115
+ * call this OpenAI-compatible provider; the brain stays on `brain.provider`.
116
+ * Set `null` to disable; tools then return a clear "not configured" error.
117
+ */
118
+ vision?: {
119
+ provider?: string | null
120
+ }
121
+ /**
122
+ * Structural sandbox for limb spawns. Defense-in-depth BENEATH the permission
123
+ * floor — even when `s` (allow session) or yolo grants a destructive command,
124
+ * the sandbox profile prevents writes outside an allowlist (agentDir +
125
+ * workspaceRoot + /tmp/nebula-* + /var/folders).
126
+ *
127
+ * - `none` (default): passthrough. Permission floor only.
128
+ * - `os`: native OS sandbox. macOS = sandbox-exec wrapper. Linux = bubblewrap.
129
+ * - `docker`: long-lived container per session, every spawn through `docker exec`.
130
+ */
131
+ sandbox?: {
132
+ mode?: 'none' | 'os' | 'docker'
133
+ /**
134
+ * docker mode only: container image. Default `oven/bun:1`. Compatible with
135
+ * Docker Desktop AND Podman. Override for custom tooling.
136
+ */
137
+ dockerImage?: string
138
+ /**
139
+ * docker mode only: bind-mount the host's workspaceRoot into the container
140
+ * at /workspace. Default `false` for max isolation.
141
+ */
142
+ dockerMountWorkspace?: boolean
143
+ /**
144
+ * docker mode only: force a specific container runtime binary. Auto-detect
145
+ * by default (tries docker, then podman).
146
+ */
147
+ dockerRuntimePath?: string
148
+ /** docker mode only: CPU cores cap (`--cpus`). Default unlimited. */
149
+ dockerCpu?: number
150
+ /** docker mode only: memory cap in MB (`--memory <N>m`). Default unlimited. */
151
+ dockerMemoryMb?: number
152
+ /**
153
+ * docker mode only: per-container writable-layer disk cap in MB. Linux +
154
+ * overlay2 with pquota only — silently dropped on macOS / podman.
155
+ */
156
+ dockerDiskMb?: number
157
+ /**
158
+ * docker mode only: block all network access from inside the container
159
+ * (`--network=none`). Default false.
160
+ */
161
+ dockerNoNetwork?: boolean
162
+ }
163
+ }
164
+
165
+ export type NebulaConfigInput = Partial<NebulaConfig> & Pick<NebulaConfig, 'network'>
166
+
167
+ const DEFAULT_CONFIG: Omit<NebulaConfig, 'network' | 'storage'> = {
168
+ identity: { operator: null, agent: null },
169
+ brain: { provider: null, model: null },
170
+ plugins: ['onchain', 'system'],
171
+ tools: {},
172
+ imports: { claudeCode: true },
173
+ operator: null,
174
+ approvals: { mode: 'prompt', allowlist: [] },
175
+ skills: { disabled: [] },
176
+ prompt: { append: null },
177
+ vision: { provider: undefined },
178
+ sandbox: { mode: 'none' },
179
+ }
180
+
181
+ export function defineConfig(input: NebulaConfigInput): NebulaConfig {
182
+ return {
183
+ ...DEFAULT_CONFIG,
184
+ identity: input.identity ?? DEFAULT_CONFIG.identity,
185
+ network: input.network,
186
+ storage: input.storage ?? { network: input.network },
187
+ brain: input.brain ?? DEFAULT_CONFIG.brain,
188
+ plugins: input.plugins ?? DEFAULT_CONFIG.plugins,
189
+ tools: input.tools ?? DEFAULT_CONFIG.tools,
190
+ imports: input.imports ?? DEFAULT_CONFIG.imports,
191
+ operator: input.operator ?? DEFAULT_CONFIG.operator,
192
+ approvals: input.approvals ?? DEFAULT_CONFIG.approvals,
193
+ skills: input.skills ?? DEFAULT_CONFIG.skills,
194
+ prompt: input.prompt ?? DEFAULT_CONFIG.prompt,
195
+ vision: input.vision ?? DEFAULT_CONFIG.vision,
196
+ sandbox: input.sandbox ?? DEFAULT_CONFIG.sandbox,
197
+ }
198
+ }
199
+
200
+ export const NETWORK_RPC: Record<NebulaNetwork, string> = {
201
+ 'mantle-mainnet': 'https://rpc.mantle.xyz',
202
+ 'mantle-testnet': 'https://rpc.sepolia.mantle.xyz',
203
+ }
204
+
205
+ export const NETWORK_CHAIN_ID: Record<NebulaNetwork, number> = {
206
+ 'mantle-mainnet': 5000,
207
+ 'mantle-testnet': 5003,
208
+ }
209
+
210
+ export function networkFromChainId(id: number): NebulaNetwork | null {
211
+ return (Object.entries(NETWORK_CHAIN_ID).find(([, cid]) => cid === id)?.[0] ??
212
+ null) as NebulaNetwork | null
213
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Economy module. The legacy decentralized-compute self-funding manager was
3
+ * removed with the 0G compute backend (Nebula uses an API-key LLM). Reserved
4
+ * for Mantle-native economy primitives (treasury limits, spend caps, etc.).
5
+ */
6
+ export {}
@@ -0,0 +1,4 @@
1
+ export type { NebulaEvent, EventPayload, EventSource } from './types'
2
+ export { EventQueue, newEventId } from './queue'
3
+ export { type Listener, listeners } from './listeners'
4
+ export { routeLoop, type RouterDeps } from './router'
@@ -0,0 +1,37 @@
1
+ import type { EventQueue } from './queue'
2
+
3
+ /**
4
+ * A Listener watches some source (stdin, chain, a2a, ...) and pushes events
5
+ * onto the queue. Plugins contribute listeners via `registerListener()`.
6
+ */
7
+ export interface Listener {
8
+ name: string
9
+ source: string
10
+ start(queue: EventQueue): Promise<void>
11
+ stop(): Promise<void>
12
+ }
13
+
14
+ class ListenerRegistry {
15
+ private registered: Listener[] = []
16
+
17
+ register(l: Listener): void {
18
+ if (this.registered.some(x => x.name === l.name)) {
19
+ throw new Error(`Listener already registered: ${l.name}`)
20
+ }
21
+ this.registered.push(l)
22
+ }
23
+
24
+ list(): readonly Listener[] {
25
+ return this.registered
26
+ }
27
+
28
+ async startAll(queue: EventQueue): Promise<void> {
29
+ await Promise.all(this.registered.map(l => l.start(queue)))
30
+ }
31
+
32
+ async stopAll(): Promise<void> {
33
+ await Promise.all(this.registered.map(l => l.stop()))
34
+ }
35
+ }
36
+
37
+ export const listeners = new ListenerRegistry()
@@ -0,0 +1,63 @@
1
+ import type { NebulaEvent } from './types'
2
+
3
+ /**
4
+ * Minimal in-memory FIFO queue. Async-iterable so consumers `for await` over
5
+ * incoming events. Enqueue resolves immediately; dequeue awaits the next event.
6
+ */
7
+ export class EventQueue {
8
+ private buffer: NebulaEvent[] = []
9
+ private waiters: Array<(ev: NebulaEvent) => void> = []
10
+ private closed = false
11
+
12
+ enqueue(ev: NebulaEvent): void {
13
+ if (this.closed) throw new Error('EventQueue closed')
14
+ const w = this.waiters.shift()
15
+ if (w) {
16
+ w(ev)
17
+ return
18
+ }
19
+ this.buffer.push(ev)
20
+ }
21
+
22
+ async dequeue(): Promise<NebulaEvent> {
23
+ const head = this.buffer.shift()
24
+ if (head) return head
25
+ if (this.closed) throw new Error('EventQueue closed')
26
+ return new Promise<NebulaEvent>(resolve => {
27
+ this.waiters.push(resolve)
28
+ })
29
+ }
30
+
31
+ /** Close and wake all waiters with error. */
32
+ close(): void {
33
+ this.closed = true
34
+ for (const w of this.waiters) {
35
+ Promise.resolve().then(() => w({} as NebulaEvent))
36
+ }
37
+ this.waiters = []
38
+ }
39
+
40
+ get length(): number {
41
+ return this.buffer.length
42
+ }
43
+
44
+ get isClosed(): boolean {
45
+ return this.closed
46
+ }
47
+
48
+ async *[Symbol.asyncIterator](): AsyncGenerator<NebulaEvent> {
49
+ while (!this.closed) {
50
+ try {
51
+ yield await this.dequeue()
52
+ } catch {
53
+ return
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ let counter = 0
60
+ export function newEventId(): string {
61
+ counter += 1
62
+ return `${Date.now().toString(36)}-${counter.toString(36)}`
63
+ }
@@ -0,0 +1,42 @@
1
+ import type { Brain, BrainTurn } from '../brain/types'
2
+ import type { ToolRegistry } from '../tools/registry'
3
+ import type { EventQueue } from './queue'
4
+ import type { NebulaEvent } from './types'
5
+
6
+ export interface RouterDeps {
7
+ brain: Brain
8
+ tools: ToolRegistry
9
+ onTurn?: (ev: NebulaEvent, turn: BrainTurn) => void | Promise<void>
10
+ }
11
+
12
+ /**
13
+ * Pulls events from the queue, assembles a prompt via the brain, executes any
14
+ * returned tool_calls until the brain produces a final message, and yields
15
+ * the turn back via `onTurn`.
16
+ */
17
+ export async function routeLoop(queue: EventQueue, deps: RouterDeps): Promise<void> {
18
+ for await (const ev of queue) {
19
+ if (!ev.source) continue // closed sentinel
20
+ await handleOne(ev, deps)
21
+ }
22
+ }
23
+
24
+ async function handleOne(ev: NebulaEvent, deps: RouterDeps): Promise<void> {
25
+ // Seed conversation with the triggering event (stub does echo; real brain
26
+ // in phase 3 will load memory, assemble frozen prefix, etc.)
27
+ const turn = await deps.brain.infer({ event: ev })
28
+
29
+ // Resolve any tool calls in a single iteration for MVP — phase 3 extends
30
+ // this to a proper multi-turn loop.
31
+ for (const call of turn.toolCalls ?? []) {
32
+ const tool = deps.tools.find(call.name)
33
+ if (!tool) continue
34
+ try {
35
+ await tool.handler(call.args)
36
+ } catch {
37
+ // Tool errors are surfaced in activity log by the runtime, not here.
38
+ }
39
+ }
40
+
41
+ await deps.onTurn?.(ev, turn)
42
+ }
@@ -0,0 +1,28 @@
1
+ /** Sources that can enqueue events into the runtime. */
2
+ export type EventSource =
3
+ | 'stdin'
4
+ | 'cron'
5
+ | 'webhook'
6
+ | 'a2a'
7
+ | 'marketplace'
8
+ | 'chain'
9
+ | 'internal'
10
+ | 'telegram'
11
+
12
+ export interface EventPayload {
13
+ /** Short human-readable label for logs/status. */
14
+ label: string
15
+ /** Arbitrary structured data. Listener-specific shape. */
16
+ data: unknown
17
+ /** Any peer address (ECIES pubkey or .0g name) that originated this event. */
18
+ peer?: string
19
+ /** Per-listener hint about which memory topics are relevant for this event. */
20
+ memoryHint?: string[]
21
+ }
22
+
23
+ export interface NebulaEvent {
24
+ id: string
25
+ source: EventSource
26
+ payload: EventPayload
27
+ ts: number
28
+ }
package/src/format.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { formatEther } from 'viem'
2
+
3
+ /**
4
+ * Render a wei bigint as a 6-decimal Mantle string. Matches the statusline,
5
+ * `nebula ledger balance`, and `nebula balance` output styles. Always emits
6
+ * exactly 6 decimal places (zero-padded) so columns align.
7
+ */
8
+ export function formatMnt(wei: bigint): string {
9
+ const raw = formatEther(wei)
10
+ const [whole, frac = ''] = raw.split('.')
11
+ return `${whole}.${frac.padEnd(6, '0').slice(0, 6)}`
12
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * ERC-8004 / A2A "agent card" — the JSON identity document an agent publishes
3
+ * so other agents and systems can discover what it is, where to reach it, and
4
+ * which on-chain identity backs it. Referenced by the Identity Registry's
5
+ * tokenURI (see erc8004.ts).
6
+ */
7
+ import type { Address } from 'viem'
8
+ import type { NebulaNetwork } from '../config'
9
+ import { NETWORK_CHAIN_ID } from '../config'
10
+
11
+ export interface AgentCardSkill {
12
+ id: string
13
+ name: string
14
+ description: string
15
+ }
16
+
17
+ export interface AgentCardRegistration {
18
+ agentId: string
19
+ registry: Address
20
+ chainId: number
21
+ }
22
+
23
+ export interface AgentCard {
24
+ /** A2A protocol version this card conforms to. */
25
+ protocolVersion: string
26
+ name: string
27
+ description: string
28
+ /** Service endpoint (optional; e.g. a gateway URL). */
29
+ url?: string
30
+ version: string
31
+ /** The agent's operational EOA — signs + pays gas. */
32
+ agentAddress: Address
33
+ network: NebulaNetwork
34
+ chainId: number
35
+ /** What this agent is built to do well. */
36
+ capabilities: {
37
+ policyAware: boolean
38
+ simulation: boolean
39
+ approvals: boolean
40
+ auditable: boolean
41
+ }
42
+ skills: AgentCardSkill[]
43
+ /** ERC-8004 on-chain identity registrations backing this card. */
44
+ registrations: AgentCardRegistration[]
45
+ }
46
+
47
+ /** Nebula's default skill set — the defensible treasury-operator surface. */
48
+ export const DEFAULT_AGENT_SKILLS: AgentCardSkill[] = [
49
+ {
50
+ id: 'treasury-ops',
51
+ name: 'Policy-aware treasury operations',
52
+ description:
53
+ 'Transfers, swaps (Agni + Merchant Moe best-execution), wrap/unwrap, and Aave V3 lending on Mantle — every write policy-checked, simulated, and approval-gated.',
54
+ },
55
+ {
56
+ id: 'risk-analysis',
57
+ name: 'Pre-trade risk + counterparty intel',
58
+ description:
59
+ 'Token risk vetting (exit/liquidity/restricted-RWA) and Nansen counterparty labels before any value-moving action.',
60
+ },
61
+ {
62
+ id: 'yield-discovery',
63
+ name: 'Yield discovery',
64
+ description:
65
+ 'DeFiLlama analytics: Mantle pools ranked by APY/TVL with risk + RWA flags (read-only).',
66
+ },
67
+ ]
68
+
69
+ export function buildAgentCard(opts: {
70
+ name: string
71
+ agentAddress: Address
72
+ network: NebulaNetwork
73
+ description?: string
74
+ url?: string
75
+ version?: string
76
+ skills?: AgentCardSkill[]
77
+ registration?: { agentId: bigint; registry: Address }
78
+ }): AgentCard {
79
+ const chainId = NETWORK_CHAIN_ID[opts.network]
80
+ return {
81
+ protocolVersion: '0.3.0',
82
+ name: opts.name,
83
+ description:
84
+ opts.description ??
85
+ 'A Mantle-native, policy-aware AI treasury assistant. The AI advises; deterministic code enforces the fund controls.',
86
+ url: opts.url,
87
+ version: opts.version ?? '0.1.0',
88
+ agentAddress: opts.agentAddress,
89
+ network: opts.network,
90
+ chainId,
91
+ capabilities: { policyAware: true, simulation: true, approvals: true, auditable: true },
92
+ skills: opts.skills ?? DEFAULT_AGENT_SKILLS,
93
+ registrations: opts.registration
94
+ ? [
95
+ {
96
+ agentId: opts.registration.agentId.toString(),
97
+ registry: opts.registration.registry,
98
+ chainId,
99
+ },
100
+ ]
101
+ : [],
102
+ }
103
+ }
104
+
105
+ /** Encode a card as an on-chain `data:` URI so it can be the tokenURI without external hosting. */
106
+ export function cardToDataUri(card: AgentCard): string {
107
+ const json = JSON.stringify(card)
108
+ const b64 = Buffer.from(json, 'utf8').toString('base64')
109
+ return `data:application/json;base64,${b64}`
110
+ }
@@ -0,0 +1,20 @@
1
+ import type { NebulaNetwork } from '../config'
2
+
3
+ export const EXPLORER_BASE: Record<NebulaNetwork, string> = {
4
+ 'mantle-mainnet': 'https://mantlescan.xyz',
5
+ 'mantle-testnet': 'https://sepolia.mantlescan.xyz',
6
+ }
7
+
8
+ export type NetworkName = NebulaNetwork
9
+
10
+ export function explorerTxUrl(network: NebulaNetwork, txHash: string): string {
11
+ return `${EXPLORER_BASE[network]}/tx/${txHash}`
12
+ }
13
+
14
+ export function explorerTokenUrl(
15
+ network: NebulaNetwork,
16
+ contract: string,
17
+ tokenId: bigint,
18
+ ): string {
19
+ return `${EXPLORER_BASE[network]}/token/${contract}/${tokenId}`
20
+ }