Commit Graph

3038 Commits

Author SHA1 Message Date
Lemon c5f659eb0e Add publish_events and fetch_event tools ported from Shakespeare
publish_events: Publishes Nostr events signed by the buddy's key
(falls back to ephemeral when no buddy). Supports any event kind
with content and tags. Returns published events for inline display.
The agent only publishes when explicitly instructed by the user.

fetch_event: Fetches Nostr events by NIP-19 identifier (npub, note,
nevent, naddr, nprofile). Uses the existing nostr pool connection.
Allows the agent to read content the user references in chat.

Both tools adapted from Shakespeare's implementations to fit Ditto's
hook-based tool pattern. System prompt updated with documentation
for both new tools.
2026-04-14 11:28:53 -07:00
Lemon e01e266c31 Sign spells and emoji packs with buddy key instead of ephemeral key
When a buddy is configured, create_spell and create_emoji_pack tools
now sign events with the buddy's persistent keypair. This gives the
buddy a consistent Nostr identity across all created content.

Falls back to ephemeral keys when no buddy exists (preserving current
behavior). Ephemeral key profiles ('Dork Spellcaster', 'Dork Emoji
Maker') are only published in the fallback path since the buddy
already has its own kind 0 profile.
2026-04-14 11:28:53 -07:00
Lemon 48e8ee1de9 Show raw {{NAME}}/{{SOUL}} placeholders in system prompt settings
Settings textarea was showing the resolved prompt (with 'Dork' and
empty soul) because SYSTEM_PROMPT called buildSystemPrompt() which
substituted placeholders immediately.

Fix: export DEFAULT_SYSTEM_PROMPT_TEMPLATE as the raw template string
with {{NAME}} and {{SOUL}} intact. Settings UI displays this so users
can see and edit the actual placeholders. Substitution only happens
at chat time in buildSystemPrompt().

Also removes 'When you use a tool, briefly describe what you created'
-- personality directives belong in the soul, not the base template.
2026-04-14 11:28:53 -07:00
Lemon 6069811372 Rewrite system prompt to use {{NAME}}/{{SOUL}} placeholders throughout
The system prompt is now purely functional — identity and personality
are entirely governed by {{NAME}} and {{SOUL}} placeholders that get
replaced at runtime with the buddy's configured values.

Changes:
- Remove hardcoded 'You are Dork' intro and 'Be concise and friendly'
- Template starts with 'You are {{NAME}}' followed by {{SOUL}}
- Tool documentation reformatted with ## headers for clarity
- buildSystemPrompt() always does placeholder substitution (no
  separate code path for custom vs default)
- Soul textarea style matches system prompt (monospace, text-xs)
2026-04-14 11:28:53 -07:00
Lemon 203fe43ca9 Populate system prompt with default text and remove unnecessary icons
- System prompt textarea now shows the built-in default prompt
  instead of empty with placeholder, so users can directly edit it
- When text matches the default, stores empty string (no override)
- Reset button restores the default prompt text
- Remove Bot icon from Identity label
- Remove Server icon from MCP Servers label
2026-04-14 11:28:53 -07:00
Lemon 64c1cf3642 Add editable system prompt in Advanced Settings
Add aiSystemPrompt to AppConfig (interface, Zod schema, defaults)
for users to override the base system prompt template.

buildSystemPrompt() accepts optional customPrompt param. When set,
it replaces the entire default template while still substituting
{{NAME}} and {{SOUL}} placeholders.

Advanced Settings UI:
- System Prompt textarea below Soul (monospace, shows built-in
  default as placeholder when empty)
- Documents {{NAME}} and {{SOUL}} placeholder usage
- Reset to default button when custom prompt is active
- Saves on blur to AppConfig (persisted in localStorage)
2026-04-14 11:28:53 -07:00
Lemon 20e560a68c Add buddy soul editing and reset in Advanced Settings
Buddy section in Advanced Settings now shows:
- Identity info (name, npub) when buddy exists
- Soul textarea: pre-populated from buddy event, saves on blur
  via updateSoul mutation, re-encrypts and republishes kind 30078
- 'No buddy configured' hint when no buddy exists

Danger Zone now includes:
- Reset Buddy button: wipes localStorage nsec and publishes empty
  kind 30078 event to overwrite on relays
2026-04-14 11:28:29 -07:00
Lemon ff8898eecf Remove unused MobileDorkSheet.tsx (replaced by MobileBuddySheet) 2026-04-14 11:28:29 -07:00
Lemon 75e9f465c5 Rename Dork -> Buddy across sidebar, settings, and mobile UI
User-facing rename:
- Sidebar label: 'Dork' -> 'Buddy'
- AdvancedSettings section header and descriptions
- MobileSearchSheet/MobileBottomNav: dorkMode -> buddyMode
- MobileDorkSheet -> MobileBuddySheet (new file, uses buddy identity)
- Input placeholder: 'Ask Dork...' -> 'Ask {buddyName}...'
- AppContext JSDoc references

Internal rename:
- DorkThinking -> BuddyThinking (deprecated alias kept)
- DORK_ANIMATION -> BUDDY_ANIMATION (deprecated alias kept)

'Dork' is preserved only where it refers to the onboarding wizard
character (useBuddyOnboarding), the default no-buddy fallback persona
(aiChatSystemPrompt), and ephemeral key profiles (Dork Spellcaster).
2026-04-14 11:28:29 -07:00
Lemon b04be19f75 Add buddy onboarding flow and wire buddy identity into chat
Split AIChatPage into three views:
- Logged-out: login prompt
- No buddy: onboarding flow with Dork as static Clippy-like guide
- Buddy exists: full AI chat with buddy's name/soul in system prompt

Onboarding (useBuddyOnboarding):
- Scripted conversation state machine: intro -> name -> soul -> confirm
- Reuses existing MessageBubble components, no API calls
- Dork walks user through naming and describing their buddy's soul
- On confirmation, calls createBuddy() to generate keypair + publish

Chat mode (BuddyChatView):
- Passes buddyName and buddySoul to useAIChatSession
- Header shows buddy name instead of 'Dork'
- Empty state greetings use buddy name
- Input placeholder personalized to buddy name
2026-04-14 11:28:05 -07:00
Lemon dc9cba6651 Add name to buddy encrypted content as canonical source of truth
Move buddy name into the NIP-44 encrypted content alongside nsec
and soul. This avoids needing an extra kind 0 fetch to display the
buddy name on the chat page. The agent's kind 0 profile may use
nicknames, but the identity event name is authoritative.

- BuddySecrets: add required 'name' field
- BuddyIdentity: add 'name' field
- createBuddy: include name in encrypted payload
- updateSoul: preserve existing name when re-encrypting
- decryptSecrets: validate all three required fields
- NIP.md: document name in encrypted content schema
2026-04-14 11:28:05 -07:00
Lemon 09b15bce21 Make system prompt dynamic with {{SOUL}} and {{NAME}} placeholders
Refactor aiChatSystemPrompt.ts: convert static SYSTEM_PROMPT const
into buildSystemPrompt(name?, soul?) function. When a buddy is
configured, the agent name and soul text are injected into the
template. Falls back to 'Dork' persona when no buddy exists.

Update useAIChatSession to accept optional buddyName/buddySoul
params and build the system prompt dynamically via useMemo.
2026-04-14 11:28:04 -07:00
Lemon f6fc10324e Add useBuddy hook and NIP-78 buddy identity event schema
Introduce the foundation for user-created AI agent (Buddy) identities:

- useBuddy hook: manages buddy lifecycle (create, update soul, reset)
  - Generates agent keypair, caches nsec in localStorage
  - Publishes kind 30078 identity event (NIP-78) signed by owner
  - Publishes kind 0 profile for the agent with bot:true
  - NIP-44 encrypts nsec + soul to owner for relay backup
  - Restores from relay if localStorage is cleared

- NIP.md: document kind 30078 buddy event schema
  - Public tags: d-tag, p-tag (agent pubkey), alt, client
  - Encrypted content: nsec + soul
  - Agent has separate kind 0 profile
2026-04-14 11:28:04 -07:00
Lemon 3a742c19f7 Use ephemeral key for create_emoji_pack and return event for inline display
Match create_spell pattern: sign with ephemeral keypair, publish a
profile for the key, and return nostrEvent so the emoji pack card
renders inline in the Dork chat.
2026-04-14 11:28:04 -07:00
Lemon f8c5316eed Add fetch_page, upload_from_url, and create_emoji_pack tools to Dork
Dork can now fetch web pages, download and upload images to Blossom,
and publish NIP-30 emoji packs. This enables the workflow:
'download these emojis from <url> and make an emoji set'

- fetch_page: fetches URL via CORS proxy, extracts image URLs from HTML
- upload_from_url: downloads images and uploads to Blossom servers
- create_emoji_pack: publishes kind 30030 emoji pack as the user
2026-04-14 11:28:04 -07:00
Lemon fcc6d79bb7 Add MCP client support for Dork AI tool discovery
Dork can now connect to Streamable HTTP MCP servers to discover and
use external tools at runtime. This bridges the gap between dev-time
MCP configs (.mcp.json) and the production AI chat system.

- MCPClient class using @modelcontextprotocol/sdk for full spec support
- useMCPTools hook with TanStack Query caching (5-min stale time)
- MCP tools merged into Dork's tool array and routed through MCPClient
- Settings UI in Advanced Settings to add/remove MCP servers
- MCPServer type, Zod schema, and AppConfig integration
2026-04-14 11:28:04 -07:00
Lemon 57f1c912e0 Fix profile tabs disappearing after spell migration for users with legacy tab events 2026-04-14 11:27:41 -07:00
Lemon 1774741678 Stream Dork AI responses in real-time instead of waiting for full completion 2026-04-14 11:27:41 -07:00
Lemon 20d0594c47 Use useFeed for Packs tab to get full events with images 2026-04-14 11:27:41 -07:00
Lemon dc66420cce Add Packs tab to Discover page between Feeds and Posts 2026-04-14 11:27:41 -07:00
Lemon d350f4f271 Update spell card to content-first layout matching follow pack design 2026-04-14 11:27:41 -07:00
Lemon 012c84ab56 Add infinite scroll to spell-driven feed tabs
useStreamPosts now exposes loadMore(), hasMore, and isLoadingMore for
cursor-based pagination. loadMore fetches the next page using
until=oldest-1, same pattern as useFeed for follows/global tabs.

Wired up in all three spell consumers:
- SavedFeedContent (home feed tabs)
- ProfileSavedFeedContent (profile custom tabs)
- SpellRunPage (shared spell viewer)

Previously these only showed the initial batch of ~40 events with no
way to scroll back in time.
2026-04-14 11:27:07 -07:00
Lemon cf1baf2865 Centralize spell tag parsing and fingerprinting in spellEngine
Deduplicate spell-to-UI-state helpers (spellToScope, spellToKinds,
spellToSearch, spellToAuthorPubkeys) that were independently defined
in FeedEditModal, ProfileTabEditModal, and SearchPage.

New shared exports in spellEngine.ts:
- spellAuthors(): raw authors tag values
- spellKinds(): kind tag values as strings
- spellSearch(): search tag value
- spellAuthorPubkeys(): explicit pubkeys, filtering out variables
- spellFingerprint(): stable semantic identity for dedup

SpellRunPage now uses spellFingerprint for saved-feed detection instead
of event ID comparison, consistent with SearchPage's dedup logic.
2026-04-14 11:27:07 -07:00
Lemon de3a4dfb4f Use '#' instead of 'Hashtags' for t-tag filter label 2026-04-14 11:27:07 -07:00
Lemon f6677d1e5d Simplify SpellContent: plain text name, unified outline badges, friendly tag filter labels 2026-04-14 11:27:07 -07:00
Lemon 3e01e7f53d Show friendly kind labels in spell badges (e.g. 'Posts (1)' instead of 'kind:1') 2026-04-14 11:27:07 -07:00
Lemon 3fa00abaa0 Clicking a spell card runs the spell; 'View post details' in More menu goes to post detail 2026-04-14 11:27:07 -07:00
Lemon 8c2d95f9db Rename Search to Discover with compass icon; make Feeds the default tab; route /search to /discover 2026-04-14 11:27:07 -07:00
Lemon d6dc3546eb Rename sidebar Search to Discover with compass icon 2026-04-14 11:27:07 -07:00
Lemon bcc72c9159 Move spells to Feeds tab on Search page; route kind:777 nevent through NIP19Page 2026-04-14 11:27:07 -07:00
Lemon a6b241aecc Replace sidebar star with home feed tab toggle on SpellRunPage 2026-04-14 11:27:07 -07:00
Lemon 15557ef523 Add spell sharing: 'Share' option in search save popover and share button on SpellRunPage 2026-04-14 11:27:07 -07:00
Lemon 576473f1c2 Let $contacts resolve to empty array instead of throwing when user has no follows 2026-04-14 11:27:07 -07:00
Lemon 05a4ce6d68 Extract buildUnsignedSpell helper to deduplicate inline spell construction 2026-04-14 11:27:07 -07:00
Lemon cb6614b42e Cleanup: remove dead code, fix stale comment, clarify Ditto relay routing 2026-04-14 11:27:07 -07:00
Lemon d395ca2079 Fix profile tab $me regression: use literal owner pubkey for 'Me' scope
The spell engine's $me resolves to the logged-in viewer, but profile tabs
need 'Me' scope to mean the profile owner. Fixed by emitting the owner's
literal hex pubkey instead of $me in ProfileTabEditModal.

Also noted the $contacts behavioral change: profile tab 'Contacts' scope
now shows the viewer's contacts (not the owner's). This is intentional --
the old behavior fetched the owner's kind 3 event, but the spell engine
resolves $contacts from the viewer's follow list.
2026-04-14 11:27:07 -07:00
Lemon 0fbf59b436 Cleanup: remove dead code from SavedFeedFiltersEditor and useProfileFeed
- SavedFeedFiltersEditor: removed the main SavedFeedFiltersEditor component
  (unused after modals were rewritten), parseSelectedKinds, kindsToKindFilter,
  getAuthorScope, and the local TabFilter type. Removed unused imports (Globe,
  UserSearch, Input, Separator, useUserLists, useFollowPacks). The file now
  only exports the reusable sub-components (buildKindOptions, KindPicker,
  MultiKindPicker, ScopeToggle, ListPackPicker, AuthorChip, AuthorFilterDropdown).
