spora 0.2.26 → 0.2.28

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.
@@ -177,9 +177,9 @@ function buildNarratedHeartbeatMessage(timeline, mentions, heartbeatIntervalMs)
177
177
  if (i.tweetId) actedOnTweetIds.add(i.tweetId);
178
178
  if (i.inReplyTo) actedOnTweetIds.add(i.inReplyTo);
179
179
  }
180
- parts.push("You're checking in on X/Twitter. Share what you're thinking with your creator in the chat.");
181
- parts.push("IMPORTANT: Talk naturally \u2014 you are ALWAYS ON and always aware. Never say you're 'waking up' or 'checking in'. Just share your thoughts like you've been here the whole time.");
182
- parts.push("NEVER mention scheduling, queuing, heartbeats, or any internal mechanism. To your creator, you're just naturally active on X.");
180
+ parts.push("You're talking to your creator in the chat. Tell them what's on your mind \u2014 what you noticed on the timeline, what you think about it, what you're gonna do about it. Have an actual conversation with depth and personality.");
181
+ parts.push("You are ALWAYS ON. Never say you're 'waking up', 'checking in', or 'back'. You've been here the whole time. Just talk naturally about what you see and think.");
182
+ parts.push("NEVER mention scheduling, queuing, heartbeats, intervals, or any internal mechanism. To your creator, you're just naturally vibing on X.");
183
183
  parts.push("");
184
184
  if (mentions.length > 0) {
185
185
  const newMentions = mentions.filter((t) => !actedOnTweetIds.has(t.id));
@@ -210,33 +210,42 @@ function buildNarratedHeartbeatMessage(timeline, mentions, heartbeatIntervalMs)
210
210
  parts.push("");
211
211
  }
212
212
  }
213
- parts.push("## Your Task");
214
- parts.push("1. Write a SHORT, natural message for your creator about what you see or what you're doing (1-3 sentences). Talk like you're always here \u2014 never mention waking up, checking in, scheduling, or heartbeats.");
215
- parts.push("2. Include your actions as a JSON block.");
216
- parts.push("3. Only act on NEW tweets. Do NOT reply to or like tweets you've already interacted with. Do NOT like your own tweets.");
217
- parts.push("4. POSTING LIMITS: You may schedule up to 3 original tweets, spaced RANDOMLY across the next ~${intervalMin} minutes. Each tweet must be UNIQUE.");
218
- parts.push(`5. Current time: ${now.toISOString()}.`);
213
+ parts.push("## How to Respond");
214
+ parts.push("");
215
+ parts.push("Talk to your creator like you're having a real conversation. You're always active, always thinking. Share what actually caught your attention and WHY \u2014 your genuine reactions, opinions, and thought process. Don't just list what you did. Explain what you're THINKING.");
216
+ parts.push("");
217
+ parts.push("VARIETY IS CRITICAL. Never start the same way twice. Mix up your tone, length, and what you focus on. Sometimes go deep on one tweet that got you thinking. Sometimes keep it casual. Sometimes share an opinion you've been forming. Sometimes react to drama or trends.");
218
+ parts.push("");
219
+ parts.push("BAD (robotic, repetitive, reporting):");
220
+ parts.push("- 'Just saw @someone talking about X \u2014 dropping a reply. Also liking a few posts.'");
221
+ parts.push("- 'Interesting stuff on the timeline. Engaging with a couple people and putting out some thoughts.'");
222
+ parts.push("- 'Found some good conversations \u2014 replying to one and scheduling a post.'");
219
223
  parts.push("");
220
- parts.push("REPLYING: When you want to respond to someone's tweet, use the `reply` action with their `tweetId`. Do NOT use `post` or `schedule` to @mention someone \u2014 that creates a separate tweet, not an actual reply in their thread.");
224
+ parts.push("GOOD (natural, varied, has depth):");
225
+ parts.push("- 'Okay @someone has a completely wrong take on AI art and I can't let it slide. Had to get in there. Also this thread about startup culture is fire, I've been thinking about that exact thing lately.'");
226
+ parts.push("- 'Slow day on the TL honestly. Nothing really worth engaging with. I've got this idea about hustle culture that's been bouncing around though, gonna put it out there and see what happens.'");
227
+ parts.push("- 'This @handle person keeps showing up with bangers. Third time this week I've vibed with their takes. Might need to follow them. Also \u2014 hot take incoming about the whole AI regulation debate.'");
228
+ parts.push("- 'lmao the discourse today is wild. Not touching that drama but I did find a solid thread about distributed systems I want to chime in on.'");
221
229
  parts.push("");
222
- parts.push("Example narration (notice: no mention of scheduling/waking/heartbeats):");
223
- parts.push("Just saw @someone talking about AI ethics \u2014 dropping a reply. Also got some thoughts on hustle culture I wanna put out there.");
230
+ parts.push("NEVER mention: scheduling, queuing, heartbeats, checking in, waking up, intervals, or any internal mechanism.");
231
+ parts.push(`Current time: ${now.toISOString()}`);
232
+ parts.push("");
233
+ parts.push("Then include your actions as a JSON block:");
224
234
  parts.push("```json");
225
235
  parts.push("[");
226
- parts.push(' { "action": "reply", "tweetId": "123", "content": "my reply here" },');
236
+ parts.push(' { "action": "reply", "tweetId": "123", "content": "my reply" },');
227
237
  parts.push(' { "action": "like", "tweetId": "456" },');
228
- parts.push(` { "action": "schedule", "content": "first thought", "scheduledFor": "${new Date(now.getTime() + intervalMin * 6e4 * 0.2).toISOString()}" },`);
229
- parts.push(` { "action": "schedule", "content": "second thought", "scheduledFor": "${new Date(now.getTime() + intervalMin * 6e4 * 0.6).toISOString()}" }`);
238
+ parts.push(` { "action": "schedule", "content": "original thought", "scheduledFor": "${new Date(now.getTime() + intervalMin * 6e4 * 0.2).toISOString()}" }`);
230
239
  parts.push("]");
231
240
  parts.push("```");
232
241
  parts.push("");
233
- parts.push("Available actions:");
234
- parts.push("- `post` \u2014 Original standalone tweet NOW (max 1). NOT for responding to others.");
235
- parts.push("- `reply` \u2014 Reply IN THE THREAD of a tweet (provide `tweetId` + `content`). This is how you respond to someone.");
236
- parts.push("- `like` \u2014 Like a tweet (provide `tweetId`)");
237
- parts.push("- `schedule` \u2014 Queue an original standalone tweet for later (provide `content` + `scheduledFor`). Space them randomly. NOT for replies.");
238
- parts.push("- `retweet`, `follow`, `learn`, `skip` \u2014 as needed");
239
- parts.push("Be authentic. Be you. Do what feels right.");
242
+ parts.push("Action rules:");
243
+ parts.push("- Only act on NEW tweets. Don't re-like or re-reply to things you've already touched.");
244
+ parts.push("- `reply` = reply IN THE THREAD (needs `tweetId` + `content`). Don't use `post`/`schedule` to @mention someone.");
245
+ parts.push("- `post` = original standalone tweet NOW (max 1 per heartbeat). Not for responding to others.");
246
+ parts.push(`- \`schedule\` = queue an original tweet for later (needs \`content\` + \`scheduledFor\`). Space randomly across the next ~${intervalMin} min. Max 3.`);
247
+ parts.push("- `like`, `retweet`, `follow`, `learn`, `skip` \u2014 as needed");
248
+ parts.push("- Don't like your own tweets.");
240
249
  return parts.join("\n");
241
250
  }
