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.
- package/LICENSE +26 -0
- package/README.md +237 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +265 -0
- package/bin/cli.js +823 -0
- package/bin/local.sh +85 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +26 -0
- package/config/instrumentation.js +62 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_nostalgic_sersi.sql +11 -0
- package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
- package/drizzle/0003_rename_code_workspaces.sql +5 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0001_snapshot.json +390 -0
- package/drizzle/meta/0002_snapshot.json +411 -0
- package/drizzle/meta/0003_snapshot.json +419 -0
- package/drizzle/meta/_journal.json +34 -0
- package/lib/actions.js +44 -0
- package/lib/ai/agent.js +86 -0
- package/lib/ai/index.js +342 -0
- package/lib/ai/model.js +180 -0
- package/lib/ai/tools.js +269 -0
- package/lib/ai/web-search.js +42 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +62 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +579 -0
- package/lib/chat/api.js +140 -0
- package/lib/chat/components/app-sidebar.js +213 -0
- package/lib/chat/components/app-sidebar.jsx +279 -0
- package/lib/chat/components/chat-header.js +192 -0
- package/lib/chat/components/chat-header.jsx +223 -0
- package/lib/chat/components/chat-input.js +236 -0
- package/lib/chat/components/chat-input.jsx +249 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +99 -0
- package/lib/chat/components/chat-page.jsx +121 -0
- package/lib/chat/components/chat.js +153 -0
- package/lib/chat/components/chat.jsx +199 -0
- package/lib/chat/components/chats-page.js +367 -0
- package/lib/chat/components/chats-page.jsx +394 -0
- package/lib/chat/components/code-mode-toggle.js +132 -0
- package/lib/chat/components/code-mode-toggle.jsx +163 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/greeting.js +11 -0
- package/lib/chat/components/greeting.jsx +16 -0
- package/lib/chat/components/icons.js +805 -0
- package/lib/chat/components/icons.jsx +751 -0
- package/lib/chat/components/index.js +20 -0
- package/lib/chat/components/message.js +363 -0
- package/lib/chat/components/message.jsx +422 -0
- package/lib/chat/components/messages.js +65 -0
- package/lib/chat/components/messages.jsx +74 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/pull-requests-page.js +103 -0
- package/lib/chat/components/pull-requests-page.jsx +113 -0
- package/lib/chat/components/settings-layout.js +39 -0
- package/lib/chat/components/settings-layout.jsx +53 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +138 -0
- package/lib/chat/components/sidebar-history-item.jsx +119 -0
- package/lib/chat/components/sidebar-history.js +167 -0
- package/lib/chat/components/sidebar-history.jsx +220 -0
- package/lib/chat/components/sidebar-user-nav.js +61 -0
- package/lib/chat/components/sidebar-user-nav.jsx +77 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/tool-call.js +89 -0
- package/lib/chat/components/tool-call.jsx +107 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/combobox.js +98 -0
- package/lib/chat/components/ui/combobox.jsx +114 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +194 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
- package/lib/chat/components/ui/rename-dialog.js +78 -0
- package/lib/chat/components/ui/rename-dialog.jsx +74 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +228 -0
- package/lib/chat/components/ui/sidebar.jsx +246 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/code/actions.js +153 -0
- package/lib/code/code-page.js +22 -0
- package/lib/code/code-page.jsx +25 -0
- package/lib/code/index.js +1 -0
- package/lib/code/terminal-view.js +201 -0
- package/lib/code/terminal-view.jsx +224 -0
- package/lib/code/ws-proxy.js +80 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +168 -0
- package/lib/db/code-workspaces.js +110 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +66 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/paths.js +42 -0
- package/lib/tools/create-job.js +97 -0
- package/lib/tools/docker.js +146 -0
- package/lib/tools/github.js +271 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +104 -0
- package/lib/utils/render-md.js +111 -0
- package/package.json +118 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +105 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-cloud.mjs +833 -0
- package/setup/setup-local.mjs +377 -0
- package/setup/setup-telegram.mjs +265 -0
- package/setup/setup.mjs +87 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +104 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +308 -0
- package/templates/app/api/[...gigaclaw]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +9 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
- package/templates/app/components/ascii-logo.jsx +12 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +33 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/pull-requests/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/CODE_PLANNING.md +14 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_AGENT.md +30 -0
- package/templates/config/JOB_PLANNING.md +240 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
- package/templates/config/SOUL.md +48 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
- package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
- package/templates/docker/claude-code-job/Dockerfile +34 -0
- package/templates/docker/claude-code-job/entrypoint.sh +149 -0
- package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
- package/templates/docker/claude-code-workspace/Dockerfile +61 -0
- package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
- package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
- package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
- package/templates/docker-compose.local.yml +78 -0
- package/templates/docker-compose.yml +64 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +23 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/server.js +25 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/google-docs/SKILL.md +23 -0
- package/templates/skills/google-docs/create.sh +69 -0
- package/templates/skills/google-drive/SKILL.md +47 -0
- package/templates/skills/google-drive/delete.sh +47 -0
- package/templates/skills/google-drive/download.sh +50 -0
- package/templates/skills/google-drive/list.sh +41 -0
- package/templates/skills/google-drive/upload.sh +76 -0
- package/templates/skills/kie-ai/SKILL.md +38 -0
- package/templates/skills/kie-ai/generate-image.sh +77 -0
- package/templates/skills/kie-ai/generate-video.sh +69 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- package/templates/skills/modify-self/SKILL.md +12 -0
- package/templates/skills/youtube-transcript/SKILL.md +41 -0
- package/templates/skills/youtube-transcript/package-lock.json +24 -0
- package/templates/skills/youtube-transcript/package.json +8 -0
- package/templates/skills/youtube-transcript/transcript.js +84 -0
package/lib/ai/index.js
ADDED
|
@@ -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 };
|
package/lib/ai/model.js
ADDED
|
@@ -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
|
+
}
|