@harbinger-ai/harbinger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +406 -0
  3. package/agents/README.md +76 -0
  4. package/agents/_template/CONFIG.yaml +7 -0
  5. package/agents/_template/HEARTBEAT.md +59 -0
  6. package/agents/_template/IDENTITY.md +4 -0
  7. package/agents/_template/SKILLS.md +1 -0
  8. package/agents/_template/SOUL.md +25 -0
  9. package/agents/_template/TOOLS.md +3 -0
  10. package/agents/binary-reverser/CONFIG.yaml +21 -0
  11. package/agents/binary-reverser/HEARTBEAT.md +65 -0
  12. package/agents/binary-reverser/IDENTITY.md +1 -0
  13. package/agents/binary-reverser/SKILLS.md +1 -0
  14. package/agents/binary-reverser/SOUL.md +23 -0
  15. package/agents/binary-reverser/TOOLS.md +99 -0
  16. package/agents/browser-agent/CONFIG.yaml +20 -0
  17. package/agents/browser-agent/HEARTBEAT.md +79 -0
  18. package/agents/browser-agent/IDENTITY.md +5 -0
  19. package/agents/browser-agent/SKILLS.md +86 -0
  20. package/agents/browser-agent/SOUL.md +23 -0
  21. package/agents/browser-agent/TOOLS.md +186 -0
  22. package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
  23. package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
  24. package/agents/cloud-infiltrator/IDENTITY.md +1 -0
  25. package/agents/cloud-infiltrator/SKILLS.md +1 -0
  26. package/agents/cloud-infiltrator/SOUL.md +23 -0
  27. package/agents/cloud-infiltrator/TOOLS.md +68 -0
  28. package/agents/coding-assistant/CONFIG.yaml +22 -0
  29. package/agents/coding-assistant/HEARTBEAT.md +57 -0
  30. package/agents/coding-assistant/IDENTITY.md +5 -0
  31. package/agents/coding-assistant/SKILLS.md +69 -0
  32. package/agents/coding-assistant/SOUL.md +60 -0
  33. package/agents/coding-assistant/TOOLS.md +168 -0
  34. package/agents/learning-agent/CONFIG.yaml +21 -0
  35. package/agents/learning-agent/HEARTBEAT.md +63 -0
  36. package/agents/learning-agent/IDENTITY.md +5 -0
  37. package/agents/learning-agent/SKILLS.md +86 -0
  38. package/agents/learning-agent/SOUL.md +77 -0
  39. package/agents/learning-agent/TOOLS.md +145 -0
  40. package/agents/maintainer/CONFIG.yaml +31 -0
  41. package/agents/maintainer/HEARTBEAT.md +28 -0
  42. package/agents/maintainer/IDENTITY.md +33 -0
  43. package/agents/maintainer/SKILLS.md +24 -0
  44. package/agents/maintainer/SOUL.md +61 -0
  45. package/agents/maintainer/TOOLS.md +29 -0
  46. package/agents/maintainer/lib/engine.js +279 -0
  47. package/agents/maintainer/lib/safe-fixer.js +183 -0
  48. package/agents/morning-brief/CONFIG.yaml +22 -0
  49. package/agents/morning-brief/HEARTBEAT.md +60 -0
  50. package/agents/morning-brief/IDENTITY.md +5 -0
  51. package/agents/morning-brief/SKILLS.md +56 -0
  52. package/agents/morning-brief/SOUL.md +64 -0
  53. package/agents/morning-brief/TOOLS.md +112 -0
  54. package/agents/osint-detective/CONFIG.yaml +24 -0
  55. package/agents/osint-detective/HEARTBEAT.md +66 -0
  56. package/agents/osint-detective/IDENTITY.md +1 -0
  57. package/agents/osint-detective/SKILLS.md +1 -0
  58. package/agents/osint-detective/SOUL.md +23 -0
  59. package/agents/osint-detective/TOOLS.md +81 -0
  60. package/agents/recon-scout/CONFIG.yaml +22 -0
  61. package/agents/recon-scout/HEARTBEAT.md +79 -0
  62. package/agents/recon-scout/IDENTITY.md +1 -0
  63. package/agents/recon-scout/SKILLS.md +1 -0
  64. package/agents/recon-scout/SOUL.md +23 -0
  65. package/agents/recon-scout/TOOLS.md +93 -0
  66. package/agents/report-writer/CONFIG.yaml +21 -0
  67. package/agents/report-writer/HEARTBEAT.md +63 -0
  68. package/agents/report-writer/IDENTITY.md +1 -0
  69. package/agents/report-writer/SKILLS.md +1 -0
  70. package/agents/report-writer/SOUL.md +23 -0
  71. package/agents/report-writer/TOOLS.md +69 -0
  72. package/agents/shared/README.md +13 -0
  73. package/agents/web-hacker/CONFIG.yaml +24 -0
  74. package/agents/web-hacker/HEARTBEAT.md +78 -0
  75. package/agents/web-hacker/IDENTITY.md +1 -0
  76. package/agents/web-hacker/SKILLS.md +1 -0
  77. package/agents/web-hacker/SOUL.md +23 -0
  78. package/agents/web-hacker/TOOLS.md +86 -0
  79. package/api/CLAUDE.md +19 -0
  80. package/api/index.js +274 -0
  81. package/bin/cli.js +620 -0
  82. package/bin/local.sh +31 -0
  83. package/bin/postinstall.js +63 -0
  84. package/config/index.js +24 -0
  85. package/config/instrumentation.js +93 -0
  86. package/drizzle/0000_initial.sql +52 -0
  87. package/drizzle/0001_bounty_and_registry.sql +82 -0
  88. package/drizzle/0002_sync_columns.sql +7 -0
  89. package/drizzle/0003_graceful_bloodscream.sql +86 -0
  90. package/drizzle/meta/0000_snapshot.json +321 -0
  91. package/drizzle/meta/0003_snapshot.json +878 -0
  92. package/drizzle/meta/_journal.json +34 -0
  93. package/drizzle/relations.ts +3 -0
  94. package/drizzle/schema.ts +145 -0
  95. package/lib/actions.js +47 -0
  96. package/lib/agents.js +166 -0
  97. package/lib/ai/agent.js +96 -0
  98. package/lib/ai/autonomous-engine.js +261 -0
  99. package/lib/ai/index.js +359 -0
  100. package/lib/ai/model-router.js +254 -0
  101. package/lib/ai/model.js +73 -0
  102. package/lib/ai/tools.js +84 -0
  103. package/lib/auth/actions.js +28 -0
  104. package/lib/auth/config.js +27 -0
  105. package/lib/auth/edge-config.js +27 -0
  106. package/lib/auth/index.js +27 -0
  107. package/lib/auth/middleware.js +53 -0
  108. package/lib/bounty/actions.js +119 -0
  109. package/lib/bounty/findings.js +64 -0
  110. package/lib/bounty/programs.js +34 -0
  111. package/lib/bounty/sync-targets.js +267 -0
  112. package/lib/bounty/targets.js +33 -0
  113. package/lib/channels/base.js +56 -0
  114. package/lib/channels/index.js +15 -0
  115. package/lib/channels/telegram.js +148 -0
  116. package/lib/chat/actions.js +288 -0
  117. package/lib/chat/api.js +135 -0
  118. package/lib/chat/components/app-sidebar.js +237 -0
  119. package/lib/chat/components/app-sidebar.jsx +289 -0
  120. package/lib/chat/components/chat-header.js +27 -0
  121. package/lib/chat/components/chat-header.jsx +37 -0
  122. package/lib/chat/components/chat-input.js +230 -0
  123. package/lib/chat/components/chat-input.jsx +228 -0
  124. package/lib/chat/components/chat-nav-context.js +11 -0
  125. package/lib/chat/components/chat-nav-context.jsx +11 -0
  126. package/lib/chat/components/chat-page.js +81 -0
  127. package/lib/chat/components/chat-page.jsx +100 -0
  128. package/lib/chat/components/chat.js +150 -0
  129. package/lib/chat/components/chat.jsx +182 -0
  130. package/lib/chat/components/chats-page.js +302 -0
  131. package/lib/chat/components/chats-page.jsx +330 -0
  132. package/lib/chat/components/crons-page.js +172 -0
  133. package/lib/chat/components/crons-page.jsx +244 -0
  134. package/lib/chat/components/enhanced-tool-call.js +103 -0
  135. package/lib/chat/components/enhanced-tool-call.jsx +139 -0
  136. package/lib/chat/components/findings-page.js +175 -0
  137. package/lib/chat/components/findings-page.jsx +214 -0
  138. package/lib/chat/components/greeting.js +22 -0
  139. package/lib/chat/components/greeting.jsx +26 -0
  140. package/lib/chat/components/icons.js +777 -0
  141. package/lib/chat/components/icons.jsx +741 -0
  142. package/lib/chat/components/index.js +26 -0
  143. package/lib/chat/components/mcp-page.js +260 -0
  144. package/lib/chat/components/mcp-page.jsx +355 -0
  145. package/lib/chat/components/message.js +289 -0
  146. package/lib/chat/components/message.jsx +315 -0
  147. package/lib/chat/components/messages.js +66 -0
  148. package/lib/chat/components/messages.jsx +77 -0
  149. package/lib/chat/components/notifications-page.js +56 -0
  150. package/lib/chat/components/notifications-page.jsx +87 -0
  151. package/lib/chat/components/page-layout.js +21 -0
  152. package/lib/chat/components/page-layout.jsx +28 -0
  153. package/lib/chat/components/registry-page.js +222 -0
  154. package/lib/chat/components/registry-page.jsx +255 -0
  155. package/lib/chat/components/settings-layout.js +40 -0
  156. package/lib/chat/components/settings-layout.jsx +54 -0
  157. package/lib/chat/components/settings-secrets-page.js +216 -0
  158. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  159. package/lib/chat/components/sidebar-history-item.js +132 -0
  160. package/lib/chat/components/sidebar-history-item.jsx +113 -0
  161. package/lib/chat/components/sidebar-history.js +115 -0
  162. package/lib/chat/components/sidebar-history.jsx +157 -0
  163. package/lib/chat/components/sidebar-user-nav.js +63 -0
  164. package/lib/chat/components/sidebar-user-nav.jsx +73 -0
  165. package/lib/chat/components/status-bar.js +39 -0
  166. package/lib/chat/components/status-bar.jsx +51 -0
  167. package/lib/chat/components/swarm-page.js +157 -0
  168. package/lib/chat/components/swarm-page.jsx +210 -0
  169. package/lib/chat/components/targets-page.js +376 -0
  170. package/lib/chat/components/targets-page.jsx +389 -0
  171. package/lib/chat/components/tool-call.js +86 -0
  172. package/lib/chat/components/tool-call.jsx +104 -0
  173. package/lib/chat/components/tool-panel.js +107 -0
  174. package/lib/chat/components/tool-panel.jsx +145 -0
  175. package/lib/chat/components/triggers-page.js +153 -0
  176. package/lib/chat/components/triggers-page.jsx +221 -0
  177. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  178. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  179. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  180. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  181. package/lib/chat/components/ui/rename-dialog.js +74 -0
  182. package/lib/chat/components/ui/rename-dialog.jsx +72 -0
  183. package/lib/chat/components/ui/scroll-area.js +13 -0
  184. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  185. package/lib/chat/components/ui/separator.js +21 -0
  186. package/lib/chat/components/ui/separator.jsx +18 -0
  187. package/lib/chat/components/ui/sheet.js +75 -0
  188. package/lib/chat/components/ui/sheet.jsx +95 -0
  189. package/lib/chat/components/ui/sidebar.js +227 -0
  190. package/lib/chat/components/ui/sidebar.jsx +245 -0
  191. package/lib/chat/components/ui/tooltip.js +56 -0
  192. package/lib/chat/components/ui/tooltip.jsx +66 -0
  193. package/lib/chat/components/upgrade-dialog.js +151 -0
  194. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  195. package/lib/chat/utils.js +11 -0
  196. package/lib/cron.js +246 -0
  197. package/lib/db/api-keys.js +163 -0
  198. package/lib/db/chats.js +145 -0
  199. package/lib/db/index.js +52 -0
  200. package/lib/db/notifications.js +99 -0
  201. package/lib/db/schema.js +145 -0
  202. package/lib/db/update-check.js +96 -0
  203. package/lib/db/users.js +89 -0
  204. package/lib/mcp/actions.js +104 -0
  205. package/lib/mcp/client.js +79 -0
  206. package/lib/mcp/handler.js +57 -0
  207. package/lib/mcp/server.js +165 -0
  208. package/lib/paths.js +46 -0
  209. package/lib/registry/actions.js +164 -0
  210. package/lib/registry/catalog.js +137 -0
  211. package/lib/registry/tools.js +71 -0
  212. package/lib/tools/create-job.js +99 -0
  213. package/lib/tools/github.js +217 -0
  214. package/lib/tools/openai.js +35 -0
  215. package/lib/tools/telegram.js +292 -0
  216. package/lib/triggers.js +118 -0
  217. package/lib/utils/render-md.js +102 -0
  218. package/package.json +103 -0
  219. package/setup/lib/auth.mjs +81 -0
  220. package/setup/lib/env.mjs +21 -0
  221. package/setup/lib/fs-utils.mjs +20 -0
  222. package/setup/lib/github.mjs +149 -0
  223. package/setup/lib/prerequisites.mjs +155 -0
  224. package/setup/lib/prompts.mjs +267 -0
  225. package/setup/lib/providers.mjs +48 -0
  226. package/setup/lib/sync.mjs +125 -0
  227. package/setup/lib/targets.mjs +45 -0
  228. package/setup/lib/telegram-verify.mjs +63 -0
  229. package/setup/lib/telegram.mjs +76 -0
  230. package/setup/setup-telegram.mjs +264 -0
  231. package/setup/setup.mjs +842 -0
  232. package/templates/.dockerignore +5 -0
  233. package/templates/.env.example +63 -0
  234. package/templates/.github/workflows/auto-merge.yml +117 -0
  235. package/templates/.github/workflows/build-image.yml +36 -0
  236. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  237. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  238. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  239. package/templates/.github/workflows/run-job.yml +89 -0
  240. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  241. package/templates/.gitignore.template +45 -0
  242. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  243. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  244. package/templates/CLAUDE.md +29 -0
  245. package/templates/CLAUDE.md.template +307 -0
  246. package/templates/app/api/[...thepopebot]/route.js +1 -0
  247. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  248. package/templates/app/chat/[chatId]/page.js +8 -0
  249. package/templates/app/chats/page.js +7 -0
  250. package/templates/app/components/ascii-logo.jsx +10 -0
  251. package/templates/app/components/login-form.jsx +92 -0
  252. package/templates/app/components/setup-form.jsx +82 -0
  253. package/templates/app/components/theme-provider.jsx +11 -0
  254. package/templates/app/components/theme-toggle.jsx +38 -0
  255. package/templates/app/components/ui/button.jsx +21 -0
  256. package/templates/app/components/ui/card.jsx +23 -0
  257. package/templates/app/components/ui/input.jsx +10 -0
  258. package/templates/app/components/ui/label.jsx +10 -0
  259. package/templates/app/crons/page.js +5 -0
  260. package/templates/app/findings/page.js +7 -0
  261. package/templates/app/globals.css +90 -0
  262. package/templates/app/layout.js +19 -0
  263. package/templates/app/login/page.js +15 -0
  264. package/templates/app/notifications/page.js +7 -0
  265. package/templates/app/page.js +7 -0
  266. package/templates/app/settings/crons/page.js +5 -0
  267. package/templates/app/settings/layout.js +7 -0
  268. package/templates/app/settings/mcp/page.js +5 -0
  269. package/templates/app/settings/page.js +5 -0
  270. package/templates/app/settings/secrets/page.js +5 -0
  271. package/templates/app/settings/triggers/page.js +5 -0
  272. package/templates/app/stream/chat/route.js +1 -0
  273. package/templates/app/swarm/page.js +7 -0
  274. package/templates/app/targets/page.js +7 -0
  275. package/templates/app/toolbox/page.js +7 -0
  276. package/templates/app/triggers/page.js +5 -0
  277. package/templates/config/AGENT.md +34 -0
  278. package/templates/config/CRONS.json +56 -0
  279. package/templates/config/EVENT_HANDLER.md +224 -0
  280. package/templates/config/HEARTBEAT.md +3 -0
  281. package/templates/config/JOB_SUMMARY.md +130 -0
  282. package/templates/config/MCP_SERVERS.json +1 -0
  283. package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
  284. package/templates/config/SOUL.md +17 -0
  285. package/templates/config/TRIGGERS.json +58 -0
  286. package/templates/docker/event-handler/Dockerfile +20 -0
  287. package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
  288. package/templates/docker/job-claude-code/Dockerfile +34 -0
  289. package/templates/docker/job-claude-code/entrypoint.sh +139 -0
  290. package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
  291. package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
  292. package/templates/docker-compose.yml +63 -0
  293. package/templates/instrumentation.js +6 -0
  294. package/templates/middleware.js +1 -0
  295. package/templates/next.config.mjs +3 -0
  296. package/templates/postcss.config.mjs +5 -0
  297. package/templates/skills/LICENSE +21 -0
  298. package/templates/skills/README.md +119 -0
  299. package/templates/skills/brave-search/SKILL.md +79 -0
  300. package/templates/skills/brave-search/content.js +86 -0
  301. package/templates/skills/brave-search/package-lock.json +621 -0
  302. package/templates/skills/brave-search/package.json +14 -0
  303. package/templates/skills/brave-search/search.js +199 -0
  304. package/templates/skills/browser-tools/SKILL.md +196 -0
  305. package/templates/skills/browser-tools/browser-content.js +103 -0
  306. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  307. package/templates/skills/browser-tools/browser-eval.js +53 -0
  308. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  309. package/templates/skills/browser-tools/browser-nav.js +44 -0
  310. package/templates/skills/browser-tools/browser-pick.js +162 -0
  311. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  312. package/templates/skills/browser-tools/browser-start.js +87 -0
  313. package/templates/skills/browser-tools/package-lock.json +2556 -0
  314. package/templates/skills/browser-tools/package.json +19 -0
  315. package/templates/skills/llm-secrets/SKILL.md +34 -0
  316. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  317. package/templates/skills/modify-self/SKILL.md +12 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * AutonomousEngine — Background thinking loop for agents.