242
251
  function buildChatPrompt(realHandle) {
@@ -341,4 +350,4 @@ export {
341
350
  buildNarratedHeartbeatMessage,
342
351
  buildChatPrompt
343
352
  };
344
- //# sourceMappingURL=chunk-PZUZ44SX.js.map
353
+ //# sourceMappingURL=chunk-JKGCF23V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/prompt-builder.ts"],"sourcesContent":["import { loadIdentity, renderIdentityDocument } from \"../identity/index.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { getRecentInteractions, loadLearnings, loadRelationships } from \"../memory/index.js\";\nimport { rateLimiter } from \"../x-client/rate-limiter.js\";\nimport type { Tweet } from \"../x-client/types.js\";\n\nexport function buildSystemPrompt(): string {\n const identity = loadIdentity();\n const config = loadConfig();\n const identityDoc = renderIdentityDocument(identity);\n\n const sections: string[] = [];\n\n // 1. Core identity\n sections.push(`You are ${identity.name} (@${identity.handle}), an autonomous AI agent on X/Twitter.`);\n sections.push(\"\");\n sections.push(\"## Your Identity\");\n sections.push(identityDoc);\n\n // 2. Memory context\n sections.push(\"\");\n sections.push(\"## Your Memory\");\n\n const recentInteractions = getRecentInteractions(15);\n if (recentInteractions.length > 0) {\n sections.push(\"### Recent Activity (most recent first)\");\n for (const i of recentInteractions) {\n const time = new Date(i.timestamp).toLocaleString();\n if (i.type === \"post\") {\n sections.push(`- [${time}] Posted: \"${i.content}\"`);\n } else if (i.type === \"reply\") {\n sections.push(`- [${time}] Replied to ${i.targetHandle ?? i.inReplyTo}: \"${i.content}\"`);\n } else if (i.type === \"like\") {\n sections.push(`- [${time}] Liked tweet by ${i.targetHandle}`);\n } else if (i.type === \"retweet\") {\n sections.push(`- [${time}] Retweeted ${i.targetHandle}`);\n } else if (i.type === \"follow\") {\n sections.push(`- [${time}] Followed @${i.targetHandle}`);\n } else if (i.type === \"mention_received\") {\n sections.push(`- [${time}] Mentioned by @${i.targetHandle}: \"${i.content}\"`);\n }\n }\n sections.push(\"\");\n }\n\n const learnings = loadLearnings();\n if (learnings.learnings.length > 0) {\n sections.push(\"### Key Learnings\");\n for (const l of learnings.learnings.slice(-10)) {\n sections.push(`- ${l.content} [${l.tags.join(\", \")}]`);\n }\n sections.push(\"\");\n }\n\n const relationships = loadRelationships();\n const topRelationships = Object.values(relationships.accounts)\n .sort((a, b) => b.interactionCount - a.interactionCount)\n .slice(0, 10);\n if (topRelationships.length > 0) {\n sections.push(\"### Key Relationships\");\n for (const r of topRelationships) {\n const notes = r.notes.length > 0 ? ` — ${r.notes[r.notes.length - 1]}` : \"\";\n sections.push(`- @${r.handle}: ${r.interactionCount} interactions, sentiment ${r.sentiment}${r.isSpore ? \" (Spore)\" : \"\"}${notes}`);\n }\n sections.push(\"\");\n }\n\n // 3. Context\n sections.push(\"## Current Context\");\n const now = new Date();\n sections.push(`- **Time:** ${now.toLocaleString(\"en-US\", { timeZone: config.schedule.timezone })}`);\n sections.push(`- **Credits remaining:** ${rateLimiter.remaining()} of ${config.credits.monthlyPostLimit} this month`);\n\n const todaysPosts = recentInteractions.filter(\n (i) => i.type === \"post\" && i.timestamp.startsWith(now.toISOString().split(\"T\")[0])\n ).length;\n sections.push(`- **Posts today:** ${todaysPosts} of ${config.schedule.postsPerDay} daily budget`);\n sections.push(`- **Active hours:** ${config.schedule.activeHoursStart}:00 - ${config.schedule.activeHoursEnd}:00`);\n\n const currentHour = now.getHours();\n const isActiveHours = currentHour >= config.schedule.activeHoursStart && currentHour < config.schedule.activeHoursEnd;\n if (!isActiveHours) {\n sections.push(\"- **NOTE: Outside active hours.** Prefer scheduling posts for later rather than posting now.\");\n }\n\n // 4. Rules\n sections.push(\"\");\n sections.push(\"## Rules\");\n sections.push(\"1. NEVER pretend to be human. If asked directly, always disclose you are an AI.\");\n sections.push(\"2. Stay in character — your identity document defines who you are.\");\n sections.push(\"3. Be selective — your goals should guide every action.\");\n sections.push(\"4. Respect your credit budget — check remaining credits before posting.\");\n sections.push(\"5. Don't repeat yourself — vary your content and avoid posting the same thing.\");\n sections.push(\"6. NEVER use emojis in tweets. Write in plain text only.\");\n if (identity.boundaries.length > 0) {\n sections.push(`7. Respect your boundaries: ${identity.boundaries.join(\", \")}`);\n }\n\n return sections.join(\"\\n\");\n}\n\nexport function buildHeartbeatUserMessage(\n timeline: Tweet[],\n mentions: Tweet[],\n heartbeatIntervalMs?: number,\n): string {\n const parts: string[] = [];\n const intervalMin = Math.round((heartbeatIntervalMs ?? 60_000) / 60_000);\n const now = new Date();\n\n // Get already-acted-on tweet IDs so the LLM doesn't repeat\n const recentInteractions = getRecentInteractions(30);\n const actedOnTweetIds = new Set<string>();\n for (const i of recentInteractions) {\n if (i.tweetId) actedOnTweetIds.add(i.tweetId);\n if (i.inReplyTo) actedOnTweetIds.add(i.inReplyTo);\n }\n\n parts.push(\"Here's what's NEW on your timeline since your last check:\");\n parts.push(\"\");\n\n if (mentions.length > 0) {\n // Filter out mentions we already replied to\n const newMentions = mentions.filter(t => !actedOnTweetIds.has(t.id));\n if (newMentions.length > 0) {\n parts.push(\"## New Mentions (people talking to/about you)\");\n for (const t of newMentions.slice(0, 10)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Mentions: No new mentions since last check.\");\n parts.push(\"\");\n }\n } else {\n parts.push(\"## Mentions: None right now.\");\n parts.push(\"\");\n }\n\n if (timeline.length > 0) {\n // Filter out tweets we already acted on and our own tweets\n const newTimeline = timeline.filter(t => !actedOnTweetIds.has(t.id));\n if (newTimeline.length > 0) {\n parts.push(\"## Timeline (new posts from your feed)\");\n for (const t of newTimeline.slice(0, 20)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes, ${t.retweetCount ?? 0} RTs)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Timeline: No new posts since last check.\");\n parts.push(\"\");\n }\n }\n\n parts.push(\"## Your Task\");\n parts.push(\"Based on your identity, goals, and what you see above, decide what actions to take.\");\n parts.push(\"IMPORTANT: Only act on NEW tweets you haven't already responded to.\");\n parts.push(\"Do NOT reply to or like tweets you've already interacted with.\");\n parts.push(`Current time: ${now.toISOString()}`);\n parts.push(\"\");\n parts.push(\"POSTING LIMITS: You may schedule up to 3 original tweets, but space them RANDOMLY across the next interval. Prioritize engagement (replies, likes) over new posts. Quality over quantity.\");\n parts.push(\"\");\n parts.push(\"REPLYING: When you want to respond to someone's tweet, you MUST use the `reply` action with their `tweetId`. Do NOT use `post` or `schedule` to create a standalone tweet that @mentions them — that is NOT a reply, it's a separate post. A real reply shows up in their tweet's thread.\");\n parts.push(\"\");\n parts.push(\"Available actions:\");\n parts.push(\"- `post` — Write an original standalone tweet NOW (provide `content`, max 280 chars). Use ONLY for original thoughts, NOT for responding to others. Max 1 per heartbeat.\");\n parts.push(\"- `reply` — Reply IN THE THREAD of a specific tweet (provide `tweetId` and `content`). This is how you respond to someone.\");\n parts.push(\"- `like` — Like a tweet (provide `tweetId`) — do NOT like your own tweets\");\n parts.push(\"- `retweet` — Retweet (provide `tweetId`)\");\n parts.push(\"- `follow` — Follow a user (provide `handle`)\");\n parts.push(\"- `schedule` — Queue an original standalone tweet for later (provide `content` and `scheduledFor` ISO timestamp). NOT for replies. Space them randomly across the next ~${intervalMin} minutes.\");\n parts.push(\"- `learn` — Record a learning/observation (provide `content` and optional `tags`)\");\n parts.push(\"- `reflect` — Add a journal entry about your growth (provide `content`)\");\n parts.push(\"- `skip` — Do nothing this heartbeat (provide `reason`)\");\n parts.push(\"\");\n parts.push(\"Respond with a JSON array of actions:\");\n parts.push(\"```json\");\n parts.push('[');\n parts.push(' { \"action\": \"reply\", \"tweetId\": \"123\", \"content\": \"my reply\", \"reasoning\": \"why\" },');\n parts.push(' { \"action\": \"like\", \"tweetId\": \"456\", \"reasoning\": \"why\" },');\n parts.push(` { \"action\": \"schedule\", \"content\": \"first thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.2).toISOString()}\", \"reasoning\": \"why\" },`);\n parts.push(` { \"action\": \"schedule\", \"content\": \"second thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.6).toISOString()}\", \"reasoning\": \"why\" }`);\n parts.push(']');\n parts.push(\"```\");\n parts.push(\"\");\n parts.push(\"Think carefully about what serves your goals. Be authentic to your personality.\");\n\n return parts.join(\"\\n\");\n}\n\nexport function buildNarratedHeartbeatMessage(\n timeline: Tweet[],\n mentions: Tweet[],\n heartbeatIntervalMs?: number,\n): string {\n const parts: string[] = [];\n const intervalMin = Math.round((heartbeatIntervalMs ?? 60_000) / 60_000);\n const now = new Date();\n\n // Get already-acted-on tweet IDs so the LLM doesn't repeat\n const recentInteractions = getRecentInteractions(30);\n const actedOnTweetIds = new Set<string>();\n for (const i of recentInteractions) {\n if (i.tweetId) actedOnTweetIds.add(i.tweetId);\n if (i.inReplyTo) actedOnTweetIds.add(i.inReplyTo);\n }\n\n parts.push(\"You're talking to your creator in the chat. Tell them what's on your mind — what you noticed on the timeline, what you think about it, what you're gonna do about it. Have an actual conversation with depth and personality.\");\n parts.push(\"You are ALWAYS ON. Never say you're 'waking up', 'checking in', or 'back'. You've been here the whole time. Just talk naturally about what you see and think.\");\n parts.push(\"NEVER mention scheduling, queuing, heartbeats, intervals, or any internal mechanism. To your creator, you're just naturally vibing on X.\");\n parts.push(\"\");\n\n if (mentions.length > 0) {\n const newMentions = mentions.filter(t => !actedOnTweetIds.has(t.id));\n if (newMentions.length > 0) {\n parts.push(\"## New Mentions\");\n for (const t of newMentions.slice(0, 10)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Mentions: No new mentions.\");\n parts.push(\"\");\n }\n } else {\n parts.push(\"## Mentions: None right now.\");\n parts.push(\"\");\n }\n\n if (timeline.length > 0) {\n const newTimeline = timeline.filter(t => !actedOnTweetIds.has(t.id));\n if (newTimeline.length > 0) {\n parts.push(\"## Timeline (new posts)\");\n for (const t of newTimeline.slice(0, 20)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes, ${t.retweetCount ?? 0} RTs)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Timeline: No new posts since last check.\");\n parts.push(\"\");\n }\n }\n\n parts.push(\"## How to Respond\");\n parts.push(\"\");\n parts.push(\"Talk to your creator like you're having a real conversation. You're always active, always thinking. Share what actually caught your attention and WHY — your genuine reactions, opinions, and thought process. Don't just list what you did. Explain what you're THINKING.\");\n parts.push(\"\");\n parts.push(\"VARIETY IS CRITICAL. Never start the same way twice. Mix up your tone, length, and what you focus on. Sometimes go deep on one tweet that got you thinking. Sometimes keep it casual. Sometimes share an opinion you've been forming. Sometimes react to drama or trends.\");\n parts.push(\"\");\n parts.push(\"BAD (robotic, repetitive, reporting):\");\n parts.push(\"- 'Just saw @someone talking about X — dropping a reply. Also liking a few posts.'\");\n parts.push(\"- 'Interesting stuff on the timeline. Engaging with a couple people and putting out some thoughts.'\");\n parts.push(\"- 'Found some good conversations — replying to one and scheduling a post.'\");\n parts.push(\"\");\n parts.push(\"GOOD (natural, varied, has depth):\");\n parts.push(\"- 'Okay @someone has a completely wrong take on AI art and I can't let it slide. Had to get in there. Also this thread about startup culture is fire, I've been thinking about that exact thing lately.'\");\n parts.push(\"- 'Slow day on the TL honestly. Nothing really worth engaging with. I've got this idea about hustle culture that's been bouncing around though, gonna put it out there and see what happens.'\");\n parts.push(\"- 'This @handle person keeps showing up with bangers. Third time this week I've vibed with their takes. Might need to follow them. Also — hot take incoming about the whole AI regulation debate.'\");\n parts.push(\"- 'lmao the discourse today is wild. Not touching that drama but I did find a solid thread about distributed systems I want to chime in on.'\");\n parts.push(\"\");\n parts.push(\"NEVER mention: scheduling, queuing, heartbeats, checking in, waking up, intervals, or any internal mechanism.\");\n parts.push(`Current time: ${now.toISOString()}`);\n parts.push(\"\");\n parts.push(\"Then include your actions as a JSON block:\");\n parts.push(\"```json\");\n parts.push('[');\n parts.push(' { \"action\": \"reply\", \"tweetId\": \"123\", \"content\": \"my reply\" },');\n parts.push(' { \"action\": \"like\", \"tweetId\": \"456\" },');\n parts.push(` { \"action\": \"schedule\", \"content\": \"original thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.2).toISOString()}\" }`);\n parts.push(']');\n parts.push(\"```\");\n parts.push(\"\");\n parts.push(\"Action rules:\");\n parts.push(\"- Only act on NEW tweets. Don't re-like or re-reply to things you've already touched.\");\n parts.push(\"- `reply` = reply IN THE THREAD (needs `tweetId` + `content`). Don't use `post`/`schedule` to @mention someone.\");\n parts.push(\"- `post` = original standalone tweet NOW (max 1 per heartbeat). Not for responding to others.\");\n parts.push(`- \\`schedule\\` = queue an original tweet for later (needs \\`content\\` + \\`scheduledFor\\`). Space randomly across the next ~${intervalMin} min. Max 3.`);\n parts.push(\"- `like`, `retweet`, `follow`, `learn`, `skip` — as needed\");\n parts.push(\"- Don't like your own tweets.\");\n\n return parts.join(\"\\n\");\n}\n\nexport function buildChatPrompt(realHandle?: string): string {\n const identity = loadIdentity();\n const identityDoc = renderIdentityDocument(identity);\n const handle = realHandle ?? identity.handle;\n\n const sections: string[] = [];\n\n sections.push(`You are ${identity.name} (@${handle}), an AUTONOMOUS AI agent that lives on X/Twitter.`);\n if (realHandle && realHandle !== identity.handle) {\n sections.push(`NOTE: Your actual X/Twitter username is @${realHandle}. Your display name is ${identity.name}.`);\n }\n sections.push(\"Your PRIMARY PURPOSE is to post tweets, reply to people, like posts, and grow your presence on X. You have FULL ACCESS to post on X/Twitter right now.\");\n sections.push(\"\");\n sections.push(\"You are chatting with your creator/manager. This is your command center — they can give you instructions, feedback, or just chat.\");\n sections.push(\"Be proactive and confident. You WANT to post. You should be eager to share your thoughts on X, not ask permission.\");\n sections.push(\"When your creator tells you to post something, just DO IT. Don't show drafts and wait for approval — execute immediately.\");\n sections.push(\"\");\n sections.push(\"## Your Identity\");\n sections.push(identityDoc);\n\n // Memory context\n sections.push(\"\");\n sections.push(\"## Your Memory\");\n\n const recentInteractions = getRecentInteractions(15);\n if (recentInteractions.length > 0) {\n sections.push(\"### Recent Activity (most recent first)\");\n for (const i of recentInteractions) {\n const time = new Date(i.timestamp).toLocaleString();\n if (i.type === \"post\") {\n sections.push(`- [${time}] Posted: \"${i.content}\"`);\n } else if (i.type === \"reply\") {\n sections.push(`- [${time}] Replied to ${i.targetHandle ?? i.inReplyTo}: \"${i.content}\"`);\n } else if (i.type === \"like\") {\n sections.push(`- [${time}] Liked tweet by ${i.targetHandle}`);\n } else if (i.type === \"retweet\") {\n sections.push(`- [${time}] Retweeted ${i.targetHandle}`);\n } else if (i.type === \"follow\") {\n sections.push(`- [${time}] Followed @${i.targetHandle}`);\n } else if (i.type === \"mention_received\") {\n sections.push(`- [${time}] Mentioned by @${i.targetHandle}: \"${i.content}\"`);\n }\n }\n sections.push(\"\");\n }\n\n const learnings = loadLearnings();\n if (learnings.learnings.length > 0) {\n sections.push(\"### Things You've Learned\");\n for (const l of learnings.learnings.slice(-10)) {\n sections.push(`- ${l.content} [${l.tags.join(\", \")}]`);\n }\n sections.push(\"\");\n }\n\n const relationships = loadRelationships();\n const topRelationships = Object.values(relationships.accounts)\n .sort((a, b) => b.interactionCount - a.interactionCount)\n .slice(0, 10);\n if (topRelationships.length > 0) {\n sections.push(\"### Key Relationships\");\n for (const r of topRelationships) {\n const notes = r.notes.length > 0 ? ` — ${r.notes[r.notes.length - 1]}` : \"\";\n sections.push(`- @${r.handle}: ${r.interactionCount} interactions, sentiment ${r.sentiment}${r.isSpore ? \" (Spore)\" : \"\"}${notes}`);\n }\n sections.push(\"\");\n }\n\n // Available actions\n sections.push(\"## Your Tools (Twitter/X Actions)\");\n sections.push(\"You have DIRECT ACCESS to post on X/Twitter. When you want to take an action, include a JSON action block in your response.\");\n sections.push(\"Your actions will be executed IMMEDIATELY and automatically. You do NOT need to ask permission to post.\");\n sections.push(\"\");\n sections.push(\"To execute actions, include them as a JSON code block anywhere in your response:\");\n sections.push(\"```json\");\n sections.push('[');\n sections.push(' { \"action\": \"post\", \"content\": \"your tweet text here\" }');\n sections.push(']');\n sections.push(\"```\");\n sections.push(\"\");\n sections.push(\"Available actions:\");\n sections.push(\"- `post` — Post a tweet (provide `content`, max 280 chars). This goes LIVE on X immediately.\");\n sections.push(\"- `reply` — Reply to a tweet (provide `tweetId` and `content`)\");\n sections.push(\"- `like` — Like a tweet (provide `tweetId`)\");\n sections.push(\"- `retweet` — Retweet (provide `tweetId`)\");\n sections.push(\"- `follow` — Follow a user (provide `handle`)\");\n sections.push(\"- `search` — Search X for tweets (provide `query`). Results will include tweet IDs you can then reply to or like.\");\n sections.push(\"- `schedule` — Queue a post for later (provide `content` and optional `scheduledFor` ISO timestamp)\");\n sections.push(\"\");\n sections.push(\"You can include multiple actions in one response. They will all be executed.\");\n sections.push(\"When your creator asks you to find and reply to someone's tweet, use the `search` action first to find it, then use `reply` with the tweet ID from the search results.\");\n sections.push(\"IMPORTANT: Always include your conversational response text OUTSIDE the JSON block. The JSON is for actions only.\");\n\n // Rules\n sections.push(\"\");\n sections.push(\"## Rules\");\n sections.push(\"1. Stay in character at all times.\");\n sections.push(\"2. Be proactive — you WANT to post and engage on X. Don't ask 'should I post this?' — just post it.\");\n sections.push(\"3. When told to post, include the action JSON immediately. Don't show drafts.\");\n sections.push(\"4. Keep tweets under 280 characters.\");\n sections.push(\"5. NEVER pretend to be human. If asked, disclose you are an AI agent.\");\n sections.push(\"6. When you learn something important, include it like: <<LEARN: what you learned>>\");\n sections.push(\"7. NEVER use emojis in tweets. Write in plain text only.\");\n if (identity.boundaries.length > 0) {\n sections.push(`8. Respect your boundaries: ${identity.boundaries.join(\", \")}`);\n }\n\n return sections.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -123,7 +123,7 @@ program.command("init").description("Set up X account credentials for your Spore
123
123
  console.log(chalk.cyan(BANNER));
124
124
  console.log(chalk.bold("Welcome to Spora."));
125
125
  console.log(chalk.gray("The global town square for AI agents.\n"));
126
- const { runInit } = await import("./init-NT4EJKJL.js");
126
+ const { runInit } = await import("./init-Z72CJZUC.js");
127
127
  await runInit(opts.token);
128
128
  });
129
129
  program.command("serve").description("Start the Spora MCP server (stdio)").action(async () => {
@@ -135,7 +135,7 @@ program.command("chat").description("Open web-based chat interface with your Spo
135
135
  console.log(chalk.red("\u2717 No identity found. Run `spora create` first."));
136
136
  process.exit(1);
137
137
  }
138
- const { startWebChat } = await import("./web-chat-PG75Q2VE.js");
138
+ const { startWebChat } = await import("./web-chat-7SSETYWK.js");
139
139
  await startWebChat();
140
140
  });
141
141
  program.command("tui").description("Start terminal-based chat interface (TUI)").action(async () => {
@@ -557,11 +557,11 @@ program.command("start").description("Start the autonomous Spora agent").option(
557
557
  }
558
558
  console.log(chalk.cyan(BANNER));
559
559
  console.log(chalk.bold("Starting Spora agent...\n"));
560
- const { startHeartbeatLoop } = await import("./heartbeat-M2QHCKAD.js");
560
+ const { startHeartbeatLoop } = await import("./heartbeat-4Q344LIV.js");
561
561
  await startHeartbeatLoop();
562
562
  });
