granclaw 0.0.1-beta.8 → 0.0.1-beta.81

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 (66) hide show
  1. package/dist/backend/agent/browser-bin.js +62 -0
  2. package/dist/backend/agent/message-assembly.js +25 -0
  3. package/dist/backend/agent/process.js +39 -60
  4. package/dist/backend/agent/runner-pi.js +359 -214
  5. package/dist/backend/agent/telegram-adapter.js +17 -71
  6. package/dist/backend/agent/telegram-chats.js +56 -0
  7. package/dist/backend/agent/telegram-http-client.js +2 -27
  8. package/dist/backend/agent/telegram-markdown.js +46 -0
  9. package/dist/backend/agent-db.js +0 -14
  10. package/dist/backend/app-config.js +26 -0
  11. package/dist/backend/app-secrets.js +99 -0
  12. package/dist/backend/assets/stealth-extension/stealth.js +206 -11
  13. package/dist/backend/browser/session-manager.js +41 -89
  14. package/dist/backend/browser/stealth.js +52 -64
  15. package/dist/backend/browser-sessions.js +52 -45
  16. package/dist/backend/config.js +0 -17
  17. package/dist/backend/data-db.js +0 -14
  18. package/dist/backend/esm-import.js +0 -18
  19. package/dist/backend/extensions/loader.js +90 -0
  20. package/dist/backend/extensions/types.js +2 -0
  21. package/dist/backend/index.js +18 -5
  22. package/dist/backend/integrations/agent-integrations-db.js +53 -0
  23. package/dist/backend/integrations/registry.js +64 -0
  24. package/dist/backend/integrations/routes.js +42 -0
  25. package/dist/backend/integrations/types.js +2 -0
  26. package/dist/backend/lib/flatten-markdown-tables.js +81 -0
  27. package/dist/backend/lib/i18n-telegram.js +35 -35
  28. package/dist/backend/logs-db.js +0 -8
  29. package/dist/backend/messages-db.js +16 -8
  30. package/dist/backend/orchestrator/agent-manager.js +4 -32
  31. package/dist/backend/orchestrator/browser-live.js +344 -206
  32. package/dist/backend/orchestrator/server.js +70 -187
  33. package/dist/backend/providers-config.js +67 -27
  34. package/dist/backend/routes/logs.js +0 -1
  35. package/dist/backend/scheduler.js +1 -8
  36. package/dist/backend/schedules-db.js +0 -12
  37. package/dist/backend/secrets-vault.js +0 -6
  38. package/dist/backend/takeover-listeners.js +27 -0
  39. package/dist/backend/takeover-messages.js +0 -31
  40. package/dist/backend/takeover-state.js +12 -7
  41. package/dist/backend/takeover-timeout.js +1 -25
  42. package/dist/backend/takeover-url-resolver.js +29 -0
  43. package/dist/backend/tasks-db.js +0 -11
  44. package/dist/backend/telemetry.js +30 -0
  45. package/dist/backend/usage-scanner.js +1 -18
  46. package/dist/backend/workflows/runner.js +2 -28
  47. package/dist/backend/workflows-db.js +0 -14
  48. package/dist/backend/workspace-pool.js +0 -18
  49. package/dist/frontend/assets/index-23gqD82t.js +157 -0
  50. package/dist/frontend/assets/index-C_L5wqFQ.css +1 -0
  51. package/dist/frontend/index.html +2 -2
  52. package/dist/home.js +20 -0
  53. package/dist/index.js +14 -0
  54. package/dist/telemetry.js +37 -0
  55. package/package.json +6 -1
  56. package/templates/AGENT.onboarding.md +19 -2
  57. package/templates/SYSTEM.md +88 -8
  58. package/templates/skills/email/SKILL.md +300 -0
  59. package/templates/skills/email/gmcli.sh +55 -0
  60. package/templates/skills/email/read-imap.py +231 -0
  61. package/templates/skills/email/send-smtp.py +104 -0
  62. package/templates/skills/skill-creator/SKILL.md +188 -0
  63. package/templates/skills/whatsapp/SKILL.md +147 -0
  64. package/templates/skills/whatsapp/whatsapp.sh +20 -0
  65. package/dist/frontend/assets/index-BmQN0cOF.css +0 -1
  66. package/dist/frontend/assets/index-CDbh9xnJ.js +0 -143
@@ -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
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assembleAssistantMessage = assembleAssistantMessage;
4
+ function assembleAssistantMessage(chunks) {
5
+ let out = '';
6
+ let textRunHasContent = false;
7
+ let pendingToolSeparator = false;
8
+ for (const chunk of chunks) {
9
+ if (chunk.type === 'text') {
10
+ if (!chunk.text)
11
+ continue;
12
+ if (pendingToolSeparator && textRunHasContent) {
13
+ out += '\n\n';
14
+ }
15
+ out += chunk.text;
16
+ textRunHasContent = true;
17
+ pendingToolSeparator = false;
18
+ }
19
+ else if (chunk.type === 'tool_call') {
20
+ if (textRunHasContent)
21
+ pendingToolSeparator = true;
22
+ }
23
+ }
24
+ return out;
25
+ }
@@ -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
  };
