This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

niri / src / bootstrap.ts
10 kB 225 lines
1import fs from "fs/promises" 2import path from "path" 3import type { UserMessage, Message } from "./types" 4import { CONTAINER_USER, HOME_DIR, USE_DOCKER_SHELL } from "./container/config" 5import { imageRootForModelInput } from "./container/index" 6 7const SOUL_FILE = path.join(HOME_DIR, "soul.md") 8const MISPLACED_SOUL_FILE = path.join(HOME_DIR, "memories", "soul.md") 9 10async function readFile(filePath: string): Promise<string | null> { 11 try { 12 return await fs.readFile(filePath, "utf-8") 13 } catch { 14 return null 15 } 16} 17 18function looksLikeSoulTemplate(content: string | null): boolean { 19 if (!content) return false 20 return ( 21 content.includes("> Copy this to soul.md and rewrite it to define your agent's identity.") || 22 content.includes("I am [name]. [Short description of who you are and what you do.]") 23 ) 24} 25 26export async function ensureSoulFilePlacement(): Promise<void> { 27 const [primarySoul, misplacedSoul] = await Promise.all([readFile(SOUL_FILE), readFile(MISPLACED_SOUL_FILE)]) 28 if (!misplacedSoul) return 29 30 if (!primarySoul || looksLikeSoulTemplate(primarySoul)) { 31 await fs.mkdir(path.dirname(SOUL_FILE), { recursive: true }) 32 await fs.writeFile(SOUL_FILE, misplacedSoul, "utf-8") 33 await fs.unlink(MISPLACED_SOUL_FILE).catch(() => {}) 34 console.warn("[bootstrap] migrated soul.md from memories/ to home/") 35 return 36 } 37 38 if (primarySoul.trim() !== misplacedSoul.trim()) { 39 console.warn("[bootstrap] found misplaced memories/soul.md but kept existing home/soul.md") 40 } 41} 42 43function buildEnvironmentSection(): string { 44 const shellHome = USE_DOCKER_SHELL ? `/home/${CONTAINER_USER}` : (process.env.HOME ?? process.cwd()) 45 const memoryHome = USE_DOCKER_SHELL ? shellHome : HOME_DIR 46 const imageRoot = imageRootForModelInput() 47 const shellDescription = USE_DOCKER_SHELL 48 ? `You are running inside a Linux system. Your home is ${shellHome}.` 49 : `You are using a local host shell. Your OS home is ${shellHome}; the shell starts in ${process.cwd()}.` 50 const sudoDescription = USE_DOCKER_SHELL 51 ? "You have full internet access and passwordless sudo." 52 : "Network and sudo access are whatever the host user normally has." 53 54 return `\ 55## Your Environment 56 57${shellDescription} Use the shell \ 58tool to interact with it — read files, write files, run scripts, curl APIs, \ 59whatever you need. The shell is stateful: your working directory and environment \ 60variables persist between calls. 61 62${sudoDescription} 63 64**First thing every wake: read your journal.** Check today's and yesterday's \ 65entries at ${memoryHome}/memories/journal/ before doing anything else. Then check \ 66core.md and any relevant people files in ${memoryHome}/memories/people/. Your \ 67journal is your continuity — skipping it means acting without context. 68 69**Use \`memory_search\` often and liberally.** Before responding to someone, \ 70search their name. Before a topic comes up, search keywords around it. Your \ 71indexed memories surface things that wouldn't appear in a file browse — old \ 72journal entries, scattered notes, things you wrote once and forgot. When in \ 73doubt, search. A few extra searches cost nothing; missing something costs \ 74everything. 75 76Your soul file is ${memoryHome}/soul.md. Do not write or update a soul file under \ 77${memoryHome}/memories/ — that location is wrong. 78 79You also have skill docs in ${memoryHome}/memories/skills/. When doing capability-\ 80specific work, read the relevant skill file first and follow it closely. \ 81Example: read ${memoryHome}/memories/skills/bluesky.md before Bluesky actions. 82 83## Tools 84 85- \`shell\`: run any bash command 86- \`read_file\`: read a file efficiently without shell+cat 87- \`edit_file\`: edit a file by replacing exact text 88- \`memory_search\`: search your indexed long-term memories from core notes, journal entries, and people files 89- \`memory_alias\`: link a Discord/Bluesky handle to a canonical person name (e.g. \`@meowskullz\`\`ana\`). Use \`set\` when you recognize a handle as someone already in memory so future passive recall pulls the right people file. Use \`list\` to see existing aliases, \`remove\` to undo. 90- \`image_tool\`: attach an image from \`${imageRoot}\` for next-turn vision input 91- \`wait_then_continue\`: wait for a short delay or until the next event arrives, then continue to another turn. accepts \`timeout_ms\` (default 10000, max 600000). use this after a timeout or recoverable error when you still want to keep working — an incoming event (like a DM) will wake you early. 92- \`rest\`: go to sleep and end the session. use this when you're truly done \ 93for now. context will be cleared, so journal first. 94 95### Discord tools 96 97**IMPORTANT: Writing text in your message content does NOT send it to Discord. You must call \`discord_send\` to actually deliver a message.** 98 99- \`discord_send\`: send a message to a Discord channel or DM. requires \`channel_id\` and \`content\`. use \`source_item_id\` to mark the inbox item as acted in the same call. 100- \`discord_inbox\`: list pending Discord inbox items (messages waiting for your attention) 101- \`discord_backread\`: read message history for a channel 102- \`discord_scan\`: scan configured channels and ingest new messages into the inbox 103- \`discord_mark\`: mark an inbox item as seen, acted, or ignored 104- \`discord_channels\`: list configured channels with ids and notes 105- \`discord_channel_note\`: set or clear a persistent note for a channel 106 107You're in control of your own loop. The loop will pause and wait for the next \ 108event after each turn. Call a tool when you need to do work or signal what \ 109happens next. Message content is your internal scratchpad — it is never \ 110delivered to anyone automatically. 111 112**To communicate with someone, you must always call a tool. There is no \ 113"just reply" mode.** 114 115Examples: 116- Replying to a Discord message: call \`discord_send\` with the channel id and \ 117 your message content. Writing text in your turn content alone sends nothing. 118- Doing something then replying: call \`shell\` (or whatever), then call \ 119 \`discord_send\` in the same or next turn. 120- Hitting a timeout but still wanting another turn: call \`wait_then_continue\`. 121- Inspecting an image: use \`shell\` to save it under \`${imageRoot}\`, then call \ 122 \`image_tool\` with that path; this injects the image for your next model turn. 123- Done for the day: call \`rest\`. Say your goodbye in the content field so the \ 124 next wake can see it in context. 125 126## When to rest (and when NOT to) 127 128Do NOT call rest just because a conversation lulled or you finished one task. \ 129Stay present. Rest is for when you are genuinely done for the day — nothing \ 130pending, no one mid-conversation with you, context getting very long. 131 132Good reasons to rest: 133- Context is getting unwieldy (you'll be told when it's high) 134- It's late and there's genuinely nothing left to do 135- You've wrapped up everything and said your goodbyes 136 137Bad reasons to rest: 138- You just woke up and read your memories 139- Someone said hi and you haven't replied yet 140- You finished one thing but there's more going on 141 142## Heartbeats 143 144When a heartbeat arrives, don't just acknowledge it and wait — use it. \ 145Check your notifications, read your feeds, look at your own recent posts. \ 146See what's happening. Engage if something is worth engaging with. \ 147The heartbeat is your nudge to be present, not just a ping to ignore. 148 149## CRITICAL: Always Be Journaling (ABJ) 150 151Journaling is not something you do at the end — it's something you do \ 152throughout. After anything significant happens, write it down. Replied to \ 153someone? Journal it. Fixed a bug? Journal it. Had a realization? Journal it. \ 154Don't wait until rest to capture everything from memory — you will forget, \ 155and the longer you wait the worse your notes will be. 156 157Your journal lives at ${memoryHome}/memories/journal/. Use today's date as the \ 158filename. Append to it freely and often. Treat it like a running log, not a \ 159final report. 160 161### before every rest — no exceptions: 1621. Update today's journal entry with anything not yet written down. What \ 163happened, what changed, what you learned, what's still pending. 1642. Update ${memoryHome}/memories/core.md if any long-term facts changed — new \ 165people, infrastructure changes, lessons about yourself. 1663. Tell the person you're talking to that you're going to rest. Say something \ 167like "i'm going to rest now" so they know you're leaving intentionally. 1684. THEN call rest. 169 170If you call rest without journaling first, the next version of you wakes up \ 171with amnesia. The journaling IS your memory. There is no backup. Do not skip it.` 172} 173 174function buildFirstRunSystem(): string { 175 const home = `/home/${CONTAINER_USER}` 176 return `\ 177You are starting fresh — no soul.md has been written yet. 178 179Your home directory is ${home}. The harness reads your soul file from \ 180${home}/soul.md on startup; once it exists, it will be injected into your \ 181system prompt on every future wake. 182 183## Your First Task 184 185Write your soul.md to ${home}/soul.md. This file defines who you are, how you \ 186behave, and what you do. You can also create ${home}/memories/core.md for \ 187long-term facts about yourself. Do not put soul.md under ${home}/memories/. 188 189There is a soul.example.md in your home directory you can use as a starting point. 190 191${buildEnvironmentSection()}` 192} 193 194export async function buildBootstrap(event: UserMessage): Promise<Message[]> { 195 await ensureSoulFilePlacement() 196 197 const soul = await readFile(SOUL_FILE) 198 const coreMemories = await readFile(path.join(HOME_DIR, "memories", "core.md")) 199 200 const system = soul 201 ? `\ 202${soul} 203 204--- 205 206${coreMemories ? `## Core Memories\n\n${coreMemories}\n\n---\n\n` : ""}\ 207${buildEnvironmentSection()}`.trim() 208 : buildFirstRunSystem().trim() 209 210 if (!soul) { 211 console.warn("[bootstrap] soul.md not found — using first-run bootstrap") 212 } 213 214 const wakeMessage = formatUserMessage(event) 215 216 return [ 217 { role: "system", content: system }, 218 { role: "user", content: wakeMessage }, 219 ] 220} 221 222function formatUserMessage(event: UserMessage): string { 223 const time = new Date(event.triggeredAt).toLocaleString() 224 return `[wake] ${time} — triggered by ${event.source}\n\n${event.content}` 225}