granclaw 0.0.1-beta.6 → 0.0.1-beta.60
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/dist/backend/agent/browser-bin.js +44 -0
- package/dist/backend/agent/captcha-detect.js +50 -0
- package/dist/backend/agent/process.js +20 -58
- package/dist/backend/agent/runner-pi.js +276 -211
- package/dist/backend/agent/telegram-adapter.js +50 -67
- package/dist/backend/agent/telegram-http-client.js +2 -27
- package/dist/backend/agent-db.js +0 -14
- package/dist/backend/app-config.js +26 -0
- package/dist/backend/app-secrets.js +99 -0
- package/dist/backend/assets/stealth-extension/stealth.js +206 -11
- package/dist/backend/browser/session-manager.js +36 -81
- package/dist/backend/browser/stealth.js +52 -64
- package/dist/backend/browser-sessions.js +1 -35
- package/dist/backend/config.js +0 -17
- package/dist/backend/data-db.js +0 -22
- package/dist/backend/esm-import.js +0 -18
- package/dist/backend/extensions/loader.js +90 -0
- package/dist/backend/extensions/types.js +2 -0
- package/dist/backend/index.js +5 -5
- package/dist/backend/integrations/agent-integrations-db.js +53 -0
- package/dist/backend/integrations/registry.js +64 -0
- package/dist/backend/integrations/routes.js +42 -0
- package/dist/backend/integrations/types.js +2 -0
- package/dist/backend/lib/flatten-markdown-tables.js +81 -0
- package/dist/backend/lib/i18n-telegram.js +35 -35
- package/dist/backend/logs-db.js +0 -8
- package/dist/backend/messages-db.js +0 -8
- package/dist/backend/orchestrator/agent-manager.js +6 -33
- package/dist/backend/orchestrator/browser-live.js +208 -173
- package/dist/backend/orchestrator/server.js +67 -191
- package/dist/backend/providers-config.js +67 -27
- package/dist/backend/routes/logs.js +0 -1
- package/dist/backend/scheduler.js +1 -8
- package/dist/backend/schedules-db.js +0 -12
- package/dist/backend/secrets-vault.js +14 -20
- package/dist/backend/takeover-messages.js +0 -31
- package/dist/backend/takeover-state.js +12 -7
- package/dist/backend/takeover-timeout.js +1 -25
- package/dist/backend/tasks-db.js +0 -11
- package/dist/backend/telemetry.js +30 -0
- package/dist/backend/usage-scanner.js +1 -18
- package/dist/backend/workflows/runner.js +2 -28
- package/dist/backend/workflows-db.js +0 -14
- package/dist/backend/workspace-pool.js +7 -17
- package/dist/frontend/assets/index-Bk8kmOwH.css +1 -0
- package/dist/frontend/assets/index-D_JdGJgj.js +157 -0
- package/dist/frontend/index.html +2 -2
- package/dist/home.js +20 -0
- package/dist/index.js +10 -0
- package/dist/telemetry.js +37 -0
- package/package.json +6 -1
- package/templates/AGENT.onboarding.md +19 -2
- package/templates/SYSTEM.md +71 -8
- package/templates/skills/email/SKILL.md +300 -0
- package/templates/skills/email/gmcli.sh +55 -0
- package/templates/skills/email/read-imap.py +231 -0
- package/templates/skills/email/send-smtp.py +104 -0
- package/templates/skills/skill-creator/SKILL.md +188 -0
- package/templates/skills/whatsapp/SKILL.md +147 -0
- package/templates/skills/whatsapp/whatsapp.sh +20 -0
- package/dist/frontend/assets/index-7sH8_RHK.css +0 -1
- package/dist/frontend/assets/index-CavSoqWA.js +0 -143
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerBrowserProvider = registerBrowserProvider;
|
|
7
|
+
exports._resetBrowserProvidersForTests = _resetBrowserProvidersForTests;
|
|
8
|
+
exports.buildArgv = buildArgv;
|
|
9
|
+
exports.resolveBrowserBinary = resolveBrowserBinary;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const stealth_js_1 = require("../browser/stealth.js");
|
|
13
|
+
const providers = [];
|
|
14
|
+
function registerBrowserProvider(provider) {
|
|
15
|
+
providers.push(provider);
|
|
16
|
+
}
|
|
17
|
+
function _resetBrowserProvidersForTests() {
|
|
18
|
+
providers.length = 0;
|
|
19
|
+
}
|
|
20
|
+
function buildArgv(res, command, args) {
|
|
21
|
+
return [...res.preCommandArgs, command, ...args, ...res.postCommandArgs];
|
|
22
|
+
}
|
|
23
|
+
function resolveBrowserBinary(agentId, workspaceDir) {
|
|
24
|
+
for (const provider of providers) {
|
|
25
|
+
const resolution = provider(agentId, workspaceDir);
|
|
26
|
+
if (resolution)
|
|
27
|
+
return resolution;
|
|
28
|
+
}
|
|
29
|
+
const bin = process.env.AGENT_BROWSER_BIN ?? 'agent-browser';
|
|
30
|
+
const profileDir = path_1.default.join(workspaceDir, '.browser-profile');
|
|
31
|
+
const preCommandArgs = ['--session', agentId];
|
|
32
|
+
if (fs_1.default.existsSync(profileDir)) {
|
|
33
|
+
preCommandArgs.push('--profile', profileDir);
|
|
34
|
+
}
|
|
35
|
+
preCommandArgs.push(...(0, stealth_js_1.stealthArgv)());
|
|
36
|
+
return {
|
|
37
|
+
bin,
|
|
38
|
+
preCommandArgs,
|
|
39
|
+
postCommandArgs: [],
|
|
40
|
+
env: {},
|
|
41
|
+
isRemote: false,
|
|
42
|
+
recordingSupported: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CAPTCHA_DETECT_JS = void 0;
|
|
4
|
+
exports.CAPTCHA_DETECT_JS = `(function() {
|
|
5
|
+
var title = (document.title || '').toLowerCase();
|
|
6
|
+
|
|
7
|
+
// Cloudflare JS interstitials — the stealth extension can clear these.
|
|
8
|
+
var cfTitlePatterns = [
|
|
9
|
+
'just a moment', // Cloudflare interstitial (EN)
|
|
10
|
+
'un momento', // CF Spanish
|
|
11
|
+
'un instant', // CF French
|
|
12
|
+
'einen moment', // CF German
|
|
13
|
+
'checking your browser', // CF older / generic anti-bot
|
|
14
|
+
'please wait', // generic "please wait while we check"
|
|
15
|
+
];
|
|
16
|
+
var cfSelectors = [
|
|
17
|
+
'#challenge-stage',
|
|
18
|
+
'#challenge-running',
|
|
19
|
+
'#cf-challenge-running',
|
|
20
|
+
'.cf-browser-verification',
|
|
21
|
+
'.cf-im-under-attack',
|
|
22
|
+
];
|
|
23
|
+
for (var i = 0; i < cfTitlePatterns.length; i++) {
|
|
24
|
+
if (title.indexOf(cfTitlePatterns[i]) !== -1) return 'interstitial';
|
|
25
|
+
}
|
|
26
|
+
for (var j = 0; j < cfSelectors.length; j++) {
|
|
27
|
+
if (document.querySelector(cfSelectors[j])) return 'interstitial';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Actual captcha widgets — need human intervention.
|
|
31
|
+
var captchaSelectors = [
|
|
32
|
+
'iframe[src*="captcha-delivery"]', // DataDome
|
|
33
|
+
'iframe[src*="geo.captcha"]', // DataDome geo
|
|
34
|
+
'iframe[src*="recaptcha"]', // reCaptcha v2/v3
|
|
35
|
+
'iframe[src*="hcaptcha"]', // hCaptcha
|
|
36
|
+
'iframe[src*="challenges.cloudflare"]', // Cloudflare Turnstile
|
|
37
|
+
'iframe[src*="arkoselabs"]', // Arkose FunCaptcha
|
|
38
|
+
'iframe[src*="funcaptcha"]',
|
|
39
|
+
'.g-recaptcha',
|
|
40
|
+
'.h-captcha',
|
|
41
|
+
'[class*="captcha"]',
|
|
42
|
+
'#px-captcha', // PerimeterX / HUMAN
|
|
43
|
+
];
|
|
44
|
+
// "Attention Required" is a CF WAF block — not a JS interstitial, needs human.
|
|
45
|
+
if (title.indexOf('attention required') !== -1) return 'captcha';
|
|
46
|
+
for (var k = 0; k < captchaSelectors.length; k++) {
|
|
47
|
+
if (document.querySelector(captchaSelectors[k])) return 'captcha';
|
|
48
|
+
}
|
|
49
|
+
return 'clear';
|
|
50
|
+
})()`;
|
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* agent/process.ts
|
|
4
|
-
*
|
|
5
|
-
* Standalone agent process — spawned by the orchestrator, one per agent.
|
|
6
|
-
*
|
|
7
|
-
* Architecture:
|
|
8
|
-
* WS server → receives messages → [BB evaluate] → enqueue()
|
|
9
|
-
* Queue worker (poll loop) → dequeueNext() → runAgent() → broadcastToChannel chunks
|
|
10
|
-
*
|
|
11
|
-
* WebSocket protocol:
|
|
12
|
-
* Client → Agent: { type: 'message', text: string, channelId?: string }
|
|
13
|
-
* Agent → Client: { type: 'queued' }
|
|
14
|
-
* { type: 'chunk', chunk: StreamChunk }
|
|
15
|
-
* { type: 'error', message: string }
|
|
16
|
-
* { type: 'blocked', reason: string }
|
|
17
|
-
*/
|
|
18
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
4
|
};
|
|
@@ -37,7 +21,6 @@ if (!agentId || !port) {
|
|
|
37
21
|
console.error('[agent/process] AGENT_ID and AGENT_PORT env vars are required');
|
|
38
22
|
process.exit(1);
|
|
39
23
|
}
|
|
40
|
-
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
41
24
|
function main() {
|
|
42
25
|
const agent = (0, config_js_1.getAgent)(agentId);
|
|
43
26
|
if (!agent) {
|
|
@@ -45,13 +28,11 @@ function main() {
|
|
|
45
28
|
process.exit(1);
|
|
46
29
|
}
|
|
47
30
|
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
|
|
48
|
-
|
|
31
|
+
(0, runner_pi_js_1.bootstrapWorkspace)(workspaceDir, agentId);
|
|
49
32
|
const cleaned = (0, agent_db_js_1.cleanupStaleJobs)(workspaceDir);
|
|
50
33
|
if (cleaned > 0)
|
|
51
34
|
console.log(`[agent:${agentId}] cleaned up ${cleaned} stale processing jobs`);
|
|
52
|
-
// ── WebSocket server ───────────────────────────────────────────────────────
|
|
53
35
|
const wss = new ws_1.WebSocketServer({ port });
|
|
54
|
-
// Map from channelId → set of WS clients subscribed to that channel
|
|
55
36
|
const channelClients = new Map();
|
|
56
37
|
function getChannelClients(channelId) {
|
|
57
38
|
if (!channelClients.has(channelId))
|
|
@@ -59,7 +40,7 @@ function main() {
|
|
|
59
40
|
return channelClients.get(channelId);
|
|
60
41
|
}
|
|
61
42
|
wss.on('connection', (ws) => {
|
|
62
|
-
let clientChannelId = 'ui';
|
|
43
|
+
let clientChannelId = 'ui';
|
|
63
44
|
console.log(`[agent:${agentId}] client connected`);
|
|
64
45
|
ws.on('message', (raw) => {
|
|
65
46
|
let msg;
|
|
@@ -76,8 +57,6 @@ function main() {
|
|
|
76
57
|
ws.send(JSON.stringify({ type: 'stopped', killed: stopped }));
|
|
77
58
|
}
|
|
78
59
|
else if (msg.type === 'subscribe' && msg.channelId) {
|
|
79
|
-
// Subscribe this WS client to a channel without sending a message.
|
|
80
|
-
// Used by the frontend to receive live chunks from scheduled runs.
|
|
81
60
|
clientChannelId = msg.channelId;
|
|
82
61
|
getChannelClients(clientChannelId).add(ws);
|
|
83
62
|
}
|
|
@@ -89,7 +68,6 @@ function main() {
|
|
|
89
68
|
}
|
|
90
69
|
});
|
|
91
70
|
ws.on('close', () => {
|
|
92
|
-
// Remove from whichever channel set it was in
|
|
93
71
|
for (const [id, set] of channelClients.entries()) {
|
|
94
72
|
set.delete(ws);
|
|
95
73
|
if (set.size === 0)
|
|
@@ -99,16 +77,11 @@ function main() {
|
|
|
99
77
|
});
|
|
100
78
|
});
|
|
101
79
|
console.log(`[agent:${agentId}] WS listening on ws://localhost:${port}`);
|
|
102
|
-
// ── Telegram adapter ───────────────────────────────────────────────────────
|
|
103
|
-
// Started automatically if TELEGRAM_BOT_TOKEN is set (via Secrets in the UI).
|
|
104
|
-
// The user adds TELEGRAM_BOT_TOKEN as a secret → orchestrator injects it as
|
|
105
|
-
// an env var when spawning this process → adapter picks it up here.
|
|
106
80
|
let telegramAdapter = null;
|
|
107
81
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
108
82
|
if (telegramBotToken) {
|
|
109
83
|
telegramAdapter = new telegram_adapter_js_1.TelegramAdapter(agentId, telegramBotToken, workspaceDir);
|
|
110
84
|
}
|
|
111
|
-
// ── Queue worker ───────────────────────────────────────────────────────────
|
|
112
85
|
function broadcastToChannel(channelId, data) {
|
|
113
86
|
const json = JSON.stringify(data);
|
|
114
87
|
const targets = channelClients.get(channelId);
|
|
@@ -119,14 +92,13 @@ function main() {
|
|
|
119
92
|
ws.send(json);
|
|
120
93
|
}
|
|
121
94
|
}
|
|
122
|
-
// Track busy state per channel type so UI chat can run while workflows/schedules execute
|
|
123
95
|
const busyChannels = new Set();
|
|
124
96
|
function channelType(channelId) {
|
|
125
97
|
if (channelId.startsWith('wf-'))
|
|
126
98
|
return 'workflow';
|
|
127
99
|
if (channelId === 'schedule')
|
|
128
100
|
return 'schedule';
|
|
129
|
-
return channelId;
|
|
101
|
+
return channelId;
|
|
130
102
|
}
|
|
131
103
|
async function processNext() {
|
|
132
104
|
const job = (0, agent_db_js_1.dequeueNext)(workspaceDir, agentId, busyChannels);
|
|
@@ -136,27 +108,15 @@ function main() {
|
|
|
136
108
|
busyChannels.add(lane);
|
|
137
109
|
try {
|
|
138
110
|
const isTelegramJob = telegramAdapter !== null && job.channelId.startsWith('telegram:');
|
|
139
|
-
// Save the prompt so it's visible in run history immediately
|
|
140
111
|
try {
|
|
141
112
|
(0, messages_db_js_1.saveMessage)({ id: (0, crypto_1.randomUUID)(), agentId: agentId, channelId: job.channelId, role: 'user', content: job.message });
|
|
142
113
|
}
|
|
143
|
-
catch {
|
|
144
|
-
// Stream chunks directly to channel clients.
|
|
145
|
-
//
|
|
146
|
-
// tool_call rows are persisted to the DB the moment they arrive
|
|
147
|
-
// (not batched at turn end). Reason: if a user leaves the chat view
|
|
148
|
-
// mid-turn and navigates to /dashboard, ChatPage unmounts and loses
|
|
149
|
-
// its in-memory streaming state. On return, it refetches history
|
|
150
|
-
// from the DB — if tool_calls were still buffered in memory, the
|
|
151
|
-
// user would see an empty chat while the agent was clearly still
|
|
152
|
-
// working. Persisting as-they-happen makes the live state
|
|
153
|
-
// refetchable. See regression A (view-switch-state.spec.ts).
|
|
114
|
+
catch { }
|
|
154
115
|
let fullResponse = '';
|
|
155
116
|
let toolCallCount = 0;
|
|
156
|
-
// Inject context message if a human takeover was pending
|
|
157
117
|
let messageText = job.message;
|
|
158
118
|
if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
159
|
-
(0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
|
|
119
|
+
(0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
|
|
160
120
|
messageText =
|
|
161
121
|
`[User completed browser interaction]\n` +
|
|
162
122
|
`User said: "${job.message}"`;
|
|
@@ -169,6 +129,18 @@ function main() {
|
|
|
169
129
|
telegramAdapter.appendChunk(job.channelId, chunk.text);
|
|
170
130
|
}
|
|
171
131
|
}
|
|
132
|
+
if (chunk.type === 'takeover_requested') {
|
|
133
|
+
const takeoverUrl = chunk.takeoverUrl;
|
|
134
|
+
if (isTelegramJob) {
|
|
135
|
+
telegramAdapter.notifyTakeover(job.channelId, takeoverUrl);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
broadcastToChannel(job.channelId, {
|
|
139
|
+
type: 'chunk',
|
|
140
|
+
chunk: { type: 'text', text: `\n\n🔗 **Takeover link:** ${takeoverUrl}\n\n` },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
172
144
|
if (chunk.type === 'tool_call') {
|
|
173
145
|
const tcString = `${chunk.tool}(${JSON.stringify(chunk.input)})`;
|
|
174
146
|
toolCallCount++;
|
|
@@ -181,16 +153,12 @@ function main() {
|
|
|
181
153
|
content: tcString,
|
|
182
154
|
});
|
|
183
155
|
}
|
|
184
|
-
catch {
|
|
156
|
+
catch { }
|
|
185
157
|
if (isTelegramJob) {
|
|
186
|
-
// Live status update — appears in the user's chat as the
|
|
187
|
-
// acknowledgment message gets edited to show progress.
|
|
188
158
|
void telegramAdapter.appendToolStep(job.channelId, chunk.tool);
|
|
189
159
|
}
|
|
190
160
|
}
|
|
191
161
|
}, { channelId: job.channelId });
|
|
192
|
-
// Persist the final assistant message. tool_call rows were already
|
|
193
|
-
// saved one-by-one above, so no batch here.
|
|
194
162
|
try {
|
|
195
163
|
if (fullResponse) {
|
|
196
164
|
(0, messages_db_js_1.saveMessage)({
|
|
@@ -199,13 +167,12 @@ function main() {
|
|
|
199
167
|
channelId: job.channelId,
|
|
200
168
|
role: 'assistant',
|
|
201
169
|
content: fullResponse,
|
|
202
|
-
createdAt: Date.now() + toolCallCount,
|
|
170
|
+
createdAt: Date.now() + toolCallCount,
|
|
203
171
|
});
|
|
204
172
|
}
|
|
205
173
|
}
|
|
206
|
-
catch {
|
|
174
|
+
catch { }
|
|
207
175
|
(0, agent_db_js_1.markDone)(workspaceDir, job.id);
|
|
208
|
-
// Arm 10-minute timeout if the agent registered a takeover during this run
|
|
209
176
|
if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
210
177
|
const timer = setTimeout(() => {
|
|
211
178
|
(0, takeover_timeout_js_1.handleTakeoverTimeout)(agentId, workspaceDir).catch((err) => {
|
|
@@ -214,14 +181,9 @@ function main() {
|
|
|
214
181
|
}, takeover_state_js_1.TAKEOVER_TIMEOUT_MS);
|
|
215
182
|
(0, takeover_state_js_1.updateTakeoverTimer)(agentId, timer);
|
|
216
183
|
}
|
|
217
|
-
// Belt-and-suspenders: if the agent left a browser session open (e.g.
|
|
218
|
-
// forgot to call close), finalize it so recordings don't stay "active"
|
|
219
|
-
// forever and stream subscribers detach cleanly.
|
|
220
|
-
// Skip if a human takeover is pending — browser session must stay alive
|
|
221
184
|
if (!(0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
222
185
|
(0, browser_sessions_js_1.forceCloseActiveSession)(agentId);
|
|
223
186
|
}
|
|
224
|
-
// Send the full reply back to Telegram once the turn is complete
|
|
225
187
|
if (isTelegramJob) {
|
|
226
188
|
await telegramAdapter.flushReply(job.channelId);
|
|
227
189
|
}
|