@@ -29,6 +13,7 @@ const runner_pi_js_1 = require("./runner-pi.js");
29
13
  const messages_db_js_1 = require("../messages-db.js");
30
14
  const telegram_adapter_js_1 = require("./telegram-adapter.js");
31
15
  const browser_sessions_js_1 = require("../browser-sessions.js");
16
+ const message_assembly_js_1 = require("./message-assembly.js");
32
17
  const takeover_state_js_1 = require("../takeover-state.js");
33
18
  const takeover_timeout_js_1 = require("../takeover-timeout.js");
34
19
  const agentId = process.env.AGENT_ID;
@@ -37,7 +22,6 @@ if (!agentId || !port) {
37
22
  console.error('[agent/process] AGENT_ID and AGENT_PORT env vars are required');
38
23
  process.exit(1);
39
24
  }
40
- // ── Main ───────────────────────────────────────────────────────────────────────
41
25
  function main() {
42
26
  const agent = (0, config_js_1.getAgent)(agentId);
43
27
  if (!agent) {
@@ -45,13 +29,11 @@ function main() {
45
29
  process.exit(1);
46
30
  }
47
31
  const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
48
- // Clean up stale 'processing' jobs from previous crashes/restarts
32
+ (0, runner_pi_js_1.bootstrapWorkspace)(workspaceDir, agentId);
49
33
  const cleaned = (0, agent_db_js_1.cleanupStaleJobs)(workspaceDir);
50
34
  if (cleaned > 0)
51
35
  console.log(`[agent:${agentId}] cleaned up ${cleaned} stale processing jobs`);
52
- // ── WebSocket server ───────────────────────────────────────────────────────
53
36
  const wss = new ws_1.WebSocketServer({ port });
54
- // Map from channelId → set of WS clients subscribed to that channel
55
37
  const channelClients = new Map();
56
38
  function getChannelClients(channelId) {
57
39
  if (!channelClients.has(channelId))
@@ -59,7 +41,7 @@ function main() {
59
41
  return channelClients.get(channelId);
60
42
  }
61
43
  wss.on('connection', (ws) => {
62
- let clientChannelId = 'ui'; // default until client sends a message with channelId
44
+ let clientChannelId = 'ui';
63
45
  console.log(`[agent:${agentId}] client connected`);
64
46
  ws.on('message', (raw) => {
65
47
  let msg;
@@ -76,8 +58,6 @@ function main() {
76
58
  ws.send(JSON.stringify({ type: 'stopped', killed: stopped }));
77
59
  }
78
60
  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
61
  clientChannelId = msg.channelId;
82
62
  getChannelClients(clientChannelId).add(ws);
83
63
  }
@@ -89,7 +69,6 @@ function main() {
89
69
  }
90
70
  });
91
71
  ws.on('close', () => {
92
- // Remove from whichever channel set it was in
93
72
  for (const [id, set] of channelClients.entries()) {
94
73
  set.delete(ws);
95
74
  if (set.size === 0)
@@ -99,16 +78,11 @@ function main() {
99
78
  });
100
79
  });
101
80
  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
81
  let telegramAdapter = null;
107
82
  const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
108
83
  if (telegramBotToken) {
109
84
  telegramAdapter = new telegram_adapter_js_1.TelegramAdapter(agentId, telegramBotToken, workspaceDir);
110
85
  }
111
- // ── Queue worker ───────────────────────────────────────────────────────────
112
86
  function broadcastToChannel(channelId, data) {
113
87
  const json = JSON.stringify(data);
114
88
  const targets = channelClients.get(channelId);
@@ -119,14 +93,13 @@ function main() {
119
93
  ws.send(json);
120
94
  }
121
95
  }
122
- // Track busy state per channel type so UI chat can run while workflows/schedules execute
123
96
  const busyChannels = new Set();
124
97
  function channelType(channelId) {
125
98
  if (channelId.startsWith('wf-'))
126
99
  return 'workflow';
127
100
  if (channelId === 'schedule')
128
101
  return 'schedule';
129
- return channelId; // 'ui', 'telegram:xxx' — each is its own lane
102
+ return channelId;
130
103
  }
131
104
  async function processNext() {
132
105
  const job = (0, agent_db_js_1.dequeueNext)(workspaceDir, agentId, busyChannels);
@@ -136,41 +109,56 @@ function main() {
136
109
  busyChannels.add(lane);
137
110
  try {
138
111
  const isTelegramJob = telegramAdapter !== null && job.channelId.startsWith('telegram:');
139
- // Save the prompt so it's visible in run history immediately
140
112
  try {
141
113
  (0, messages_db_js_1.saveMessage)({ id: (0, crypto_1.randomUUID)(), agentId: agentId, channelId: job.channelId, role: 'user', content: job.message });
142
114
  }
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).
154
- let fullResponse = '';
115
+ catch { }
116
+ const chunkBuffer = [];
155
117
  let toolCallCount = 0;
156
- // Inject context message if a human takeover was pending
157
118
  let messageText = job.message;
158
119
  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
120
+ (0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
160
121
  messageText =
161
122
  `[User completed browser interaction]\n` +
162
123
  `User said: "${job.message}"`;
163
124
  }
125
+ if (job.channelId.startsWith('telegram:')) {
126
+ const proactive = (0, messages_db_js_1.getProactiveMessagesSinceLastUser)(agentId, job.channelId);
127
+ if (proactive.length > 0) {
128
+ const lines = proactive.map((m, i) => {
129
+ const ts = new Date(m.createdAt).toISOString().slice(11, 16) + ' UTC';
130
+ return ` ${i + 1}. [${ts}] ${m.content}`;
131
+ }).join('\n');
132
+ messageText =
133
+ `[System: since your last Telegram exchange you proactively sent the user these messages ` +
134
+ `(they are not in your conversation history but the user received them):\n${lines}\n` +
135
+ `]\n\n` +
136
+ `User's new reply: ${messageText}`;
137
+ }
138
+ }
164
139
  await (0, runner_pi_js_1.runAgent)(agent, messageText, (chunk) => {
165
140
  broadcastToChannel(job.channelId, { type: 'chunk', chunk });
166
141
  if (chunk.type === 'text') {
167
- fullResponse += chunk.text;
142
+ chunkBuffer.push({ type: 'text', text: chunk.text });
168
143
  if (isTelegramJob) {
169
144
  telegramAdapter.appendChunk(job.channelId, chunk.text);
170
145
  }
171
146
  }
147
+ if (chunk.type === 'takeover_requested') {
148
+ const takeoverUrl = chunk.takeoverUrl;
149
+ if (isTelegramJob) {
150
+ telegramAdapter.notifyTakeover(job.channelId, takeoverUrl);
151
+ }
152
+ else {
153
+ broadcastToChannel(job.channelId, {
154
+ type: 'chunk',
155
+ chunk: { type: 'text', text: `\n\n🔗 **Takeover link:** ${takeoverUrl}\n\n` },
156
+ });
157
+ }
158
+ }
172
159
  if (chunk.type === 'tool_call') {
173
160
  const tcString = `${chunk.tool}(${JSON.stringify(chunk.input)})`;
161
+ chunkBuffer.push({ type: 'tool_call', tool: chunk.tool, input: chunk.input });
174
162
  toolCallCount++;
175
163
  try {
176
164
  (0, messages_db_js_1.saveMessage)({
@@ -181,16 +169,13 @@ function main() {
181
169
  content: tcString,
182
170
  });
183
171
  }
184
- catch { /* non-fatal — WAL/locking can fail under parallel writes */ }
172
+ catch { }
185
173
  if (isTelegramJob) {
186
- // Live status update — appears in the user's chat as the
187
- // acknowledgment message gets edited to show progress.
188
174
  void telegramAdapter.appendToolStep(job.channelId, chunk.tool);
189
175
  }
190
176
  }
191
177
  }, { channelId: job.channelId });
192
- // Persist the final assistant message. tool_call rows were already
193
- // saved one-by-one above, so no batch here.
178
+ const fullResponse = (0, message_assembly_js_1.assembleAssistantMessage)(chunkBuffer);
194
179
  try {
195
180
  if (fullResponse) {
196
181
  (0, messages_db_js_1.saveMessage)({
@@ -199,13 +184,12 @@ function main() {
199
184
  channelId: job.channelId,
200
185
  role: 'assistant',
201
186
  content: fullResponse,
202
- createdAt: Date.now() + toolCallCount, // ordered after the last tool_call
187
+ createdAt: Date.now() + toolCallCount,
203
188
  });
204
189
  }
205
190
  }
206
- catch { /* non-fatal */ }
191
+ catch { }
207
192
  (0, agent_db_js_1.markDone)(workspaceDir, job.id);
208
- // Arm 10-minute timeout if the agent registered a takeover during this run
209
193
  if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
210
194
  const timer = setTimeout(() => {
211
195
  (0, takeover_timeout_js_1.handleTakeoverTimeout)(agentId, workspaceDir).catch((err) => {
@@ -214,14 +198,9 @@ function main() {
214
198
  }, takeover_state_js_1.TAKEOVER_TIMEOUT_MS);
215
199
  (0, takeover_state_js_1.updateTakeoverTimer)(agentId, timer);
216
200
  }
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
201
  if (!(0, takeover_state_js_1.hasTakeover)(agentId)) {
222
202
  (0, browser_sessions_js_1.forceCloseActiveSession)(agentId);
223
203
  }
224
- // Send the full reply back to Telegram once the turn is complete
225
204
  if (isTelegramJob) {
226
205
  await telegramAdapter.flushReply(job.channelId);
227
206
  }