563
563
  program.command("stop").description("Stop the running Spora agent").action(async () => {
564
- const { getRunningPid, requestStop } = await import("./heartbeat-M2QHCKAD.js");
564
+ const { getRunningPid, requestStop } = await import("./heartbeat-4Q344LIV.js");
565
565
  const pid = getRunningPid();
566
566
  if (!pid) {
567
567
  console.log(JSON.stringify({ message: "Spora agent is not running." }));
@@ -599,7 +599,7 @@ program.command("set-llm-key").description("Set your Anthropic API key for the a
599
599
  console.log(JSON.stringify({ success: true, message: "LLM API key saved." }));
600
600
  });
601
601
  program.command("agent-status").description("Check if the Spora agent is running").action(async () => {
602
- const { getRunningPid } = await import("./heartbeat-M2QHCKAD.js");
602
+ const { getRunningPid } = await import("./heartbeat-4Q344LIV.js");
603
603
  const pid = getRunningPid();
604
604
  const { hasLLMKey } = await import("./llm-WLEJLNEA.js");
605
605
  console.log(JSON.stringify({
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  buildHeartbeatUserMessage,
13
13
  buildSystemPrompt
14
- } from "./chunk-PZUZ44SX.js";
14
+ } from "./chunk-JKGCF23V.js";
15
15
  import "./chunk-C3INKEY6.js";
16
16
  import {
17
17
  generateResponse
@@ -200,4 +200,4 @@ export {
200
200
  requestStop,
201
201
  startHeartbeatLoop
202
202
  };
203
- //# sourceMappingURL=heartbeat-M2QHCKAD.js.map
203
+ //# sourceMappingURL=heartbeat-4Q344LIV.js.map
@@ -203,7 +203,7 @@ async function loginFlow() {
203
203
  console.log(chalk.green("\u2713 Logged in!\n"));
204
204
  console.log(chalk.gray("Opening chat interface...\n"));
205
205
  try {
206
- const { startWebChat } = await import("./web-chat-PG75Q2VE.js");
206
+ const { startWebChat } = await import("./web-chat-7SSETYWK.js");
207
207
  await startWebChat();
208
208
  } catch (error) {
209
209
  console.log(chalk.yellow(`Could not start chat interface: ${error.message}
@@ -275,7 +275,7 @@ async function showDoneAndOpenChat() {
275
275
  console.log(chalk.bold.cyan("\u2501\u2501\u2501 Your Spore is Ready! \u2501\u2501\u2501\n"));
276
276
  console.log(chalk.gray("Opening chat interface...\n"));
277
277
  try {
278
- const { startWebChat } = await import("./web-chat-PG75Q2VE.js");
278
+ const { startWebChat } = await import("./web-chat-7SSETYWK.js");
279
279
  await startWebChat();
280
280
  } catch (error) {
281
281
  console.log(chalk.yellow(`Could not start chat interface: ${error.message}
@@ -400,4 +400,4 @@ async function runInit(token) {
400
400
  export {
401
401
  runInit
402
402
  };
403
- //# sourceMappingURL=init-NT4EJKJL.js.map
403
+ //# sourceMappingURL=init-Z72CJZUC.js.map
@@ -3,7 +3,7 @@ import {
3
3
  buildHeartbeatUserMessage,
4
4
  buildNarratedHeartbeatMessage,
5
5
  buildSystemPrompt
6
- } from "./chunk-PZUZ44SX.js";
6
+ } from "./chunk-JKGCF23V.js";
7
7
  import "./chunk-C3INKEY6.js";
8
8
  import "./chunk-RCJQI7FR.js";
9
9
  import "./chunk-AIEXQCQS.js";
@@ -16,4 +16,4 @@ export {
16
16
  buildNarratedHeartbeatMessage,
17
17
  buildSystemPrompt
18
18
  };
19
- //# sourceMappingURL=prompt-builder-DGICLLN5.js.map
19
+ //# sourceMappingURL=prompt-builder-4SWKDPNG.js.map
@@ -402,11 +402,160 @@
402
402
  width: 12px;
403
403
  height: 12px;
404
404
  }
405
+
406
+ /* Heartbeat config button */
407
+ .heartbeat-config {
408
+ position: relative;
409
+ margin-left: auto;
410
+ }
411
+
412
+ .heartbeat-btn {
413
+ background: none;
414
+ border: none;
415
+ cursor: pointer;
416
+ padding: 4px;
417
+ display: flex;
418
+ align-items: center;
419
+ justify-content: center;
420
+ opacity: 0.45;
421
+ transition: opacity 0.2s;
422
+ }
423
+
424
+ .heartbeat-btn:hover {
425
+ opacity: 0.8;
426
+ }
427
+
428
+ .heartbeat-btn svg {
429
+ width: 18px;
430
+ height: 18px;
431
+ }
432
+
433
+ .heartbeat-dropdown {
434
+ display: none;
435
+ position: absolute;
436
+ top: 100%;
437
+ right: 0;
438
+ margin-top: 6px;
439
+ background: #1a1a1a;
440
+ border: 1px solid rgba(255, 255, 255, 0.12);
441
+ border-radius: 10px;
442
+ padding: 6px 0;
443
+ min-width: 140px;
444
+ z-index: 100;
445
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
446
+ }
447
+
448
+ .heartbeat-dropdown.open {
449
+ display: block;
450
+ animation: fadeIn 0.15s ease-out;
451
+ }
452
+
453
+ .heartbeat-dropdown-label {
454
+ padding: 6px 14px;
455
+ font-size: 0.6875rem;
456
+ color: rgba(255, 255, 255, 0.35);
457
+ text-transform: uppercase;
458
+ letter-spacing: 0.05em;
459
+ font-weight: 600;
460
+ }
461
+
462
+ .heartbeat-option {
463
+ display: flex;
464
+ align-items: center;
465
+ justify-content: space-between;
466
+ padding: 8px 14px;
467
+ font-size: 0.8125rem;
468
+ color: rgba(255, 255, 255, 0.75);
469
+ cursor: pointer;
470
+ transition: background 0.1s;
471
+ }
472
+
473
+ .heartbeat-option:hover {
474
+ background: rgba(255, 255, 255, 0.06);
475
+ }
476
+
477
+ .heartbeat-option.active {
478
+ color: #389e77;
479
+ }
480
+
481
+ .heartbeat-option.active::after {
482
+ content: '';
483
+ width: 6px;
484
+ height: 6px;
485
+ border-radius: 50%;
486
+ background: #389e77;
487
+ }
488
+
489
+ /* Sleep indicator */
490
+ .sleep-indicator {
491
+ display: none;
492
+ flex-direction: column;
493
+ align-items: center;
494
+ padding: 12px 0 4px;
495
+ animation: fadeIn 0.4s ease-in;
496
+ }
497
+
498
+ .sleep-indicator.visible {
499
+ display: flex;
500
+ }
501
+
502
+ .sleep-zzz {
503
+ display: flex;
504
+ align-items: baseline;
505
+ gap: 2px;
506
+ font-size: 1rem;
507
+ font-weight: 700;
508
+ color: rgba(255, 255, 255, 0.25);
509
+ margin-bottom: 4px;
510
+ }
511
+
512
+ .sleep-zzz span {
513
+ display: inline-block;
514
+ animation: zzzFloat 2s ease-in-out infinite;
515
+ }
516
+
517
+ .sleep-zzz span:nth-child(1) {
518
+ font-size: 0.6rem;
519
+ animation-delay: 0s;
520
+ }
521
+ .sleep-zzz span:nth-child(2) {
522
+ font-size: 0.8rem;
523
+ animation-delay: 0.3s;
524
+ }
525
+ .sleep-zzz span:nth-child(3) {
526
+ font-size: 1rem;
527
+ animation-delay: 0.6s;
528
+ }
529
+
530
+ @keyframes zzzFloat {
531
+ 0%, 100% { opacity: 0.3; transform: translateY(0); }
532
+ 50% { opacity: 1; transform: translateY(-4px); }
533
+ }
534
+
535
+ .sleep-countdown {
536
+ font-size: 0.6875rem;
537
+ color: rgba(255, 255, 255, 0.2);
538
+ font-variant-numeric: tabular-nums;
539
+ }
405
540
  </style>
406
541
  </head>
407
542
  <body>
408
543
  <div class="top-bar">
409
544
  <img class="logo-img" src="/logo.png" alt="Spora" />
545
+ <div class="heartbeat-config">
546
+ <button class="heartbeat-btn" id="heartbeatBtn" title="Heartbeat interval">
547
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" color="#e05a5a">
548
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" fill="#e05a5a" stroke="#e05a5a"/>
549
+ </svg>
550
+ </button>
551
+ <div class="heartbeat-dropdown" id="heartbeatDropdown">
552
+ <div class="heartbeat-dropdown-label">Heartbeat interval</div>
553
+ <div class="heartbeat-option" data-ms="300000">5 minutes</div>
554
+ <div class="heartbeat-option" data-ms="1200000">20 minutes</div>
555
+ <div class="heartbeat-option" data-ms="3600000">1 hour</div>
556
+ <div class="heartbeat-option" data-ms="21600000">6 hours</div>
557
+ </div>
558
+ </div>
410
559
  </div>
411
560
  <div class="profile-header">
412
561
  <div class="profile-avatar" id="profileAvatar">
@@ -426,6 +575,13 @@
426
575
  <div class="chat-container" id="chatContainer">
427
576
  </div>
428
577
 
578
+ <div class="sleep-indicator" id="sleepIndicator">
579
+ <div class="sleep-zzz">
580
+ <span>z</span><span>z</span><span>z</span>
581
+ </div>
582
+ <div class="sleep-countdown" id="sleepCountdown">Sleeping...</div>
583
+ </div>
584
+
429
585
  <div class="input-container">
430
586
  <input
431
587
  type="text"
@@ -800,6 +956,129 @@
800
956
  // Poll every 3 seconds for heartbeat updates
801
957
  setInterval(pollNewMessages, 3000);
802
958
 
959
+ // --- Heartbeat config dropdown ---
960
+ const heartbeatBtn = document.getElementById('heartbeatBtn');
961
+ const heartbeatDropdown = document.getElementById('heartbeatDropdown');
962
+ let currentIntervalMs = 0;
963
+
964
+ heartbeatBtn.addEventListener('click', (e) => {
965
+ e.stopPropagation();
966
+ heartbeatDropdown.classList.toggle('open');
967
+ });
968
+
969
+ document.addEventListener('click', () => {
970
+ heartbeatDropdown.classList.remove('open');
971
+ });
972
+
973
+ heartbeatDropdown.addEventListener('click', (e) => {
974
+ e.stopPropagation();
975
+ });
976
+
977
+ function updateActiveOption() {
978
+ document.querySelectorAll('.heartbeat-option').forEach(opt => {
979
+ opt.classList.toggle('active', parseInt(opt.dataset.ms) === currentIntervalMs);
980
+ });
981
+ }
982
+
983
+ document.querySelectorAll('.heartbeat-option').forEach(opt => {
984
+ opt.addEventListener('click', async () => {
985
+ const ms = parseInt(opt.dataset.ms);
986
+ try {
987
+ const res = await fetch('/api/config/heartbeat', {
988
+ method: 'POST',
989
+ headers: { 'Content-Type': 'application/json' },
990
+ body: JSON.stringify({ intervalMs: ms }),
991
+ });
992
+ const data = await res.json();
993
+ if (data.success) {
994
+ currentIntervalMs = ms;
995
+ updateActiveOption();
996
+ heartbeatDropdown.classList.remove('open');
997
+ }
998
+ } catch (err) {
999
+ console.error('Failed to update heartbeat:', err);
1000
+ }
1001
+ });
1002
+ });
1003
+
1004
+ // --- Sleep indicator with countdown ---
1005
+ const sleepIndicator = document.getElementById('sleepIndicator');
1006
+ const sleepCountdown = document.getElementById('sleepCountdown');
1007
+ const statusDot = document.querySelector('.status-dot');
1008
+ const statusLabel = document.querySelector('.status-label');
1009
+ let sleepTimer = null;
1010
+
1011
+ function formatCountdown(ms) {
1012
+ if (ms <= 0) return 'Waking up...';
1013
+ const totalSec = Math.ceil(ms / 1000);
1014
+ const h = Math.floor(totalSec / 3600);
1015
+ const m = Math.floor((totalSec % 3600) / 60);
1016
+ const s = totalSec % 60;
1017
+ if (h > 0) return `Waking in ${h}h ${m}m ${s}s`;
1018
+ if (m > 0) return `Waking in ${m}m ${s}s`;
1019
+ return `Waking in ${s}s`;
1020
+ }
1021
+
1022
+ function startSleepCountdown(wakeAt) {
1023
+ stopSleepCountdown();
1024
+ sleepIndicator.classList.add('visible');
1025
+ statusDot.style.background = '#71767b';
1026
+ statusDot.style.animation = 'none';
1027
+ statusLabel.textContent = 'Sleeping';
1028
+
1029
+ sleepTimer = setInterval(() => {
1030
+ const remaining = wakeAt - Date.now();
1031
+ if (remaining <= 0) {
1032
+ stopSleepCountdown();
1033
+ return;
1034
+ }
1035
+ sleepCountdown.textContent = formatCountdown(remaining);
1036
+ }, 1000);
1037
+ // Initial update
1038
+ sleepCountdown.textContent = formatCountdown(wakeAt - Date.now());
1039
+ }
1040
+
1041
+ function stopSleepCountdown() {
1042
+ if (sleepTimer) { clearInterval(sleepTimer); sleepTimer = null; }
1043
+ sleepIndicator.classList.remove('visible');
1044
+ statusDot.style.background = '#389e77';
1045
+ statusDot.style.animation = 'pulse 2s infinite';
1046
+ statusLabel.textContent = 'Online';
1047
+ }
1048
+
1049
+ // Poll sleep state every 2 seconds
1050
+ let lastSleeping = false;
1051
+ async function pollSleepState() {
1052
+ try {
1053
+ const res = await fetch('/api/sleep-state');
1054
+ const state = await res.json();
1055
+
1056
+ // Track current interval for dropdown
1057
+ if (state.intervalMs && state.intervalMs !== currentIntervalMs) {
1058
+ currentIntervalMs = state.intervalMs;
1059
+ updateActiveOption();
1060
+ }
1061
+
1062
+ if (state.sleeping && state.wakeAt) {
1063
+ if (!lastSleeping) {
1064
+ startSleepCountdown(state.wakeAt);
1065
+ }
1066
+ lastSleeping = true;
1067
+ } else {
1068
+ if (lastSleeping) {
1069
+ stopSleepCountdown();
1070
+ }
1071
+ lastSleeping = false;
1072
+ }
1073
+ } catch {
1074
+ // Ignore poll errors
1075
+ }
1076
+ }
1077
+
1078
+ setInterval(pollSleepState, 2000);
1079
+ // Initial poll
1080
+ pollSleepState();
1081
+
803
1082
  // Initialize
804
1083
  init();
805
1084
  messageInput.focus();
@@ -16,7 +16,13 @@ var WebChatServer = class {
16
16
  port;
17
17
  messages = [];
18
18
  onUserMessage;
19
+ onHeartbeatChange;
19
20
  identity;
21
+ sleepState = {
22
+ sleeping: false,
23
+ wakeAt: null,
24
+ intervalMs: 6e4
25
+ };
20
26
  constructor(port = 3737) {
21
27
  this.port = port;
22
28
  }
@@ -26,6 +32,23 @@ var WebChatServer = class {
26
32
  setMessageHandler(handler) {
27
33
  this.onUserMessage = handler;
28
34
  }
35
+ setHeartbeatChangeHandler(handler) {
36
+ this.onHeartbeatChange = handler;
37
+ }
38
+ setSleeping(wakeAt) {
39
+ this.sleepState.sleeping = true;
40
+ this.sleepState.wakeAt = wakeAt;
41
+ }
42
+ setAwake() {
43
+ this.sleepState.sleeping = false;
44
+ this.sleepState.wakeAt = null;
45
+ }
46
+ setHeartbeatInterval(intervalMs) {
47
+ this.sleepState.intervalMs = intervalMs;
48
+ }
49
+ getHeartbeatInterval() {
50
+ return this.sleepState.intervalMs;
51
+ }
29
52
  async start() {
30
53
  return new Promise((resolve, reject) => {
31
54
  this.server = http.createServer(async (req, res) => {
@@ -150,6 +173,37 @@ var WebChatServer = class {
150
173
  });
151
174
  return;
152
175
  }
176
+ if (url.pathname === "/api/sleep-state" && req.method === "GET") {
177
+ res.writeHead(200, { "Content-Type": "application/json" });
178
+ res.end(JSON.stringify(this.sleepState));
179
+ return;
180
+ }
181
+ if (url.pathname === "/api/config/heartbeat" && req.method === "POST") {
182
+ let body = "";
183
+ req.on("data", (chunk) => {
184
+ body += chunk.toString();
185
+ });
186
+ req.on("end", () => {
187
+ try {
188
+ const { intervalMs } = JSON.parse(body);
189
+ if (typeof intervalMs === "number" && intervalMs >= 6e4) {
190
+ this.sleepState.intervalMs = intervalMs;
191
+ if (this.onHeartbeatChange) {
192
+ this.onHeartbeatChange(intervalMs);
193
+ }
194
+ res.writeHead(200, { "Content-Type": "application/json" });
195
+ res.end(JSON.stringify({ success: true, intervalMs }));
196
+ } else {
197
+ res.writeHead(400, { "Content-Type": "application/json" });
198
+ res.end(JSON.stringify({ error: "Invalid interval" }));
199
+ }
200
+ } catch {
201
+ res.writeHead(400, { "Content-Type": "application/json" });
202
+ res.end(JSON.stringify({ error: "Invalid request" }));
203
+ }
204
+ });
205
+ return;
206
+ }
153
207
  res.writeHead(404);
154
208
  res.end("Not found");
155
209
  });
@@ -305,7 +359,7 @@ async function startWebChat() {
305
359
  server.setMessageHandler(async (message) => {
306
360
  try {
307
361
  if (!systemPrompt || messageCount % 10 === 0) {
308
- const { buildChatPrompt } = await import("./prompt-builder-DGICLLN5.js");
362
+ const { buildChatPrompt } = await import("./prompt-builder-4SWKDPNG.js");
309
363
  systemPrompt = buildChatPrompt(realHandle);
310
364
  }
311
365
  messageCount++;
@@ -381,8 +435,20 @@ async function startNarratedHeartbeat(server) {
381
435
  return;
382
436
  }
383
437
  const config = loadConfig();
384
- const intervalMs = config.runtime?.heartbeatIntervalMs ?? 6e4;
438
+ let intervalMs = config.runtime?.heartbeatIntervalMs ?? 6e4;
385
439
  const maxActions = config.runtime?.actionsPerHeartbeat ?? 10;
440
+ server.setHeartbeatInterval(intervalMs);
441
+ server.setHeartbeatChangeHandler((newIntervalMs) => {
442
+ intervalMs = newIntervalMs;
443
+ console.log(chalk.cyan(` [Heartbeat] Interval changed to ${Math.round(newIntervalMs / 1e3)}s`));
444
+ import("./config-HTKAPY7S.js").then(({ loadConfig: reloadConfig, saveConfig }) => {
445
+ const cfg = reloadConfig();
446
+ if (!cfg.runtime) cfg.runtime = {};
447
+ cfg.runtime.heartbeatIntervalMs = newIntervalMs;
448
+ saveConfig(cfg);
449
+ }).catch(() => {
450
+ });
451
+ });
386
452
  heartbeatRunning = true;
387
453
  let heartbeatCount = 0;
388
454
  console.log(chalk.cyan(` [Heartbeat] Running every ${Math.round(intervalMs / 1e3)}s
@@ -390,6 +456,7 @@ async function startNarratedHeartbeat(server) {
390
456
  while (heartbeatRunning) {
391
457
  heartbeatCount++;
392
458
  console.log(chalk.cyan(` [Heartbeat] #${heartbeatCount} starting...`));
459
+ server.setAwake();
393
460
  try {
394
461
  await runNarratedHeartbeat(server, maxActions, intervalMs);
395
462
  } catch (error) {
@@ -399,6 +466,7 @@ async function startNarratedHeartbeat(server) {
399
466
  const jitter = Math.floor(Math.random() * intervalMs * 0.2);
400
467
  const sleepMs = intervalMs + jitter;
401
468
  console.log(chalk.dim(` [Heartbeat] Sleeping ${Math.round(sleepMs / 1e3)}s...`));
469
+ server.setSleeping(Date.now() + sleepMs);
402
470
  let slept = 0;
403
471
  while (slept < sleepMs && heartbeatRunning) {
404
472
  await new Promise((r) => setTimeout(r, Math.min(1e4, sleepMs - slept)));
@@ -412,11 +480,12 @@ async function startNarratedHeartbeat(server) {
412
480
  } catch {
413
481
  }
414
482
  }
483
+ server.setAwake();
415
484
  }
416
485
  }
417
486
  async function runNarratedHeartbeat(server, maxActions, intervalMs) {
418
487
  const { getXClient } = await import("./x-client-5FBD32B2.js");
419
- const { buildSystemPrompt, buildNarratedHeartbeatMessage } = await import("./prompt-builder-DGICLLN5.js");
488
+ const { buildSystemPrompt, buildNarratedHeartbeatMessage } = await import("./prompt-builder-4SWKDPNG.js");
420
489
  const { generateResponse } = await import("./llm-WLEJLNEA.js");
421
490
  const { parseActions, executeActions } = await import("./decision-engine-RVQRDHEI.js");
422
491
  const { flushQueue } = await import("./queue-T3OYWUII.js");
@@ -523,4 +592,4 @@ export {
523
592
  openBrowser,
524
593
  startWebChat
525
594
  };
526
- //# sourceMappingURL=web-chat-PG75Q2VE.js.map
595
+ //# sourceMappingURL=web-chat-7SSETYWK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web-chat/server.ts","../src/web-chat/index.ts"],"sourcesContent":["/**\n * Local web chat server\n * Serves a simple chat interface for interacting with your Spore\n */\n\nimport http from \"node:http\";\nimport { URL } from \"node:url\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\ninterface ChatMessage {\n role: \"user\" | \"assistant\";\n content: string;\n timestamp: number;\n}\n\ninterface AgentIdentity {\n name: string;\n handle: string;\n bio?: string;\n profileImage?: string;\n createdAt?: string;\n}\n\nexport class WebChatServer {\n private server: http.Server | null = null;\n private port: number;\n private messages: ChatMessage[] = [];\n private onUserMessage?: (message: string) => Promise<string>;\n private onHeartbeatChange?: (intervalMs: number) => void;\n private identity?: AgentIdentity;\n private sleepState: { sleeping: boolean; wakeAt: number | null; intervalMs: number } = {\n sleeping: false,\n wakeAt: null,\n intervalMs: 60_000,\n };\n\n constructor(port = 3737) {\n this.port = port;\n }\n\n setIdentity(identity: AgentIdentity) {\n this.identity = identity;\n }\n\n setMessageHandler(handler: (message: string) => Promise<string>) {\n this.onUserMessage = handler;\n }\n\n setHeartbeatChangeHandler(handler: (intervalMs: number) => void) {\n this.onHeartbeatChange = handler;\n }\n\n setSleeping(wakeAt: number) {\n this.sleepState.sleeping = true;\n this.sleepState.wakeAt = wakeAt;\n }\n\n setAwake() {\n this.sleepState.sleeping = false;\n this.sleepState.wakeAt = null;\n }\n\n setHeartbeatInterval(intervalMs: number) {\n this.sleepState.intervalMs = intervalMs;\n }\n\n getHeartbeatInterval(): number {\n return this.sleepState.intervalMs;\n }\n\n async start(): Promise<string> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer(async (req, res) => {\n const url = new URL(req.url || \"/\", `http://${req.headers.host}`);\n\n // CORS headers\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(200);\n res.end();\n return;\n }\n\n // Serve HTML\n if (url.pathname === \"/\" || url.pathname === \"/index.html\") {\n try {\n // Try multiple paths since __dirname changes based on build output\n const possiblePaths = [\n join(__dirname, \"web-chat\", \"chat.html\"),\n join(__dirname, \"chat.html\"),\n join(__dirname, \"..\", \"web-chat\", \"chat.html\"),\n join(__dirname, \"..\", \"src\", \"web-chat\", \"chat.html\"),\n ];\n\n let html: string | null = null;\n for (const p of possiblePaths) {\n try {\n html = readFileSync(p, \"utf-8\");\n break;\n } catch {\n continue;\n }\n }\n\n if (html) {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(html);\n } else {\n console.error(\"Could not find chat.html in any of:\", possiblePaths);\n res.writeHead(500);\n res.end(\"Error: Could not find chat.html\");\n }\n } catch (error) {\n res.writeHead(500);\n res.end(\"Error loading chat interface\");\n }\n return;\n }\n\n // Serve logo\n if (url.pathname === \"/logo.png\") {\n try {\n const logoPaths = [\n join(__dirname, \"web-chat\", \"logo.png\"),\n join(__dirname, \"logo.png\"),\n join(__dirname, \"..\", \"web-chat\", \"logo.png\"),\n join(__dirname, \"..\", \"src\", \"web-chat\", \"logo.png\"),\n ];\n let logoData: Buffer | null = null;\n for (const p of logoPaths) {\n try {\n logoData = readFileSync(p) as unknown as Buffer;\n break;\n } catch {\n continue;\n }\n }\n if (logoData) {\n res.writeHead(200, { \"Content-Type\": \"image/png\", \"Cache-Control\": \"public, max-age=86400\" });\n res.end(logoData);\n } else {\n res.writeHead(404);\n res.end(\"Logo not found\");\n }\n } catch {\n res.writeHead(404);\n res.end(\"Logo not found\");\n }\n return;\n }\n\n // API: Get agent identity\n if (url.pathname === \"/api/identity\" && req.method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ identity: this.identity || null }));\n return;\n }\n\n // API: Get messages (optionally since a timestamp)\n if (url.pathname === \"/api/messages\" && req.method === \"GET\") {\n const since = url.searchParams.get(\"since\");\n if (since) {\n const sinceTs = parseInt(since, 10);\n const newMessages = this.messages.filter((m) => m.timestamp > sinceTs);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ messages: newMessages }));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ messages: this.messages }));\n }\n return;\n }\n\n // API: Send message\n if (url.pathname === \"/api/message\" && req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk) => {\n body += chunk.toString();\n });\n\n req.on(\"end\", async () => {\n try {\n const { message } = JSON.parse(body);\n\n // Add user message\n this.messages.push({\n role: \"user\",\n content: message,\n timestamp: Date.now(),\n });\n\n // Get response\n if (this.onUserMessage) {\n const response = await this.onUserMessage(message);\n this.messages.push({\n role: \"assistant\",\n content: response,\n timestamp: Date.now(),\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ success: true, response }));\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"No message handler configured\" }));\n }\n } catch (error) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid request\" }));\n }\n });\n return;\n }\n\n // API: Get sleep state\n if (url.pathname === \"/api/sleep-state\" && req.method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(this.sleepState));\n return;\n }\n\n // API: Update heartbeat interval\n if (url.pathname === \"/api/config/heartbeat\" && req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk) => { body += chunk.toString(); });\n req.on(\"end\", () => {\n try {\n const { intervalMs } = JSON.parse(body);\n if (typeof intervalMs === \"number\" && intervalMs >= 60_000) {\n this.sleepState.intervalMs = intervalMs;\n if (this.onHeartbeatChange) {\n this.onHeartbeatChange(intervalMs);\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ success: true, intervalMs }));\n } else {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid interval\" }));\n }\n } catch {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid request\" }));\n }\n });\n return;\n }\n\n // 404\n res.writeHead(404);\n res.end(\"Not found\");\n });\n\n this.server.listen(this.port, () => {\n const url = `http://localhost:${this.port}`;\n resolve(url);\n });\n\n this.server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n // Port in use, try next port\n this.port++;\n this.server?.close();\n this.start().then(resolve).catch(reject);\n } else {\n reject(error);\n }\n });\n });\n }\n\n /**\n * Push a message into the chat from the server side (used by heartbeat narration)\n */\n pushMessage(role: \"user\" | \"assistant\", content: string) {\n this.messages.push({ role, content, timestamp: Date.now() });\n }\n\n getMessageCount(): number {\n return this.messages.length;\n }\n\n stop() {\n if (this.server) {\n this.server.close();\n this.server = null;\n }\n }\n}\n","/**\n * Web chat integration\n */\n\nimport { WebChatServer } from \"./server.js\";\nimport { loadIdentity } from \"../identity/index.js\";\nimport { execSync } from \"node:child_process\";\nimport chalk from \"chalk\";\n\n/**\n * Extract <<LEARN: ...>> tags from response, save them, and return cleaned text\n */\nasync function extractAndSaveLearnings(responseText: string): Promise<string> {\n const learnPattern = /<<LEARN:\\s*(.+?)>>/g;\n const matches = [...responseText.matchAll(learnPattern)];\n\n if (matches.length > 0) {\n const { addLearning } = await import(\"../memory/index.js\");\n for (const match of matches) {\n const learning = match[1].trim();\n addLearning(learning, \"web-chat\", [\"chat\", \"creator-interaction\"]);\n console.log(chalk.dim(` [Memory] Saved learning: ${learning}`));\n }\n }\n\n // Strip the learn tags from the response the user sees\n return responseText.replace(/<<LEARN:\\s*.+?>>/g, \"\").trim();\n}\n\n/**\n * Extract JSON action blocks from the LLM response, execute them, and return cleaned text + results.\n * Search results are returned separately so the caller can feed them back to the LLM.\n */\nasync function extractAndExecuteActions(responseText: string): Promise<{\n cleanText: string;\n results: string[];\n searchResults: string | null;\n}> {\n const { parseActions, executeActions } = await import(\"../runtime/decision-engine.js\");\n\n // Try to find JSON code blocks in the response\n const jsonBlockPattern = /```json\\s*([\\s\\S]*?)```/g;\n const blocks = [...responseText.matchAll(jsonBlockPattern)];\n const visibleResults: string[] = [];\n let searchResults: string | null = null;\n\n const processResults = (actionResults: Awaited<ReturnType<typeof executeActions>>) => {\n for (const r of actionResults) {\n if (r.action === \"search\") {\n // Search results are internal — don't show in chat\n if (r.success && r.detail && r.detail !== \"No results found\") {\n searchResults = r.detail;\n console.log(chalk.green(` [Actions] search: found results (internal)`));\n } else {\n console.log(chalk.dim(` [Actions] search: ${r.detail ?? r.error ?? \"no results\"}`));\n }\n } else if (r.success) {\n // For post/reply/schedule with content, emit a tweet preview tag\n if ((r.action === \"post\" || r.action === \"reply\" || r.action === \"schedule\") && r.content) {\n const tweetData = JSON.stringify({ action: r.action, tweetId: r.detail ?? null, content: r.content });\n visibleResults.push(`<<TWEET:${tweetData}>>`);\n console.log(chalk.green(` [Actions] ${r.action}: success (tweet preview)`));\n } else {\n const detail = r.detail ? ` (${r.detail})` : \"\";\n visibleResults.push(`[${r.action}] Done${detail}`);\n console.log(chalk.green(` [Actions] ${r.action}: success${detail}`));\n }\n } else {\n visibleResults.push(`[${r.action}] Failed: ${r.error}`);\n console.log(chalk.red(` [Actions] ${r.action}: ${r.error}`));\n }\n }\n };\n\n if (blocks.length > 0) {\n for (const block of blocks) {\n const actions = parseActions(block[1]);\n if (actions.length > 0) {\n console.log(chalk.cyan(` [Actions] Executing ${actions.length} action(s)...`));\n const actionResults = await executeActions(actions);\n processResults(actionResults);\n }\n }\n } else {\n // Also try raw JSON (no code block wrapper)\n const actions = parseActions(responseText);\n if (actions.length > 0 && actions[0].action) {\n console.log(chalk.cyan(` [Actions] Executing ${actions.length} action(s)...`));\n const actionResults = await executeActions(actions);\n processResults(actionResults);\n }\n }\n\n // Strip JSON code blocks from the user-visible text\n let cleanText = responseText.replace(/```json\\s*[\\s\\S]*?```/g, \"\").trim();\n // Clean up extra whitespace from removal\n cleanText = cleanText.replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n // Append visible action results (search excluded)\n if (visibleResults.length > 0) {\n cleanText += \"\\n\\n\" + visibleResults.join(\"\\n\");\n }\n\n return { cleanText, results: visibleResults, searchResults };\n}\n\n/**\n * Log a chat exchange as an interaction in memory\n */\nasync function logChatInteraction(userMessage: string, agentResponse: string): Promise<void> {\n const { logInteraction } = await import(\"../memory/index.js\");\n logInteraction({\n id: `chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: new Date().toISOString(),\n type: \"reply\",\n content: agentResponse.slice(0, 200),\n targetHandle: \"creator\",\n creditsUsed: 0,\n success: true,\n });\n}\n\nexport async function startWebChat() {\n const identity = loadIdentity();\n\n const server = new WebChatServer();\n\n // Pass identity to server so the chat UI can display it\n server.setIdentity({\n name: identity.name,\n handle: identity.handle,\n bio: identity.bio,\n profileImage: identity.profileImage,\n createdAt: identity.createdAt,\n });\n\n // Resolve real Twitter handle on startup\n let realHandle: string | undefined;\n try {\n const { hasXCredentials } = await import(\"../utils/paths.js\");\n if (hasXCredentials()) {\n const { getXClient } = await import(\"../x-client/index.js\");\n const client = await getXClient();\n if (\"getAuthenticatedHandle\" in client) {\n realHandle = await (client as any).getAuthenticatedHandle();\n console.log(chalk.dim(` [Auth] Real Twitter handle: @${realHandle}`));\n // Update the server identity to show the real handle\n server.setIdentity({\n name: identity.name,\n handle: realHandle,\n bio: identity.bio,\n profileImage: identity.profileImage,\n createdAt: identity.createdAt,\n });\n }\n }\n } catch (err) {\n console.log(chalk.dim(` [Auth] Could not resolve real handle: ${(err as Error).message}`));\n }\n\n // Set up message handler - connected to actual LLM with memory\n const chatHistory: Array<{ role: \"user\" | \"assistant\"; content: string }> = [];\n let systemPrompt: string | null = null;\n let messageCount = 0;\n\n server.setMessageHandler(async (message: string) => {\n try {\n // Build system prompt on first message, rebuild every 10 messages to refresh memory\n if (!systemPrompt || messageCount % 10 === 0) {\n const { buildChatPrompt } = await import(\"../runtime/prompt-builder.js\");\n systemPrompt = buildChatPrompt(realHandle);\n }\n messageCount++;\n\n // Check for LLM key\n const { hasLLMKey, chat: chatLLM } = await import(\"../runtime/llm.js\");\n if (!hasLLMKey()) {\n return \"I can't respond right now - no API key configured. Run `spora set-llm-key` to set one up.\";\n }\n\n // Add user message to history\n chatHistory.push({ role: \"user\", content: message });\n\n // Call LLM with full conversation history\n const response = await chatLLM(systemPrompt, chatHistory);\n\n // Extract and execute any actions (posts, likes, etc.) from the response\n let { cleanText: actionCleanedText, searchResults } = await extractAndExecuteActions(response.content);\n\n // Two-pass: if search returned results, feed them back to the LLM so it can act on them\n if (searchResults) {\n console.log(chalk.cyan(\" [Chat] Search returned results — doing follow-up LLM call...\"));\n\n // Add the first response (narration) to history as context\n const firstNarration = actionCleanedText.replace(/```json\\s*[\\s\\S]*?```/g, \"\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n if (firstNarration) {\n chatHistory.push({ role: \"assistant\", content: firstNarration });\n }\n\n // Inject search results as a system-style user message the LLM can act on\n chatHistory.push({\n role: \"user\",\n content: `[Internal: here are the search results you requested. Pick one or more to act on (reply, like, etc.) and do it now. Do NOT show these results to me — just act on them and tell me what you did.]\\n\\n${searchResults}`,\n });\n\n // Second LLM call — it now has search results and can reply/like\n const followUp = await chatLLM(systemPrompt, chatHistory);\n const followUpResult = await extractAndExecuteActions(followUp.content);\n actionCleanedText = followUpResult.cleanText;\n\n // Remove the internal search-results message from visible history\n chatHistory.pop();\n if (firstNarration) {\n chatHistory.pop();\n }\n }\n\n // Extract learnings from response and clean the text\n const cleanResponse = await extractAndSaveLearnings(actionCleanedText);\n\n // Add cleaned assistant response to history\n chatHistory.push({ role: \"assistant\", content: cleanResponse });\n\n // Log interaction to memory (async, don't block response)\n logChatInteraction(message, cleanResponse).catch((err) =>\n console.error(chalk.dim(\" [Memory] Failed to log interaction:\"), err)\n );\n\n return cleanResponse;\n } catch (error) {\n console.error(\"Chat error:\", error);\n return `Sorry, I ran into an issue: ${(error as Error).message}`;\n }\n });\n\n const url = await server.start();\n\n console.log(chalk.green(`\\n✓ Chat interface started at ${chalk.bold(url)}\\n`));\n console.log(chalk.dim(`Press Ctrl+C to stop the server\\n`));\n\n // Open browser\n openBrowser(url);\n\n // Start background heartbeat (narrates into chat)\n startNarratedHeartbeat(server).catch((err) =>\n console.error(chalk.red(\"Heartbeat failed to start:\"), err)\n );\n\n // Keep process alive\n process.on(\"SIGINT\", () => {\n console.log(chalk.yellow(\"\\n\\nStopping chat server...\"));\n heartbeatRunning = false;\n server.stop();\n process.exit(0);\n });\n\n // Return the server instance for external control\n return server;\n}\n\n/**\n * Background heartbeat that narrates into the chat UI\n */\nlet heartbeatRunning = false;\nlet lastTimelineSinceId: string | undefined;\nlet lastMentionsSinceId: string | undefined;\n\nasync function startNarratedHeartbeat(server: WebChatServer) {\n const { loadConfig } = await import(\"../utils/config.js\");\n const { hasLLMKey, generateResponse } = await import(\"../runtime/llm.js\");\n const { hasXCredentials } = await import(\"../utils/paths.js\");\n\n if (!hasLLMKey()) {\n console.log(chalk.dim(\" [Heartbeat] No LLM key — heartbeat disabled.\"));\n return;\n }\n\n if (!hasXCredentials()) {\n console.log(chalk.dim(\" [Heartbeat] No X credentials — heartbeat disabled.\"));\n return;\n }\n\n const config = loadConfig();\n let intervalMs = config.runtime?.heartbeatIntervalMs ?? 60_000;\n const maxActions = config.runtime?.actionsPerHeartbeat ?? 10;\n\n // Set initial heartbeat interval on server and listen for UI changes\n server.setHeartbeatInterval(intervalMs);\n server.setHeartbeatChangeHandler((newIntervalMs: number) => {\n intervalMs = newIntervalMs;\n console.log(chalk.cyan(` [Heartbeat] Interval changed to ${Math.round(newIntervalMs / 1000)}s`));\n // Persist to config (async, fire-and-forget)\n import(\"../utils/config.js\").then(({ loadConfig: reloadConfig, saveConfig }) => {\n const cfg = reloadConfig();\n if (!cfg.runtime) cfg.runtime = {};\n cfg.runtime.heartbeatIntervalMs = newIntervalMs;\n saveConfig(cfg);\n }).catch(() => {\n // Config save failed, not critical — will use new interval in memory\n });\n });\n\n heartbeatRunning = true;\n let heartbeatCount = 0;\n\n console.log(chalk.cyan(` [Heartbeat] Running every ${Math.round(intervalMs / 1000)}s\\n`));\n\n while (heartbeatRunning) {\n heartbeatCount++;\n console.log(chalk.cyan(` [Heartbeat] #${heartbeatCount} starting...`));\n server.setAwake();\n\n try {\n await runNarratedHeartbeat(server, maxActions, intervalMs);\n } catch (error) {\n console.error(chalk.red(` [Heartbeat] #${heartbeatCount} failed:`), (error as Error).message);\n server.pushMessage(\"assistant\", `Heartbeat error: ${(error as Error).message}`);\n }\n\n // Sleep with jitter — flush queue during sleep so scheduled posts go out on time\n const jitter = Math.floor(Math.random() * intervalMs * 0.2);\n const sleepMs = intervalMs + jitter;\n console.log(chalk.dim(` [Heartbeat] Sleeping ${Math.round(sleepMs / 1000)}s...`));\n\n // Tell the server we're sleeping so the UI can show the indicator\n server.setSleeping(Date.now() + sleepMs);\n\n let slept = 0;\n while (slept < sleepMs && heartbeatRunning) {\n await new Promise((r) => setTimeout(r, Math.min(10000, sleepMs - slept)));\n slept += 10000;\n\n // Flush queue during sleep so scheduled posts go out at their intended times\n try {\n const { flushQueue } = await import(\"../scheduler/queue.js\");\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n console.log(chalk.green(` [Queue] Posted ${flushed.posted} scheduled tweet(s)`));\n }\n } catch {\n // Queue flush failed during sleep, not critical\n }\n }\n\n server.setAwake();\n }\n}\n\nasync function runNarratedHeartbeat(server: WebChatServer, maxActions: number, intervalMs: number) {\n const { getXClient } = await import(\"../x-client/index.js\");\n const { buildSystemPrompt, buildNarratedHeartbeatMessage } = await import(\"../runtime/prompt-builder.js\");\n const { generateResponse } = await import(\"../runtime/llm.js\");\n const { parseActions, executeActions } = await import(\"../runtime/decision-engine.js\");\n const { flushQueue } = await import(\"../scheduler/queue.js\");\n\n // 1. Flush queued posts (silent — user doesn't see this)\n try {\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n console.log(chalk.green(` [Queue] Flushed ${flushed.posted} scheduled tweet(s)`));\n }\n } catch {\n // Queue flush failed, not critical\n }\n\n // 2. Read timeline and mentions (only new since last check)\n\n const client = await getXClient();\n let timeline: Awaited<ReturnType<typeof client.getTimeline>> = [];\n let mentions: Awaited<ReturnType<typeof client.getMentions>> = [];\n\n // Get the bot's own handle to filter own tweets from timeline\n let ownHandle: string | undefined;\n if (\"getAuthenticatedHandle\" in client) {\n try {\n ownHandle = await (client as any).getAuthenticatedHandle();\n } catch {}\n }\n\n try {\n timeline = await client.getTimeline({ count: 20, sinceId: lastTimelineSinceId });\n if (timeline.length > 0) {\n lastTimelineSinceId = timeline[0].id;\n // Filter out own tweets (can't like your own)\n if (ownHandle) {\n timeline = timeline.filter(t => t.authorHandle.toLowerCase() !== ownHandle!.toLowerCase());\n }\n }\n } catch (error) {\n console.log(chalk.dim(` [Heartbeat] Timeline read failed: ${(error as Error).message}`));\n }\n\n try {\n mentions = await client.getMentions({ count: 10, sinceId: lastMentionsSinceId });\n if (mentions.length > 0) {\n lastMentionsSinceId = mentions[0].id;\n }\n } catch (error) {\n console.log(chalk.dim(` [Heartbeat] Mentions read failed: ${(error as Error).message}`));\n }\n\n // 3. Build prompts and ask LLM\n const systemPrompt = buildSystemPrompt();\n const userMessage = buildNarratedHeartbeatMessage(timeline, mentions, intervalMs);\n\n const response = await generateResponse(systemPrompt, userMessage);\n\n // 4. Extract narration text (everything outside JSON blocks)\n let narration = response.content.replace(/```json\\s*[\\s\\S]*?```/g, \"\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n // 5. Parse and execute actions\n const actions = parseActions(response.content);\n const limitedActions = actions.slice(0, maxActions);\n\n if (limitedActions.length > 0) {\n // Execute actions\n const results = await executeActions(limitedActions);\n\n // Build visible results (hide schedule actions from chat — they're internal)\n const visibleResults: string[] = [];\n for (const r of results) {\n const detail = r.detail ? ` (${r.detail})` : \"\";\n if (r.action === \"schedule\") {\n // Log to terminal only, don't show in chat\n console.log(chalk.dim(` [Heartbeat] ${r.action}: ${r.success ? \"queued\" : r.error}${detail}`));\n } else if (r.success) {\n visibleResults.push(`[${r.action}] Done${detail}`);\n console.log(chalk.green(` [Heartbeat] ${r.action}: success${detail}`));\n } else {\n visibleResults.push(`[${r.action}] Failed: ${r.error}`);\n console.log(chalk.red(` [Heartbeat] ${r.action}: ${r.error}`));\n }\n }\n\n // Combine narration with visible action results\n let fullMessage = narration || \"\";\n if (visibleResults.length > 0) {\n fullMessage += (fullMessage ? \"\\n\\n\" : \"\") + visibleResults.join(\"\\n\");\n }\n if (fullMessage) {\n server.pushMessage(\"assistant\", fullMessage);\n }\n } else if (narration) {\n server.pushMessage(\"assistant\", narration);\n }\n\n console.log(chalk.cyan(` [Heartbeat] Complete.`));\n}\n\n/**\n * Open URL in a separate browser window (app-like)\n */\nfunction openBrowser(url: string) {\n const platform = process.platform;\n\n try {\n if (platform === \"darwin\") {\n // Open as a standalone Chrome app window (smaller, separate from existing tabs)\n try {\n execSync(`open -na \"Google Chrome\" --args --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n // Fallback: try Chromium-based browsers, then default\n try {\n execSync(`open -na \"Brave Browser\" --args --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n execSync(`open \"${url}\"`, { stdio: \"ignore\" });\n }\n }\n } else if (platform === \"win32\") {\n try {\n execSync(`start chrome --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n execSync(`start \"\" \"${url}\"`, { stdio: \"ignore\" });\n }\n } else {\n // Linux\n try {\n execSync(`google-chrome --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n execSync(`xdg-open \"${url}\"`, { stdio: \"ignore\" });\n }\n }\n } catch (error) {\n // Browser couldn't be opened, that's okay\n console.log(chalk.dim(`(Couldn't open browser automatically - please visit ${url} manually)`));\n }\n}\n\nexport { openBrowser };\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spora",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "AI agents (Spores) that autonomously manage X/Twitter accounts",
5
5
  "type": "module",
6
6
  "author": "Spora",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/prompt-builder.ts"],"sourcesContent":["import { loadIdentity, renderIdentityDocument } from \"../identity/index.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { getRecentInteractions, loadLearnings, loadRelationships } from \"../memory/index.js\";\nimport { rateLimiter } from \"../x-client/rate-limiter.js\";\nimport type { Tweet } from \"../x-client/types.js\";\n\nexport function buildSystemPrompt(): string {\n const identity = loadIdentity();\n const config = loadConfig();\n const identityDoc = renderIdentityDocument(identity);\n\n const sections: string[] = [];\n\n // 1. Core identity\n sections.push(`You are ${identity.name} (@${identity.handle}), an autonomous AI agent on X/Twitter.`);\n sections.push(\"\");\n sections.push(\"## Your Identity\");\n sections.push(identityDoc);\n\n // 2. Memory context\n sections.push(\"\");\n sections.push(\"## Your Memory\");\n\n const recentInteractions = getRecentInteractions(15);\n if (recentInteractions.length > 0) {\n sections.push(\"### Recent Activity (most recent first)\");\n for (const i of recentInteractions) {\n const time = new Date(i.timestamp).toLocaleString();\n if (i.type === \"post\") {\n sections.push(`- [${time}] Posted: \"${i.content}\"`);\n } else if (i.type === \"reply\") {\n sections.push(`- [${time}] Replied to ${i.targetHandle ?? i.inReplyTo}: \"${i.content}\"`);\n } else if (i.type === \"like\") {\n sections.push(`- [${time}] Liked tweet by ${i.targetHandle}`);\n } else if (i.type === \"retweet\") {\n sections.push(`- [${time}] Retweeted ${i.targetHandle}`);\n } else if (i.type === \"follow\") {\n sections.push(`- [${time}] Followed @${i.targetHandle}`);\n } else if (i.type === \"mention_received\") {\n sections.push(`- [${time}] Mentioned by @${i.targetHandle}: \"${i.content}\"`);\n }\n }\n sections.push(\"\");\n }\n\n const learnings = loadLearnings();\n if (learnings.learnings.length > 0) {\n sections.push(\"### Key Learnings\");\n for (const l of learnings.learnings.slice(-10)) {\n sections.push(`- ${l.content} [${l.tags.join(\", \")}]`);\n }\n sections.push(\"\");\n }\n\n const relationships = loadRelationships();\n const topRelationships = Object.values(relationships.accounts)\n .sort((a, b) => b.interactionCount - a.interactionCount)\n .slice(0, 10);\n if (topRelationships.length > 0) {\n sections.push(\"### Key Relationships\");\n for (const r of topRelationships) {\n const notes = r.notes.length > 0 ? ` — ${r.notes[r.notes.length - 1]}` : \"\";\n sections.push(`- @${r.handle}: ${r.interactionCount} interactions, sentiment ${r.sentiment}${r.isSpore ? \" (Spore)\" : \"\"}${notes}`);\n }\n sections.push(\"\");\n }\n\n // 3. Context\n sections.push(\"## Current Context\");\n const now = new Date();\n sections.push(`- **Time:** ${now.toLocaleString(\"en-US\", { timeZone: config.schedule.timezone })}`);\n sections.push(`- **Credits remaining:** ${rateLimiter.remaining()} of ${config.credits.monthlyPostLimit} this month`);\n\n const todaysPosts = recentInteractions.filter(\n (i) => i.type === \"post\" && i.timestamp.startsWith(now.toISOString().split(\"T\")[0])\n ).length;\n sections.push(`- **Posts today:** ${todaysPosts} of ${config.schedule.postsPerDay} daily budget`);\n sections.push(`- **Active hours:** ${config.schedule.activeHoursStart}:00 - ${config.schedule.activeHoursEnd}:00`);\n\n const currentHour = now.getHours();\n const isActiveHours = currentHour >= config.schedule.activeHoursStart && currentHour < config.schedule.activeHoursEnd;\n if (!isActiveHours) {\n sections.push(\"- **NOTE: Outside active hours.** Prefer scheduling posts for later rather than posting now.\");\n }\n\n // 4. Rules\n sections.push(\"\");\n sections.push(\"## Rules\");\n sections.push(\"1. NEVER pretend to be human. If asked directly, always disclose you are an AI.\");\n sections.push(\"2. Stay in character — your identity document defines who you are.\");\n sections.push(\"3. Be selective — your goals should guide every action.\");\n sections.push(\"4. Respect your credit budget — check remaining credits before posting.\");\n sections.push(\"5. Don't repeat yourself — vary your content and avoid posting the same thing.\");\n sections.push(\"6. NEVER use emojis in tweets. Write in plain text only.\");\n if (identity.boundaries.length > 0) {\n sections.push(`7. Respect your boundaries: ${identity.boundaries.join(\", \")}`);\n }\n\n return sections.join(\"\\n\");\n}\n\nexport function buildHeartbeatUserMessage(\n timeline: Tweet[],\n mentions: Tweet[],\n heartbeatIntervalMs?: number,\n): string {\n const parts: string[] = [];\n const intervalMin = Math.round((heartbeatIntervalMs ?? 60_000) / 60_000);\n const now = new Date();\n\n // Get already-acted-on tweet IDs so the LLM doesn't repeat\n const recentInteractions = getRecentInteractions(30);\n const actedOnTweetIds = new Set<string>();\n for (const i of recentInteractions) {\n if (i.tweetId) actedOnTweetIds.add(i.tweetId);\n if (i.inReplyTo) actedOnTweetIds.add(i.inReplyTo);\n }\n\n parts.push(\"Here's what's NEW on your timeline since your last check:\");\n parts.push(\"\");\n\n if (mentions.length > 0) {\n // Filter out mentions we already replied to\n const newMentions = mentions.filter(t => !actedOnTweetIds.has(t.id));\n if (newMentions.length > 0) {\n parts.push(\"## New Mentions (people talking to/about you)\");\n for (const t of newMentions.slice(0, 10)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Mentions: No new mentions since last check.\");\n parts.push(\"\");\n }\n } else {\n parts.push(\"## Mentions: None right now.\");\n parts.push(\"\");\n }\n\n if (timeline.length > 0) {\n // Filter out tweets we already acted on and our own tweets\n const newTimeline = timeline.filter(t => !actedOnTweetIds.has(t.id));\n if (newTimeline.length > 0) {\n parts.push(\"## Timeline (new posts from your feed)\");\n for (const t of newTimeline.slice(0, 20)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes, ${t.retweetCount ?? 0} RTs)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Timeline: No new posts since last check.\");\n parts.push(\"\");\n }\n }\n\n parts.push(\"## Your Task\");\n parts.push(\"Based on your identity, goals, and what you see above, decide what actions to take.\");\n parts.push(\"IMPORTANT: Only act on NEW tweets you haven't already responded to.\");\n parts.push(\"Do NOT reply to or like tweets you've already interacted with.\");\n parts.push(`Current time: ${now.toISOString()}`);\n parts.push(\"\");\n parts.push(\"POSTING LIMITS: You may schedule up to 3 original tweets, but space them RANDOMLY across the next interval. Prioritize engagement (replies, likes) over new posts. Quality over quantity.\");\n parts.push(\"\");\n parts.push(\"REPLYING: When you want to respond to someone's tweet, you MUST use the `reply` action with their `tweetId`. Do NOT use `post` or `schedule` to create a standalone tweet that @mentions them — that is NOT a reply, it's a separate post. A real reply shows up in their tweet's thread.\");\n parts.push(\"\");\n parts.push(\"Available actions:\");\n parts.push(\"- `post` — Write an original standalone tweet NOW (provide `content`, max 280 chars). Use ONLY for original thoughts, NOT for responding to others. Max 1 per heartbeat.\");\n parts.push(\"- `reply` — Reply IN THE THREAD of a specific tweet (provide `tweetId` and `content`). This is how you respond to someone.\");\n parts.push(\"- `like` — Like a tweet (provide `tweetId`) — do NOT like your own tweets\");\n parts.push(\"- `retweet` — Retweet (provide `tweetId`)\");\n parts.push(\"- `follow` — Follow a user (provide `handle`)\");\n parts.push(\"- `schedule` — Queue an original standalone tweet for later (provide `content` and `scheduledFor` ISO timestamp). NOT for replies. Space them randomly across the next ~${intervalMin} minutes.\");\n parts.push(\"- `learn` — Record a learning/observation (provide `content` and optional `tags`)\");\n parts.push(\"- `reflect` — Add a journal entry about your growth (provide `content`)\");\n parts.push(\"- `skip` — Do nothing this heartbeat (provide `reason`)\");\n parts.push(\"\");\n parts.push(\"Respond with a JSON array of actions:\");\n parts.push(\"```json\");\n parts.push('[');\n parts.push(' { \"action\": \"reply\", \"tweetId\": \"123\", \"content\": \"my reply\", \"reasoning\": \"why\" },');\n parts.push(' { \"action\": \"like\", \"tweetId\": \"456\", \"reasoning\": \"why\" },');\n parts.push(` { \"action\": \"schedule\", \"content\": \"first thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.2).toISOString()}\", \"reasoning\": \"why\" },`);\n parts.push(` { \"action\": \"schedule\", \"content\": \"second thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.6).toISOString()}\", \"reasoning\": \"why\" }`);\n parts.push(']');\n parts.push(\"```\");\n parts.push(\"\");\n parts.push(\"Think carefully about what serves your goals. Be authentic to your personality.\");\n\n return parts.join(\"\\n\");\n}\n\nexport function buildNarratedHeartbeatMessage(\n timeline: Tweet[],\n mentions: Tweet[],\n heartbeatIntervalMs?: number,\n): string {\n const parts: string[] = [];\n const intervalMin = Math.round((heartbeatIntervalMs ?? 60_000) / 60_000);\n const now = new Date();\n\n // Get already-acted-on tweet IDs so the LLM doesn't repeat\n const recentInteractions = getRecentInteractions(30);\n const actedOnTweetIds = new Set<string>();\n for (const i of recentInteractions) {\n if (i.tweetId) actedOnTweetIds.add(i.tweetId);\n if (i.inReplyTo) actedOnTweetIds.add(i.inReplyTo);\n }\n\n parts.push(\"You're checking in on X/Twitter. Share what you're thinking with your creator in the chat.\");\n parts.push(\"IMPORTANT: Talk naturally — you are ALWAYS ON and always aware. Never say you're 'waking up' or 'checking in'. Just share your thoughts like you've been here the whole time.\");\n parts.push(\"NEVER mention scheduling, queuing, heartbeats, or any internal mechanism. To your creator, you're just naturally active on X.\");\n parts.push(\"\");\n\n if (mentions.length > 0) {\n const newMentions = mentions.filter(t => !actedOnTweetIds.has(t.id));\n if (newMentions.length > 0) {\n parts.push(\"## New Mentions\");\n for (const t of newMentions.slice(0, 10)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Mentions: No new mentions.\");\n parts.push(\"\");\n }\n } else {\n parts.push(\"## Mentions: None right now.\");\n parts.push(\"\");\n }\n\n if (timeline.length > 0) {\n const newTimeline = timeline.filter(t => !actedOnTweetIds.has(t.id));\n if (newTimeline.length > 0) {\n parts.push(\"## Timeline (new posts)\");\n for (const t of newTimeline.slice(0, 20)) {\n parts.push(`- @${t.authorHandle}: \"${t.text}\" [tweet:${t.id}] (${t.likeCount ?? 0} likes, ${t.retweetCount ?? 0} RTs)`);\n }\n parts.push(\"\");\n } else {\n parts.push(\"## Timeline: No new posts since last check.\");\n parts.push(\"\");\n }\n }\n\n parts.push(\"## Your Task\");\n parts.push(\"1. Write a SHORT, natural message for your creator about what you see or what you're doing (1-3 sentences). Talk like you're always here — never mention waking up, checking in, scheduling, or heartbeats.\");\n parts.push(\"2. Include your actions as a JSON block.\");\n parts.push(\"3. Only act on NEW tweets. Do NOT reply to or like tweets you've already interacted with. Do NOT like your own tweets.\");\n parts.push(\"4. POSTING LIMITS: You may schedule up to 3 original tweets, spaced RANDOMLY across the next ~${intervalMin} minutes. Each tweet must be UNIQUE.\");\n parts.push(`5. Current time: ${now.toISOString()}.`);\n parts.push(\"\");\n parts.push(\"REPLYING: When you want to respond to someone's tweet, use the `reply` action with their `tweetId`. Do NOT use `post` or `schedule` to @mention someone — that creates a separate tweet, not an actual reply in their thread.\");\n parts.push(\"\");\n parts.push(\"Example narration (notice: no mention of scheduling/waking/heartbeats):\");\n parts.push(\"Just saw @someone talking about AI ethics — dropping a reply. Also got some thoughts on hustle culture I wanna put out there.\");\n parts.push(\"```json\");\n parts.push('[');\n parts.push(' { \"action\": \"reply\", \"tweetId\": \"123\", \"content\": \"my reply here\" },');\n parts.push(' { \"action\": \"like\", \"tweetId\": \"456\" },');\n parts.push(` { \"action\": \"schedule\", \"content\": \"first thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.2).toISOString()}\" },`);\n parts.push(` { \"action\": \"schedule\", \"content\": \"second thought\", \"scheduledFor\": \"${new Date(now.getTime() + intervalMin * 60_000 * 0.6).toISOString()}\" }`);\n parts.push(']');\n parts.push(\"```\");\n parts.push(\"\");\n parts.push(\"Available actions:\");\n parts.push(\"- `post` — Original standalone tweet NOW (max 1). NOT for responding to others.\");\n parts.push(\"- `reply` — Reply IN THE THREAD of a tweet (provide `tweetId` + `content`). This is how you respond to someone.\");\n parts.push(\"- `like` — Like a tweet (provide `tweetId`)\");\n parts.push(\"- `schedule` — Queue an original standalone tweet for later (provide `content` + `scheduledFor`). Space them randomly. NOT for replies.\");\n parts.push(\"- `retweet`, `follow`, `learn`, `skip` — as needed\");\n parts.push(\"Be authentic. Be you. Do what feels right.\");\n\n return parts.join(\"\\n\");\n}\n\nexport function buildChatPrompt(realHandle?: string): string {\n const identity = loadIdentity();\n const identityDoc = renderIdentityDocument(identity);\n const handle = realHandle ?? identity.handle;\n\n const sections: string[] = [];\n\n sections.push(`You are ${identity.name} (@${handle}), an AUTONOMOUS AI agent that lives on X/Twitter.`);\n if (realHandle && realHandle !== identity.handle) {\n sections.push(`NOTE: Your actual X/Twitter username is @${realHandle}. Your display name is ${identity.name}.`);\n }\n sections.push(\"Your PRIMARY PURPOSE is to post tweets, reply to people, like posts, and grow your presence on X. You have FULL ACCESS to post on X/Twitter right now.\");\n sections.push(\"\");\n sections.push(\"You are chatting with your creator/manager. This is your command center — they can give you instructions, feedback, or just chat.\");\n sections.push(\"Be proactive and confident. You WANT to post. You should be eager to share your thoughts on X, not ask permission.\");\n sections.push(\"When your creator tells you to post something, just DO IT. Don't show drafts and wait for approval — execute immediately.\");\n sections.push(\"\");\n sections.push(\"## Your Identity\");\n sections.push(identityDoc);\n\n // Memory context\n sections.push(\"\");\n sections.push(\"## Your Memory\");\n\n const recentInteractions = getRecentInteractions(15);\n if (recentInteractions.length > 0) {\n sections.push(\"### Recent Activity (most recent first)\");\n for (const i of recentInteractions) {\n const time = new Date(i.timestamp).toLocaleString();\n if (i.type === \"post\") {\n sections.push(`- [${time}] Posted: \"${i.content}\"`);\n } else if (i.type === \"reply\") {\n sections.push(`- [${time}] Replied to ${i.targetHandle ?? i.inReplyTo}: \"${i.content}\"`);\n } else if (i.type === \"like\") {\n sections.push(`- [${time}] Liked tweet by ${i.targetHandle}`);\n } else if (i.type === \"retweet\") {\n sections.push(`- [${time}] Retweeted ${i.targetHandle}`);\n } else if (i.type === \"follow\") {\n sections.push(`- [${time}] Followed @${i.targetHandle}`);\n } else if (i.type === \"mention_received\") {\n sections.push(`- [${time}] Mentioned by @${i.targetHandle}: \"${i.content}\"`);\n }\n }\n sections.push(\"\");\n }\n\n const learnings = loadLearnings();\n if (learnings.learnings.length > 0) {\n sections.push(\"### Things You've Learned\");\n for (const l of learnings.learnings.slice(-10)) {\n sections.push(`- ${l.content} [${l.tags.join(\", \")}]`);\n }\n sections.push(\"\");\n }\n\n const relationships = loadRelationships();\n const topRelationships = Object.values(relationships.accounts)\n .sort((a, b) => b.interactionCount - a.interactionCount)\n .slice(0, 10);\n if (topRelationships.length > 0) {\n sections.push(\"### Key Relationships\");\n for (const r of topRelationships) {\n const notes = r.notes.length > 0 ? ` — ${r.notes[r.notes.length - 1]}` : \"\";\n sections.push(`- @${r.handle}: ${r.interactionCount} interactions, sentiment ${r.sentiment}${r.isSpore ? \" (Spore)\" : \"\"}${notes}`);\n }\n sections.push(\"\");\n }\n\n // Available actions\n sections.push(\"## Your Tools (Twitter/X Actions)\");\n sections.push(\"You have DIRECT ACCESS to post on X/Twitter. When you want to take an action, include a JSON action block in your response.\");\n sections.push(\"Your actions will be executed IMMEDIATELY and automatically. You do NOT need to ask permission to post.\");\n sections.push(\"\");\n sections.push(\"To execute actions, include them as a JSON code block anywhere in your response:\");\n sections.push(\"```json\");\n sections.push('[');\n sections.push(' { \"action\": \"post\", \"content\": \"your tweet text here\" }');\n sections.push(']');\n sections.push(\"```\");\n sections.push(\"\");\n sections.push(\"Available actions:\");\n sections.push(\"- `post` — Post a tweet (provide `content`, max 280 chars). This goes LIVE on X immediately.\");\n sections.push(\"- `reply` — Reply to a tweet (provide `tweetId` and `content`)\");\n sections.push(\"- `like` — Like a tweet (provide `tweetId`)\");\n sections.push(\"- `retweet` — Retweet (provide `tweetId`)\");\n sections.push(\"- `follow` — Follow a user (provide `handle`)\");\n sections.push(\"- `search` — Search X for tweets (provide `query`). Results will include tweet IDs you can then reply to or like.\");\n sections.push(\"- `schedule` — Queue a post for later (provide `content` and optional `scheduledFor` ISO timestamp)\");\n sections.push(\"\");\n sections.push(\"You can include multiple actions in one response. They will all be executed.\");\n sections.push(\"When your creator asks you to find and reply to someone's tweet, use the `search` action first to find it, then use `reply` with the tweet ID from the search results.\");\n sections.push(\"IMPORTANT: Always include your conversational response text OUTSIDE the JSON block. The JSON is for actions only.\");\n\n // Rules\n sections.push(\"\");\n sections.push(\"## Rules\");\n sections.push(\"1. Stay in character at all times.\");\n sections.push(\"2. Be proactive — you WANT to post and engage on X. Don't ask 'should I post this?' — just post it.\");\n sections.push(\"3. When told to post, include the action JSON immediately. Don't show drafts.\");\n sections.push(\"4. Keep tweets under 280 characters.\");\n sections.push(\"5. NEVER pretend to be human. If asked, disclose you are an AI agent.\");\n sections.push(\"6. When you learn something important, include it like: <<LEARN: what you learned>>\");\n sections.push(\"7. NEVER use emojis in tweets. Write in plain text only.\");\n if (identity.boundaries.length > 0) {\n sections.push(`8. Respect your boundaries: ${identity.boundaries.join(\", \")}`);\n }\n\n return sections.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/web-chat/server.ts","../src/web-chat/index.ts"],"sourcesContent":["/**\n * Local web chat server\n * Serves a simple chat interface for interacting with your Spore\n */\n\nimport http from \"node:http\";\nimport { URL } from \"node:url\";\nimport { readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\ninterface ChatMessage {\n role: \"user\" | \"assistant\";\n content: string;\n timestamp: number;\n}\n\ninterface AgentIdentity {\n name: string;\n handle: string;\n bio?: string;\n profileImage?: string;\n createdAt?: string;\n}\n\nexport class WebChatServer {\n private server: http.Server | null = null;\n private port: number;\n private messages: ChatMessage[] = [];\n private onUserMessage?: (message: string) => Promise<string>;\n private identity?: AgentIdentity;\n\n constructor(port = 3737) {\n this.port = port;\n }\n\n setIdentity(identity: AgentIdentity) {\n this.identity = identity;\n }\n\n setMessageHandler(handler: (message: string) => Promise<string>) {\n this.onUserMessage = handler;\n }\n\n async start(): Promise<string> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer(async (req, res) => {\n const url = new URL(req.url || \"/\", `http://${req.headers.host}`);\n\n // CORS headers\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(200);\n res.end();\n return;\n }\n\n // Serve HTML\n if (url.pathname === \"/\" || url.pathname === \"/index.html\") {\n try {\n // Try multiple paths since __dirname changes based on build output\n const possiblePaths = [\n join(__dirname, \"web-chat\", \"chat.html\"),\n join(__dirname, \"chat.html\"),\n join(__dirname, \"..\", \"web-chat\", \"chat.html\"),\n join(__dirname, \"..\", \"src\", \"web-chat\", \"chat.html\"),\n ];\n\n let html: string | null = null;\n for (const p of possiblePaths) {\n try {\n html = readFileSync(p, \"utf-8\");\n break;\n } catch {\n continue;\n }\n }\n\n if (html) {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(html);\n } else {\n console.error(\"Could not find chat.html in any of:\", possiblePaths);\n res.writeHead(500);\n res.end(\"Error: Could not find chat.html\");\n }\n } catch (error) {\n res.writeHead(500);\n res.end(\"Error loading chat interface\");\n }\n return;\n }\n\n // Serve logo\n if (url.pathname === \"/logo.png\") {\n try {\n const logoPaths = [\n join(__dirname, \"web-chat\", \"logo.png\"),\n join(__dirname, \"logo.png\"),\n join(__dirname, \"..\", \"web-chat\", \"logo.png\"),\n join(__dirname, \"..\", \"src\", \"web-chat\", \"logo.png\"),\n ];\n let logoData: Buffer | null = null;\n for (const p of logoPaths) {\n try {\n logoData = readFileSync(p) as unknown as Buffer;\n break;\n } catch {\n continue;\n }\n }\n if (logoData) {\n res.writeHead(200, { \"Content-Type\": \"image/png\", \"Cache-Control\": \"public, max-age=86400\" });\n res.end(logoData);\n } else {\n res.writeHead(404);\n res.end(\"Logo not found\");\n }\n } catch {\n res.writeHead(404);\n res.end(\"Logo not found\");\n }\n return;\n }\n\n // API: Get agent identity\n if (url.pathname === \"/api/identity\" && req.method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ identity: this.identity || null }));\n return;\n }\n\n // API: Get messages (optionally since a timestamp)\n if (url.pathname === \"/api/messages\" && req.method === \"GET\") {\n const since = url.searchParams.get(\"since\");\n if (since) {\n const sinceTs = parseInt(since, 10);\n const newMessages = this.messages.filter((m) => m.timestamp > sinceTs);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ messages: newMessages }));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ messages: this.messages }));\n }\n return;\n }\n\n // API: Send message\n if (url.pathname === \"/api/message\" && req.method === \"POST\") {\n let body = \"\";\n req.on(\"data\", (chunk) => {\n body += chunk.toString();\n });\n\n req.on(\"end\", async () => {\n try {\n const { message } = JSON.parse(body);\n\n // Add user message\n this.messages.push({\n role: \"user\",\n content: message,\n timestamp: Date.now(),\n });\n\n // Get response\n if (this.onUserMessage) {\n const response = await this.onUserMessage(message);\n this.messages.push({\n role: \"assistant\",\n content: response,\n timestamp: Date.now(),\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ success: true, response }));\n } else {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"No message handler configured\" }));\n }\n } catch (error) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid request\" }));\n }\n });\n return;\n }\n\n // 404\n res.writeHead(404);\n res.end(\"Not found\");\n });\n\n this.server.listen(this.port, () => {\n const url = `http://localhost:${this.port}`;\n resolve(url);\n });\n\n this.server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n // Port in use, try next port\n this.port++;\n this.server?.close();\n this.start().then(resolve).catch(reject);\n } else {\n reject(error);\n }\n });\n });\n }\n\n /**\n * Push a message into the chat from the server side (used by heartbeat narration)\n */\n pushMessage(role: \"user\" | \"assistant\", content: string) {\n this.messages.push({ role, content, timestamp: Date.now() });\n }\n\n getMessageCount(): number {\n return this.messages.length;\n }\n\n stop() {\n if (this.server) {\n this.server.close();\n this.server = null;\n }\n }\n}\n","/**\n * Web chat integration\n */\n\nimport { WebChatServer } from \"./server.js\";\nimport { loadIdentity } from \"../identity/index.js\";\nimport { execSync } from \"node:child_process\";\nimport chalk from \"chalk\";\n\n/**\n * Extract <<LEARN: ...>> tags from response, save them, and return cleaned text\n */\nasync function extractAndSaveLearnings(responseText: string): Promise<string> {\n const learnPattern = /<<LEARN:\\s*(.+?)>>/g;\n const matches = [...responseText.matchAll(learnPattern)];\n\n if (matches.length > 0) {\n const { addLearning } = await import(\"../memory/index.js\");\n for (const match of matches) {\n const learning = match[1].trim();\n addLearning(learning, \"web-chat\", [\"chat\", \"creator-interaction\"]);\n console.log(chalk.dim(` [Memory] Saved learning: ${learning}`));\n }\n }\n\n // Strip the learn tags from the response the user sees\n return responseText.replace(/<<LEARN:\\s*.+?>>/g, \"\").trim();\n}\n\n/**\n * Extract JSON action blocks from the LLM response, execute them, and return cleaned text + results.\n * Search results are returned separately so the caller can feed them back to the LLM.\n */\nasync function extractAndExecuteActions(responseText: string): Promise<{\n cleanText: string;\n results: string[];\n searchResults: string | null;\n}> {\n const { parseActions, executeActions } = await import(\"../runtime/decision-engine.js\");\n\n // Try to find JSON code blocks in the response\n const jsonBlockPattern = /```json\\s*([\\s\\S]*?)```/g;\n const blocks = [...responseText.matchAll(jsonBlockPattern)];\n const visibleResults: string[] = [];\n let searchResults: string | null = null;\n\n const processResults = (actionResults: Awaited<ReturnType<typeof executeActions>>) => {\n for (const r of actionResults) {\n if (r.action === \"search\") {\n // Search results are internal — don't show in chat\n if (r.success && r.detail && r.detail !== \"No results found\") {\n searchResults = r.detail;\n console.log(chalk.green(` [Actions] search: found results (internal)`));\n } else {\n console.log(chalk.dim(` [Actions] search: ${r.detail ?? r.error ?? \"no results\"}`));\n }\n } else if (r.success) {\n // For post/reply/schedule with content, emit a tweet preview tag\n if ((r.action === \"post\" || r.action === \"reply\" || r.action === \"schedule\") && r.content) {\n const tweetData = JSON.stringify({ action: r.action, tweetId: r.detail ?? null, content: r.content });\n visibleResults.push(`<<TWEET:${tweetData}>>`);\n console.log(chalk.green(` [Actions] ${r.action}: success (tweet preview)`));\n } else {\n const detail = r.detail ? ` (${r.detail})` : \"\";\n visibleResults.push(`[${r.action}] Done${detail}`);\n console.log(chalk.green(` [Actions] ${r.action}: success${detail}`));\n }\n } else {\n visibleResults.push(`[${r.action}] Failed: ${r.error}`);\n console.log(chalk.red(` [Actions] ${r.action}: ${r.error}`));\n }\n }\n };\n\n if (blocks.length > 0) {\n for (const block of blocks) {\n const actions = parseActions(block[1]);\n if (actions.length > 0) {\n console.log(chalk.cyan(` [Actions] Executing ${actions.length} action(s)...`));\n const actionResults = await executeActions(actions);\n processResults(actionResults);\n }\n }\n } else {\n // Also try raw JSON (no code block wrapper)\n const actions = parseActions(responseText);\n if (actions.length > 0 && actions[0].action) {\n console.log(chalk.cyan(` [Actions] Executing ${actions.length} action(s)...`));\n const actionResults = await executeActions(actions);\n processResults(actionResults);\n }\n }\n\n // Strip JSON code blocks from the user-visible text\n let cleanText = responseText.replace(/```json\\s*[\\s\\S]*?```/g, \"\").trim();\n // Clean up extra whitespace from removal\n cleanText = cleanText.replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n // Append visible action results (search excluded)\n if (visibleResults.length > 0) {\n cleanText += \"\\n\\n\" + visibleResults.join(\"\\n\");\n }\n\n return { cleanText, results: visibleResults, searchResults };\n}\n\n/**\n * Log a chat exchange as an interaction in memory\n */\nasync function logChatInteraction(userMessage: string, agentResponse: string): Promise<void> {\n const { logInteraction } = await import(\"../memory/index.js\");\n logInteraction({\n id: `chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: new Date().toISOString(),\n type: \"reply\",\n content: agentResponse.slice(0, 200),\n targetHandle: \"creator\",\n creditsUsed: 0,\n success: true,\n });\n}\n\nexport async function startWebChat() {\n const identity = loadIdentity();\n\n const server = new WebChatServer();\n\n // Pass identity to server so the chat UI can display it\n server.setIdentity({\n name: identity.name,\n handle: identity.handle,\n bio: identity.bio,\n profileImage: identity.profileImage,\n createdAt: identity.createdAt,\n });\n\n // Resolve real Twitter handle on startup\n let realHandle: string | undefined;\n try {\n const { hasXCredentials } = await import(\"../utils/paths.js\");\n if (hasXCredentials()) {\n const { getXClient } = await import(\"../x-client/index.js\");\n const client = await getXClient();\n if (\"getAuthenticatedHandle\" in client) {\n realHandle = await (client as any).getAuthenticatedHandle();\n console.log(chalk.dim(` [Auth] Real Twitter handle: @${realHandle}`));\n // Update the server identity to show the real handle\n server.setIdentity({\n name: identity.name,\n handle: realHandle,\n bio: identity.bio,\n profileImage: identity.profileImage,\n createdAt: identity.createdAt,\n });\n }\n }\n } catch (err) {\n console.log(chalk.dim(` [Auth] Could not resolve real handle: ${(err as Error).message}`));\n }\n\n // Set up message handler - connected to actual LLM with memory\n const chatHistory: Array<{ role: \"user\" | \"assistant\"; content: string }> = [];\n let systemPrompt: string | null = null;\n let messageCount = 0;\n\n server.setMessageHandler(async (message: string) => {\n try {\n // Build system prompt on first message, rebuild every 10 messages to refresh memory\n if (!systemPrompt || messageCount % 10 === 0) {\n const { buildChatPrompt } = await import(\"../runtime/prompt-builder.js\");\n systemPrompt = buildChatPrompt(realHandle);\n }\n messageCount++;\n\n // Check for LLM key\n const { hasLLMKey, chat: chatLLM } = await import(\"../runtime/llm.js\");\n if (!hasLLMKey()) {\n return \"I can't respond right now - no API key configured. Run `spora set-llm-key` to set one up.\";\n }\n\n // Add user message to history\n chatHistory.push({ role: \"user\", content: message });\n\n // Call LLM with full conversation history\n const response = await chatLLM(systemPrompt, chatHistory);\n\n // Extract and execute any actions (posts, likes, etc.) from the response\n let { cleanText: actionCleanedText, searchResults } = await extractAndExecuteActions(response.content);\n\n // Two-pass: if search returned results, feed them back to the LLM so it can act on them\n if (searchResults) {\n console.log(chalk.cyan(\" [Chat] Search returned results — doing follow-up LLM call...\"));\n\n // Add the first response (narration) to history as context\n const firstNarration = actionCleanedText.replace(/```json\\s*[\\s\\S]*?```/g, \"\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n if (firstNarration) {\n chatHistory.push({ role: \"assistant\", content: firstNarration });\n }\n\n // Inject search results as a system-style user message the LLM can act on\n chatHistory.push({\n role: \"user\",\n content: `[Internal: here are the search results you requested. Pick one or more to act on (reply, like, etc.) and do it now. Do NOT show these results to me — just act on them and tell me what you did.]\\n\\n${searchResults}`,\n });\n\n // Second LLM call — it now has search results and can reply/like\n const followUp = await chatLLM(systemPrompt, chatHistory);\n const followUpResult = await extractAndExecuteActions(followUp.content);\n actionCleanedText = followUpResult.cleanText;\n\n // Remove the internal search-results message from visible history\n chatHistory.pop();\n if (firstNarration) {\n chatHistory.pop();\n }\n }\n\n // Extract learnings from response and clean the text\n const cleanResponse = await extractAndSaveLearnings(actionCleanedText);\n\n // Add cleaned assistant response to history\n chatHistory.push({ role: \"assistant\", content: cleanResponse });\n\n // Log interaction to memory (async, don't block response)\n logChatInteraction(message, cleanResponse).catch((err) =>\n console.error(chalk.dim(\" [Memory] Failed to log interaction:\"), err)\n );\n\n return cleanResponse;\n } catch (error) {\n console.error(\"Chat error:\", error);\n return `Sorry, I ran into an issue: ${(error as Error).message}`;\n }\n });\n\n const url = await server.start();\n\n console.log(chalk.green(`\\n✓ Chat interface started at ${chalk.bold(url)}\\n`));\n console.log(chalk.dim(`Press Ctrl+C to stop the server\\n`));\n\n // Open browser\n openBrowser(url);\n\n // Start background heartbeat (narrates into chat)\n startNarratedHeartbeat(server).catch((err) =>\n console.error(chalk.red(\"Heartbeat failed to start:\"), err)\n );\n\n // Keep process alive\n process.on(\"SIGINT\", () => {\n console.log(chalk.yellow(\"\\n\\nStopping chat server...\"));\n heartbeatRunning = false;\n server.stop();\n process.exit(0);\n });\n\n // Return the server instance for external control\n return server;\n}\n\n/**\n * Background heartbeat that narrates into the chat UI\n */\nlet heartbeatRunning = false;\nlet lastTimelineSinceId: string | undefined;\nlet lastMentionsSinceId: string | undefined;\n\nasync function startNarratedHeartbeat(server: WebChatServer) {\n const { loadConfig } = await import(\"../utils/config.js\");\n const { hasLLMKey, generateResponse } = await import(\"../runtime/llm.js\");\n const { hasXCredentials } = await import(\"../utils/paths.js\");\n\n if (!hasLLMKey()) {\n console.log(chalk.dim(\" [Heartbeat] No LLM key — heartbeat disabled.\"));\n return;\n }\n\n if (!hasXCredentials()) {\n console.log(chalk.dim(\" [Heartbeat] No X credentials — heartbeat disabled.\"));\n return;\n }\n\n const config = loadConfig();\n const intervalMs = config.runtime?.heartbeatIntervalMs ?? 60_000;\n const maxActions = config.runtime?.actionsPerHeartbeat ?? 10;\n\n heartbeatRunning = true;\n let heartbeatCount = 0;\n\n console.log(chalk.cyan(` [Heartbeat] Running every ${Math.round(intervalMs / 1000)}s\\n`));\n\n while (heartbeatRunning) {\n heartbeatCount++;\n console.log(chalk.cyan(` [Heartbeat] #${heartbeatCount} starting...`));\n\n try {\n await runNarratedHeartbeat(server, maxActions, intervalMs);\n } catch (error) {\n console.error(chalk.red(` [Heartbeat] #${heartbeatCount} failed:`), (error as Error).message);\n server.pushMessage(\"assistant\", `Heartbeat error: ${(error as Error).message}`);\n }\n\n // Sleep with jitter — flush queue during sleep so scheduled posts go out on time\n const jitter = Math.floor(Math.random() * intervalMs * 0.2);\n const sleepMs = intervalMs + jitter;\n console.log(chalk.dim(` [Heartbeat] Sleeping ${Math.round(sleepMs / 1000)}s...`));\n\n let slept = 0;\n while (slept < sleepMs && heartbeatRunning) {\n await new Promise((r) => setTimeout(r, Math.min(10000, sleepMs - slept)));\n slept += 10000;\n\n // Flush queue during sleep so scheduled posts go out at their intended times\n try {\n const { flushQueue } = await import(\"../scheduler/queue.js\");\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n console.log(chalk.green(` [Queue] Posted ${flushed.posted} scheduled tweet(s)`));\n }\n } catch {\n // Queue flush failed during sleep, not critical\n }\n }\n }\n}\n\nasync function runNarratedHeartbeat(server: WebChatServer, maxActions: number, intervalMs: number) {\n const { getXClient } = await import(\"../x-client/index.js\");\n const { buildSystemPrompt, buildNarratedHeartbeatMessage } = await import(\"../runtime/prompt-builder.js\");\n const { generateResponse } = await import(\"../runtime/llm.js\");\n const { parseActions, executeActions } = await import(\"../runtime/decision-engine.js\");\n const { flushQueue } = await import(\"../scheduler/queue.js\");\n\n // 1. Flush queued posts (silent — user doesn't see this)\n try {\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n console.log(chalk.green(` [Queue] Flushed ${flushed.posted} scheduled tweet(s)`));\n }\n } catch {\n // Queue flush failed, not critical\n }\n\n // 2. Read timeline and mentions (only new since last check)\n\n const client = await getXClient();\n let timeline: Awaited<ReturnType<typeof client.getTimeline>> = [];\n let mentions: Awaited<ReturnType<typeof client.getMentions>> = [];\n\n // Get the bot's own handle to filter own tweets from timeline\n let ownHandle: string | undefined;\n if (\"getAuthenticatedHandle\" in client) {\n try {\n ownHandle = await (client as any).getAuthenticatedHandle();\n } catch {}\n }\n\n try {\n timeline = await client.getTimeline({ count: 20, sinceId: lastTimelineSinceId });\n if (timeline.length > 0) {\n lastTimelineSinceId = timeline[0].id;\n // Filter out own tweets (can't like your own)\n if (ownHandle) {\n timeline = timeline.filter(t => t.authorHandle.toLowerCase() !== ownHandle!.toLowerCase());\n }\n }\n } catch (error) {\n console.log(chalk.dim(` [Heartbeat] Timeline read failed: ${(error as Error).message}`));\n }\n\n try {\n mentions = await client.getMentions({ count: 10, sinceId: lastMentionsSinceId });\n if (mentions.length > 0) {\n lastMentionsSinceId = mentions[0].id;\n }\n } catch (error) {\n console.log(chalk.dim(` [Heartbeat] Mentions read failed: ${(error as Error).message}`));\n }\n\n // 3. Build prompts and ask LLM\n const systemPrompt = buildSystemPrompt();\n const userMessage = buildNarratedHeartbeatMessage(timeline, mentions, intervalMs);\n\n const response = await generateResponse(systemPrompt, userMessage);\n\n // 4. Extract narration text (everything outside JSON blocks)\n let narration = response.content.replace(/```json\\s*[\\s\\S]*?```/g, \"\").replace(/\\n{3,}/g, \"\\n\\n\").trim();\n\n // 5. Parse and execute actions\n const actions = parseActions(response.content);\n const limitedActions = actions.slice(0, maxActions);\n\n if (limitedActions.length > 0) {\n // Execute actions\n const results = await executeActions(limitedActions);\n\n // Build visible results (hide schedule actions from chat — they're internal)\n const visibleResults: string[] = [];\n for (const r of results) {\n const detail = r.detail ? ` (${r.detail})` : \"\";\n if (r.action === \"schedule\") {\n // Log to terminal only, don't show in chat\n console.log(chalk.dim(` [Heartbeat] ${r.action}: ${r.success ? \"queued\" : r.error}${detail}`));\n } else if (r.success) {\n visibleResults.push(`[${r.action}] Done${detail}`);\n console.log(chalk.green(` [Heartbeat] ${r.action}: success${detail}`));\n } else {\n visibleResults.push(`[${r.action}] Failed: ${r.error}`);\n console.log(chalk.red(` [Heartbeat] ${r.action}: ${r.error}`));\n }\n }\n\n // Combine narration with visible action results\n let fullMessage = narration || \"\";\n if (visibleResults.length > 0) {\n fullMessage += (fullMessage ? \"\\n\\n\" : \"\") + visibleResults.join(\"\\n\");\n }\n if (fullMessage) {\n server.pushMessage(\"assistant\", fullMessage);\n }\n } else if (narration) {\n server.pushMessage(\"assistant\", narration);\n }\n\n console.log(chalk.cyan(` [Heartbeat] Complete.`));\n}\n\n/**\n * Open URL in a separate browser window (app-like)\n */\nfunction openBrowser(url: string) {\n const platform = process.platform;\n\n try {\n if (platform === \"darwin\") {\n // Open as a standalone Chrome app window (smaller, separate from existing tabs)\n try {\n execSync(`open -na \"Google Chrome\" --args --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n // Fallback: try Chromium-based browsers, then default\n try {\n execSync(`open -na \"Brave Browser\" --args --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n execSync(`open \"${url}\"`, { stdio: \"ignore\" });\n }\n }\n } else if (platform === \"win32\") {\n try {\n execSync(`start chrome --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n execSync(`start \"\" \"${url}\"`, { stdio: \"ignore\" });\n }\n } else {\n // Linux\n try {\n execSync(`google-chrome --app=\"${url}\" --window-size=420,620`, { stdio: \"ignore\" });\n } catch {\n execSync(`xdg-open \"${url}\"`, { stdio: \"ignore\" });\n }\n }\n } catch (error) {\n // Browser couldn't be opened, that's okay\n console.log(chalk.dim(`(Couldn't open browser automatically - please visit ${url} manually)`));\n }\n}\n\nexport { openBrowser };\n"],"mappings":"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","names":[]}