granclaw 0.0.1-beta.59 → 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 +0 -21
- package/dist/backend/agent/process.js +7 -66
- package/dist/backend/agent/runner-pi.js +16 -258
- package/dist/backend/agent/telegram-adapter.js +3 -83
- package/dist/backend/agent/telegram-http-client.js +2 -27
- package/dist/backend/agent-db.js +0 -14
- package/dist/backend/app-config.js +1 -5
- package/dist/backend/app-secrets.js +99 -0
- package/dist/backend/browser/session-manager.js +7 -88
- package/dist/backend/browser/stealth.js +0 -69
- package/dist/backend/browser-sessions.js +1 -35
- package/dist/backend/config.js +0 -17
- package/dist/backend/data-db.js +0 -14
- 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 +0 -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 +2 -29
- package/dist/backend/lib/i18n-telegram.js +0 -50
- package/dist/backend/logs-db.js +0 -8
- package/dist/backend/messages-db.js +0 -8
- package/dist/backend/orchestrator/agent-manager.js +2 -32
- package/dist/backend/orchestrator/browser-live.js +168 -190
- package/dist/backend/orchestrator/server.js +24 -185
- package/dist/backend/providers-config.js +2 -25
- 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 +0 -6
- package/dist/backend/takeover-messages.js +0 -31
- package/dist/backend/takeover-state.js +2 -22
- package/dist/backend/takeover-timeout.js +1 -25
- package/dist/backend/tasks-db.js +0 -11
- package/dist/backend/telemetry.js +1 -6
- 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 +0 -18
- package/dist/frontend/assets/index-D_JdGJgj.js +157 -0
- package/dist/frontend/index.html +1 -1
- package/package.json +1 -1
- package/dist/frontend/assets/index-d0qjk25P.js +0 -157
|
@@ -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
|
+
}
|
|
@@ -1,25 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* captcha-detect.ts
|
|
4
|
-
*
|
|
5
|
-
* A stringified IIFE we ship to `agent-browser eval` to decide whether the
|
|
6
|
-
* currently-open page is blocked behind some kind of bot/captcha wall. The
|
|
7
|
-
* runner polls this snippet after every navigation command so the stealth
|
|
8
|
-
* extension has a chance to clear Cloudflare interstitials before the agent
|
|
9
|
-
* starts reading the page.
|
|
10
|
-
*
|
|
11
|
-
* Returns one of three values:
|
|
12
|
-
* 'interstitial' — Cloudflare JS challenge ("Just a moment..."). The stealth
|
|
13
|
-
* extension can clear these; the runner should poll and wait.
|
|
14
|
-
* 'captcha' — An actual captcha widget (reCAPTCHA, hCaptcha, Turnstile,
|
|
15
|
-
* etc.) that requires human intervention. The runner should
|
|
16
|
-
* request a takeover immediately.
|
|
17
|
-
* 'clear' — No challenge detected.
|
|
18
|
-
*
|
|
19
|
-
* IMPORTANT: this runs as a string inside Chrome, NOT in Node. Keep it
|
|
20
|
-
* ES5-flavoured so it stays safe if the agent-browser build ever targets an
|
|
21
|
-
* older chromium.
|
|
22
|
-
*/
|
|
23
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
3
|
exports.CAPTCHA_DETECT_JS = void 0;
|
|
25
4
|
exports.CAPTCHA_DETECT_JS = `(function() {
|
|
@@ -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,20 +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
|
-
// Eager bootstrap on startup: syncs skills from the image template dir
|
|
49
|
-
// BEFORE the first user turn, so "[runner-pi] synced skill ..." and any
|
|
50
|
-
// frontmatter-validation warnings land in docker logs at container start
|
|
51
|
-
// instead of only after a message has been processed. runAgent() also
|
|
52
|
-
// calls bootstrapWorkspace() defensively on every turn, but the per-
|
|
53
|
-
// process memoisation there makes it a no-op after this call.
|
|
54
31
|
(0, runner_pi_js_1.bootstrapWorkspace)(workspaceDir, agentId);
|
|
55
|
-
// Clean up stale 'processing' jobs from previous crashes/restarts
|
|
56
32
|
const cleaned = (0, agent_db_js_1.cleanupStaleJobs)(workspaceDir);
|
|
57
33
|
if (cleaned > 0)
|
|
58
34
|
console.log(`[agent:${agentId}] cleaned up ${cleaned} stale processing jobs`);
|
|
59
|
-
// ── WebSocket server ───────────────────────────────────────────────────────
|
|
60
35
|
const wss = new ws_1.WebSocketServer({ port });
|
|
61
|
-
// Map from channelId → set of WS clients subscribed to that channel
|
|
62
36
|
const channelClients = new Map();
|
|
63
37
|
function getChannelClients(channelId) {
|
|
64
38
|
if (!channelClients.has(channelId))
|
|
@@ -66,7 +40,7 @@ function main() {
|
|
|
66
40
|
return channelClients.get(channelId);
|
|
67
41
|
}
|
|
68
42
|
wss.on('connection', (ws) => {
|
|
69
|
-
let clientChannelId = 'ui';
|
|
43
|
+
let clientChannelId = 'ui';
|
|
70
44
|
console.log(`[agent:${agentId}] client connected`);
|
|
71
45
|
ws.on('message', (raw) => {
|
|
72
46
|
let msg;
|
|
@@ -83,8 +57,6 @@ function main() {
|
|
|
83
57
|
ws.send(JSON.stringify({ type: 'stopped', killed: stopped }));
|
|
84
58
|
}
|
|
85
59
|
else if (msg.type === 'subscribe' && msg.channelId) {
|
|
86
|
-
// Subscribe this WS client to a channel without sending a message.
|
|
87
|
-
// Used by the frontend to receive live chunks from scheduled runs.
|
|
88
60
|
clientChannelId = msg.channelId;
|
|
89
61
|
getChannelClients(clientChannelId).add(ws);
|
|
90
62
|
}
|
|
@@ -96,7 +68,6 @@ function main() {
|
|
|
96
68
|
}
|
|
97
69
|
});
|
|
98
70
|
ws.on('close', () => {
|
|
99
|
-
// Remove from whichever channel set it was in
|
|
100
71
|
for (const [id, set] of channelClients.entries()) {
|
|
101
72
|
set.delete(ws);
|
|
102
73
|
if (set.size === 0)
|
|
@@ -106,16 +77,11 @@ function main() {
|
|
|
106
77
|
});
|
|
107
78
|
});
|
|
108
79
|
console.log(`[agent:${agentId}] WS listening on ws://localhost:${port}`);
|
|
109
|
-
// ── Telegram adapter ───────────────────────────────────────────────────────
|
|
110
|
-
// Started automatically if TELEGRAM_BOT_TOKEN is set (via Secrets in the UI).
|
|
111
|
-
// The user adds TELEGRAM_BOT_TOKEN as a secret → orchestrator injects it as
|
|
112
|
-
// an env var when spawning this process → adapter picks it up here.
|
|
113
80
|
let telegramAdapter = null;
|
|
114
81
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
115
82
|
if (telegramBotToken) {
|
|
116
83
|
telegramAdapter = new telegram_adapter_js_1.TelegramAdapter(agentId, telegramBotToken, workspaceDir);
|
|
117
84
|
}
|
|
118
|
-
// ── Queue worker ───────────────────────────────────────────────────────────
|
|
119
85
|
function broadcastToChannel(channelId, data) {
|
|
120
86
|
const json = JSON.stringify(data);
|
|
121
87
|
const targets = channelClients.get(channelId);
|
|
@@ -126,14 +92,13 @@ function main() {
|
|
|
126
92
|
ws.send(json);
|
|
127
93
|
}
|
|
128
94
|
}
|
|
129
|
-
// Track busy state per channel type so UI chat can run while workflows/schedules execute
|
|
130
95
|
const busyChannels = new Set();
|
|
131
96
|
function channelType(channelId) {
|
|
132
97
|
if (channelId.startsWith('wf-'))
|
|
133
98
|
return 'workflow';
|
|
134
99
|
if (channelId === 'schedule')
|
|
135
100
|
return 'schedule';
|
|
136
|
-
return channelId;
|
|
101
|
+
return channelId;
|
|
137
102
|
}
|
|
138
103
|
async function processNext() {
|
|
139
104
|
const job = (0, agent_db_js_1.dequeueNext)(workspaceDir, agentId, busyChannels);
|
|
@@ -143,27 +108,15 @@ function main() {
|
|
|
143
108
|
busyChannels.add(lane);
|
|
144
109
|
try {
|
|
145
110
|
const isTelegramJob = telegramAdapter !== null && job.channelId.startsWith('telegram:');
|
|
146
|
-
// Save the prompt so it's visible in run history immediately
|
|
147
111
|
try {
|
|
148
112
|
(0, messages_db_js_1.saveMessage)({ id: (0, crypto_1.randomUUID)(), agentId: agentId, channelId: job.channelId, role: 'user', content: job.message });
|
|
149
113
|
}
|
|
150
|
-
catch {
|
|
151
|
-
// Stream chunks directly to channel clients.
|
|
152
|
-
//
|
|
153
|
-
// tool_call rows are persisted to the DB the moment they arrive
|
|
154
|
-
// (not batched at turn end). Reason: if a user leaves the chat view
|
|
155
|
-
// mid-turn and navigates to /dashboard, ChatPage unmounts and loses
|
|
156
|
-
// its in-memory streaming state. On return, it refetches history
|
|
157
|
-
// from the DB — if tool_calls were still buffered in memory, the
|
|
158
|
-
// user would see an empty chat while the agent was clearly still
|
|
159
|
-
// working. Persisting as-they-happen makes the live state
|
|
160
|
-
// refetchable. See regression A (view-switch-state.spec.ts).
|
|
114
|
+
catch { }
|
|
161
115
|
let fullResponse = '';
|
|
162
116
|
let toolCallCount = 0;
|
|
163
|
-
// Inject context message if a human takeover was pending
|
|
164
117
|
let messageText = job.message;
|
|
165
118
|
if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
166
|
-
(0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
|
|
119
|
+
(0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
|
|
167
120
|
messageText =
|
|
168
121
|
`[User completed browser interaction]\n` +
|
|
169
122
|
`User said: "${job.message}"`;
|
|
@@ -182,8 +135,6 @@ function main() {
|
|
|
182
135
|
telegramAdapter.notifyTakeover(job.channelId, takeoverUrl);
|
|
183
136
|
}
|
|
184
137
|
else {
|
|
185
|
-
// For UI/WebSocket channels inject the URL as a text chunk so it
|
|
186
|
-
// appears inline in the streaming response.
|
|
187
138
|
broadcastToChannel(job.channelId, {
|
|
188
139
|
type: 'chunk',
|
|
189
140
|
chunk: { type: 'text', text: `\n\n🔗 **Takeover link:** ${takeoverUrl}\n\n` },
|
|
@@ -202,16 +153,12 @@ function main() {
|
|
|
202
153
|
content: tcString,
|
|
203
154
|
});
|
|
204
155
|
}
|
|
205
|
-
catch {
|
|
156
|
+
catch { }
|
|
206
157
|
if (isTelegramJob) {
|
|
207
|
-
// Live status update — appears in the user's chat as the
|
|
208
|
-
// acknowledgment message gets edited to show progress.
|
|
209
158
|
void telegramAdapter.appendToolStep(job.channelId, chunk.tool);
|
|
210
159
|
}
|
|
211
160
|
}
|
|
212
161
|
}, { channelId: job.channelId });
|
|
213
|
-
// Persist the final assistant message. tool_call rows were already
|
|
214
|
-
// saved one-by-one above, so no batch here.
|
|
215
162
|
try {
|
|
216
163
|
if (fullResponse) {
|
|
217
164
|
(0, messages_db_js_1.saveMessage)({
|
|
@@ -220,13 +167,12 @@ function main() {
|
|
|
220
167
|
channelId: job.channelId,
|
|
221
168
|
role: 'assistant',
|
|
222
169
|
content: fullResponse,
|
|
223
|
-
createdAt: Date.now() + toolCallCount,
|
|
170
|
+
createdAt: Date.now() + toolCallCount,
|
|
224
171
|
});
|
|
225
172
|
}
|
|
226
173
|
}
|
|
227
|
-
catch {
|
|
174
|
+
catch { }
|
|
228
175
|
(0, agent_db_js_1.markDone)(workspaceDir, job.id);
|
|
229
|
-
// Arm 10-minute timeout if the agent registered a takeover during this run
|
|
230
176
|
if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
231
177
|
const timer = setTimeout(() => {
|
|
232
178
|
(0, takeover_timeout_js_1.handleTakeoverTimeout)(agentId, workspaceDir).catch((err) => {
|
|
@@ -235,14 +181,9 @@ function main() {
|
|
|
235
181
|
}, takeover_state_js_1.TAKEOVER_TIMEOUT_MS);
|
|
236
182
|
(0, takeover_state_js_1.updateTakeoverTimer)(agentId, timer);
|
|
237
183
|
}
|
|
238
|
-
// Belt-and-suspenders: if the agent left a browser session open (e.g.
|
|
239
|
-
// forgot to call close), finalize it so recordings don't stay "active"
|
|
240
|
-
// forever and stream subscribers detach cleanly.
|
|
241
|
-
// Skip if a human takeover is pending — browser session must stay alive
|
|
242
184
|
if (!(0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
243
185
|
(0, browser_sessions_js_1.forceCloseActiveSession)(agentId);
|
|
244
186
|
}
|
|
245
|
-
// Send the full reply back to Telegram once the turn is complete
|
|
246
187
|
if (isTelegramJob) {
|
|
247
188
|
await telegramAdapter.flushReply(job.channelId);
|
|
248
189
|
}
|