granclaw 0.0.1-beta.7 → 0.0.1-beta.70

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 (63) hide show
  1. package/dist/backend/agent/browser-bin.js +62 -0
  2. package/dist/backend/agent/process.js +34 -58
  3. package/dist/backend/agent/runner-pi.js +300 -211
  4. package/dist/backend/agent/telegram-adapter.js +17 -71
  5. package/dist/backend/agent/telegram-chats.js +56 -0
  6. package/dist/backend/agent/telegram-http-client.js +2 -27
  7. package/dist/backend/agent/telegram-markdown.js +46 -0
  8. package/dist/backend/agent-db.js +0 -14
  9. package/dist/backend/app-config.js +26 -0
  10. package/dist/backend/app-secrets.js +99 -0
  11. package/dist/backend/assets/stealth-extension/stealth.js +206 -11
  12. package/dist/backend/browser/session-manager.js +36 -81
  13. package/dist/backend/browser/stealth.js +52 -64
  14. package/dist/backend/browser-sessions.js +1 -35
  15. package/dist/backend/config.js +0 -17
  16. package/dist/backend/data-db.js +0 -22
  17. package/dist/backend/esm-import.js +0 -18
  18. package/dist/backend/extensions/loader.js +90 -0
  19. package/dist/backend/extensions/types.js +2 -0
  20. package/dist/backend/index.js +5 -5
  21. package/dist/backend/integrations/agent-integrations-db.js +53 -0
  22. package/dist/backend/integrations/registry.js +64 -0
  23. package/dist/backend/integrations/routes.js +42 -0
  24. package/dist/backend/integrations/types.js +2 -0
  25. package/dist/backend/lib/flatten-markdown-tables.js +81 -0
  26. package/dist/backend/lib/i18n-telegram.js +35 -35
  27. package/dist/backend/logs-db.js +0 -8
  28. package/dist/backend/messages-db.js +16 -8
  29. package/dist/backend/orchestrator/agent-manager.js +6 -33
  30. package/dist/backend/orchestrator/browser-live.js +222 -172
  31. package/dist/backend/orchestrator/server.js +72 -193
  32. package/dist/backend/providers-config.js +67 -27
  33. package/dist/backend/routes/logs.js +0 -1
  34. package/dist/backend/scheduler.js +1 -8
  35. package/dist/backend/schedules-db.js +0 -12
  36. package/dist/backend/secrets-vault.js +14 -20
  37. package/dist/backend/takeover-messages.js +0 -31
  38. package/dist/backend/takeover-state.js +12 -7
  39. package/dist/backend/takeover-timeout.js +1 -25
  40. package/dist/backend/tasks-db.js +0 -11
  41. package/dist/backend/telemetry.js +30 -0
  42. package/dist/backend/usage-scanner.js +1 -18
  43. package/dist/backend/workflows/runner.js +2 -28
  44. package/dist/backend/workflows-db.js +0 -14
  45. package/dist/backend/workspace-pool.js +7 -17
  46. package/dist/frontend/assets/index-D6ymPAzT.js +157 -0
  47. package/dist/frontend/assets/index-DR4thYOU.css +1 -0
  48. package/dist/frontend/index.html +2 -2
  49. package/dist/home.js +20 -0
  50. package/dist/index.js +14 -0
  51. package/dist/telemetry.js +37 -0
  52. package/package.json +6 -1
  53. package/templates/AGENT.onboarding.md +19 -2
  54. package/templates/SYSTEM.md +71 -8
  55. package/templates/skills/email/SKILL.md +300 -0
  56. package/templates/skills/email/gmcli.sh +55 -0
  57. package/templates/skills/email/read-imap.py +231 -0
  58. package/templates/skills/email/send-smtp.py +104 -0
  59. package/templates/skills/skill-creator/SKILL.md +188 -0
  60. package/templates/skills/whatsapp/SKILL.md +147 -0
  61. package/templates/skills/whatsapp/whatsapp.sh +20 -0
  62. package/dist/frontend/assets/index-CYJChurq.js +0 -143
  63. package/dist/frontend/assets/index-DETqMNr5.css +0 -1
@@ -0,0 +1,62 @@
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
+ async function resolveBrowserBinary(agentId, workspaceDir) {
24
+ for (const provider of providers) {
25
+ const resolution = await provider(agentId, workspaceDir);
26
+ if (resolution)
27
+ return resolution;
28
+ }
29
+ const cdpFile = `/tmp/granclaw-cdp-${agentId}.url`;
30
+ if (fs_1.default.existsSync(cdpFile)) {
31
+ try {
32
+ const wsUrl = fs_1.default.readFileSync(cdpFile, 'utf8').trim();
33
+ const port = new URL(wsUrl.replace('ws://', 'http://')).port;
34
+ if (port) {
35
+ return {
36
+ bin: process.env.AGENT_BROWSER_BIN ?? 'agent-browser',
37
+ preCommandArgs: ['--cdp', port, '--session', agentId],
38
+ postCommandArgs: [],
39
+ env: {},
40
+ isRemote: false,
41
+ recordingSupported: true,
42
+ };
43
+ }
44
+ }
45
+ catch { }
46
+ }
47
+ const bin = process.env.AGENT_BROWSER_BIN ?? 'agent-browser';
48
+ const profileDir = path_1.default.join(workspaceDir, '.browser-profile');
49
+ const preCommandArgs = ['--session', agentId];
50
+ if (fs_1.default.existsSync(profileDir)) {
51
+ preCommandArgs.push('--profile', profileDir);
52
+ }
53
+ preCommandArgs.push(...(0, stealth_js_1.stealthArgv)());
54
+ return {
55
+ bin,
56
+ preCommandArgs,
57
+ postCommandArgs: [],
58
+ env: {},
59
+ isRemote: false,
60
+ recordingSupported: true,
61
+ };
62
+ }
@@ -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
- // Clean up stale 'processing' jobs from previous crashes/restarts
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'; // default until client sends a message with channelId
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; // 'ui', 'telegram:xxx' — each is its own lane
101
+ return channelId;
130
102
  }