3
+ *
4
+ * Every agent runs this engine to continuously analyze context, identify
5
+ * improvements, calculate efficiency, and propose automations. Thoughts are
6
+ * stored in-memory and logged (no external API needed).
7
+ *
8
+ * Adapted from Harbinger's agents/shared/autonomous-engine.js.
9
+ * Key change: stores thoughts locally instead of POSTing to swarm API.
10
+ *
11
+ * COST_BENEFIT = (TIME_SAVED * FREQUENCY) / (IMPL_COST + RUNNING_COST)
12
+ * Only proposals with cost_benefit > 1.0 get surfaced.
13
+ */
14
+
15
+ import { discoverAgents } from '../agents.js';
16
+
17
+ const MAX_THOUGHTS = 200; // Rolling buffer size
18
+
19
+ export class AutonomousEngine {
20
+ constructor(opts = {}) {
21
+ this.agentId = opts.agentId || 'unknown';
22
+ this.agentName = opts.agentName || 'AGENT';
23
+ this.agentType = opts.agentType || 'general';
24
+ this.interval = opts.interval || 60000; // 60-second thinking cycle
25
+ this._timer = null;
26
+ this._running = false;
27
+ this._cycleCount = 0;
28
+ this._lastContext = null;
29
+ this._thoughts = []; // in-memory thought storage
30
+ }
31
+
32
+ start() {
33
+ if (this._running) return;
34
+ this._running = true;
35
+ console.log(`[${this.agentName}] Autonomous engine started (${this.interval / 1000}s cycle)`);
36
+
37
+ // Run first cycle immediately, then on interval
38
+ this._think();
39
+ this._timer = setInterval(() => this._think(), this.interval);
40
+ }
41
+
42
+ stop() {
43
+ this._running = false;
44
+ if (this._timer) {
45
+ clearInterval(this._timer);
46
+ this._timer = null;
47
+ }
48
+ console.log(`[${this.agentName}] Autonomous engine stopped after ${this._cycleCount} cycles`);
49
+ }
50
+
51
+ async _think() {
52
+ this._cycleCount++;
53
+ try {
54
+ // 1. Gather context — what agents are available locally?
55
+ const context = this._gatherContext();
56
+ this._lastContext = context;
57
+
58
+ // 2. Identify enhancements — scan 5 dimensions
59
+ const enhancements = this._identifyEnhancements(context);
60
+
61
+ // 3. For each enhancement, calculate efficiency and classify
62
+ for (const enhancement of enhancements) {
63
+ const efficiency = this._calculateEfficiency(enhancement);
64
+
65
+ // Only store if cost-benefit ratio is positive
66
+ if (efficiency.cost_benefit >= 1.0) {
67
+ enhancement.efficiency = efficiency;
68
+ enhancement.efficiency.automation_type = this._classifyAutomation(enhancement);
69
+ this._storeThought(enhancement);
70
+ }
71
+ }
72
+ } catch (err) {
73
+ // Silent fail — don't crash the agent over thinking errors
74
+ if (this._cycleCount <= 3) {
75
+ console.error(`[${this.agentName}] Think cycle error:`, err.message);
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Gather local context — discover agents and their statuses.
82
+ * No HTTP calls — everything is in-process.
83
+ */
84
+ _gatherContext() {
85
+ try {
86
+ const agents = discoverAgents();
87
+ return {
88
+ agents,
89
+ agentCount: agents.length,
90
+ self: agents.find(a => a.id === this.agentId) || null,
91
+ thoughtCount: this._thoughts.length,
92
+ cycleCount: this._cycleCount,
93
+ };
94
+ } catch {
95
+ return { agents: [], agentCount: 0, self: null, thoughtCount: 0, cycleCount: this._cycleCount };
96
+ }
97
+ }
98
+
99
+ // Scan 5 dimensions for potential enhancements
100
+ _identifyEnhancements(context) {
101
+ const enhancements = [];
102
+
103
+ // Dimension 1: Performance — detect high thought volume
104
+ if (context.thoughtCount > 50) {
105
+ enhancements.push({
106
+ type: 'enhancement',
107
+ category: 'performance',
108
+ title: `${this.agentName} generating high thought volume`,
109
+ content: `Engine has ${context.thoughtCount} thoughts. Consider batch processing or reducing scan frequency.`,
110
+ priority: 2,
111
+ });
112
+ }
113
+
114
+ // Dimension 2: Accuracy — check cycle health
115
+ if (context.cycleCount > 100 && context.thoughtCount === 0) {
116
+ enhancements.push({
117
+ type: 'observation',
118
+ category: 'accuracy',
119
+ title: 'No actionable thoughts after many cycles',
120
+ content: `${context.cycleCount} cycles completed with no thoughts. Consider adjusting thresholds.`,
121
+ priority: 3,
122
+ });
123
+ }
124
+
125
+ // Dimension 3: Cost — monitor cycle count
126
+ if (context.cycleCount > 1000) {
127
+ enhancements.push({
128
+ type: 'alert',
129
+ category: 'cost',
130
+ title: 'High cycle count — consider throttling',
131
+ content: `Engine has run ${context.cycleCount} cycles. Consider increasing interval to reduce resource usage.`,
132
+ priority: 4,
133
+ });
134
+ }
135
+
136
+ // Dimension 4: Automation — detect idle potential
137
+ if (context.agentCount > 0) {
138
+ enhancements.push({
139
+ type: 'proposal',
140
+ category: 'automation',
141
+ title: `${context.agentCount} agent profiles available`,
142
+ content: `Agent profiles detected: ${context.agents.map(a => a.codename || a.id).join(', ')}. Could schedule proactive scans.`,
143
+ priority: 2,
144
+ });
145
+ }
146
+
147
+ // Dimension 5: Collaboration — multi-agent coordination
148
+ if (context.agentCount > 3) {
149
+ enhancements.push({
150
+ type: 'proposal',
151
+ category: 'collaboration',
152
+ title: 'Multiple agents available — suggest coordinated workflow',
153
+ content: `${context.agentCount} agent profiles loaded. Consider launching coordinated assessments.`,
154
+ priority: 3,
155
+ });
156
+ }
157
+
158
+ return enhancements;
159
+ }
160
+
161
+ // Calculate cost-benefit ratio for an enhancement
162
+ _calculateEfficiency(enhancement) {
163
+ const baseTimeSaved = { performance: 2, accuracy: 1, cost: 3, automation: 4, collaboration: 2 };
164
+ const baseFrequency = { performance: 7, accuracy: 3, cost: 1, automation: 14, collaboration: 2 };
165
+ const baseCost = { performance: 4, accuracy: 2, cost: 1, automation: 8, collaboration: 6 };
166
+
167
+ const cat = enhancement.category || 'automation';
168
+ const timeSaved = (baseTimeSaved[cat] || 1) * (enhancement.priority / 3);
169
+ const frequency = baseFrequency[cat] || 1;
170
+ const implCost = baseCost[cat] || 4;
171
+ const runningCost = implCost * 0.1;
172
+
173
+ const costBenefit = (timeSaved * frequency) / (implCost + runningCost);
174
+
175
+ return {
176
+ time_saved: Math.round(timeSaved * 100) / 100,
177
+ frequency,
178
+ implementation_cost: implCost,
179
+ running_cost: Math.round(runningCost * 100) / 100,
180
+ cost_benefit: Math.round(costBenefit * 100) / 100,
181
+ automation_type: 'script',
182
+ };
183
+ }
184
+
185
+ _classifyAutomation(enhancement) {
186
+ const cat = enhancement.category;
187
+ if (cat === 'automation') return 'script';
188
+ if (cat === 'performance') return 'code_change';
189
+ if (cat === 'collaboration') return 'workflow';
190
+ if (cat === 'accuracy') return 'skill';
191
+ return 'script';
192
+ }
193
+
194
+ /**
195
+ * Store thought in the local rolling buffer.
196
+ */
197
+ _storeThought(thought) {
198
+ const entry = {
199
+ timestamp: new Date().toISOString(),
200
+ agent_id: this.agentId,
201
+ agent_name: this.agentName,
202
+ type: thought.type || 'observation',
203
+ category: thought.category || '',
204
+ title: thought.title || 'Untitled thought',
205
+ content: thought.content || '',
206
+ priority: thought.priority || 3,
207
+ efficiency: thought.efficiency || null,
208
+ };
209
+
210
+ this._thoughts.push(entry);
211
+
212
+ // Rolling buffer — trim oldest
213
+ if (this._thoughts.length > MAX_THOUGHTS) {
214
+ this._thoughts = this._thoughts.slice(-MAX_THOUGHTS);
215
+ }
216
+ }
217
+
218
+ /** Get all stored thoughts. */
219
+ getThoughts() {
220
+ return [...this._thoughts];
221
+ }
222
+
223
+ /** Get the last gathered context. */
224
+ getLastContext() {
225
+ return this._lastContext;
226
+ }
227
+
228
+ /** Get engine status. */
229
+ getStatus() {
230
+ return {
231
+ running: this._running,
232
+ cycles: this._cycleCount,
233
+ agent: this.agentName,
234
+ interval: this.interval,
235
+ thoughtCount: this._thoughts.length,
236
+ };
237
+ }
238
+ }
239
+
240
+ let _engine = null;
241
+
242
+ /** Get the AutonomousEngine singleton. */
243
+ export function getAutonomousEngine() {
244
+ return _engine;
245
+ }
246
+
247
+ /** Start the autonomous engine with the given options. */
248
+ export function startAutonomousEngine(opts = {}) {
249
+ if (_engine) return _engine;
250
+ _engine = new AutonomousEngine(opts);
251
+ _engine.start();
252
+ return _engine;
253
+ }
254
+
255
+ /** Stop the autonomous engine. */
256
+ export function stopAutonomousEngine() {
257
+ if (_engine) {
258
+ _engine.stop();
259
+ _engine = null;
260
+ }
261
+ }
@@ -0,0 +1,359 @@
1
+ import { HumanMessage, AIMessage } from '@langchain/core/messages';
2
+ import { z } from 'zod';
3
+ import { getAgent, getAgentForProfile } from './agent.js';
4
+ import { createModel } from './model.js';
5
+ import { jobSummaryMd } from '../paths.js';
6
+ import { render_md } from '../utils/render-md.js';
7
+ import { getChatById, createChat, saveMessage, updateChatTitle } from '../db/chats.js';
8
+ import { discoverAgents } from '../agents.js';
9
+
10
+ /**
11
+ * Detect @AGENT_NAME mentions at the start of a message.
12
+ * Returns { agentId, cleanMessage } or null if no mention found.
13
+ * Matches @codename or @id (case-insensitive) against discovered agent profiles.
14
+ */
15
+ let _agentMap = null;
16
+ function detectAgentMention(message) {
17
+ if (!message) return null;
18
+ const match = message.match(/^@(\w[\w-]*)\s+/i);
19
+ if (!match) return null;
20
+
21
+ const mention = match[1].toLowerCase();
22
+
23
+ // Lazy-load agent map
24
+ if (!_agentMap) {
25
+ try {
26
+ const agents = discoverAgents();
27
+ _agentMap = new Map();
28
+ for (const a of agents) {
29
+ if (a.codename) _agentMap.set(a.codename.toLowerCase(), a.id);
30
+ if (a.id) _agentMap.set(a.id.toLowerCase(), a.id);
31
+ if (a.name) _agentMap.set(a.name.toLowerCase(), a.id);
32
+ }
33
+ } catch {
34
+ _agentMap = new Map();
35
+ }
36
+ }
37
+
38
+ const agentId = _agentMap.get(mention);
39
+ if (!agentId) return null;
40
+ return { agentId, cleanMessage: message.slice(match[0].length) };
41
+ }
42
+
43
+ /**
44
+ * Ensure a chat exists in the DB and save a message.
45
+ * Centralized so every channel gets persistence automatically.
46
+ *
47
+ * @param {string} threadId - Chat/thread ID
48
+ * @param {string} role - 'user' or 'assistant'
49
+ * @param {string} text - Message text
50
+ * @param {object} [options] - { userId, chatTitle }
51
+ */
52
+ function persistMessage(threadId, role, text, options = {}) {
53
+ try {
54
+ if (!getChatById(threadId)) {
55
+ createChat(options.userId || 'unknown', options.chatTitle || 'New Chat', threadId);
56
+ }
57
+ saveMessage(threadId, role, text);
58
+ } catch (err) {
59
+ // DB persistence is best-effort — don't break chat if DB fails
60
+ console.error('Failed to persist message:', err);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Process a chat message through the LangGraph agent.
66
+ * Saves user and assistant messages to the DB automatically.
67
+ *
68
+ * @param {string} threadId - Conversation thread ID (from channel adapter)
69
+ * @param {string} message - User's message text
70
+ * @param {Array} [attachments=[]] - Normalized attachments from adapter
71
+ * @param {object} [options] - { userId, chatTitle } for DB persistence
72
+ * @returns {Promise<string>} AI response text
73
+ */
74
+ async function chat(threadId, message, attachments = [], options = {}) {
75
+ // Detect @AGENT_NAME mention or use explicit agentId option
76
+ let effectiveMessage = message;
77
+ let agentId = options.agentId || null;
78
+ if (!agentId && message) {
79
+ const mention = detectAgentMention(message);
80
+ if (mention) {
81
+ agentId = mention.agentId;
82
+ effectiveMessage = mention.cleanMessage;
83
+ }
84
+ }
85
+ const agent = agentId ? await getAgentForProfile(agentId) : await getAgent();
86
+
87
+ // Save user message to DB (save original message with @mention)
88
+ persistMessage(threadId, 'user', message || '[attachment]', options);
89
+
90
+ // Build content blocks: text + any image attachments as base64 vision
91
+ // Use effectiveMessage (with @mention stripped) for the LLM
92
+ const content = [];
93
+
94
+ if (effectiveMessage) {
95
+ content.push({ type: 'text', text: effectiveMessage });
96
+ }
97
+
98
+ for (const att of attachments) {
99
+ if (att.category === 'image') {
100
+ content.push({
101
+ type: 'image_url',
102
+ image_url: {
103
+ url: `data:${att.mimeType};base64,${att.data.toString('base64')}`,
104
+ },
105
+ });
106
+ }
107
+ // Documents: future handling
108
+ }
109
+
110
+ // If only text and no attachments, simplify to a string
111
+ const messageContent = content.length === 1 && content[0].type === 'text'
112
+ ? content[0].text
113
+ : content;
114
+
115
+ const result = await agent.invoke(
116
+ { messages: [new HumanMessage({ content: messageContent })] },
117
+ { configurable: { thread_id: threadId } }
118
+ );
119
+
120
+ const lastMessage = result.messages[result.messages.length - 1];
121
+
122
+ // LangChain message content can be a string or an array of content blocks
123
+ let response;
124
+ if (typeof lastMessage.content === 'string') {
125
+ response = lastMessage.content;
126
+ } else {
127
+ response = lastMessage.content
128
+ .filter((block) => block.type === 'text')
129
+ .map((block) => block.text)
130
+ .join('\n');
131
+ }
132
+
133
+ // Save assistant response to DB
134
+ persistMessage(threadId, 'assistant', response, options);
135
+
136
+ // Auto-generate title for new chats
137
+ if (options.userId && message) {
138
+ autoTitle(threadId, message).catch(() => {});
139
+ }
140
+
141
+ return response;
142
+ }
143
+
144
+ /**
145
+ * Process a chat message with streaming (for channels that support it).
146
+ * Saves user and assistant messages to the DB automatically.
147
+ *
148
+ * @param {string} threadId - Conversation thread ID
149
+ * @param {string} message - User's message text
150
+ * @param {Array} [attachments=[]] - Image/PDF attachments: { category, mimeType, dataUrl }
151
+ * @param {object} [options] - { userId, chatTitle, skipUserPersist } for DB persistence
152
+ * @returns {AsyncIterableIterator<string>} Stream of text chunks
153
+ */
154
+ async function* chatStream(threadId, message, attachments = [], options = {}) {
155
+ // Detect @AGENT_NAME mention or use explicit agentId option
156
+ let effectiveMessage = message;
157
+ let agentId = options.agentId || null;
158
+ if (!agentId && message) {
159
+ const mention = detectAgentMention(message);
160
+ if (mention) {
161
+ agentId = mention.agentId;
162
+ effectiveMessage = mention.cleanMessage;
163
+ }
164
+ }
165
+ const agent = agentId ? await getAgentForProfile(agentId) : await getAgent();
166
+
167
+ // Save user message to DB (skip on regeneration — message already exists)
168
+ if (!options.skipUserPersist) {
169
+ persistMessage(threadId, 'user', message || '[attachment]', options);
170
+ }
171
+
172
+ // Build content blocks: text + any image/PDF attachments as vision
173
+ // Use effectiveMessage (with @mention stripped) for the LLM
174
+ const content = [];
175
+
176
+ if (effectiveMessage) {
177
+ content.push({ type: 'text', text: effectiveMessage });
178
+ }
179
+
180
+ for (const att of attachments) {
181
+ if (att.category === 'image') {
182
+ // Support both dataUrl (web) and Buffer (Telegram) formats
183
+ const url = att.dataUrl
184
+ ? att.dataUrl
185
+ : `data:${att.mimeType};base64,${att.data.toString('base64')}`;
186
+ content.push({
187
+ type: 'image_url',
188
+ image_url: { url },
189
+ });
190
+ }
191
+ }
192
+
193
+ // If only text and no attachments, simplify to a string
194
+ const messageContent = content.length === 1 && content[0].type === 'text'
195
+ ? content[0].text
196
+ : content;
197
+
198
+ try {
199
+ const stream = await agent.stream(
200
+ { messages: [new HumanMessage({ content: messageContent })] },
201
+ { configurable: { thread_id: threadId }, streamMode: 'messages' }
202
+ );
203
+
204
+ let fullText = '';
205
+
206
+ for await (const event of stream) {
207
+ // streamMode: 'messages' yields [message, metadata] tuples
208
+ const msg = Array.isArray(event) ? event[0] : event;
209
+ const msgType = msg._getType?.();
210
+
211
+ if (msgType === 'ai') {
212
+ // Tool calls — AIMessage.tool_calls is an array of { id, name, args }
213
+ if (msg.tool_calls?.length > 0) {
214
+ for (const tc of msg.tool_calls) {
215
+ yield {
216
+ type: 'tool-call',
217
+ toolCallId: tc.id,
218
+ toolName: tc.name,
219
+ args: tc.args,
220
+ };
221
+ }
222
+ }
223
+
224
+ // Text content (wrapped in structured object)
225
+ let text = '';
226
+ if (typeof msg.content === 'string') {
227
+ text = msg.content;
228
+ } else if (Array.isArray(msg.content)) {
229
+ text = msg.content
230
+ .filter((b) => b.type === 'text' && b.text)
231
+ .map((b) => b.text)
232
+ .join('');
233
+ }
234
+
235
+ if (text) {
236
+ fullText += text;
237
+ yield { type: 'text', text };
238
+ }
239
+ } else if (msgType === 'tool') {
240
+ // Tool result — ToolMessage has tool_call_id and content
241
+ yield {
242
+ type: 'tool-result',
243
+ toolCallId: msg.tool_call_id,
244
+ result: msg.content,
245
+ };
246
+ }
247
+ // Skip other message types (human, system)
248
+ }
249
+
250
+ // Save assistant response to DB
251
+ if (fullText) {
252
+ persistMessage(threadId, 'assistant', fullText, options);
253
+ }
254
+
255
+ // Auto-generate title for new chats
256
+ if (options.userId && message) {
257
+ autoTitle(threadId, message).catch(() => {});
258
+ }
259
+ } catch (err) {
260
+ console.error('[chatStream] error:', err);
261
+ throw err;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Auto-generate a chat title from the first user message (fire-and-forget).
267
+ * Uses structured output to avoid thinking-token leaks with extended-thinking models.
268
+ */
269
+ async function autoTitle(threadId, firstMessage) {
270
+ try {
271
+ const chat = getChatById(threadId);
272
+ if (!chat || chat.title !== 'New Chat') return;
273
+
274
+ const model = await createModel({ maxTokens: 250 });
275
+ const response = await model.withStructuredOutput(z.object({ title: z.string() })).invoke([
276
+ ['system', 'Generate a descriptive (8-12 word) title for this chat based on the user\'s first message.'],
277
+ ['human', firstMessage],
278
+ ]);
279
+ if (response.title.trim()) {
280
+ updateChatTitle(threadId, response.title.trim());
281
+ }
282
+ } catch (err) {
283
+ console.error('[autoTitle] Failed to generate title:', err.message);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * One-shot summarization with a different system prompt and no memory.
289
+ * Used for job completion summaries sent via GitHub webhook.
290
+ *
291
+ * @param {object} results - Job results from webhook payload
292
+ * @returns {Promise<string>} Summary text
293
+ */
294
+ async function summarizeJob(results) {
295
+ try {
296
+ const model = await createModel({ maxTokens: 1024 });
297
+ const systemPrompt = render_md(jobSummaryMd);
298
+
299
+ if (!systemPrompt) {
300
+ console.error(`[summarizeJob] Empty system prompt — JOB_SUMMARY.md not found or empty at: ${jobSummaryMd}`);
301
+ }
302
+
303
+ const userMessage = [
304
+ results.job ? `## Task\n${results.job}` : '',
305
+ results.commit_message ? `## Commit Message\n${results.commit_message}` : '',
306
+ results.changed_files?.length ? `## Changed Files\n${results.changed_files.join('\n')}` : '',
307
+ results.status ? `## Status\n${results.status}` : '',
308
+ results.merge_result ? `## Merge Result\n${results.merge_result}` : '',
309
+ results.pr_url ? `## PR URL\n${results.pr_url}` : '',
310
+ results.run_url ? `## Run URL\n${results.run_url}` : '',
311
+ results.log ? `## Agent Log\n${results.log}` : '',
312
+ ]
313
+ .filter(Boolean)
314
+ .join('\n\n');
315
+
316
+ console.log(`[summarizeJob] System prompt: ${systemPrompt.length} chars, user message: ${userMessage.length} chars`);
317
+
318
+ const response = await model.invoke([
319
+ ['system', systemPrompt],
320
+ ['human', userMessage],
321
+ ]);
322
+
323
+ const text =
324
+ typeof response.content === 'string'
325
+ ? response.content
326
+ : response.content
327
+ .filter((block) => block.type === 'text')
328
+ .map((block) => block.text)
329
+ .join('\n');
330
+
331
+ console.log(`[summarizeJob] Result: ${text.length} chars — ${text.slice(0, 200)}`);
332
+
333
+ return text.trim() || 'Job finished.';
334
+ } catch (err) {
335
+ console.error('[summarizeJob] Failed to summarize job:', err);
336
+ return 'Job finished.';
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Inject a message into a thread's memory so the agent has context
342
+ * for future conversations (e.g., job completion summaries).
343
+ *
344
+ * @param {string} threadId - Conversation thread ID
345
+ * @param {string} text - Message text to inject as an assistant message
346
+ */
347
+ async function addToThread(threadId, text) {
348
+ try {
349
+ const agent = await getAgent();
350
+ await agent.updateState(
351
+ { configurable: { thread_id: threadId } },
352
+ { messages: [new AIMessage(text)] }
353
+ );
354
+ } catch (err) {
355
+ console.error('Failed to add message to thread:', err);
356
+ }
357
+ }
358
+
359
+ export { chat, chatStream, summarizeJob, addToThread, persistMessage };