gigaclaw 1.4.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 (249) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +237 -0
  3. package/api/CLAUDE.md +19 -0
  4. package/api/index.js +265 -0
  5. package/bin/cli.js +823 -0
  6. package/bin/local.sh +85 -0
  7. package/bin/postinstall.js +63 -0
  8. package/config/index.js +26 -0
  9. package/config/instrumentation.js +62 -0
  10. package/drizzle/0000_initial.sql +52 -0
  11. package/drizzle/0001_nostalgic_sersi.sql +11 -0
  12. package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
  13. package/drizzle/0003_rename_code_workspaces.sql +5 -0
  14. package/drizzle/meta/0000_snapshot.json +321 -0
  15. package/drizzle/meta/0001_snapshot.json +390 -0
  16. package/drizzle/meta/0002_snapshot.json +411 -0
  17. package/drizzle/meta/0003_snapshot.json +419 -0
  18. package/drizzle/meta/_journal.json +34 -0
  19. package/lib/actions.js +44 -0
  20. package/lib/ai/agent.js +86 -0
  21. package/lib/ai/index.js +342 -0
  22. package/lib/ai/model.js +180 -0
  23. package/lib/ai/tools.js +269 -0
  24. package/lib/ai/web-search.js +42 -0
  25. package/lib/auth/actions.js +28 -0
  26. package/lib/auth/config.js +27 -0
  27. package/lib/auth/edge-config.js +27 -0
  28. package/lib/auth/index.js +27 -0
  29. package/lib/auth/middleware.js +62 -0
  30. package/lib/channels/base.js +56 -0
  31. package/lib/channels/index.js +15 -0
  32. package/lib/channels/telegram.js +148 -0
  33. package/lib/chat/actions.js +579 -0
  34. package/lib/chat/api.js +140 -0
  35. package/lib/chat/components/app-sidebar.js +213 -0
  36. package/lib/chat/components/app-sidebar.jsx +279 -0
  37. package/lib/chat/components/chat-header.js +192 -0
  38. package/lib/chat/components/chat-header.jsx +223 -0
  39. package/lib/chat/components/chat-input.js +236 -0
  40. package/lib/chat/components/chat-input.jsx +249 -0
  41. package/lib/chat/components/chat-nav-context.js +11 -0
  42. package/lib/chat/components/chat-nav-context.jsx +11 -0
  43. package/lib/chat/components/chat-page.js +99 -0
  44. package/lib/chat/components/chat-page.jsx +121 -0
  45. package/lib/chat/components/chat.js +153 -0
  46. package/lib/chat/components/chat.jsx +199 -0
  47. package/lib/chat/components/chats-page.js +367 -0
  48. package/lib/chat/components/chats-page.jsx +394 -0
  49. package/lib/chat/components/code-mode-toggle.js +132 -0
  50. package/lib/chat/components/code-mode-toggle.jsx +163 -0
  51. package/lib/chat/components/crons-page.js +172 -0
  52. package/lib/chat/components/crons-page.jsx +244 -0
  53. package/lib/chat/components/greeting.js +11 -0
  54. package/lib/chat/components/greeting.jsx +16 -0
  55. package/lib/chat/components/icons.js +805 -0
  56. package/lib/chat/components/icons.jsx +751 -0
  57. package/lib/chat/components/index.js +20 -0
  58. package/lib/chat/components/message.js +363 -0
  59. package/lib/chat/components/message.jsx +422 -0
  60. package/lib/chat/components/messages.js +65 -0
  61. package/lib/chat/components/messages.jsx +74 -0
  62. package/lib/chat/components/notifications-page.js +56 -0
  63. package/lib/chat/components/notifications-page.jsx +87 -0
  64. package/lib/chat/components/page-layout.js +21 -0
  65. package/lib/chat/components/page-layout.jsx +28 -0
  66. package/lib/chat/components/pull-requests-page.js +103 -0
  67. package/lib/chat/components/pull-requests-page.jsx +113 -0
  68. package/lib/chat/components/settings-layout.js +39 -0
  69. package/lib/chat/components/settings-layout.jsx +53 -0
  70. package/lib/chat/components/settings-secrets-page.js +216 -0
  71. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  72. package/lib/chat/components/sidebar-history-item.js +138 -0
  73. package/lib/chat/components/sidebar-history-item.jsx +119 -0
  74. package/lib/chat/components/sidebar-history.js +167 -0
  75. package/lib/chat/components/sidebar-history.jsx +220 -0
  76. package/lib/chat/components/sidebar-user-nav.js +61 -0
  77. package/lib/chat/components/sidebar-user-nav.jsx +77 -0
  78. package/lib/chat/components/swarm-page.js +157 -0
  79. package/lib/chat/components/swarm-page.jsx +210 -0
  80. package/lib/chat/components/tool-call.js +89 -0
  81. package/lib/chat/components/tool-call.jsx +107 -0
  82. package/lib/chat/components/triggers-page.js +153 -0
  83. package/lib/chat/components/triggers-page.jsx +221 -0
  84. package/lib/chat/components/ui/combobox.js +98 -0
  85. package/lib/chat/components/ui/combobox.jsx +114 -0
  86. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  87. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  88. package/lib/chat/components/ui/dropdown-menu.js +194 -0
  89. package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
  90. package/lib/chat/components/ui/rename-dialog.js +78 -0
  91. package/lib/chat/components/ui/rename-dialog.jsx +74 -0
  92. package/lib/chat/components/ui/scroll-area.js +13 -0
  93. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  94. package/lib/chat/components/ui/separator.js +21 -0
  95. package/lib/chat/components/ui/separator.jsx +18 -0
  96. package/lib/chat/components/ui/sheet.js +75 -0
  97. package/lib/chat/components/ui/sheet.jsx +95 -0
  98. package/lib/chat/components/ui/sidebar.js +228 -0
  99. package/lib/chat/components/ui/sidebar.jsx +246 -0
  100. package/lib/chat/components/ui/tooltip.js +56 -0
  101. package/lib/chat/components/ui/tooltip.jsx +66 -0
  102. package/lib/chat/components/upgrade-dialog.js +151 -0
  103. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  104. package/lib/chat/utils.js +11 -0
  105. package/lib/code/actions.js +153 -0
  106. package/lib/code/code-page.js +22 -0
  107. package/lib/code/code-page.jsx +25 -0
  108. package/lib/code/index.js +1 -0
  109. package/lib/code/terminal-view.js +201 -0
  110. package/lib/code/terminal-view.jsx +224 -0
  111. package/lib/code/ws-proxy.js +80 -0
  112. package/lib/cron.js +246 -0
  113. package/lib/db/api-keys.js +163 -0
  114. package/lib/db/chats.js +168 -0
  115. package/lib/db/code-workspaces.js +110 -0
  116. package/lib/db/index.js +52 -0
  117. package/lib/db/notifications.js +99 -0
  118. package/lib/db/schema.js +66 -0
  119. package/lib/db/update-check.js +96 -0
  120. package/lib/db/users.js +89 -0
  121. package/lib/paths.js +42 -0
  122. package/lib/tools/create-job.js +97 -0
  123. package/lib/tools/docker.js +146 -0
  124. package/lib/tools/github.js +271 -0
  125. package/lib/tools/openai.js +35 -0
  126. package/lib/tools/telegram.js +292 -0
  127. package/lib/triggers.js +104 -0
  128. package/lib/utils/render-md.js +111 -0
  129. package/package.json +118 -0
  130. package/setup/lib/auth.mjs +81 -0
  131. package/setup/lib/env.mjs +21 -0
  132. package/setup/lib/fs-utils.mjs +20 -0
  133. package/setup/lib/github.mjs +149 -0
  134. package/setup/lib/prerequisites.mjs +155 -0
  135. package/setup/lib/prompts.mjs +267 -0
  136. package/setup/lib/providers.mjs +105 -0
  137. package/setup/lib/sync.mjs +125 -0
  138. package/setup/lib/targets.mjs +45 -0
  139. package/setup/lib/telegram-verify.mjs +63 -0
  140. package/setup/lib/telegram.mjs +76 -0
  141. package/setup/setup-cloud.mjs +833 -0
  142. package/setup/setup-local.mjs +377 -0
  143. package/setup/setup-telegram.mjs +265 -0
  144. package/setup/setup.mjs +87 -0
  145. package/templates/.dockerignore +5 -0
  146. package/templates/.env.example +104 -0
  147. package/templates/.github/workflows/auto-merge.yml +117 -0
  148. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  149. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  150. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  151. package/templates/.github/workflows/run-job.yml +89 -0
  152. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  153. package/templates/.gitignore.template +45 -0
  154. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  155. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  156. package/templates/CLAUDE.md +29 -0
  157. package/templates/CLAUDE.md.template +308 -0
  158. package/templates/app/api/[...gigaclaw]/route.js +1 -0
  159. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  160. package/templates/app/chat/[chatId]/page.js +9 -0
  161. package/templates/app/chats/page.js +7 -0
  162. package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
  163. package/templates/app/components/ascii-logo.jsx +12 -0
  164. package/templates/app/components/login-form.jsx +92 -0
  165. package/templates/app/components/setup-form.jsx +82 -0
  166. package/templates/app/components/theme-provider.jsx +11 -0
  167. package/templates/app/components/theme-toggle.jsx +38 -0
  168. package/templates/app/components/ui/button.jsx +21 -0
  169. package/templates/app/components/ui/card.jsx +23 -0
  170. package/templates/app/components/ui/input.jsx +10 -0
  171. package/templates/app/components/ui/label.jsx +10 -0
  172. package/templates/app/crons/page.js +5 -0
  173. package/templates/app/globals.css +90 -0
  174. package/templates/app/layout.js +33 -0
  175. package/templates/app/login/page.js +15 -0
  176. package/templates/app/notifications/page.js +7 -0
  177. package/templates/app/page.js +7 -0
  178. package/templates/app/pull-requests/page.js +7 -0
  179. package/templates/app/settings/crons/page.js +5 -0
  180. package/templates/app/settings/layout.js +7 -0
  181. package/templates/app/settings/page.js +5 -0
  182. package/templates/app/settings/secrets/page.js +5 -0
  183. package/templates/app/settings/triggers/page.js +5 -0
  184. package/templates/app/stream/chat/route.js +1 -0
  185. package/templates/app/swarm/page.js +7 -0
  186. package/templates/app/triggers/page.js +5 -0
  187. package/templates/config/CODE_PLANNING.md +14 -0
  188. package/templates/config/CRONS.json +56 -0
  189. package/templates/config/HEARTBEAT.md +3 -0
  190. package/templates/config/JOB_AGENT.md +30 -0
  191. package/templates/config/JOB_PLANNING.md +240 -0
  192. package/templates/config/JOB_SUMMARY.md +130 -0
  193. package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
  194. package/templates/config/SOUL.md +48 -0
  195. package/templates/config/TRIGGERS.json +58 -0
  196. package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
  197. package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
  198. package/templates/docker/claude-code-job/Dockerfile +34 -0
  199. package/templates/docker/claude-code-job/entrypoint.sh +149 -0
  200. package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
  201. package/templates/docker/claude-code-workspace/Dockerfile +61 -0
  202. package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
  203. package/templates/docker/event-handler/Dockerfile +20 -0
  204. package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
  205. package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
  206. package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
  207. package/templates/docker-compose.local.yml +78 -0
  208. package/templates/docker-compose.yml +64 -0
  209. package/templates/instrumentation.js +6 -0
  210. package/templates/middleware.js +23 -0
  211. package/templates/next.config.mjs +3 -0
  212. package/templates/postcss.config.mjs +5 -0
  213. package/templates/public/favicon.ico +0 -0
  214. package/templates/server.js +25 -0
  215. package/templates/skills/LICENSE +21 -0
  216. package/templates/skills/README.md +119 -0
  217. package/templates/skills/brave-search/SKILL.md +79 -0
  218. package/templates/skills/brave-search/content.js +86 -0
  219. package/templates/skills/brave-search/package-lock.json +621 -0
  220. package/templates/skills/brave-search/package.json +14 -0
  221. package/templates/skills/brave-search/search.js +199 -0
  222. package/templates/skills/browser-tools/SKILL.md +196 -0
  223. package/templates/skills/browser-tools/browser-content.js +103 -0
  224. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  225. package/templates/skills/browser-tools/browser-eval.js +53 -0
  226. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  227. package/templates/skills/browser-tools/browser-nav.js +44 -0
  228. package/templates/skills/browser-tools/browser-pick.js +162 -0
  229. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  230. package/templates/skills/browser-tools/browser-start.js +87 -0
  231. package/templates/skills/browser-tools/package-lock.json +2556 -0
  232. package/templates/skills/browser-tools/package.json +19 -0
  233. package/templates/skills/google-docs/SKILL.md +23 -0
  234. package/templates/skills/google-docs/create.sh +69 -0
  235. package/templates/skills/google-drive/SKILL.md +47 -0
  236. package/templates/skills/google-drive/delete.sh +47 -0
  237. package/templates/skills/google-drive/download.sh +50 -0
  238. package/templates/skills/google-drive/list.sh +41 -0
  239. package/templates/skills/google-drive/upload.sh +76 -0
  240. package/templates/skills/kie-ai/SKILL.md +38 -0
  241. package/templates/skills/kie-ai/generate-image.sh +77 -0
  242. package/templates/skills/kie-ai/generate-video.sh +69 -0
  243. package/templates/skills/llm-secrets/SKILL.md +34 -0
  244. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  245. package/templates/skills/modify-self/SKILL.md +12 -0
  246. package/templates/skills/youtube-transcript/SKILL.md +41 -0
  247. package/templates/skills/youtube-transcript/package-lock.json +24 -0
  248. package/templates/skills/youtube-transcript/package.json +8 -0
  249. package/templates/skills/youtube-transcript/transcript.js +84 -0