131
103
  async function processNext() {
132
104
  const job = (0, agent_db_js_1.dequeueNext)(workspaceDir, agentId, busyChannels);
@@ -136,31 +108,33 @@ 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 { /* non-fatal */ }
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); // stop 10min timeout — entry stays for runner-pi to restore handle
119
+ (0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
160
120
  messageText =
161
121
  `[User completed browser interaction]\n` +
162
122
  `User said: "${job.message}"`;
163
123
  }
124
+ if (job.channelId.startsWith('telegram:')) {
125
+ const proactive = (0, messages_db_js_1.getProactiveMessagesSinceLastUser)(agentId, job.channelId);
126
+ if (proactive.length > 0) {
127
+ const lines = proactive.map((m, i) => {
128
+ const ts = new Date(m.createdAt).toISOString().slice(11, 16) + ' UTC';
129
+ return ` ${i + 1}. [${ts}] ${m.content}`;
130
+ }).join('\n');
131
+ messageText =
132
+ `[System: since your last Telegram exchange you proactively sent the user these messages ` +
133
+ `(they are not in your conversation history but the user received them):\n${lines}\n` +
134
+ `]\n\n` +
135
+ `User's new reply: ${messageText}`;
136
+ }
137
+ }
164
138
  await (0, runner_pi_js_1.runAgent)(agent, messageText, (chunk) => {
165
139
  broadcastToChannel(job.channelId, { type: 'chunk', chunk });
166
140
  if (chunk.type === 'text') {
@@ -169,6 +143,18 @@ function main() {
169
143
  telegramAdapter.appendChunk(job.channelId, chunk.text);
170
144
  }
171
145
  }
146
+ if (chunk.type === 'takeover_requested') {
147
+ const takeoverUrl = chunk.takeoverUrl;
148
+ if (isTelegramJob) {
149
+ telegramAdapter.notifyTakeover(job.channelId, takeoverUrl);
150
+ }
151
+ else {
152
+ broadcastToChannel(job.channelId, {
153
+ type: 'chunk',
154
+ chunk: { type: 'text', text: `\n\n🔗 **Takeover link:** ${takeoverUrl}\n\n` },
155
+ });
156
+ }
157
+ }
172
158
  if (chunk.type === 'tool_call') {
173
159
  const tcString = `${chunk.tool}(${JSON.stringify(chunk.input)})`;
174
160
  toolCallCount++;
@@ -181,16 +167,12 @@ function main() {
181
167
  content: tcString,
182
168
  });
183
169
  }
184
- catch { /* non-fatal — WAL/locking can fail under parallel writes */ }
170
+ catch { }
185
171
  if (isTelegramJob) {
186
- // Live status update — appears in the user's chat as the
187
- // acknowledgment message gets edited to show progress.
188
172
  void telegramAdapter.appendToolStep(job.channelId, chunk.tool);
189
173
  }
190
174
  }
191
175
  }, { 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
176
  try {
195
177
  if (fullResponse) {
196
178
  (0, messages_db_js_1.saveMessage)({
@@ -199,13 +181,12 @@ function main() {
199
181
  channelId: job.channelId,
200
182
  role: 'assistant',
201
183
  content: fullResponse,
202
- createdAt: Date.now() + toolCallCount, // ordered after the last tool_call
184
+ createdAt: Date.now() + toolCallCount,
203
185
  });
204
186
  }
205
187
  }
206
- catch { /* non-fatal */ }
188
+ catch { }
207
189
  (0, agent_db_js_1.markDone)(workspaceDir, job.id);
208
- // Arm 10-minute timeout if the agent registered a takeover during this run
209
190
  if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
210
191
  const timer = setTimeout(() => {
211
192
  (0, takeover_timeout_js_1.handleTakeoverTimeout)(agentId, workspaceDir).catch((err) => {
@@ -214,14 +195,9 @@ function main() {
214
195
  }, takeover_state_js_1.TAKEOVER_TIMEOUT_MS);
215
196
  (0, takeover_state_js_1.updateTakeoverTimer)(agentId, timer);
216
197
  }
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
198
  if (!(0, takeover_state_js_1.hasTakeover)(agentId)) {
222
199
  (0, browser_sessions_js_1.forceCloseActiveSession)(agentId);
223
200
  }
224
- // Send the full reply back to Telegram once the turn is complete
225
201
  if (isTelegramJob) {
226
202
  await telegramAdapter.flushReply(job.channelId);
227
203
  }