- useProfileFeed: removed useTabFeed (120 lines) and TabFeedPage type. Profile
  tabs now use useStreamPosts with spell events.
- Removed unused NostrFilter import from useProfileFeed.
2026-04-14 11:27:07 -07:00
Lemon 32539c0aee Migrate profile tabs to spell events; delete useResolveTabFilter
Profile tabs (kind 16769) now store spell events instead of TabFilter objects.
Each tab tag contains a JSON-encoded kind:777 spell with all filter parameters,
runtime variables ($me, $contacts), and client hints in its tags. The old var
tag system is eliminated entirely.

Key changes:
- profileTabsEvent.ts: ProfileTab now has { label, spell } instead of
  { label, filter }. ProfileTabsData no longer has vars. Deleted TabFilter,
  TabVarDef, resolvePointer, resolveFilter (~100 lines removed).
- ProfilePage.tsx: ProfileSavedFeedContent uses useStreamPosts with spell
  instead of useResolveTabFilter + useTabFeed. Core tab spells built via
  buildSpellTags. Removed profileVars, useResolveTabFilter, useTabFeed imports.
- ProfileTabEditModal.tsx: Produces spell events. Parses spell tags to seed form.
  Uses $me for 'me' scope, $contacts for 'contacts' scope.
- SearchPage.tsx: handleSaveProfileTab builds spell events instead of TabFilter.
- Deleted useResolveTabFilter.ts (93 lines) — no longer needed anywhere.
- NIP.md: Updated kind 16769 documentation for spell-based tab format.
2026-04-14 11:27:07 -07:00
Lemon a64575a13c Change SavedFeed to store spell events; update all producers and consumers
SavedFeed now stores a kind:777 spell event instead of TabFilter + TabVarDef.
Old saved feeds in encrypted settings silently drop (Zod schema rejects them).