@@ -0,0 +1,342 @@
1
+ import { HumanMessage, AIMessage } from '@langchain/core/messages';
2
+ import { z } from 'zod';
3
+ import { getJobAgent, getCodeAgent } 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, linkChatToWorkspace } from '../db/chats.js';
8
+
9
+ /**
10
+ * Ensure a chat exists in the DB and save a message.
11
+ * Centralized so every channel gets persistence automatically.
12
+ *
13
+ * @param {string} threadId - Chat/thread ID
14
+ * @param {string} role - 'user' or 'assistant'
15
+ * @param {string} text - Message text
16
+ * @param {object} [options] - { userId, chatTitle }
17
+ */
18
+ function persistMessage(threadId, role, text, options = {}) {
19
+ try {
20
+ if (!getChatById(threadId)) {
21
+ createChat(options.userId || 'unknown', options.chatTitle || 'New Chat', threadId);
22
+ }
23
+ saveMessage(threadId, role, text);
24
+ } catch (err) {
25
+ // DB persistence is best-effort — don't break chat if DB fails
26
+ console.error('Failed to persist message:', err);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Process a chat message through the LangGraph agent.
32
+ * Saves user and assistant messages to the DB automatically.
33
+ *
34
+ * @param {string} threadId - Conversation thread ID (from channel adapter)
35
+ * @param {string} message - User's message text
36
+ * @param {Array} [attachments=[]] - Normalized attachments from adapter
37
+ * @param {object} [options] - { userId, chatTitle } for DB persistence
38
+ * @returns {Promise<string>} AI response text
39
+ */
40
+ async function chat(threadId, message, attachments = [], options = {}) {
41
+ const agent = await getJobAgent();
42
+
43
+ // Save user message to DB
44
+ persistMessage(threadId, 'user', message || '[attachment]', options);
45
+
46
+ // Build content blocks: text + any image attachments as base64 vision
47
+ const content = [];
48
+
49
+ if (message) {
50
+ content.push({ type: 'text', text: message });
51
+ }
52
+
53
+ for (const att of attachments) {
54
+ if (att.category === 'image') {
55
+ content.push({
56
+ type: 'image_url',
57
+ image_url: {
58
+ url: `data:${att.mimeType};base64,${att.data.toString('base64')}`,
59
+ },
60
+ });
61
+ }
62
+ // Documents: future handling
63
+ }
64
+
65
+ // If only text and no attachments, simplify to a string
66
+ const messageContent = content.length === 1 && content[0].type === 'text'
67
+ ? content[0].text
68
+ : content;
69
+
70
+ const result = await agent.invoke(
71
+ { messages: [new HumanMessage({ content: messageContent })] },
72
+ { configurable: { thread_id: threadId } }
73
+ );
74
+
75
+ const lastMessage = result.messages[result.messages.length - 1];
76
+
77
+ // LangChain message content can be a string or an array of content blocks
78
+ let response;
79
+ if (typeof lastMessage.content === 'string') {
80
+ response = lastMessage.content;
81
+ } else {
82
+ response = lastMessage.content
83
+ .filter((block) => block.type === 'text')
84
+ .map((block) => block.text)
85
+ .join('\n');
86
+ }
87
+
88
+ // Save assistant response to DB
89
+ persistMessage(threadId, 'assistant', response, options);
90
+
91
+ // Auto-generate title for new chats
92
+ if (options.userId && message) {
93
+ autoTitle(threadId, message).catch(() => {});
94
+ }
95
+
96
+ return response;
97
+ }
98
+
99
+ /**
100
+ * Process a chat message with streaming (for channels that support it).
101
+ * Saves user and assistant messages to the DB automatically.
102
+ *
103
+ * @param {string} threadId - Conversation thread ID
104
+ * @param {string} message - User's message text
105
+ * @param {Array} [attachments=[]] - Image/PDF attachments: { category, mimeType, dataUrl }
106
+ * @param {object} [options] - { userId, chatTitle, skipUserPersist } for DB persistence
107
+ * @returns {AsyncIterableIterator<string>} Stream of text chunks
108
+ */
109
+ async function* chatStream(threadId, message, attachments = [], options = {}) {
110
+ let agent;
111
+
112
+ // Code mode: set up workspace + code agent
113
+ if (options.repo && options.branch) {
114
+ const existingChat = getChatById(threadId);
115
+ let workspaceId;
116
+
117
+ if (!existingChat) {
118
+ // First message: create workspace row, then chat, then link them
119
+ const { createCodeWorkspace } = await import('../db/code-workspaces.js');
120
+ const workspace = createCodeWorkspace(options.userId || 'unknown', {
121
+ repo: options.repo,
122
+ branch: options.branch,
123
+ });
124
+ workspaceId = workspace.id;
125
+ createChat(options.userId || 'unknown', 'New Chat', threadId);
126
+ linkChatToWorkspace(threadId, workspaceId);
127
+ } else {
128
+ workspaceId = existingChat.codeWorkspaceId;
129
+ }
130
+
131
+ agent = await getCodeAgent({
132
+ repo: options.repo,
133
+ branch: options.branch,
134
+ workspaceId,
135
+ chatId: threadId,
136
+ });
137
+ } else {
138
+ agent = await getJobAgent();
139
+ }
140
+
141
+ // Save user message to DB (skip on regeneration — message already exists)
142
+ if (!options.skipUserPersist) {
143
+ persistMessage(threadId, 'user', message || '[attachment]', options);
144
+ }
145
+
146
+ // Build content blocks: text + any image/PDF attachments as vision
147
+ const content = [];
148
+
149
+ if (message) {
150
+ content.push({ type: 'text', text: message });
151
+ }
152
+
153
+ for (const att of attachments) {
154
+ if (att.category === 'image') {
155
+ // Support both dataUrl (web) and Buffer (Telegram) formats
156
+ const url = att.dataUrl
157
+ ? att.dataUrl
158
+ : `data:${att.mimeType};base64,${att.data.toString('base64')}`;
159
+ content.push({
160
+ type: 'image_url',
161
+ image_url: { url },
162
+ });
163
+ }
164
+ }
165
+
166
+ // If only text and no attachments, simplify to a string
167
+ const messageContent = content.length === 1 && content[0].type === 'text'
168
+ ? content[0].text
169
+ : content;
170
+
171
+ try {
172
+ const stream = await agent.stream(
173
+ { messages: [new HumanMessage({ content: messageContent })] },
174
+ { configurable: { thread_id: threadId }, streamMode: 'messages' }
175
+ );
176
+
177
+ let fullText = '';
178
+ let webSearchInput = '';
179
+
180
+ for await (const event of stream) {
181
+ // streamMode: 'messages' yields [message, metadata] tuples
182
+ const msg = Array.isArray(event) ? event[0] : event;
183
+ const msgType = msg._getType?.();
184
+
185
+ if (msgType === 'ai') {
186
+ // Debug: log web search content blocks to see actual shape
187
+ if (Array.isArray(msg.content)) {
188
+ for (const block of msg.content) {
189
+ if (block.type === 'server_tool_use' || block.type === 'server_tool_call' || block.type === 'web_search_tool_result') {
190
+ console.log(`[chatStream] ${block.type}:`, JSON.stringify(block));
191
+ }
192
+ }
193
+ }
194
+
195
+ // Tool calls — AIMessage.tool_calls is an array of { id, name, args }
196
+ if (msg.tool_calls?.length > 0) {
197
+ for (const tc of msg.tool_calls) {
198
+ yield {
199
+ type: 'tool-call',
200
+ toolCallId: tc.id,
201
+ toolName: tc.name,
202
+ args: tc.args,
203
+ };
204
+ }
205
+ }
206
+
207
+ // Text content (wrapped in structured object)
208
+ let text = '';
209
+ if (typeof msg.content === 'string') {
210
+ text = msg.content;
211
+ } else if (Array.isArray(msg.content)) {
212
+ text = msg.content
213
+ .filter((b) => b.type === 'text' && b.text)
214
+ .map((b) => b.text)
215
+ .join('');
216
+ }
217
+
218
+ if (text) {
219
+ fullText += text;
220
+ yield { type: 'text', text };
221
+ }
222
+ } else if (msgType === 'tool') {
223
+ // Tool result — ToolMessage has tool_call_id and content
224
+ yield {
225
+ type: 'tool-result',
226
+ toolCallId: msg.tool_call_id,
227
+ result: msg.content,
228
+ };
229
+ }
230
+ // Skip other message types (human, system)
231
+ }
232
+
233
+ // Save assistant response to DB
234
+ if (fullText) {
235
+ persistMessage(threadId, 'assistant', fullText, options);
236
+ }
237
+
238
+ // Auto-generate title for new chats
239
+ if (options.userId && message) {
240
+ autoTitle(threadId, message).catch(() => {});
241
+ }
242
+ } catch (err) {
243
+ console.error('[chatStream] error:', err);
244
+ throw err;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Auto-generate a chat title from the first user message (fire-and-forget).
250
+ * Uses structured output to avoid thinking-token leaks with extended-thinking models.
251
+ */
252
+ async function autoTitle(threadId, firstMessage) {
253
+ try {
254
+ const chat = getChatById(threadId);
255
+ if (!chat || chat.title !== 'New Chat') return;
256
+
257
+ const model = await createModel({ maxTokens: 250 });
258
+ const response = await model.withStructuredOutput(z.object({ title: z.string() })).invoke([
259
+ ['system', 'Generate a descriptive (8-12 word) title for this chat based on the user\'s first message.'],
260
+ ['human', firstMessage],
261
+ ]);
262
+ if (response.title.trim()) {
263
+ updateChatTitle(threadId, response.title.trim());
264
+ }
265
+ } catch (err) {
266
+ console.error('[autoTitle] Failed to generate title:', err.message);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * One-shot summarization with a different system prompt and no memory.
272
+ * Used for job completion summaries sent via GitHub webhook.
273
+ *
274
+ * @param {object} results - Job results from webhook payload
275
+ * @returns {Promise<string>} Summary text
276
+ */
277
+ async function summarizeJob(results) {
278
+ try {
279
+ const model = await createModel({ maxTokens: 1024 });
280
+ const systemPrompt = render_md(jobSummaryMd);
281
+
282
+ if (!systemPrompt) {
283
+ console.error(`[summarizeJob] Empty system prompt — JOB_SUMMARY.md not found or empty at: ${jobSummaryMd}`);
284
+ }
285
+
286
+ const userMessage = [
287
+ results.job ? `## Task\n${results.job}` : '',
288
+ results.commit_message ? `## Commit Message\n${results.commit_message}` : '',
289
+ results.changed_files?.length ? `## Changed Files\n${results.changed_files.join('\n')}` : '',
290
+ results.status ? `## Status\n${results.status}` : '',
291
+ results.merge_result ? `## Merge Result\n${results.merge_result}` : '',
292
+ results.pr_url ? `## PR URL\n${results.pr_url}` : '',
293
+ results.run_url ? `## Run URL\n${results.run_url}` : '',
294
+ results.log ? `## Agent Log\n${results.log}` : '',
295
+ ]
296
+ .filter(Boolean)
297
+ .join('\n\n');
298
+
299
+ console.log(`[summarizeJob] System prompt: ${systemPrompt.length} chars, user message: ${userMessage.length} chars`);
300
+
301
+ const response = await model.invoke([
302
+ ['system', systemPrompt],
303
+ ['human', userMessage],
304
+ ]);
305
+
306
+ const text =
307
+ typeof response.content === 'string'
308
+ ? response.content
309
+ : response.content
310
+ .filter((block) => block.type === 'text')
311
+ .map((block) => block.text)
312
+ .join('\n');
313
+
314
+ console.log(`[summarizeJob] Result: ${text.length} chars — ${text.slice(0, 200)}`);
315
+
316
+ return text.trim() || 'Job finished.';
317
+ } catch (err) {
318
+ console.error('[summarizeJob] Failed to summarize job:', err);
319
+ return 'Job finished.';
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Inject a message into a thread's memory so the agent has context
325
+ * for future conversations (e.g., job completion summaries).
326
+ *
327
+ * @param {string} threadId - Conversation thread ID
328
+ * @param {string} text - Message text to inject as an assistant message
329
+ */
330
+ async function addToThread(threadId, text) {
331
+ try {
332
+ const agent = await getJobAgent();
333
+ await agent.updateState(
334
+ { configurable: { thread_id: threadId } },
335
+ { messages: [new AIMessage(text)] }
336
+ );
337
+ } catch (err) {
338
+ console.error('Failed to add message to thread:', err);
339
+ }
340
+ }
341
+
342
+ export { chat, chatStream, summarizeJob, addToThread, persistMessage };
@@ -0,0 +1,180 @@
1
+ import { ChatAnthropic } from '@langchain/anthropic';
2
+
3
+ const DEFAULT_MODELS = {
4
+ anthropic: 'claude-sonnet-4-20250514',
5
+ openai: 'gpt-4o',
6
+ google: 'gemini-2.5-pro',
7
+ pragatigpt: 'pragatigpt-1',
8
+ ollama: 'llama3.2',
9
+ custom: 'gpt-4o',
10
+ };
11
+
12
+ /**
13
+ * Create a LangChain chat model based on environment configuration.
14
+ *
15
+ * Supported providers (set via LLM_PROVIDER env var):
16
+ * anthropic — Claude models via Anthropic API (default)
17
+ * openai — GPT models via OpenAI API
18
+ * google — Gemini models via Google AI Studio
19
+ * pragatigpt — PragatiGPT via Gignaati edge API (India-first, on-premise)
20
+ * ollama — Any model running locally via Ollama (zero cloud dependency)
21
+ * custom — Any OpenAI-compatible API endpoint
22
+ *
23
+ * Config env vars:
24
+ * LLM_PROVIDER — Provider name (see above)
25
+ * LLM_MODEL — Model name override
26
+ * LLM_MAX_TOKENS — Max tokens (default: 4096)
27
+ * ANTHROPIC_API_KEY — Required for anthropic
28
+ * OPENAI_API_KEY — Required for openai
29
+ * OPENAI_BASE_URL — Custom base URL for openai/custom
30
+ * GOOGLE_API_KEY — Required for google
31
+ * PRAGATIGPT_API_KEY — Required for pragatigpt
32
+ * PRAGATIGPT_BASE_URL — PragatiGPT endpoint (default: https://api.pragatigpt.in/v1)
33
+ * OLLAMA_BASE_URL — Ollama server URL (default: http://localhost:11434)
34
+ * CUSTOM_API_KEY — API key for custom endpoints
35
+ *
36
+ * @param {object} [options]
37
+ * @param {number} [options.maxTokens=4096] - Max tokens for the response
38
+ * @returns {import('@langchain/core/language_models/chat_models').BaseChatModel}
39
+ */
40
+ export async function createModel(options = {}) {
41
+ const provider = options.providerOverride || process.env.LLM_PROVIDER || 'anthropic';
42
+ const modelName = options.modelOverride || process.env.LLM_MODEL || DEFAULT_MODELS[provider] || DEFAULT_MODELS.anthropic;
43
+ const maxTokens = options.maxTokens || Number(process.env.LLM_MAX_TOKENS) || 4096;
44
+
45
+ switch (provider) {
46
+ case 'anthropic': {
47
+ const apiKey = process.env.ANTHROPIC_API_KEY;
48
+ if (!apiKey) {
49
+ throw new Error(
50
+ 'ANTHROPIC_API_KEY is required.\n' +
51
+ 'Get your key at: https://platform.claude.com/settings/keys\n' +
52
+ 'Then run: npx gigaclaw set-agent-secret ANTHROPIC_API_KEY'
53
+ );
54
+ }
55
+ return new ChatAnthropic({ modelName, maxTokens, anthropicApiKey: apiKey });
56
+ }
57
+
58
+ case 'openai': {
59
+ const { ChatOpenAI } = await import('@langchain/openai');
60
+ const apiKey = process.env.OPENAI_API_KEY;
61
+ const baseURL = process.env.OPENAI_BASE_URL;
62
+ if (!apiKey) {
63
+ throw new Error(
64
+ 'OPENAI_API_KEY is required.\n' +
65
+ 'Get your key at: https://platform.openai.com/settings/organization/api-keys\n' +
66
+ 'Then run: npx gigaclaw set-agent-secret OPENAI_API_KEY'
67
+ );
68
+ }
69
+ const config = { modelName, maxTokens, apiKey };
70
+ if (baseURL) config.configuration = { baseURL };
71
+ return new ChatOpenAI(config);
72
+ }
73
+
74
+ case 'google': {
75
+ const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai');
76
+ const apiKey = process.env.GOOGLE_API_KEY;
77
+ if (!apiKey) {
78
+ throw new Error(
79
+ 'GOOGLE_API_KEY is required.\n' +
80
+ 'Get your key at: https://aistudio.google.com/apikey\n' +
81
+ 'Then run: npx gigaclaw set-agent-secret GOOGLE_API_KEY'
82
+ );
83
+ }
84
+ return new ChatGoogleGenerativeAI({ model: modelName, maxOutputTokens: maxTokens, apiKey });
85
+ }
86
+
87
+ // PragatiGPT — India-first, edge-native AI by Gignaati
88
+ case 'pragatigpt': {
89
+ const { ChatOpenAI } = await import('@langchain/openai');
90
+ const apiKey = process.env.PRAGATIGPT_API_KEY;
91
+ const baseURL = process.env.PRAGATIGPT_BASE_URL || 'https://api.pragatigpt.in/v1';
92
+ if (!apiKey) {
93
+ throw new Error(
94
+ 'PRAGATIGPT_API_KEY is required.\n' +
95
+ 'Get your key at: https://www.gignaati.com/pragatigpt\n' +
96
+ 'Then run: npx gigaclaw set-agent-secret PRAGATIGPT_API_KEY'
97
+ );
98
+ }
99
+ return new ChatOpenAI({
100
+ modelName: modelName || 'pragatigpt-1',
101
+ maxTokens,
102
+ apiKey,
103
+ configuration: { baseURL },
104
+ });
105
+ }
106
+
107
+ // Ollama — run any model locally with zero cloud dependency
108
+ case 'ollama': {
109
+ const { ChatOpenAI } = await import('@langchain/openai');
110
+ const baseURL = (process.env.OLLAMA_BASE_URL || 'http://localhost:11434') + '/v1';
111
+ return new ChatOpenAI({
112
+ modelName: modelName || 'llama3.2',
113
+ maxTokens,
114
+ apiKey: 'ollama',
115
+ configuration: { baseURL },
116
+ });
117
+ }
118
+
119
+ // Custom OpenAI-compatible endpoint (vLLM, LM Studio, Together AI, etc.)
120
+ case 'custom': {
121
+ const { ChatOpenAI } = await import('@langchain/openai');
122
+ const apiKey = process.env.CUSTOM_API_KEY || 'not-needed';
123
+ const baseURL = process.env.OPENAI_BASE_URL;
124
+ if (!baseURL) {
125
+ throw new Error(
126
+ 'OPENAI_BASE_URL is required for the custom provider.\n' +
127
+ 'Examples:\n' +
128
+ ' http://localhost:11434/v1 (Ollama)\n' +
129
+ ' http://localhost:1234/v1 (LM Studio)\n' +
130
+ ' https://api.together.ai/v1 (Together AI)'
131
+ );
132
+ }
133
+ return new ChatOpenAI({ modelName, maxTokens, apiKey, configuration: { baseURL } });
134
+ }
135
+
136
+ default:
137
+ throw new Error(
138
+ `Unknown LLM provider: "${provider}"\n` +
139
+ 'Supported: anthropic, openai, google, pragatigpt, ollama, custom\n' +
140
+ 'Set LLM_PROVIDER in your .env file.'
141
+ );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Get a human-readable label for a provider.
147
+ * @param {string} provider
148
+ * @returns {string}
149
+ */
150
+ export function getProviderLabel(provider) {
151
+ const labels = {
152
+ anthropic: 'Claude (Anthropic)',
153
+ openai: 'GPT (OpenAI)',
154
+ google: 'Gemini (Google)',
155
+ pragatigpt: 'PragatiGPT (Gignaati — India-first)',
156
+ ollama: 'Ollama (Local — Zero Cloud)',
157
+ custom: 'Custom OpenAI-Compatible API',
158
+ };
159
+ return labels[provider] || provider;
160
+ }
161
+
162
+ /**
163
+ * Validate that required env vars are set for a provider.
164
+ * @param {string} provider
165
+ * @returns {{ valid: boolean, error?: string }}
166
+ */
167
+ export function validateProviderConfig(provider) {
168
+ const checks = {
169
+ anthropic: () => process.env.ANTHROPIC_API_KEY ? null : 'ANTHROPIC_API_KEY is not set',
170
+ openai: () => process.env.OPENAI_API_KEY ? null : 'OPENAI_API_KEY is not set',
171
+ google: () => process.env.GOOGLE_API_KEY ? null : 'GOOGLE_API_KEY is not set',
172
+ pragatigpt: () => process.env.PRAGATIGPT_API_KEY ? null : 'PRAGATIGPT_API_KEY is not set',
173
+ ollama: () => null,
174
+ custom: () => process.env.OPENAI_BASE_URL ? null : 'OPENAI_BASE_URL is not set',
175
+ };
176
+ const check = checks[provider];
177
+ if (!check) return { valid: false, error: `Unknown provider: ${provider}` };
178
+ const error = check();
179
+ return error ? { valid: false, error } : { valid: true };
180
+ }