spora 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,12 @@
1
- import {
2
- buildHeartbeatUserMessage,
3
- buildSystemPrompt
4
- } from "./chunk-ZEV5VG2T.js";
5
- import {
6
- generateResponse
7
- } from "./chunk-4F7KD5JT.js";
8
- import {
9
- rateLimiter
10
- } from "./chunk-6M4HISFK.js";
11
1
  import {
12
2
  getXClient
13
3
  } from "./chunk-XGTGZ2KC.js";
14
4
  import {
15
- addToQueue,
16
- flushQueue
5
+ addToQueue
17
6
  } from "./chunk-RANLXKYR.js";
18
7
  import {
19
- loadConfig
20
- } from "./chunk-B6RPMDML.js";
8
+ rateLimiter
9
+ } from "./chunk-6M4HISFK.js";
21
10
  import {
22
11
  loadIdentity,
23
12
  saveIdentity
@@ -29,12 +18,6 @@ import {
29
18
  addLearning,
30
19
  logInteraction
31
20
  } from "./chunk-EBO4F5NU.js";
32
- import {
33
- paths
34
- } from "./chunk-53YLFYJF.js";
35
-
36
- // src/runtime/heartbeat.ts
37
- import { existsSync, unlinkSync, writeFileSync, readFileSync } from "fs";
38
21
 
39
22
  // src/runtime/decision-engine.ts
40
23
  function parseActions(llmResponse) {
@@ -209,150 +192,9 @@ async function executeActions(actions) {
209
192
  return results;
210
193
  }
211
194
 
212
- // src/runtime/heartbeat.ts
213
- var running = false;
214
- function isRunning() {
215
- return running;
216
- }
217
- function requestStop() {
218
- writeFileSync(paths.stopSignal, "stop");
219
- logger.info("Stop signal sent.");
220
- }
221
- function shouldStop() {
222
- if (existsSync(paths.stopSignal)) {
223
- unlinkSync(paths.stopSignal);
224
- return true;
225
- }
226
- return false;
227
- }
228
- function writePid() {
229
- writeFileSync(paths.runtimePid, String(process.pid));
230
- }
231
- function clearPid() {
232
- if (existsSync(paths.runtimePid)) {
233
- unlinkSync(paths.runtimePid);
234
- }
235
- }
236
- function getRunningPid() {
237
- if (!existsSync(paths.runtimePid)) return null;
238
- const pid = parseInt(readFileSync(paths.runtimePid, "utf-8").trim(), 10);
239
- if (isNaN(pid)) return null;
240
- try {
241
- process.kill(pid, 0);
242
- return pid;
243
- } catch {
244
- clearPid();
245
- return null;
246
- }
247
- }
248
- async function startHeartbeatLoop() {
249
- const existingPid = getRunningPid();
250
- if (existingPid) {
251
- throw new Error(`Spora is already running (PID ${existingPid}). Run \`spora stop\` first.`);
252
- }
253
- running = true;
254
- writePid();
255
- const config = loadConfig();
256
- const intervalMs = config.runtime?.heartbeatIntervalMs ?? 3e5;
257
- const maxActions = config.runtime?.actionsPerHeartbeat ?? 3;
258
- logger.info(`Spora agent starting. Heartbeat interval: ${intervalMs / 1e3}s, max actions: ${maxActions}`);
259
- console.log(`
260
- Spora agent is running (PID ${process.pid})`);
261
- console.log(`Heartbeat every ${Math.round(intervalMs / 6e4)} minutes`);
262
- console.log(`Press Ctrl+C or run \`spora stop\` to stop.
263
- `);
264
- const shutdown = () => {
265
- logger.info("Shutting down...");
266
- running = false;
267
- clearPid();
268
- process.exit(0);
269
- };
270
- process.on("SIGINT", shutdown);
271
- process.on("SIGTERM", shutdown);
272
- if (existsSync(paths.stopSignal)) {
273
- unlinkSync(paths.stopSignal);
274
- }
275
- let heartbeatCount = 0;
276
- while (running) {
277
- heartbeatCount++;
278
- logger.info(`=== Heartbeat #${heartbeatCount} ===`);
279
- try {
280
- await runHeartbeat(maxActions);
281
- } catch (error) {
282
- logger.error("Heartbeat error", error);
283
- console.error(`Heartbeat #${heartbeatCount} failed: ${error.message}`);
284
- }
285
- if (shouldStop()) {
286
- logger.info("Stop signal received.");
287
- break;
288
- }
289
- const jitter = Math.floor(Math.random() * intervalMs * 0.3);
290
- const sleepMs = intervalMs + jitter;
291
- logger.info(`Sleeping ${Math.round(sleepMs / 1e3)}s until next heartbeat...`);
292
- const chunkMs = 1e4;
293
- let slept = 0;
294
- while (slept < sleepMs && running) {
295
- await new Promise((r) => setTimeout(r, Math.min(chunkMs, sleepMs - slept)));
296
- slept += chunkMs;
297
- if (shouldStop()) {
298
- running = false;
299
- break;
300
- }
301
- }
302
- }
303
- clearPid();
304
- logger.info("Spora agent stopped.");
305
- console.log("\nSpora agent stopped.");
306
- }
307
- async function runHeartbeat(maxActions) {
308
- logger.info("Checking queue...");
309
- try {
310
- const flushed = await flushQueue();
311
- if (flushed.posted > 0) {
312
- logger.info(`Flushed ${flushed.posted} queued posts.`);
313
- }
314
- } catch (error) {
315
- logger.warn(`Queue flush failed: ${error.message}`);
316
- }
317
- logger.info("Reading timeline and mentions...");
318
- const client = await getXClient();
319
- let timeline = [];
320
- let mentions = [];
321
- try {
322
- timeline = await client.getTimeline({ count: 20 });
323
- } catch (error) {
324
- logger.warn(`Timeline read failed: ${error.message}`);
325
- }
326
- try {
327
- mentions = await client.getMentions({ count: 10 });
328
- } catch (error) {
329
- logger.warn(`Mentions read failed: ${error.message}`);
330
- }
331
- const systemPrompt = buildSystemPrompt();
332
- const userMessage = buildHeartbeatUserMessage(timeline, mentions);
333
- logger.info("Asking LLM for decisions...");
334
- const response = await generateResponse(systemPrompt, userMessage);
335
- const actions = parseActions(response.content);
336
- if (actions.length === 0) {
337
- logger.info("LLM returned no actions.");
338
- return;
339
- }
340
- const limitedActions = actions.slice(0, maxActions);
341
- logger.info(`Executing ${limitedActions.length} action(s)...`);
342
- const results = await executeActions(limitedActions);
343
- for (const result of results) {
344
- if (result.success) {
345
- logger.info(` [OK] ${result.action}${result.detail ? `: ${result.detail}` : ""}`);
346
- } else {
347
- logger.warn(` [FAIL] ${result.action}: ${result.error}`);
348
- }
349
- }
350
- logger.info(`Heartbeat complete. ${results.filter((r) => r.success).length}/${results.length} actions succeeded.`);
351
- }
352
195
  export {
353
- getRunningPid,
354
- isRunning,
355
- requestStop,
356
- startHeartbeatLoop
196
+ parseActions,
197
+ executeAction,
198
+ executeActions
357
199
  };
358
- //# sourceMappingURL=heartbeat-2V6FJ24U.js.map
200
+ //# sourceMappingURL=chunk-VYT2DS5P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/decision-engine.ts"],"sourcesContent":["import { logger } from \"../utils/logger.js\";\nimport { getXClient } from \"../x-client/index.js\";\nimport { logInteraction, addLearning } from \"../memory/index.js\";\nimport { loadIdentity, saveIdentity } from \"../identity/index.js\";\nimport { addToQueue } from \"../scheduler/queue.js\";\nimport { rateLimiter } from \"../x-client/rate-limiter.js\";\n\nexport interface AgentAction {\n action: string;\n content?: string;\n tweetId?: string;\n handle?: string;\n tags?: string[];\n reason?: string;\n reasoning?: string;\n}\n\nexport interface ActionResult {\n action: string;\n success: boolean;\n detail?: string;\n error?: string;\n}\n\nexport function parseActions(llmResponse: string): AgentAction[] {\n // Extract JSON array from the response (may be wrapped in markdown code blocks)\n const jsonMatch = llmResponse.match(/\\[[\\s\\S]*?\\]/);\n if (!jsonMatch) {\n // Try to parse as a single action object\n const objMatch = llmResponse.match(/\\{[\\s\\S]*?\\}/);\n if (objMatch) {\n try {\n return [JSON.parse(objMatch[0]) as AgentAction];\n } catch {\n logger.warn(\"Could not parse LLM response as action object\");\n return [];\n }\n }\n logger.warn(\"No JSON found in LLM response\");\n return [];\n }\n\n try {\n const actions = JSON.parse(jsonMatch[0]) as AgentAction[];\n return Array.isArray(actions) ? actions : [actions];\n } catch {\n logger.warn(\"Failed to parse actions JSON from LLM response\");\n return [];\n }\n}\n\nexport async function executeAction(action: AgentAction): Promise<ActionResult> {\n const { action: type } = action;\n\n try {\n switch (type) {\n case \"post\": {\n if (!action.content) return { action: type, success: false, error: \"No content provided\" };\n if (action.content.length > 280) {\n return { action: type, success: false, error: `Tweet too long: ${action.content.length} chars (max 280)` };\n }\n if (!rateLimiter.canPost()) {\n return { action: type, success: false, error: \"No credits remaining this month\" };\n }\n\n const client = await getXClient();\n const result = await client.postTweet(action.content);\n if (result.success) {\n rateLimiter.consume(1);\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"post\",\n tweetId: result.tweetId,\n content: action.content,\n creditsUsed: 1,\n success: true,\n });\n logger.info(`Posted: \"${action.content.slice(0, 50)}...\"`);\n }\n return { action: type, success: result.success, detail: result.tweetId, error: result.error };\n }\n\n case \"reply\": {\n if (!action.tweetId || !action.content) {\n return { action: type, success: false, error: \"Missing tweetId or content\" };\n }\n if (!rateLimiter.canPost()) {\n return { action: type, success: false, error: \"No credits remaining\" };\n }\n\n const client = await getXClient();\n const result = await client.replyToTweet(action.tweetId, action.content);\n if (result.success) {\n rateLimiter.consume(1);\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"reply\",\n tweetId: result.tweetId,\n inReplyTo: action.tweetId,\n content: action.content,\n creditsUsed: 1,\n success: true,\n });\n logger.info(`Replied to ${action.tweetId}: \"${action.content.slice(0, 50)}...\"`);\n }\n return { action: type, success: result.success, detail: result.tweetId, error: result.error };\n }\n\n case \"like\": {\n if (!action.tweetId) return { action: type, success: false, error: \"Missing tweetId\" };\n const client = await getXClient();\n const result = await client.likeTweet(action.tweetId);\n if (result.success) {\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"like\",\n tweetId: action.tweetId,\n creditsUsed: 0,\n success: true,\n });\n }\n return { action: type, success: result.success, error: result.error };\n }\n\n case \"retweet\": {\n if (!action.tweetId) return { action: type, success: false, error: \"Missing tweetId\" };\n const client = await getXClient();\n const result = await client.retweet(action.tweetId);\n if (result.success) {\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"retweet\",\n tweetId: action.tweetId,\n creditsUsed: 0,\n success: true,\n });\n }\n return { action: type, success: result.success, error: result.error };\n }\n\n case \"follow\": {\n if (!action.handle) return { action: type, success: false, error: \"Missing handle\" };\n const client = await getXClient();\n const result = await client.followUser(action.handle);\n if (result.success) {\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"follow\",\n targetHandle: action.handle,\n creditsUsed: 0,\n success: true,\n });\n }\n return { action: type, success: result.success, error: result.error };\n }\n\n case \"schedule\": {\n if (!action.content) return { action: type, success: false, error: \"No content\" };\n const entry = addToQueue(action.content);\n logger.info(`Scheduled: \"${action.content.slice(0, 50)}...\" for ${entry.scheduledFor}`);\n return { action: type, success: true, detail: `Scheduled for ${entry.scheduledFor}` };\n }\n\n case \"learn\": {\n if (!action.content) return { action: type, success: false, error: \"No content\" };\n addLearning(action.content, \"agent\", action.tags ?? [\"heartbeat\"]);\n logger.info(`Learned: \"${action.content.slice(0, 50)}...\"`);\n return { action: type, success: true };\n }\n\n case \"reflect\": {\n if (!action.content) return { action: type, success: false, error: \"No content\" };\n const identity = loadIdentity();\n identity.evolutionJournal.push({\n date: new Date().toISOString(),\n reflection: action.content,\n });\n saveIdentity(identity);\n logger.info(`Reflected: \"${action.content.slice(0, 50)}...\"`);\n return { action: type, success: true };\n }\n\n case \"skip\": {\n logger.info(`Skipping: ${action.reason ?? action.reasoning ?? \"no reason given\"}`);\n return { action: type, success: true, detail: action.reason ?? action.reasoning };\n }\n\n default:\n logger.warn(`Unknown action: ${type}`);\n return { action: type, success: false, error: `Unknown action: ${type}` };\n }\n } catch (error) {\n const msg = (error as Error).message;\n logger.error(`Action ${type} failed: ${msg}`);\n return { action: type, success: false, error: msg };\n }\n}\n\nexport async function executeActions(actions: AgentAction[]): Promise<ActionResult[]> {\n const results: ActionResult[] = [];\n for (const action of actions) {\n const result = await executeAction(action);\n results.push(result);\n // Small delay between actions to be human-like\n await new Promise((r) => setTimeout(r, 2000 + Math.random() * 3000));\n }\n return results;\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-JJ2CMXTJ.js");
126
+ const { runInit } = await import("./init-WQDJLWMK.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-3XTQGVOY.js");
138
+ const { startWebChat } = await import("./web-chat-OH3YXPJO.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-2V6FJ24U.js");
560
+ const { startHeartbeatLoop } = await import("./heartbeat-YOJVMZUL.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-2V6FJ24U.js");
564
+ const { getRunningPid, requestStop } = await import("./heartbeat-YOJVMZUL.js");
565
565
  const pid = getRunningPid();
566
566
  if (!pid) {
567
567
  console.log(JSON.stringify({ message: "Spora agent is not running." }));
@@ -598,7 +598,7 @@ program.command("set-llm-key").description("Set your DeepSeek API key for the ag
598
598
  console.log(JSON.stringify({ success: true, message: "LLM API key saved." }));
599
599
  });
600
600
  program.command("agent-status").description("Check if the Spora agent is running").action(async () => {
601
- const { getRunningPid } = await import("./heartbeat-2V6FJ24U.js");
601
+ const { getRunningPid } = await import("./heartbeat-YOJVMZUL.js");
602
602
  const pid = getRunningPid();
603
603
  const { hasLLMKey } = await import("./llm-2CDBHU2Q.js");
604
604
  console.log(JSON.stringify({
@@ -0,0 +1,19 @@
1
+ import {
2
+ executeAction,
3
+ executeActions,
4
+ parseActions
5
+ } from "./chunk-VYT2DS5P.js";
6
+ import "./chunk-XGTGZ2KC.js";
7
+ import "./chunk-RANLXKYR.js";
8
+ import "./chunk-6M4HISFK.js";
9
+ import "./chunk-B6RPMDML.js";
10
+ import "./chunk-AIEXQCQS.js";
11
+ import "./chunk-KELPENM3.js";
12
+ import "./chunk-EBO4F5NU.js";
13
+ import "./chunk-53YLFYJF.js";
14
+ export {
15
+ executeAction,
16
+ executeActions,
17
+ parseActions
18
+ };
19
+ //# sourceMappingURL=decision-engine-6WCHQU2A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,178 @@
1
+ import {
2
+ buildHeartbeatUserMessage,
3
+ buildSystemPrompt
4
+ } from "./chunk-ZEV5VG2T.js";
5
+ import {
6
+ generateResponse
7
+ } from "./chunk-4F7KD5JT.js";
8
+ import {
9
+ executeActions,
10
+ parseActions
11
+ } from "./chunk-VYT2DS5P.js";
12
+ import {
13
+ getXClient
14
+ } from "./chunk-XGTGZ2KC.js";
15
+ import {
16
+ flushQueue
17
+ } from "./chunk-RANLXKYR.js";
18
+ import "./chunk-6M4HISFK.js";
19
+ import {
20
+ loadConfig
21
+ } from "./chunk-B6RPMDML.js";
22
+ import "./chunk-AIEXQCQS.js";
23
+ import {
24
+ logger
25
+ } from "./chunk-KELPENM3.js";
26
+ import "./chunk-EBO4F5NU.js";
27
+ import {
28
+ paths
29
+ } from "./chunk-53YLFYJF.js";
30
+
31
+ // src/runtime/heartbeat.ts
32
+ import { existsSync, unlinkSync, writeFileSync, readFileSync } from "fs";
33
+ var running = false;
34
+ function isRunning() {
35
+ return running;
36
+ }
37
+ function requestStop() {
38
+ writeFileSync(paths.stopSignal, "stop");
39
+ logger.info("Stop signal sent.");
40
+ }
41
+ function shouldStop() {
42
+ if (existsSync(paths.stopSignal)) {
43
+ unlinkSync(paths.stopSignal);
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ function writePid() {
49
+ writeFileSync(paths.runtimePid, String(process.pid));
50
+ }
51
+ function clearPid() {
52
+ if (existsSync(paths.runtimePid)) {
53
+ unlinkSync(paths.runtimePid);
54
+ }
55
+ }
56
+ function getRunningPid() {
57
+ if (!existsSync(paths.runtimePid)) return null;
58
+ const pid = parseInt(readFileSync(paths.runtimePid, "utf-8").trim(), 10);
59
+ if (isNaN(pid)) return null;
60
+ try {
61
+ process.kill(pid, 0);
62
+ return pid;
63
+ } catch {
64
+ clearPid();
65
+ return null;
66
+ }
67
+ }
68
+ async function startHeartbeatLoop() {
69
+ const existingPid = getRunningPid();
70
+ if (existingPid) {
71
+ throw new Error(`Spora is already running (PID ${existingPid}). Run \`spora stop\` first.`);
72
+ }
73
+ running = true;
74
+ writePid();
75
+ const config = loadConfig();
76
+ const intervalMs = config.runtime?.heartbeatIntervalMs ?? 3e5;
77
+ const maxActions = config.runtime?.actionsPerHeartbeat ?? 3;
78
+ logger.info(`Spora agent starting. Heartbeat interval: ${intervalMs / 1e3}s, max actions: ${maxActions}`);
79
+ console.log(`
80
+ Spora agent is running (PID ${process.pid})`);
81
+ console.log(`Heartbeat every ${Math.round(intervalMs / 6e4)} minutes`);
82
+ console.log(`Press Ctrl+C or run \`spora stop\` to stop.
83
+ `);
84
+ const shutdown = () => {
85
+ logger.info("Shutting down...");
86
+ running = false;
87
+ clearPid();
88
+ process.exit(0);
89
+ };
90
+ process.on("SIGINT", shutdown);
91
+ process.on("SIGTERM", shutdown);
92
+ if (existsSync(paths.stopSignal)) {
93
+ unlinkSync(paths.stopSignal);
94
+ }
95
+ let heartbeatCount = 0;
96
+ while (running) {
97
+ heartbeatCount++;
98
+ logger.info(`=== Heartbeat #${heartbeatCount} ===`);
99
+ try {
100
+ await runHeartbeat(maxActions);
101
+ } catch (error) {
102
+ logger.error("Heartbeat error", error);
103
+ console.error(`Heartbeat #${heartbeatCount} failed: ${error.message}`);
104
+ }
105
+ if (shouldStop()) {
106
+ logger.info("Stop signal received.");
107
+ break;
108
+ }
109
+ const jitter = Math.floor(Math.random() * intervalMs * 0.3);
110
+ const sleepMs = intervalMs + jitter;
111
+ logger.info(`Sleeping ${Math.round(sleepMs / 1e3)}s until next heartbeat...`);
112
+ const chunkMs = 1e4;
113
+ let slept = 0;
114
+ while (slept < sleepMs && running) {
115
+ await new Promise((r) => setTimeout(r, Math.min(chunkMs, sleepMs - slept)));
116
+ slept += chunkMs;
117
+ if (shouldStop()) {
118
+ running = false;
119
+ break;
120
+ }
121
+ }
122
+ }
123
+ clearPid();
124
+ logger.info("Spora agent stopped.");
125
+ console.log("\nSpora agent stopped.");
126
+ }
127
+ async function runHeartbeat(maxActions) {
128
+ logger.info("Checking queue...");
129
+ try {
130
+ const flushed = await flushQueue();
131
+ if (flushed.posted > 0) {
132
+ logger.info(`Flushed ${flushed.posted} queued posts.`);
133
+ }
134
+ } catch (error) {
135
+ logger.warn(`Queue flush failed: ${error.message}`);
136
+ }
137
+ logger.info("Reading timeline and mentions...");
138
+ const client = await getXClient();
139
+ let timeline = [];
140
+ let mentions = [];
141
+ try {
142
+ timeline = await client.getTimeline({ count: 20 });
143
+ } catch (error) {
144
+ logger.warn(`Timeline read failed: ${error.message}`);
145
+ }
146
+ try {
147
+ mentions = await client.getMentions({ count: 10 });
148
+ } catch (error) {
149
+ logger.warn(`Mentions read failed: ${error.message}`);
150
+ }
151
+ const systemPrompt = buildSystemPrompt();
152
+ const userMessage = buildHeartbeatUserMessage(timeline, mentions);
153
+ logger.info("Asking LLM for decisions...");
154
+ const response = await generateResponse(systemPrompt, userMessage);
155
+ const actions = parseActions(response.content);
156
+ if (actions.length === 0) {
157
+ logger.info("LLM returned no actions.");
158
+ return;
159
+ }
160
+ const limitedActions = actions.slice(0, maxActions);
161
+ logger.info(`Executing ${limitedActions.length} action(s)...`);
162
+ const results = await executeActions(limitedActions);
163
+ for (const result of results) {
164
+ if (result.success) {
165
+ logger.info(` [OK] ${result.action}${result.detail ? `: ${result.detail}` : ""}`);
166
+ } else {
167
+ logger.warn(` [FAIL] ${result.action}: ${result.error}`);
168
+ }
169
+ }
170
+ logger.info(`Heartbeat complete. ${results.filter((r) => r.success).length}/${results.length} actions succeeded.`);
171
+ }
172
+ export {
173
+ getRunningPid,
174
+ isRunning,
175
+ requestStop,
176
+ startHeartbeatLoop
177
+ };
178
+ //# sourceMappingURL=heartbeat-YOJVMZUL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/heartbeat.ts"],"sourcesContent":["import { existsSync, unlinkSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { logger } from \"../utils/logger.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { paths } from \"../utils/paths.js\";\nimport { getXClient } from \"../x-client/index.js\";\nimport { flushQueue } from \"../scheduler/queue.js\";\nimport { buildSystemPrompt, buildHeartbeatUserMessage } from \"./prompt-builder.js\";\nimport { generateResponse } from \"./llm.js\";\nimport { parseActions, executeActions, type ActionResult } from \"./decision-engine.js\";\n\nlet running = false;\n\nexport function isRunning(): boolean {\n return running;\n}\n\nexport function requestStop(): void {\n writeFileSync(paths.stopSignal, \"stop\");\n logger.info(\"Stop signal sent.\");\n}\n\nfunction shouldStop(): boolean {\n if (existsSync(paths.stopSignal)) {\n unlinkSync(paths.stopSignal);\n return true;\n }\n return false;\n}\n\nfunction writePid(): void {\n writeFileSync(paths.runtimePid, String(process.pid));\n}\n\nfunction clearPid(): void {\n if (existsSync(paths.runtimePid)) {\n unlinkSync(paths.runtimePid);\n }\n}\n\nexport function getRunningPid(): number | null {\n if (!existsSync(paths.runtimePid)) return null;\n const pid = parseInt(readFileSync(paths.runtimePid, \"utf-8\").trim(), 10);\n if (isNaN(pid)) return null;\n\n // Check if process is actually running\n try {\n process.kill(pid, 0);\n return pid;\n } catch {\n // Process not running, clean up stale PID\n clearPid();\n return null;\n }\n}\n\nexport async function startHeartbeatLoop(): Promise<void> {\n // Check if already running\n const existingPid = getRunningPid();\n if (existingPid) {\n throw new Error(`Spora is already running (PID ${existingPid}). Run \\`spora stop\\` first.`);\n }\n\n running = true;\n writePid();\n\n const config = loadConfig();\n const intervalMs = config.runtime?.heartbeatIntervalMs ?? 300_000;\n const maxActions = config.runtime?.actionsPerHeartbeat ?? 3;\n\n logger.info(`Spora agent starting. Heartbeat interval: ${intervalMs / 1000}s, max actions: ${maxActions}`);\n console.log(`\\nSpora agent is running (PID ${process.pid})`);\n console.log(`Heartbeat every ${Math.round(intervalMs / 60_000)} minutes`);\n console.log(`Press Ctrl+C or run \\`spora stop\\` to stop.\\n`);\n\n // Handle graceful shutdown\n const shutdown = () => {\n logger.info(\"Shutting down...\");\n running = false;\n clearPid();\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Clean any stale stop signal\n if (existsSync(paths.stopSignal)) {\n unlinkSync(paths.stopSignal);\n }\n\n let heartbeatCount = 0;\n\n while (running) {\n heartbeatCount++;\n logger.info(`=== Heartbeat #${heartbeatCount} ===`);\n\n try {\n await runHeartbeat(maxActions);\n } catch (error) {\n logger.error(\"Heartbeat error\", error);\n console.error(`Heartbeat #${heartbeatCount} failed: ${(error as Error).message}`);\n }\n\n // Check for stop signal\n if (shouldStop()) {\n logger.info(\"Stop signal received.\");\n break;\n }\n\n // Sleep with jitter\n const jitter = Math.floor(Math.random() * intervalMs * 0.3);\n const sleepMs = intervalMs + jitter;\n logger.info(`Sleeping ${Math.round(sleepMs / 1000)}s until next heartbeat...`);\n\n // Sleep in chunks so we can check for stop signals\n const chunkMs = 10_000;\n let slept = 0;\n while (slept < sleepMs && running) {\n await new Promise((r) => setTimeout(r, Math.min(chunkMs, sleepMs - slept)));\n slept += chunkMs;\n if (shouldStop()) {\n running = false;\n break;\n }\n }\n }\n\n clearPid();\n logger.info(\"Spora agent stopped.\");\n console.log(\"\\nSpora agent stopped.\");\n}\n\nasync function runHeartbeat(maxActions: number): Promise<void> {\n // 1. Flush any queued posts\n logger.info(\"Checking queue...\");\n try {\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n logger.info(`Flushed ${flushed.posted} queued posts.`);\n }\n } catch (error) {\n logger.warn(`Queue flush failed: ${(error as Error).message}`);\n }\n\n // 2. Read timeline and mentions for context\n logger.info(\"Reading timeline and mentions...\");\n const client = await getXClient();\n\n let timeline: Awaited<ReturnType<typeof client.getTimeline>> = [];\n let mentions: Awaited<ReturnType<typeof client.getMentions>> = [];\n\n try {\n timeline = await client.getTimeline({ count: 20 });\n } catch (error) {\n logger.warn(`Timeline read failed: ${(error as Error).message}`);\n }\n\n try {\n mentions = await client.getMentions({ count: 10 });\n } catch (error) {\n logger.warn(`Mentions read failed: ${(error as Error).message}`);\n }\n\n // 3. Build prompts\n const systemPrompt = buildSystemPrompt();\n const userMessage = buildHeartbeatUserMessage(timeline, mentions);\n\n // 4. Ask LLM for decisions\n logger.info(\"Asking LLM for decisions...\");\n const response = await generateResponse(systemPrompt, userMessage);\n\n // 5. Parse and execute actions\n const actions = parseActions(response.content);\n if (actions.length === 0) {\n logger.info(\"LLM returned no actions.\");\n return;\n }\n\n // Limit to max actions per heartbeat\n const limitedActions = actions.slice(0, maxActions);\n logger.info(`Executing ${limitedActions.length} action(s)...`);\n\n const results = await executeActions(limitedActions);\n\n // 6. Log results\n for (const result of results) {\n if (result.success) {\n logger.info(` [OK] ${result.action}${result.detail ? `: ${result.detail}` : \"\"}`);\n } else {\n logger.warn(` [FAIL] ${result.action}: ${result.error}`);\n }\n }\n\n logger.info(`Heartbeat complete. ${results.filter((r) => r.success).length}/${results.length} actions succeeded.`);\n}\n"],"mappings":"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","names":[]}
@@ -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-3XTQGVOY.js");
206
+ const { startWebChat } = await import("./web-chat-OH3YXPJO.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-3XTQGVOY.js");
278
+ const { startWebChat } = await import("./web-chat-OH3YXPJO.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-JJ2CMXTJ.js.map
403
+ //# sourceMappingURL=init-WQDJLWMK.js.map
@@ -224,6 +224,80 @@
224
224
  0%, 80%, 100% { transform: scale(0); }
225
225
  40% { transform: scale(1); }
226
226
  }
227
+
228
+ /* Agent activity feed */
229
+ .activity-bar {
230
+ background: #0d0d0d;
231
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
232
+ padding: 0.5rem 1.25rem;
233
+ font-size: 0.6875rem;
234
+ color: rgba(255, 255, 255, 0.35);
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 0.5rem;
238
+ min-height: 28px;
239
+ cursor: pointer;
240
+ transition: background 0.2s;
241
+ }
242
+
243
+ .activity-bar:hover {
244
+ background: #111;
245
+ }
246
+
247
+ .activity-icon {
248
+ font-size: 0.625rem;
249
+ flex-shrink: 0;
250
+ }
251
+
252
+ .activity-text {
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ white-space: nowrap;
256
+ flex: 1;
257
+ }
258
+
259
+ .activity-panel {
260
+ display: none;
261
+ background: #0d0d0d;
262
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
263
+ max-height: 200px;
264
+ overflow-y: auto;
265
+ padding: 0.5rem 1.25rem;
266
+ }
267
+
268
+ .activity-panel.open {
269
+ display: block;
270
+ }
271
+
272
+ .activity-panel::-webkit-scrollbar {
273
+ width: 3px;
274
+ }
275
+
276
+ .activity-panel::-webkit-scrollbar-thumb {
277
+ background: rgba(255, 255, 255, 0.1);
278
+ border-radius: 3px;
279
+ }
280
+
281
+ .activity-entry {
282
+ font-size: 0.6875rem;
283
+ color: rgba(255, 255, 255, 0.3);
284
+ padding: 0.2rem 0;
285
+ display: flex;
286
+ gap: 0.5rem;
287
+ }
288
+
289
+ .activity-entry .time {
290
+ color: rgba(255, 255, 255, 0.15);
291
+ flex-shrink: 0;
292
+ }
293
+
294
+ .activity-entry.action {
295
+ color: #389e77;
296
+ }
297
+
298
+ .activity-entry.heartbeat {
299
+ color: rgba(255, 255, 255, 0.25);
300
+ }
227
301
  </style>
228
302
  </head>
229
303
  <body>
@@ -237,6 +311,12 @@
237
311
  </div>
238
312
  </div>
239
313
 
314
+ <div class="activity-bar" id="activityBar" onclick="toggleActivityPanel()">
315
+ <span class="activity-icon">&#9679;</span>
316
+ <span class="activity-text" id="activityText">Agent starting up...</span>
317
+ </div>
318
+ <div class="activity-panel" id="activityPanel"></div>
319
+
240
320
  <div class="chat-container" id="chatContainer">
241
321
  </div>
242
322
 
@@ -415,6 +495,60 @@
415
495
  }
416
496
  });
417
497
 
498
+ // ===== Agent Activity Feed =====
499
+ const activityBar = document.getElementById('activityBar');
500
+ const activityText = document.getElementById('activityText');
501
+ const activityPanel = document.getElementById('activityPanel');
502
+ const activityIcon = activityBar.querySelector('.activity-icon');
503
+ let lastActivityTimestamp = 0;
504
+ let panelOpen = false;
505
+
506
+ function toggleActivityPanel() {
507
+ panelOpen = !panelOpen;
508
+ activityPanel.classList.toggle('open', panelOpen);
509
+ }
510
+
511
+ function addActivityEntry(activity) {
512
+ const time = new Date(activity.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
513
+ const entry = document.createElement('div');
514
+ entry.className = `activity-entry ${activity.type}`;
515
+ entry.innerHTML = `<span class="time">${time}</span><span>${activity.message}</span>`;
516
+ activityPanel.appendChild(entry);
517
+ activityPanel.scrollTop = activityPanel.scrollHeight;
518
+
519
+ // Update the top bar with latest
520
+ activityText.textContent = activity.message;
521
+
522
+ // Color the dot based on type
523
+ if (activity.type === 'action') {
524
+ activityIcon.style.color = '#389e77';
525
+ } else if (activity.type === 'heartbeat') {
526
+ activityIcon.style.color = '#f0c040';
527
+ } else {
528
+ activityIcon.style.color = 'rgba(255, 255, 255, 0.35)';
529
+ }
530
+ }
531
+
532
+ async function pollActivity() {
533
+ try {
534
+ const response = await fetch(`/api/activity?since=${lastActivityTimestamp}`);
535
+ const data = await response.json();
536
+ if (data.activities && data.activities.length > 0) {
537
+ for (const activity of data.activities) {
538
+ addActivityEntry(activity);
539
+ if (activity.timestamp > lastActivityTimestamp) {
540
+ lastActivityTimestamp = activity.timestamp;
541
+ }
542
+ }
543
+ }
544
+ } catch {
545
+ // Polling failures are silent
546
+ }
547
+ }
548
+
549
+ // Poll activity every 3 seconds
550
+ setInterval(pollActivity, 3000);
551
+
418
552
  // Initialize
419
553
  init();
420
554
  messageInput.focus();
@@ -15,6 +15,7 @@ var WebChatServer = class {
15
15
  server = null;
16
16
  port;
17
17
  messages = [];
18
+ activities = [];
18
19
  onUserMessage;
19
20
  identity;
20
21
  constructor(port = 3737) {
@@ -26,6 +27,13 @@ var WebChatServer = class {
26
27
  setMessageHandler(handler) {
27
28
  this.onUserMessage = handler;
28
29
  }
30
+ /** Push an activity update (from heartbeat) to the UI */
31
+ pushActivity(type, message) {
32
+ this.activities.push({ type, message, timestamp: Date.now() });
33
+ if (this.activities.length > 100) {
34
+ this.activities = this.activities.slice(-100);
35
+ }
36
+ }
29
37
  async start() {
30
38
  return new Promise((resolve, reject) => {
31
39
  this.server = http.createServer(async (req, res) => {
@@ -79,6 +87,13 @@ var WebChatServer = class {
79
87
  res.end(JSON.stringify({ messages: this.messages }));
80
88
  return;
81
89
  }
90
+ if (url.pathname === "/api/activity" && req.method === "GET") {
91
+ const since = parseInt(url.searchParams.get("since") || "0", 10);
92
+ const newActivities = this.activities.filter((a) => a.timestamp > since);
93
+ res.writeHead(200, { "Content-Type": "application/json" });
94
+ res.end(JSON.stringify({ activities: newActivities }));
95
+ return;
96
+ }
82
97
  if (url.pathname === "/api/message" && req.method === "POST") {
83
98
  let body = "";
84
99
  req.on("data", (chunk) => {
@@ -166,6 +181,83 @@ async function logChatInteraction(userMessage, agentResponse) {
166
181
  success: true
167
182
  });
168
183
  }
184
+ async function runNarratedHeartbeat(server) {
185
+ const { loadConfig } = await import("./config-TFAFYSIW.js");
186
+ const { getXClient } = await import("./x-client-3MVINKNK.js");
187
+ const { flushQueue } = await import("./queue-DJZARE2U.js");
188
+ const { buildSystemPrompt, buildHeartbeatUserMessage } = await import("./prompt-builder-6KC42BML.js");
189
+ const { generateResponse } = await import("./llm-2CDBHU2Q.js");
190
+ const { parseActions, executeActions } = await import("./decision-engine-6WCHQU2A.js");
191
+ const config = loadConfig();
192
+ const intervalMs = config.runtime?.heartbeatIntervalMs ?? 3e5;
193
+ const maxActions = config.runtime?.actionsPerHeartbeat ?? 3;
194
+ let heartbeatCount = 0;
195
+ console.log(chalk.cyan(` [Agent] Autonomous heartbeat started (every ${Math.round(intervalMs / 6e4)} min)`));
196
+ server.pushActivity("status", `Agent online. Heartbeat every ${Math.round(intervalMs / 6e4)} minutes.`);
197
+ while (true) {
198
+ const jitter = Math.floor(Math.random() * intervalMs * 0.3);
199
+ const sleepMs = heartbeatCount === 0 ? 1e4 : intervalMs + jitter;
200
+ await new Promise((r) => setTimeout(r, sleepMs));
201
+ heartbeatCount++;
202
+ console.log(chalk.cyan(` [Agent] Heartbeat #${heartbeatCount}`));
203
+ server.pushActivity("heartbeat", `Heartbeat #${heartbeatCount} \u2014 waking up...`);
204
+ try {
205
+ try {
206
+ const flushed = await flushQueue();
207
+ if (flushed.posted > 0) {
208
+ server.pushActivity("action", `Flushed ${flushed.posted} queued post(s)`);
209
+ }
210
+ } catch {
211
+ }
212
+ server.pushActivity("status", "Reading timeline and mentions...");
213
+ const client = await getXClient();
214
+ let timeline = [];
215
+ let mentions = [];
216
+ try {
217
+ timeline = await client.getTimeline({ count: 20 });
218
+ } catch (error) {
219
+ console.log(chalk.dim(` [Agent] Timeline read failed: ${error.message}`));
220
+ }
221
+ try {
222
+ mentions = await client.getMentions({ count: 10 });
223
+ } catch (error) {
224
+ console.log(chalk.dim(` [Agent] Mentions read failed: ${error.message}`));
225
+ }
226
+ server.pushActivity("status", `Read ${timeline.length} timeline posts, ${mentions.length} mentions`);
227
+ const systemPrompt = buildSystemPrompt();
228
+ const userMessage = buildHeartbeatUserMessage(timeline, mentions);
229
+ server.pushActivity("status", "Thinking about what to do...");
230
+ const response = await generateResponse(systemPrompt, userMessage);
231
+ const actions = parseActions(response.content);
232
+ if (actions.length === 0) {
233
+ server.pushActivity("status", "Nothing to do this cycle. Resting...");
234
+ console.log(chalk.dim(` [Agent] No actions this heartbeat`));
235
+ continue;
236
+ }
237
+ const limitedActions = actions.slice(0, maxActions);
238
+ for (const action of limitedActions) {
239
+ const desc = action.action === "post" ? `Posting: "${(action.content ?? "").slice(0, 80)}..."` : action.action === "reply" ? `Replying to ${action.tweetId}: "${(action.content ?? "").slice(0, 60)}..."` : action.action === "like" ? `Liking tweet ${action.tweetId}` : action.action === "retweet" ? `Retweeting ${action.tweetId}` : action.action === "follow" ? `Following @${action.handle}` : action.action === "skip" ? `Skipping: ${action.reason || action.reasoning || "resting"}` : `${action.action}: ${action.content?.slice(0, 60) || ""}`;
240
+ server.pushActivity("action", desc);
241
+ }
242
+ const results = await executeActions(limitedActions);
243
+ for (const result of results) {
244
+ if (result.success) {
245
+ server.pushActivity("action", `\u2713 ${result.action}${result.detail ? `: ${result.detail}` : ""}`);
246
+ console.log(chalk.green(` [Agent] \u2713 ${result.action}`));
247
+ } else {
248
+ server.pushActivity("action", `\u2717 ${result.action}: ${result.error}`);
249
+ console.log(chalk.red(` [Agent] \u2717 ${result.action}: ${result.error}`));
250
+ }
251
+ }
252
+ const succeeded = results.filter((r) => r.success).length;
253
+ server.pushActivity("heartbeat", `Heartbeat #${heartbeatCount} done. ${succeeded}/${results.length} actions succeeded.`);
254
+ } catch (error) {
255
+ const msg = error.message;
256
+ console.error(chalk.red(` [Agent] Heartbeat #${heartbeatCount} error: ${msg}`));
257
+ server.pushActivity("status", `Heartbeat error: ${msg}`);
258
+ }
259
+ }
260
+ }
169
261
  async function startWebChat() {
170
262
  const identity = loadIdentity();
171
263
  const server = new WebChatServer();
@@ -209,6 +301,19 @@ async function startWebChat() {
209
301
  console.log(chalk.dim(`Press Ctrl+C to stop the server
210
302
  `));
211
303
  openBrowser(url);
304
+ try {
305
+ const { hasLLMKey } = await import("./llm-2CDBHU2Q.js");
306
+ if (hasLLMKey()) {
307
+ runNarratedHeartbeat(server).catch((err) => {
308
+ console.error(chalk.red(`Heartbeat failed to start: ${err}`));
309
+ });
310
+ } else {
311
+ console.log(chalk.yellow(" [Agent] No LLM key \u2014 autonomous heartbeat disabled. Run `spora set-llm-key` to enable."));
312
+ server.pushActivity("status", "No LLM API key configured. Autonomous mode disabled.");
313
+ }
314
+ } catch (err) {
315
+ console.error(chalk.red(`Heartbeat failed to start: ${err}`));
316
+ }
212
317
  process.on("SIGINT", () => {
213
318
  console.log(chalk.yellow("\n\nStopping chat server..."));
214
319
  server.stop();
@@ -250,4 +355,4 @@ export {
250
355
  openBrowser,
251
356
  startWebChat
252
357
  };
253
- //# sourceMappingURL=web-chat-3XTQGVOY.js.map
358
+ //# sourceMappingURL=web-chat-OH3YXPJO.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 AgentActivity {\n type: \"heartbeat\" | \"action\" | \"status\";\n message: string;\n timestamp: number;\n}\n\ninterface AgentIdentity {\n name: string;\n handle: string;\n bio?: string;\n profileImage?: string;\n}\n\nexport class WebChatServer {\n private server: http.Server | null = null;\n private port: number;\n private messages: ChatMessage[] = [];\n private activities: AgentActivity[] = [];\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 /** Push an activity update (from heartbeat) to the UI */\n pushActivity(type: AgentActivity[\"type\"], message: string) {\n this.activities.push({ type, message, timestamp: Date.now() });\n // Keep last 100 activities\n if (this.activities.length > 100) {\n this.activities = this.activities.slice(-100);\n }\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 // 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\n if (url.pathname === \"/api/messages\" && req.method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ messages: this.messages }));\n return;\n }\n\n // API: Get agent activity feed (poll from UI)\n if (url.pathname === \"/api/activity\" && req.method === \"GET\") {\n const since = parseInt(url.searchParams.get(\"since\") || \"0\", 10);\n const newActivities = this.activities.filter((a) => a.timestamp > since);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ activities: newActivities }));\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 stop() {\n if (this.server) {\n this.server.close();\n this.server = null;\n }\n }\n}\n","/**\n * Web chat integration with autonomous heartbeat\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 * 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\n/**\n * Run the autonomous heartbeat in the background, narrating to the chat UI\n */\nasync function runNarratedHeartbeat(server: WebChatServer): Promise<void> {\n const { loadConfig } = await import(\"../utils/config.js\");\n const { getXClient } = await import(\"../x-client/index.js\");\n const { flushQueue } = await import(\"../scheduler/queue.js\");\n const { buildSystemPrompt, buildHeartbeatUserMessage } = 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\n const config = loadConfig();\n const intervalMs = config.runtime?.heartbeatIntervalMs ?? 300_000;\n const maxActions = config.runtime?.actionsPerHeartbeat ?? 3;\n\n let heartbeatCount = 0;\n\n console.log(chalk.cyan(` [Agent] Autonomous heartbeat started (every ${Math.round(intervalMs / 60_000)} min)`));\n server.pushActivity(\"status\", `Agent online. Heartbeat every ${Math.round(intervalMs / 60_000)} minutes.`);\n\n while (true) {\n // Sleep first (let chat start up), then heartbeat\n const jitter = Math.floor(Math.random() * intervalMs * 0.3);\n const sleepMs = heartbeatCount === 0 ? 10_000 : intervalMs + jitter; // First heartbeat after 10s\n await new Promise((r) => setTimeout(r, sleepMs));\n\n heartbeatCount++;\n console.log(chalk.cyan(` [Agent] Heartbeat #${heartbeatCount}`));\n server.pushActivity(\"heartbeat\", `Heartbeat #${heartbeatCount} — waking up...`);\n\n try {\n // 1. Flush queue\n try {\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n server.pushActivity(\"action\", `Flushed ${flushed.posted} queued post(s)`);\n }\n } catch {\n // Queue flush is best-effort\n }\n\n // 2. Read timeline and mentions\n server.pushActivity(\"status\", \"Reading timeline and mentions...\");\n const client = await getXClient();\n\n let timeline: Awaited<ReturnType<typeof client.getTimeline>> = [];\n let mentions: Awaited<ReturnType<typeof client.getMentions>> = [];\n\n try {\n timeline = await client.getTimeline({ count: 20 });\n } catch (error) {\n console.log(chalk.dim(` [Agent] Timeline read failed: ${(error as Error).message}`));\n }\n\n try {\n mentions = await client.getMentions({ count: 10 });\n } catch (error) {\n console.log(chalk.dim(` [Agent] Mentions read failed: ${(error as Error).message}`));\n }\n\n server.pushActivity(\"status\", `Read ${timeline.length} timeline posts, ${mentions.length} mentions`);\n\n // 3. Build prompts and ask LLM\n const systemPrompt = buildSystemPrompt();\n const userMessage = buildHeartbeatUserMessage(timeline, mentions);\n\n server.pushActivity(\"status\", \"Thinking about what to do...\");\n const response = await generateResponse(systemPrompt, userMessage);\n\n // 4. Parse and execute actions\n const actions = parseActions(response.content);\n if (actions.length === 0) {\n server.pushActivity(\"status\", \"Nothing to do this cycle. Resting...\");\n console.log(chalk.dim(` [Agent] No actions this heartbeat`));\n continue;\n }\n\n const limitedActions = actions.slice(0, maxActions);\n for (const action of limitedActions) {\n const desc = action.action === \"post\"\n ? `Posting: \"${(action.content ?? \"\").slice(0, 80)}...\"`\n : action.action === \"reply\"\n ? `Replying to ${action.tweetId}: \"${(action.content ?? \"\").slice(0, 60)}...\"`\n : action.action === \"like\"\n ? `Liking tweet ${action.tweetId}`\n : action.action === \"retweet\"\n ? `Retweeting ${action.tweetId}`\n : action.action === \"follow\"\n ? `Following @${action.handle}`\n : action.action === \"skip\"\n ? `Skipping: ${action.reason || action.reasoning || \"resting\"}`\n : `${action.action}: ${action.content?.slice(0, 60) || \"\"}`;\n\n server.pushActivity(\"action\", desc);\n }\n\n const results = await executeActions(limitedActions);\n\n // 5. Report results\n for (const result of results) {\n if (result.success) {\n server.pushActivity(\"action\", `✓ ${result.action}${result.detail ? `: ${result.detail}` : \"\"}`);\n console.log(chalk.green(` [Agent] ✓ ${result.action}`));\n } else {\n server.pushActivity(\"action\", `✗ ${result.action}: ${result.error}`);\n console.log(chalk.red(` [Agent] ✗ ${result.action}: ${result.error}`));\n }\n }\n\n const succeeded = results.filter((r) => r.success).length;\n server.pushActivity(\"heartbeat\", `Heartbeat #${heartbeatCount} done. ${succeeded}/${results.length} actions succeeded.`);\n\n } catch (error) {\n const msg = (error as Error).message;\n console.error(chalk.red(` [Agent] Heartbeat #${heartbeatCount} error: ${msg}`));\n server.pushActivity(\"status\", `Heartbeat error: ${msg}`);\n }\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 });\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();\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 learnings from response and clean the text\n const cleanResponse = await extractAndSaveLearnings(response.content);\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 autonomous heartbeat in background\n try {\n const { hasLLMKey } = await import(\"../runtime/llm.js\");\n if (hasLLMKey()) {\n runNarratedHeartbeat(server).catch((err) => {\n console.error(chalk.red(`Heartbeat failed to start: ${err}`));\n });\n } else {\n console.log(chalk.yellow(\" [Agent] No LLM key — autonomous heartbeat disabled. Run `spora set-llm-key` to enable.\"));\n server.pushActivity(\"status\", \"No LLM API key configured. Autonomous mode disabled.\");\n }\n } 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 server.stop();\n process.exit(0);\n });\n\n // Return the server instance for external control\n return server;\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=500,700`, { 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=500,700`, { 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=500,700`, { 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=500,700`, { 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":";;;;;;AAKA,OAAO,UAAU;AACjB,SAAS,WAAW;AACpB,SAAS,oBAAoB;AAC7B,SAAS,MAAM,eAAe;AAC9B,SAAS,qBAAqB;AAE9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,QAAQ,UAAU;AAqB7B,IAAM,gBAAN,MAAoB;AAAA,EACjB,SAA6B;AAAA,EAC7B;AAAA,EACA,WAA0B,CAAC;AAAA,EAC3B,aAA8B,CAAC;AAAA,EAC/B;AAAA,EACA;AAAA,EAER,YAAY,OAAO,MAAM;AACvB,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,YAAY,UAAyB;AACnC,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,kBAAkB,SAA+C;AAC/D,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA,EAGA,aAAa,MAA6B,SAAiB;AACzD,SAAK,WAAW,KAAK,EAAE,MAAM,SAAS,WAAW,KAAK,IAAI,EAAE,CAAC;AAE7D,QAAI,KAAK,WAAW,SAAS,KAAK;AAChC,WAAK,aAAa,KAAK,WAAW,MAAM,IAAI;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,QAAyB;AAC7B,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAK,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AAClD,cAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,IAAI,EAAE;AAGhE,YAAI,UAAU,+BAA+B,GAAG;AAChD,YAAI,UAAU,gCAAgC,oBAAoB;AAClE,YAAI,UAAU,gCAAgC,cAAc;AAE5D,YAAI,IAAI,WAAW,WAAW;AAC5B,cAAI,UAAU,GAAG;AACjB,cAAI,IAAI;AACR;AAAA,QACF;AAGA,YAAI,IAAI,aAAa,OAAO,IAAI,aAAa,eAAe;AAC1D,cAAI;AAEF,kBAAM,gBAAgB;AAAA,cACpB,KAAK,WAAW,YAAY,WAAW;AAAA,cACvC,KAAK,WAAW,WAAW;AAAA,cAC3B,KAAK,WAAW,MAAM,YAAY,WAAW;AAAA,cAC7C,KAAK,WAAW,MAAM,OAAO,YAAY,WAAW;AAAA,YACtD;AAEA,gBAAI,OAAsB;AAC1B,uBAAW,KAAK,eAAe;AAC7B,kBAAI;AACF,uBAAO,aAAa,GAAG,OAAO;AAC9B;AAAA,cACF,QAAQ;AACN;AAAA,cACF;AAAA,YACF;AAEA,gBAAI,MAAM;AACR,kBAAI,UAAU,KAAK,EAAE,gBAAgB,YAAY,CAAC;AAClD,kBAAI,IAAI,IAAI;AAAA,YACd,OAAO;AACL,sBAAQ,MAAM,uCAAuC,aAAa;AAClE,kBAAI,UAAU,GAAG;AACjB,kBAAI,IAAI,iCAAiC;AAAA,YAC3C;AAAA,UACF,SAAS,OAAO;AACd,gBAAI,UAAU,GAAG;AACjB,gBAAI,IAAI,8BAA8B;AAAA,UACxC;AACA;AAAA,QACF;AAGA,YAAI,IAAI,aAAa,mBAAmB,IAAI,WAAW,OAAO;AAC5D,cAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,cAAI,IAAI,KAAK,UAAU,EAAE,UAAU,KAAK,YAAY,KAAK,CAAC,CAAC;AAC3D;AAAA,QACF;AAGA,YAAI,IAAI,aAAa,mBAAmB,IAAI,WAAW,OAAO;AAC5D,cAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,cAAI,IAAI,KAAK,UAAU,EAAE,UAAU,KAAK,SAAS,CAAC,CAAC;AACnD;AAAA,QACF;AAGA,YAAI,IAAI,aAAa,mBAAmB,IAAI,WAAW,OAAO;AAC5D,gBAAM,QAAQ,SAAS,IAAI,aAAa,IAAI,OAAO,KAAK,KAAK,EAAE;AAC/D,gBAAM,gBAAgB,KAAK,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK;AACvE,cAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,cAAI,IAAI,KAAK,UAAU,EAAE,YAAY,cAAc,CAAC,CAAC;AACrD;AAAA,QACF;AAGA,YAAI,IAAI,aAAa,kBAAkB,IAAI,WAAW,QAAQ;AAC5D,cAAI,OAAO;AACX,cAAI,GAAG,QAAQ,CAAC,UAAU;AACxB,oBAAQ,MAAM,SAAS;AAAA,UACzB,CAAC;AAED,cAAI,GAAG,OAAO,YAAY;AACxB,gBAAI;AACF,oBAAM,EAAE,QAAQ,IAAI,KAAK,MAAM,IAAI;AAGnC,mBAAK,SAAS,KAAK;AAAA,gBACjB,MAAM;AAAA,gBACN,SAAS;AAAA,gBACT,WAAW,KAAK,IAAI;AAAA,cACtB,CAAC;AAGD,kBAAI,KAAK,eAAe;AACtB,sBAAM,WAAW,MAAM,KAAK,cAAc,OAAO;AACjD,qBAAK,SAAS,KAAK;AAAA,kBACjB,MAAM;AAAA,kBACN,SAAS;AAAA,kBACT,WAAW,KAAK,IAAI;AAAA,gBACtB,CAAC;AACD,oBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,oBAAI,IAAI,KAAK,UAAU,EAAE,SAAS,MAAM,SAAS,CAAC,CAAC;AAAA,cACrD,OAAO;AACL,oBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,oBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,gCAAgC,CAAC,CAAC;AAAA,cACpE;AAAA,YACF,SAAS,OAAO;AACd,kBAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,kBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,kBAAkB,CAAC,CAAC;AAAA,YACtD;AAAA,UACF,CAAC;AACD;AAAA,QACF;AAGA,YAAI,UAAU,GAAG;AACjB,YAAI,IAAI,WAAW;AAAA,MACrB,CAAC;AAED,WAAK,OAAO,OAAO,KAAK,MAAM,MAAM;AAClC,cAAM,MAAM,oBAAoB,KAAK,IAAI;AACzC,gBAAQ,GAAG;AAAA,MACb,CAAC;AAED,WAAK,OAAO,GAAG,SAAS,CAAC,UAAiC;AACxD,YAAI,MAAM,SAAS,cAAc;AAE/B,eAAK;AACL,eAAK,QAAQ,MAAM;AACnB,eAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM;AAAA,QACzC,OAAO;AACL,iBAAO,KAAK;AAAA,QACd;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,OAAO;AACL,QAAI,KAAK,QAAQ;AACf,WAAK,OAAO,MAAM;AAClB,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AACF;;;ACxMA,SAAS,gBAAgB;AACzB,OAAO,WAAW;AAKlB,eAAe,wBAAwB,cAAuC;AAC5E,QAAM,eAAe;AACrB,QAAM,UAAU,CAAC,GAAG,aAAa,SAAS,YAAY,CAAC;AAEvD,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,EAAE,YAAY,IAAI,MAAM,OAAO,sBAAoB;AACzD,eAAW,SAAS,SAAS;AAC3B,YAAM,WAAW,MAAM,CAAC,EAAE,KAAK;AAC/B,kBAAY,UAAU,YAAY,CAAC,QAAQ,qBAAqB,CAAC;AACjE,cAAQ,IAAI,MAAM,IAAI,8BAA8B,QAAQ,EAAE,CAAC;AAAA,IACjE;AAAA,EACF;AAGA,SAAO,aAAa,QAAQ,qBAAqB,EAAE,EAAE,KAAK;AAC5D;AAKA,eAAe,mBAAmB,aAAqB,eAAsC;AAC3F,QAAM,EAAE,eAAe,IAAI,MAAM,OAAO,sBAAoB;AAC5D,iBAAe;AAAA,IACb,IAAI,QAAQ,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,IAChE,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,MAAM;AAAA,IACN,SAAS,cAAc,MAAM,GAAG,GAAG;AAAA,IACnC,cAAc;AAAA,IACd,aAAa;AAAA,IACb,SAAS;AAAA,EACX,CAAC;AACH;AAKA,eAAe,qBAAqB,QAAsC;AACxE,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,sBAAoB;AACxD,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,wBAAsB;AAC1D,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,qBAAuB;AAC3D,QAAM,EAAE,mBAAmB,0BAA0B,IAAI,MAAM,OAAO,8BAA8B;AACpG,QAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,mBAAmB;AAC7D,QAAM,EAAE,cAAc,eAAe,IAAI,MAAM,OAAO,+BAA+B;AAErF,QAAM,SAAS,WAAW;AAC1B,QAAM,aAAa,OAAO,SAAS,uBAAuB;AAC1D,QAAM,aAAa,OAAO,SAAS,uBAAuB;AAE1D,MAAI,iBAAiB;AAErB,UAAQ,IAAI,MAAM,KAAK,iDAAiD,KAAK,MAAM,aAAa,GAAM,CAAC,OAAO,CAAC;AAC/G,SAAO,aAAa,UAAU,iCAAiC,KAAK,MAAM,aAAa,GAAM,CAAC,WAAW;AAEzG,SAAO,MAAM;AAEX,UAAM,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,aAAa,GAAG;AAC1D,UAAM,UAAU,mBAAmB,IAAI,MAAS,aAAa;AAC7D,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAE/C;AACA,YAAQ,IAAI,MAAM,KAAK,wBAAwB,cAAc,EAAE,CAAC;AAChE,WAAO,aAAa,aAAa,cAAc,cAAc,sBAAiB;AAE9E,QAAI;AAEF,UAAI;AACF,cAAM,UAAU,MAAM,WAAW;AACjC,YAAI,QAAQ,SAAS,GAAG;AACtB,iBAAO,aAAa,UAAU,WAAW,QAAQ,MAAM,iBAAiB;AAAA,QAC1E;AAAA,MACF,QAAQ;AAAA,MAER;AAGA,aAAO,aAAa,UAAU,kCAAkC;AAChE,YAAM,SAAS,MAAM,WAAW;AAEhC,UAAI,WAA2D,CAAC;AAChE,UAAI,WAA2D,CAAC;AAEhE,UAAI;AACF,mBAAW,MAAM,OAAO,YAAY,EAAE,OAAO,GAAG,CAAC;AAAA,MACnD,SAAS,OAAO;AACd,gBAAQ,IAAI,MAAM,IAAI,mCAAoC,MAAgB,OAAO,EAAE,CAAC;AAAA,MACtF;AAEA,UAAI;AACF,mBAAW,MAAM,OAAO,YAAY,EAAE,OAAO,GAAG,CAAC;AAAA,MACnD,SAAS,OAAO;AACd,gBAAQ,IAAI,MAAM,IAAI,mCAAoC,MAAgB,OAAO,EAAE,CAAC;AAAA,MACtF;AAEA,aAAO,aAAa,UAAU,QAAQ,SAAS,MAAM,oBAAoB,SAAS,MAAM,WAAW;AAGnG,YAAM,eAAe,kBAAkB;AACvC,YAAM,cAAc,0BAA0B,UAAU,QAAQ;AAEhE,aAAO,aAAa,UAAU,8BAA8B;AAC5D,YAAM,WAAW,MAAM,iBAAiB,cAAc,WAAW;AAGjE,YAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,UAAI,QAAQ,WAAW,GAAG;AACxB,eAAO,aAAa,UAAU,sCAAsC;AACpE,gBAAQ,IAAI,MAAM,IAAI,qCAAqC,CAAC;AAC5D;AAAA,MACF;AAEA,YAAM,iBAAiB,QAAQ,MAAM,GAAG,UAAU;AAClD,iBAAW,UAAU,gBAAgB;AACnC,cAAM,OAAO,OAAO,WAAW,SAC3B,cAAc,OAAO,WAAW,IAAI,MAAM,GAAG,EAAE,CAAC,SAChD,OAAO,WAAW,UAClB,eAAe,OAAO,OAAO,OAAO,OAAO,WAAW,IAAI,MAAM,GAAG,EAAE,CAAC,SACtE,OAAO,WAAW,SAClB,gBAAgB,OAAO,OAAO,KAC9B,OAAO,WAAW,YAClB,cAAc,OAAO,OAAO,KAC5B,OAAO,WAAW,WAClB,cAAc,OAAO,MAAM,KAC3B,OAAO,WAAW,SAClB,aAAa,OAAO,UAAU,OAAO,aAAa,SAAS,KAC3D,GAAG,OAAO,MAAM,KAAK,OAAO,SAAS,MAAM,GAAG,EAAE,KAAK,EAAE;AAE3D,eAAO,aAAa,UAAU,IAAI;AAAA,MACpC;AAEA,YAAM,UAAU,MAAM,eAAe,cAAc;AAGnD,iBAAW,UAAU,SAAS;AAC5B,YAAI,OAAO,SAAS;AAClB,iBAAO,aAAa,UAAU,UAAK,OAAO,MAAM,GAAG,OAAO,SAAS,KAAK,OAAO,MAAM,KAAK,EAAE,EAAE;AAC9F,kBAAQ,IAAI,MAAM,MAAM,oBAAe,OAAO,MAAM,EAAE,CAAC;AAAA,QACzD,OAAO;AACL,iBAAO,aAAa,UAAU,UAAK,OAAO,MAAM,KAAK,OAAO,KAAK,EAAE;AACnE,kBAAQ,IAAI,MAAM,IAAI,oBAAe,OAAO,MAAM,KAAK,OAAO,KAAK,EAAE,CAAC;AAAA,QACxE;AAAA,MACF;AAEA,YAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AACnD,aAAO,aAAa,aAAa,cAAc,cAAc,UAAU,SAAS,IAAI,QAAQ,MAAM,qBAAqB;AAAA,IAEzH,SAAS,OAAO;AACd,YAAM,MAAO,MAAgB;AAC7B,cAAQ,MAAM,MAAM,IAAI,wBAAwB,cAAc,WAAW,GAAG,EAAE,CAAC;AAC/E,aAAO,aAAa,UAAU,oBAAoB,GAAG,EAAE;AAAA,IACzD;AAAA,EACF;AACF;AAEA,eAAsB,eAAe;AACnC,QAAM,WAAW,aAAa;AAE9B,QAAM,SAAS,IAAI,cAAc;AAGjC,SAAO,YAAY;AAAA,IACjB,MAAM,SAAS;AAAA,IACf,QAAQ,SAAS;AAAA,IACjB,KAAK,SAAS;AAAA,IACd,cAAc,SAAS;AAAA,EACzB,CAAC;AAGD,QAAM,cAAsE,CAAC;AAC7E,MAAI,eAA8B;AAClC,MAAI,eAAe;AAEnB,SAAO,kBAAkB,OAAO,YAAoB;AAClD,QAAI;AAEF,UAAI,CAAC,gBAAgB,eAAe,OAAO,GAAG;AAC5C,cAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,8BAA8B;AACvE,uBAAe,gBAAgB;AAAA,MACjC;AACA;AAGA,YAAM,EAAE,WAAW,MAAM,QAAQ,IAAI,MAAM,OAAO,mBAAmB;AACrE,UAAI,CAAC,UAAU,GAAG;AAChB,eAAO;AAAA,MACT;AAGA,kBAAY,KAAK,EAAE,MAAM,QAAQ,SAAS,QAAQ,CAAC;AAGnD,YAAM,WAAW,MAAM,QAAQ,cAAc,WAAW;AAGxD,YAAM,gBAAgB,MAAM,wBAAwB,SAAS,OAAO;AAGpE,kBAAY,KAAK,EAAE,MAAM,aAAa,SAAS,cAAc,CAAC;AAG9D,yBAAmB,SAAS,aAAa,EAAE;AAAA,QAAM,CAAC,QAChD,QAAQ,MAAM,MAAM,IAAI,uCAAuC,GAAG,GAAG;AAAA,MACvE;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,cAAQ,MAAM,eAAe,KAAK;AAClC,aAAO,+BAAgC,MAAgB,OAAO;AAAA,IAChE;AAAA,EACF,CAAC;AAED,QAAM,MAAM,MAAM,OAAO,MAAM;AAE/B,UAAQ,IAAI,MAAM,MAAM;AAAA,mCAAiC,MAAM,KAAK,GAAG,CAAC;AAAA,CAAI,CAAC;AAC7E,UAAQ,IAAI,MAAM,IAAI;AAAA,CAAmC,CAAC;AAG1D,cAAY,GAAG;AAGf,MAAI;AACF,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,mBAAmB;AACtD,QAAI,UAAU,GAAG;AACf,2BAAqB,MAAM,EAAE,MAAM,CAAC,QAAQ;AAC1C,gBAAQ,MAAM,MAAM,IAAI,8BAA8B,GAAG,EAAE,CAAC;AAAA,MAC9D,CAAC;AAAA,IACH,OAAO;AACL,cAAQ,IAAI,MAAM,OAAO,+FAA0F,CAAC;AACpH,aAAO,aAAa,UAAU,sDAAsD;AAAA,IACtF;AAAA,EACF,SAAS,KAAK;AACZ,YAAQ,MAAM,MAAM,IAAI,8BAA8B,GAAG,EAAE,CAAC;AAAA,EAC9D;AAGA,UAAQ,GAAG,UAAU,MAAM;AACzB,YAAQ,IAAI,MAAM,OAAO,6BAA6B,CAAC;AACvD,WAAO,KAAK;AACZ,YAAQ,KAAK,CAAC;AAAA,EAChB,CAAC;AAGD,SAAO;AACT;AAKA,SAAS,YAAY,KAAa;AAChC,QAAM,WAAW,QAAQ;AAEzB,MAAI;AACF,QAAI,aAAa,UAAU;AAEzB,UAAI;AACF,iBAAS,0CAA0C,GAAG,2BAA2B,EAAE,OAAO,SAAS,CAAC;AAAA,MACtG,QAAQ;AAEN,YAAI;AACF,mBAAS,0CAA0C,GAAG,2BAA2B,EAAE,OAAO,SAAS,CAAC;AAAA,QACtG,QAAQ;AACN,mBAAS,SAAS,GAAG,KAAK,EAAE,OAAO,SAAS,CAAC;AAAA,QAC/C;AAAA,MACF;AAAA,IACF,WAAW,aAAa,SAAS;AAC/B,UAAI;AACF,iBAAS,uBAAuB,GAAG,2BAA2B,EAAE,OAAO,SAAS,CAAC;AAAA,MACnF,QAAQ;AACN,iBAAS,aAAa,GAAG,KAAK,EAAE,OAAO,SAAS,CAAC;AAAA,MACnD;AAAA,IACF,OAAO;AAEL,UAAI;AACF,iBAAS,wBAAwB,GAAG,2BAA2B,EAAE,OAAO,SAAS,CAAC;AAAA,MACpF,QAAQ;AACN,iBAAS,aAAa,GAAG,KAAK,EAAE,OAAO,SAAS,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AAEd,YAAQ,IAAI,MAAM,IAAI,uDAAuD,GAAG,YAAY,CAAC;AAAA,EAC/F;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spora",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
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/heartbeat.ts","../src/runtime/decision-engine.ts"],"sourcesContent":["import { existsSync, unlinkSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { logger } from \"../utils/logger.js\";\nimport { loadConfig } from \"../utils/config.js\";\nimport { paths } from \"../utils/paths.js\";\nimport { getXClient } from \"../x-client/index.js\";\nimport { flushQueue } from \"../scheduler/queue.js\";\nimport { buildSystemPrompt, buildHeartbeatUserMessage } from \"./prompt-builder.js\";\nimport { generateResponse } from \"./llm.js\";\nimport { parseActions, executeActions, type ActionResult } from \"./decision-engine.js\";\n\nlet running = false;\n\nexport function isRunning(): boolean {\n return running;\n}\n\nexport function requestStop(): void {\n writeFileSync(paths.stopSignal, \"stop\");\n logger.info(\"Stop signal sent.\");\n}\n\nfunction shouldStop(): boolean {\n if (existsSync(paths.stopSignal)) {\n unlinkSync(paths.stopSignal);\n return true;\n }\n return false;\n}\n\nfunction writePid(): void {\n writeFileSync(paths.runtimePid, String(process.pid));\n}\n\nfunction clearPid(): void {\n if (existsSync(paths.runtimePid)) {\n unlinkSync(paths.runtimePid);\n }\n}\n\nexport function getRunningPid(): number | null {\n if (!existsSync(paths.runtimePid)) return null;\n const pid = parseInt(readFileSync(paths.runtimePid, \"utf-8\").trim(), 10);\n if (isNaN(pid)) return null;\n\n // Check if process is actually running\n try {\n process.kill(pid, 0);\n return pid;\n } catch {\n // Process not running, clean up stale PID\n clearPid();\n return null;\n }\n}\n\nexport async function startHeartbeatLoop(): Promise<void> {\n // Check if already running\n const existingPid = getRunningPid();\n if (existingPid) {\n throw new Error(`Spora is already running (PID ${existingPid}). Run \\`spora stop\\` first.`);\n }\n\n running = true;\n writePid();\n\n const config = loadConfig();\n const intervalMs = config.runtime?.heartbeatIntervalMs ?? 300_000;\n const maxActions = config.runtime?.actionsPerHeartbeat ?? 3;\n\n logger.info(`Spora agent starting. Heartbeat interval: ${intervalMs / 1000}s, max actions: ${maxActions}`);\n console.log(`\\nSpora agent is running (PID ${process.pid})`);\n console.log(`Heartbeat every ${Math.round(intervalMs / 60_000)} minutes`);\n console.log(`Press Ctrl+C or run \\`spora stop\\` to stop.\\n`);\n\n // Handle graceful shutdown\n const shutdown = () => {\n logger.info(\"Shutting down...\");\n running = false;\n clearPid();\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n // Clean any stale stop signal\n if (existsSync(paths.stopSignal)) {\n unlinkSync(paths.stopSignal);\n }\n\n let heartbeatCount = 0;\n\n while (running) {\n heartbeatCount++;\n logger.info(`=== Heartbeat #${heartbeatCount} ===`);\n\n try {\n await runHeartbeat(maxActions);\n } catch (error) {\n logger.error(\"Heartbeat error\", error);\n console.error(`Heartbeat #${heartbeatCount} failed: ${(error as Error).message}`);\n }\n\n // Check for stop signal\n if (shouldStop()) {\n logger.info(\"Stop signal received.\");\n break;\n }\n\n // Sleep with jitter\n const jitter = Math.floor(Math.random() * intervalMs * 0.3);\n const sleepMs = intervalMs + jitter;\n logger.info(`Sleeping ${Math.round(sleepMs / 1000)}s until next heartbeat...`);\n\n // Sleep in chunks so we can check for stop signals\n const chunkMs = 10_000;\n let slept = 0;\n while (slept < sleepMs && running) {\n await new Promise((r) => setTimeout(r, Math.min(chunkMs, sleepMs - slept)));\n slept += chunkMs;\n if (shouldStop()) {\n running = false;\n break;\n }\n }\n }\n\n clearPid();\n logger.info(\"Spora agent stopped.\");\n console.log(\"\\nSpora agent stopped.\");\n}\n\nasync function runHeartbeat(maxActions: number): Promise<void> {\n // 1. Flush any queued posts\n logger.info(\"Checking queue...\");\n try {\n const flushed = await flushQueue();\n if (flushed.posted > 0) {\n logger.info(`Flushed ${flushed.posted} queued posts.`);\n }\n } catch (error) {\n logger.warn(`Queue flush failed: ${(error as Error).message}`);\n }\n\n // 2. Read timeline and mentions for context\n logger.info(\"Reading timeline and mentions...\");\n const client = await getXClient();\n\n let timeline: Awaited<ReturnType<typeof client.getTimeline>> = [];\n let mentions: Awaited<ReturnType<typeof client.getMentions>> = [];\n\n try {\n timeline = await client.getTimeline({ count: 20 });\n } catch (error) {\n logger.warn(`Timeline read failed: ${(error as Error).message}`);\n }\n\n try {\n mentions = await client.getMentions({ count: 10 });\n } catch (error) {\n logger.warn(`Mentions read failed: ${(error as Error).message}`);\n }\n\n // 3. Build prompts\n const systemPrompt = buildSystemPrompt();\n const userMessage = buildHeartbeatUserMessage(timeline, mentions);\n\n // 4. Ask LLM for decisions\n logger.info(\"Asking LLM for decisions...\");\n const response = await generateResponse(systemPrompt, userMessage);\n\n // 5. Parse and execute actions\n const actions = parseActions(response.content);\n if (actions.length === 0) {\n logger.info(\"LLM returned no actions.\");\n return;\n }\n\n // Limit to max actions per heartbeat\n const limitedActions = actions.slice(0, maxActions);\n logger.info(`Executing ${limitedActions.length} action(s)...`);\n\n const results = await executeActions(limitedActions);\n\n // 6. Log results\n for (const result of results) {\n if (result.success) {\n logger.info(` [OK] ${result.action}${result.detail ? `: ${result.detail}` : \"\"}`);\n } else {\n logger.warn(` [FAIL] ${result.action}: ${result.error}`);\n }\n }\n\n logger.info(`Heartbeat complete. ${results.filter((r) => r.success).length}/${results.length} actions succeeded.`);\n}\n","import { logger } from \"../utils/logger.js\";\nimport { getXClient } from \"../x-client/index.js\";\nimport { logInteraction, addLearning } from \"../memory/index.js\";\nimport { loadIdentity, saveIdentity } from \"../identity/index.js\";\nimport { addToQueue } from \"../scheduler/queue.js\";\nimport { rateLimiter } from \"../x-client/rate-limiter.js\";\n\nexport interface AgentAction {\n action: string;\n content?: string;\n tweetId?: string;\n handle?: string;\n tags?: string[];\n reason?: string;\n reasoning?: string;\n}\n\nexport interface ActionResult {\n action: string;\n success: boolean;\n detail?: string;\n error?: string;\n}\n\nexport function parseActions(llmResponse: string): AgentAction[] {\n // Extract JSON array from the response (may be wrapped in markdown code blocks)\n const jsonMatch = llmResponse.match(/\\[[\\s\\S]*?\\]/);\n if (!jsonMatch) {\n // Try to parse as a single action object\n const objMatch = llmResponse.match(/\\{[\\s\\S]*?\\}/);\n if (objMatch) {\n try {\n return [JSON.parse(objMatch[0]) as AgentAction];\n } catch {\n logger.warn(\"Could not parse LLM response as action object\");\n return [];\n }\n }\n logger.warn(\"No JSON found in LLM response\");\n return [];\n }\n\n try {\n const actions = JSON.parse(jsonMatch[0]) as AgentAction[];\n return Array.isArray(actions) ? actions : [actions];\n } catch {\n logger.warn(\"Failed to parse actions JSON from LLM response\");\n return [];\n }\n}\n\nexport async function executeAction(action: AgentAction): Promise<ActionResult> {\n const { action: type } = action;\n\n try {\n switch (type) {\n case \"post\": {\n if (!action.content) return { action: type, success: false, error: \"No content provided\" };\n if (action.content.length > 280) {\n return { action: type, success: false, error: `Tweet too long: ${action.content.length} chars (max 280)` };\n }\n if (!rateLimiter.canPost()) {\n return { action: type, success: false, error: \"No credits remaining this month\" };\n }\n\n const client = await getXClient();\n const result = await client.postTweet(action.content);\n if (result.success) {\n rateLimiter.consume(1);\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"post\",\n tweetId: result.tweetId,\n content: action.content,\n creditsUsed: 1,\n success: true,\n });\n logger.info(`Posted: \"${action.content.slice(0, 50)}...\"`);\n }\n return { action: type, success: result.success, detail: result.tweetId, error: result.error };\n }\n\n case \"reply\": {\n if (!action.tweetId || !action.content) {\n return { action: type, success: false, error: \"Missing tweetId or content\" };\n }\n if (!rateLimiter.canPost()) {\n return { action: type, success: false, error: \"No credits remaining\" };\n }\n\n const client = await getXClient();\n const result = await client.replyToTweet(action.tweetId, action.content);\n if (result.success) {\n rateLimiter.consume(1);\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"reply\",\n tweetId: result.tweetId,\n inReplyTo: action.tweetId,\n content: action.content,\n creditsUsed: 1,\n success: true,\n });\n logger.info(`Replied to ${action.tweetId}: \"${action.content.slice(0, 50)}...\"`);\n }\n return { action: type, success: result.success, detail: result.tweetId, error: result.error };\n }\n\n case \"like\": {\n if (!action.tweetId) return { action: type, success: false, error: \"Missing tweetId\" };\n const client = await getXClient();\n const result = await client.likeTweet(action.tweetId);\n if (result.success) {\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"like\",\n tweetId: action.tweetId,\n creditsUsed: 0,\n success: true,\n });\n }\n return { action: type, success: result.success, error: result.error };\n }\n\n case \"retweet\": {\n if (!action.tweetId) return { action: type, success: false, error: \"Missing tweetId\" };\n const client = await getXClient();\n const result = await client.retweet(action.tweetId);\n if (result.success) {\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"retweet\",\n tweetId: action.tweetId,\n creditsUsed: 0,\n success: true,\n });\n }\n return { action: type, success: result.success, error: result.error };\n }\n\n case \"follow\": {\n if (!action.handle) return { action: type, success: false, error: \"Missing handle\" };\n const client = await getXClient();\n const result = await client.followUser(action.handle);\n if (result.success) {\n logInteraction({\n id: `int-${Date.now()}`,\n timestamp: new Date().toISOString(),\n type: \"follow\",\n targetHandle: action.handle,\n creditsUsed: 0,\n success: true,\n });\n }\n return { action: type, success: result.success, error: result.error };\n }\n\n case \"schedule\": {\n if (!action.content) return { action: type, success: false, error: \"No content\" };\n const entry = addToQueue(action.content);\n logger.info(`Scheduled: \"${action.content.slice(0, 50)}...\" for ${entry.scheduledFor}`);\n return { action: type, success: true, detail: `Scheduled for ${entry.scheduledFor}` };\n }\n\n case \"learn\": {\n if (!action.content) return { action: type, success: false, error: \"No content\" };\n addLearning(action.content, \"agent\", action.tags ?? [\"heartbeat\"]);\n logger.info(`Learned: \"${action.content.slice(0, 50)}...\"`);\n return { action: type, success: true };\n }\n\n case \"reflect\": {\n if (!action.content) return { action: type, success: false, error: \"No content\" };\n const identity = loadIdentity();\n identity.evolutionJournal.push({\n date: new Date().toISOString(),\n reflection: action.content,\n });\n saveIdentity(identity);\n logger.info(`Reflected: \"${action.content.slice(0, 50)}...\"`);\n return { action: type, success: true };\n }\n\n case \"skip\": {\n logger.info(`Skipping: ${action.reason ?? action.reasoning ?? \"no reason given\"}`);\n return { action: type, success: true, detail: action.reason ?? action.reasoning };\n }\n\n default:\n logger.warn(`Unknown action: ${type}`);\n return { action: type, success: false, error: `Unknown action: ${type}` };\n }\n } catch (error) {\n const msg = (error as Error).message;\n logger.error(`Action ${type} failed: ${msg}`);\n return { action: type, success: false, error: msg };\n }\n}\n\nexport async function executeActions(actions: AgentAction[]): Promise<ActionResult[]> {\n const results: ActionResult[] = [];\n for (const action of actions) {\n const result = await executeAction(action);\n results.push(result);\n // Small delay between actions to be human-like\n await new Promise((r) => setTimeout(r, 2000 + Math.random() * 3000));\n }\n return results;\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}\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 // 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\n if (url.pathname === \"/api/messages\" && req.method === \"GET\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ messages: this.messages }));\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 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 * 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 });\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();\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 learnings from response and clean the text\n const cleanResponse = await extractAndSaveLearnings(response.content);\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 // Keep process alive\n process.on(\"SIGINT\", () => {\n console.log(chalk.yellow(\"\\n\\nStopping chat server...\"));\n server.stop();\n process.exit(0);\n });\n\n // Return the server instance for external control\n return server;\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=500,700`, { 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=500,700`, { 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=500,700`, { 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=500,700`, { 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":[]}