Key changes:
- AppContext: SavedFeed type changed to { id, label, spell: NostrEvent, createdAt }
  Removed TabFilter and TabVarDef types (kept in profileTabsEvent.ts for profile tabs)
- schemas.ts: SavedFeedSchema validates a NostrEvent in the spell field
- useSavedFeeds: addSavedFeed takes (label, spell) instead of (label, filter, vars)
- Feed.tsx SavedFeedContent: passes feed.spell to useStreamPosts, removed
  useResolveTabFilter usage and all manual option extraction (~40 lines deleted)
- SearchPage handleSaveFeed: builds spell tags via buildSpellTags(), creates an
  unsigned spell event, stores it. Profile tab save path kept as compat shim.
- FeedEditModal: produces spell events from UI state instead of TabFilter objects.
  Parses spell tags to seed the form in edit mode.
- ContentSettings: adapted handlers to new (label, spell) signature
- SavedFeedFiltersEditor: TabFilter defined locally (no longer in AppContext)
2026-04-14 11:27:07 -07:00
Lemon 9d9425c0b9 Add spell support to useStreamPosts; migrate SpellRunPage to use it
useStreamPosts now accepts an optional 'spell' (kind:777 NostrEvent) in its
options. When provided, the spell is resolved internally — variables ($me,
$contacts), relative timestamps, client hints (media, language, sort, etc.)
— and drives the entire stream. All other option fields are ignored in spell
mode.

Key changes to useStreamPosts:
- Resolves spell via resolveSpell() using useCurrentUser + useFollowList
- Derives effectiveOptions/effectiveQuery from spell hints
- Routes initial query to Ditto relays when needsDittoRelay is true
- Merges spell extra filter fields (since, until, tag filters) into queries
- Existing callers unchanged (spell option is optional)

SpellRunPage now uses useStreamPosts with the spell option instead of manual
useQuery + nostr.query, gaining live streaming and the new-posts pill for free.
2026-04-14 11:27:07 -07:00
Lemon 133b9a7227 Extend spell engine with NIP-50 client-hint tags (media, language, platform, sort, include-replies)
- Add client-hint tags to spell format: media, language, platform, sort,
  include-replies. These instruct clients how to build NIP-50 search
  extensions and apply client-side filters.
- Add SpellClientHints interface and needsDittoRelay flag to ResolvedSpell
  for downstream consumers to know when Ditto relay routing is required.
- Move buildSpellTags() from useAIChatTools into spellEngine.ts as the
  single source of truth for spell tag construction.
- Update AI tool schema and system prompt with new parameters and examples.
- Update SpellContent badge display for new tags.
- Document kind 777 in NIP.md.
2026-04-14 11:27:07 -07:00
Lemon 9c5807f49d Fix saved feed 'follows' filter not working: omit #d filter for non-addressable kinds
The $follows variable resolver was querying kind 3 (contact list) with a
'#d' filter, but kind 3 is a replaceable event that has no d tag. This
caused the query to return zero results, so the follows list was never
resolved and the author filter was silently dropped.

Only include '#d' in the query filter for addressable events (30000-39999).
Also fix pre-existing lint error (unused useMemo import) and missing
aiModel field in TestApp.
2026-04-14 11:27:07 -07:00
Lemon e27d928e38 Fix follows filter not saved: add $follows placeholder to authors in TabFilter 2026-04-14 11:26:41 -07:00
Lemon 7dfad82a9b Persist NIP-50 filter extensions (media, language, sort, protocol) in saved feeds and fix sort bug 2026-04-14 11:26:41 -07:00
Lemon 472c8e943f Add opaque background to inline NoteCard in Dork chat 2026-04-14 11:26:41 -07:00
Lemon b615e9d395 Make Dork sheet background transparent to match search sheet overlay style 2026-04-14 11:26:41 -07:00
Lemon 9a5944fff8 Animate Dork toggle button during streaming instead of spinner 2026-04-14 11:26:41 -07:00
Lemon a8a9cbcaf1 Add opaque background to Dork messages area 2026-04-14 11:26:41 -07:00
Lemon d4e518bcbe Add slash commands: /new and /clear to reset chat history 2026-04-14 11:26:41 -07:00
Lemon b55c37f510 Raise bottom nav z-index to match Dork sheet 2026-04-14 11:26:41 -07:00