visionclaw 0.1.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/README.md +116 -0
- package/dist/agent/context.d.ts +56 -0
- package/dist/agent/context.d.ts.map +1 -0
- package/dist/agent/context.js +142 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/loop.d.ts +18 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +323 -0
- package/dist/agent/loop.js.map +1 -0
- package/dist/agent/session.d.ts +49 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +200 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/agent/system-prompt.d.ts +10 -0
- package/dist/agent/system-prompt.d.ts.map +1 -0
- package/dist/agent/system-prompt.js +167 -0
- package/dist/agent/system-prompt.js.map +1 -0
- package/dist/calendar/google-calendar.d.ts +46 -0
- package/dist/calendar/google-calendar.d.ts.map +1 -0
- package/dist/calendar/google-calendar.js +132 -0
- package/dist/calendar/google-calendar.js.map +1 -0
- package/dist/calendar/scheduler.d.ts +7 -0
- package/dist/calendar/scheduler.d.ts.map +1 -0
- package/dist/calendar/scheduler.js +33 -0
- package/dist/calendar/scheduler.js.map +1 -0
- package/dist/channels/discord.d.ts +19 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +169 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/gmail.d.ts +31 -0
- package/dist/channels/gmail.d.ts.map +1 -0
- package/dist/channels/gmail.js +300 -0
- package/dist/channels/gmail.js.map +1 -0
- package/dist/channels/interface.d.ts +45 -0
- package/dist/channels/interface.d.ts.map +1 -0
- package/dist/channels/interface.js +2 -0
- package/dist/channels/interface.js.map +1 -0
- package/dist/channels/manager.d.ts +36 -0
- package/dist/channels/manager.d.ts.map +1 -0
- package/dist/channels/manager.js +108 -0
- package/dist/channels/manager.js.map +1 -0
- package/dist/channels/queue.d.ts +17 -0
- package/dist/channels/queue.d.ts.map +1 -0
- package/dist/channels/queue.js +85 -0
- package/dist/channels/queue.js.map +1 -0
- package/dist/channels/slack.d.ts +17 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +142 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/sms.d.ts +19 -0
- package/dist/channels/sms.d.ts.map +1 -0
- package/dist/channels/sms.js +111 -0
- package/dist/channels/sms.js.map +1 -0
- package/dist/channels/telegram.d.ts +28 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +246 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +28 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +292 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/config/index.d.ts +24 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +104 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +227 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +45 -0
- package/dist/config/types.js.map +1 -0
- package/dist/files.d.ts +20 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +82 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +384 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/store.d.ts +24 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +71 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/obs/server.d.ts +10 -0
- package/dist/obs/server.d.ts.map +1 -0
- package/dist/obs/server.js +406 -0
- package/dist/obs/server.js.map +1 -0
- package/dist/onboarding/google-auth.d.ts +11 -0
- package/dist/onboarding/google-auth.d.ts.map +1 -0
- package/dist/onboarding/google-auth.js +113 -0
- package/dist/onboarding/google-auth.js.map +1 -0
- package/dist/onboarding/index.d.ts +2 -0
- package/dist/onboarding/index.d.ts.map +1 -0
- package/dist/onboarding/index.js +213 -0
- package/dist/onboarding/index.js.map +1 -0
- package/dist/onboarding/macos-permissions.d.ts +37 -0
- package/dist/onboarding/macos-permissions.d.ts.map +1 -0
- package/dist/onboarding/macos-permissions.js +207 -0
- package/dist/onboarding/macos-permissions.js.map +1 -0
- package/dist/skills/install.d.ts +7 -0
- package/dist/skills/install.d.ts.map +1 -0
- package/dist/skills/install.js +63 -0
- package/dist/skills/install.js.map +1 -0
- package/dist/tools/browser.d.ts +7 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +202 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/calendar.d.ts +12 -0
- package/dist/tools/calendar.d.ts.map +1 -0
- package/dist/tools/calendar.js +210 -0
- package/dist/tools/calendar.js.map +1 -0
- package/dist/tools/computer-use.d.ts +28 -0
- package/dist/tools/computer-use.d.ts.map +1 -0
- package/dist/tools/computer-use.js +311 -0
- package/dist/tools/computer-use.js.map +1 -0
- package/dist/tools/coordinate-resolver.d.ts +26 -0
- package/dist/tools/coordinate-resolver.d.ts.map +1 -0
- package/dist/tools/coordinate-resolver.js +157 -0
- package/dist/tools/coordinate-resolver.js.map +1 -0
- package/dist/tools/desktop-executor.d.ts +52 -0
- package/dist/tools/desktop-executor.d.ts.map +1 -0
- package/dist/tools/desktop-executor.js +202 -0
- package/dist/tools/desktop-executor.js.map +1 -0
- package/dist/tools/finish.d.ts +5 -0
- package/dist/tools/finish.d.ts.map +1 -0
- package/dist/tools/finish.js +18 -0
- package/dist/tools/finish.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory.d.ts +14 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +269 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/notify.d.ts +12 -0
- package/dist/tools/notify.d.ts.map +1 -0
- package/dist/tools/notify.js +108 -0
- package/dist/tools/notify.js.map +1 -0
- package/dist/tools/screenshot.d.ts +7 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +189 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/skill.d.ts +8 -0
- package/dist/tools/skill.d.ts.map +1 -0
- package/dist/tools/skill.js +133 -0
- package/dist/tools/skill.js.map +1 -0
- package/dist/tools/upgrade.d.ts +5 -0
- package/dist/tools/upgrade.d.ts.map +1 -0
- package/dist/tools/upgrade.js +89 -0
- package/dist/tools/upgrade.js.map +1 -0
- package/dist/tools/wait.d.ts +5 -0
- package/dist/tools/wait.d.ts.map +1 -0
- package/dist/tools/wait.js +21 -0
- package/dist/tools/wait.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# VisionClaw
|
|
2
|
+
|
|
3
|
+
A personal assistant agent that runs on your desktop (macOS/Windows). It receives command messages from pre-configured channels (Gmail, Telegram, Discord) and executes tasks autonomously using desktop control and browser automation. Results are sent back through the same channel.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Autonomous Desktop Agent**: Runs continuously as a long-running process on your computer
|
|
8
|
+
- **Gmail Identity**: The agent has its own Gmail account for email and Google Calendar
|
|
9
|
+
- **Multi-Channel Support**: Receives commands via Gmail, Telegram, Discord
|
|
10
|
+
- **Desktop Control**: Takes screenshots, controls mouse/keyboard, runs terminal commands
|
|
11
|
+
- **Browser Automation**: Navigate and interact with web pages via Playwright
|
|
12
|
+
- **Google Calendar**: Manages its own schedule for recurring tasks and reminders
|
|
13
|
+
- **Self-Improving**: Can add new skills to itself and upgrade to new versions
|
|
14
|
+
- **Runtime Observability**: Built-in HTTP obs page for live logs while the agent is running
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
VisionClaw is built on the [Claude Agent SDK V2](https://platform.claude.com/docs/en/agent-sdk/typescript-v2-preview). It runs as a single-threaded agent with a wake/sleep loop triggered by incoming messages or a periodic heartbeat.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
src/
|
|
22
|
+
index.ts # CLI entry point
|
|
23
|
+
onboarding/ # Interactive setup wizard (Gmail, OAuth, channels)
|
|
24
|
+
agent/ # Core agent loop, session management, context
|
|
25
|
+
tools/ # Custom tools (notify, browser, calendar, screenshot, etc.)
|
|
26
|
+
channels/ # Channel adapters (Gmail, Telegram, Discord)
|
|
27
|
+
calendar/ # Google Calendar integration
|
|
28
|
+
memory/ # Persistent memory store
|
|
29
|
+
config/ # Configuration management
|
|
30
|
+
obs/ # Runtime observability HTTP server
|
|
31
|
+
gui/ # Electron desktop GUI
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
|
|
36
|
+
- Node.js >= 20
|
|
37
|
+
- An Anthropic API key
|
|
38
|
+
- A dedicated Gmail account for the agent
|
|
39
|
+
- Google Cloud OAuth2 credentials (Client ID + Secret) with Gmail and Calendar API enabled
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Install dependencies
|
|
45
|
+
pnpm install
|
|
46
|
+
|
|
47
|
+
# Build
|
|
48
|
+
pnpm run build
|
|
49
|
+
|
|
50
|
+
# Run (starts onboarding if not configured)
|
|
51
|
+
pnpm start
|
|
52
|
+
|
|
53
|
+
# Or run in development mode
|
|
54
|
+
pnpm run dev
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The first run triggers an interactive onboarding wizard that will:
|
|
58
|
+
1. Ask for your Anthropic API key
|
|
59
|
+
2. Ask for a dedicated Gmail address for the agent
|
|
60
|
+
3. Walk through Google OAuth2 authorization (Gmail + Calendar scopes)
|
|
61
|
+
4. Optionally configure Telegram and Discord
|
|
62
|
+
|
|
63
|
+
Configuration is stored per profile at `~/.visionclaw/profiles/<profile>/config.json`.
|
|
64
|
+
|
|
65
|
+
## Observability (HTTP)
|
|
66
|
+
|
|
67
|
+
When the agent is running, it serves a local observability page showing live logs.
|
|
68
|
+
|
|
69
|
+
- URL: `http://127.0.0.1:3101/obs`
|
|
70
|
+
- SSE stream: `GET /obs/events`
|
|
71
|
+
- Snapshot: `GET /obs/snapshot`
|
|
72
|
+
|
|
73
|
+
This is controlled via **advanced config** (not asked during onboarding):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"obs": {
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"host": "127.0.0.1",
|
|
80
|
+
"port": 3101,
|
|
81
|
+
"bufferSize": 1000
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## GUI
|
|
87
|
+
|
|
88
|
+
A desktop GUI (Electron) is available for configuration and monitoring:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cd gui
|
|
92
|
+
pnpm install
|
|
93
|
+
pnpm start
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Channels
|
|
97
|
+
|
|
98
|
+
| Channel | Requirements | Status |
|
|
99
|
+
|-----------|----------------------------------|-------------|
|
|
100
|
+
| Gmail | Gmail account (required) | Always on |
|
|
101
|
+
| Telegram | Bot token from @BotFather | Optional |
|
|
102
|
+
| Discord | Bot token + channel allowlist | Optional |
|
|
103
|
+
|
|
104
|
+
## Custom Tools
|
|
105
|
+
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
|---------------------|-------------|
|
|
108
|
+
| `wait` | Pause execution for a specified duration |
|
|
109
|
+
| `notify_user` | Send a message back through a channel |
|
|
110
|
+
| `finish` | Signal task completion, return to sleep |
|
|
111
|
+
| `take_screenshot` | Capture desktop screenshot |
|
|
112
|
+
| `manage_skills` | Install/list skills (built-in + user) |
|
|
113
|
+
| `upgrade` | Check for and install updates |
|
|
114
|
+
| `manage_calendar` | Manage Google Calendar |
|
|
115
|
+
| `memory` | View/create/edit files in the agent memory store |
|
|
116
|
+
| `computer_use_*` | Mouse/keyboard/scroll/drag/screenshot actions |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { VisionClawConfig } from "../config/types.js";
|
|
2
|
+
import type { CommandMessage } from "../channels/interface.js";
|
|
3
|
+
/**
|
|
4
|
+
* A content block in a multimodal user message.
|
|
5
|
+
* Matches the Anthropic Messages API content block types.
|
|
6
|
+
*/
|
|
7
|
+
export type ContentBlock = {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "image";
|
|
12
|
+
source: {
|
|
13
|
+
type: "base64";
|
|
14
|
+
media_type: "image/png";
|
|
15
|
+
data: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "url";
|
|
18
|
+
url: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Screenshot reference: either a public URL (from Google Drive)
|
|
23
|
+
* or a raw base64 string as fallback.
|
|
24
|
+
*/
|
|
25
|
+
export interface ScreenshotRef {
|
|
26
|
+
url?: string;
|
|
27
|
+
base64?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the context message injected on each agent wake.
|
|
31
|
+
* Returns a multimodal content block array with text + optional image.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildWakeContext(options: {
|
|
34
|
+
config: VisionClawConfig;
|
|
35
|
+
trigger: "heartbeat" | "message";
|
|
36
|
+
messages: CommandMessage[];
|
|
37
|
+
calendarSummary: string;
|
|
38
|
+
screenshot?: ScreenshotRef;
|
|
39
|
+
}): ContentBlock[];
|
|
40
|
+
/**
|
|
41
|
+
* Build context for an interrupt message — a new user message that arrives
|
|
42
|
+
* while the agent is already processing. Lighter than a full wake context
|
|
43
|
+
* (no screenshot or calendar).
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildInterruptContext(options: {
|
|
46
|
+
messages: CommandMessage[];
|
|
47
|
+
}): ContentBlock[];
|
|
48
|
+
/**
|
|
49
|
+
* Build a fresh context after compaction. Returns multimodal blocks.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildPostCompactionContext(options: {
|
|
52
|
+
config: VisionClawConfig;
|
|
53
|
+
calendarSummary: string;
|
|
54
|
+
pendingMessageCount: number;
|
|
55
|
+
}): ContentBlock[];
|
|
56
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/agent/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;GAGG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IACE,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EACF;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,UAAU,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GACzD;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;CAClC,CAAC;AAEN;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE;IACxC,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,WAAW,GAAG,SAAS,CAAC;IACjC,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B,GAAG,YAAY,EAAE,CAuFjB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE;IAC7C,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B,GAAG,YAAY,EAAE,CA0CjB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,MAAM,EAAE,gBAAgB,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B,GAAG,YAAY,EAAE,CAqBjB"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the context message injected on each agent wake.
|
|
3
|
+
* Returns a multimodal content block array with text + optional image.
|
|
4
|
+
*/
|
|
5
|
+
export function buildWakeContext(options) {
|
|
6
|
+
const { trigger, messages, calendarSummary, screenshot } = options;
|
|
7
|
+
const now = new Date().toLocaleString();
|
|
8
|
+
const blocks = [];
|
|
9
|
+
// --- Main text block ---
|
|
10
|
+
const parts = [];
|
|
11
|
+
parts.push(`[Wake Event] trigger=${trigger} time=${now}`);
|
|
12
|
+
parts.push("");
|
|
13
|
+
// Calendar summary
|
|
14
|
+
if (calendarSummary) {
|
|
15
|
+
parts.push("## Upcoming Calendar Events");
|
|
16
|
+
parts.push(calendarSummary);
|
|
17
|
+
parts.push("");
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
parts.push("## Calendar");
|
|
21
|
+
parts.push("No upcoming events.");
|
|
22
|
+
parts.push("");
|
|
23
|
+
}
|
|
24
|
+
// Queued messages
|
|
25
|
+
if (messages.length > 0) {
|
|
26
|
+
parts.push("## New Messages");
|
|
27
|
+
for (const msg of messages) {
|
|
28
|
+
parts.push(`- [${msg.channel}] from ${msg.sender} at ${msg.timestamp}: ${msg.text}`);
|
|
29
|
+
if (msg.attachments.length > 0) {
|
|
30
|
+
for (const att of msg.attachments) {
|
|
31
|
+
const location = att.url ?? att.localPath ?? "(embedded)";
|
|
32
|
+
parts.push(` Attachment [${att.type}]: ${location}${att.filename ? ` (${att.filename})` : ""}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
parts.push("");
|
|
37
|
+
}
|
|
38
|
+
else if (trigger === "heartbeat") {
|
|
39
|
+
parts.push("## Messages");
|
|
40
|
+
parts.push("No new messages.");
|
|
41
|
+
parts.push("");
|
|
42
|
+
}
|
|
43
|
+
// Screenshot note
|
|
44
|
+
if (screenshot?.url ?? screenshot?.base64) {
|
|
45
|
+
parts.push("## Desktop Screenshot");
|
|
46
|
+
parts.push("A screenshot of the current desktop is attached below.");
|
|
47
|
+
parts.push("");
|
|
48
|
+
}
|
|
49
|
+
parts.push("Review the above context and decide what to do. If there is nothing to do, call the `finish` tool.");
|
|
50
|
+
blocks.push({ type: "text", text: parts.join("\n") });
|
|
51
|
+
// --- Message attachment image blocks ---
|
|
52
|
+
for (const msg of messages) {
|
|
53
|
+
for (const att of msg.attachments) {
|
|
54
|
+
if (att.type === "image" && att.url) {
|
|
55
|
+
blocks.push({
|
|
56
|
+
type: "image",
|
|
57
|
+
source: { type: "url", url: att.url },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// --- Screenshot image block ---
|
|
63
|
+
if (screenshot?.url) {
|
|
64
|
+
blocks.push({
|
|
65
|
+
type: "image",
|
|
66
|
+
source: { type: "url", url: screenshot.url },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else if (screenshot?.base64) {
|
|
70
|
+
blocks.push({
|
|
71
|
+
type: "image",
|
|
72
|
+
source: {
|
|
73
|
+
type: "base64",
|
|
74
|
+
media_type: "image/png",
|
|
75
|
+
data: screenshot.base64,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return blocks;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build context for an interrupt message — a new user message that arrives
|
|
83
|
+
* while the agent is already processing. Lighter than a full wake context
|
|
84
|
+
* (no screenshot or calendar).
|
|
85
|
+
*/
|
|
86
|
+
export function buildInterruptContext(options) {
|
|
87
|
+
const { messages } = options;
|
|
88
|
+
const now = new Date().toLocaleString();
|
|
89
|
+
const blocks = [];
|
|
90
|
+
const parts = [];
|
|
91
|
+
parts.push(`[Interrupt] New message(s) received at ${now} while you are working.`);
|
|
92
|
+
parts.push("");
|
|
93
|
+
for (const msg of messages) {
|
|
94
|
+
parts.push(`- [${msg.channel}] from ${msg.sender} at ${msg.timestamp}: ${msg.text}`);
|
|
95
|
+
if (msg.attachments.length > 0) {
|
|
96
|
+
for (const att of msg.attachments) {
|
|
97
|
+
const location = att.url ?? att.localPath ?? "(embedded)";
|
|
98
|
+
parts.push(` Attachment [${att.type}]: ${location}${att.filename ? ` (${att.filename})` : ""}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
parts.push("");
|
|
103
|
+
parts.push("Decide whether to acknowledge, switch tasks, or continue your current work.");
|
|
104
|
+
blocks.push({ type: "text", text: parts.join("\n") });
|
|
105
|
+
// Attach images from the new messages
|
|
106
|
+
for (const msg of messages) {
|
|
107
|
+
for (const att of msg.attachments) {
|
|
108
|
+
if (att.type === "image" && att.url) {
|
|
109
|
+
blocks.push({
|
|
110
|
+
type: "image",
|
|
111
|
+
source: { type: "url", url: att.url },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return blocks;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build a fresh context after compaction. Returns multimodal blocks.
|
|
120
|
+
*/
|
|
121
|
+
export function buildPostCompactionContext(options) {
|
|
122
|
+
const { config, calendarSummary, pendingMessageCount } = options;
|
|
123
|
+
const now = new Date().toLocaleString();
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `[Post-Compaction State Refresh] time=${now}
|
|
128
|
+
|
|
129
|
+
Your identity: ${config.agentName} <${config.gmail}>
|
|
130
|
+
Platform: ${process.platform}
|
|
131
|
+
|
|
132
|
+
## Calendar
|
|
133
|
+
${calendarSummary || "No upcoming events."}
|
|
134
|
+
|
|
135
|
+
## Message Queue
|
|
136
|
+
${pendingMessageCount > 0 ? `${pendingMessageCount} message(s) pending in queue.` : "No pending messages."}
|
|
137
|
+
|
|
138
|
+
Continue from where you left off based on the compaction summary above.`,
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/agent/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { VisionClawConfig } from "../config/types.js";
|
|
2
|
+
import { MessageQueue } from "../channels/queue.js";
|
|
3
|
+
import { ChannelManager } from "../channels/manager.js";
|
|
4
|
+
/** Shared state visible to tools (e.g. notify_user needs channel access) */
|
|
5
|
+
export interface AgentState {
|
|
6
|
+
config: VisionClawConfig;
|
|
7
|
+
channelManager: ChannelManager;
|
|
8
|
+
messageQueue: MessageQueue;
|
|
9
|
+
busy: boolean;
|
|
10
|
+
lastMessageChannel?: string;
|
|
11
|
+
lastMessageSender?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getAgentState(): AgentState;
|
|
14
|
+
/**
|
|
15
|
+
* Main agent loop. This is the entry point that runs forever.
|
|
16
|
+
*/
|
|
17
|
+
export declare function startAgentLoop(config: VisionClawConfig): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=loop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAM3D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAMxD,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAKD,wBAAgB,aAAa,IAAI,UAAU,CAK1C;AAmBD;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,IAAI,CAAC,CAiWf"}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { buildSystemPrompt } from "./system-prompt.js";
|
|
2
|
+
import { AgentSession } from "./session.js";
|
|
3
|
+
import { buildWakeContext, buildInterruptContext } from "./context.js";
|
|
4
|
+
import { createToolServer } from "../tools/index.js";
|
|
5
|
+
import { MessageQueue } from "../channels/queue.js";
|
|
6
|
+
import { ChannelManager } from "../channels/manager.js";
|
|
7
|
+
import { takeScreenshot } from "../tools/screenshot.js";
|
|
8
|
+
import { getCalendarSummary } from "../calendar/scheduler.js";
|
|
9
|
+
import { initScreenshotUploader, uploadScreenshot } from "../files.js";
|
|
10
|
+
import { logger } from "../logger.js";
|
|
11
|
+
// Module-level state so tools can access it
|
|
12
|
+
let agentState = null;
|
|
13
|
+
export function getAgentState() {
|
|
14
|
+
if (!agentState) {
|
|
15
|
+
throw new Error("Agent state not initialized");
|
|
16
|
+
}
|
|
17
|
+
return agentState;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Log an SDK result message (success or error).
|
|
21
|
+
*/
|
|
22
|
+
function logResult(msg) {
|
|
23
|
+
const cost = msg.total_cost_usd.toFixed(4);
|
|
24
|
+
logger.result(msg.num_turns, msg.usage.input_tokens, msg.usage.output_tokens, cost);
|
|
25
|
+
if (msg.is_error) {
|
|
26
|
+
const errors = "errors" in msg ? msg.errors.join("; ") : msg.subtype;
|
|
27
|
+
logger.err(`Agent ended with error (${msg.subtype}): ${errors}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Main agent loop. This is the entry point that runs forever.
|
|
32
|
+
*/
|
|
33
|
+
export async function startAgentLoop(config) {
|
|
34
|
+
// --- Initialize components ---
|
|
35
|
+
const messageQueue = new MessageQueue();
|
|
36
|
+
const channelManager = new ChannelManager(config, messageQueue);
|
|
37
|
+
agentState = {
|
|
38
|
+
config,
|
|
39
|
+
channelManager,
|
|
40
|
+
messageQueue,
|
|
41
|
+
busy: false,
|
|
42
|
+
};
|
|
43
|
+
// Initialize S3 client for screenshot uploads
|
|
44
|
+
initScreenshotUploader();
|
|
45
|
+
// Create the in-process MCP tool server
|
|
46
|
+
const toolServer = createToolServer();
|
|
47
|
+
// Build system prompt
|
|
48
|
+
const systemPrompt = buildSystemPrompt(config);
|
|
49
|
+
// Create the agent session (uses V1 query API with full tool support)
|
|
50
|
+
const session = new AgentSession(config, systemPrompt, {
|
|
51
|
+
visionclaw: toolServer,
|
|
52
|
+
playwright: {
|
|
53
|
+
command: "npx",
|
|
54
|
+
args: [
|
|
55
|
+
"@playwright/mcp@latest",
|
|
56
|
+
"--browser",
|
|
57
|
+
process.platform === "darwin" ? "chrome" : "chromium",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
// Start all enabled channels
|
|
62
|
+
await channelManager.startAll();
|
|
63
|
+
// Listen for incoming messages to wake the agent or inject interrupts
|
|
64
|
+
let wakeResolver = null;
|
|
65
|
+
channelManager.on("message", () => {
|
|
66
|
+
// If agent is sleeping, wake it up
|
|
67
|
+
if (wakeResolver !== null) {
|
|
68
|
+
wakeResolver();
|
|
69
|
+
wakeResolver = null;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// If agent is busy, inject interrupt messages into the active session
|
|
73
|
+
if (agentState?.busy) {
|
|
74
|
+
const interruptMessages = [];
|
|
75
|
+
while (!messageQueue.isEmpty()) {
|
|
76
|
+
const msg = messageQueue.dequeue();
|
|
77
|
+
if (msg)
|
|
78
|
+
interruptMessages.push(msg);
|
|
79
|
+
}
|
|
80
|
+
if (interruptMessages.length > 0) {
|
|
81
|
+
// Update last message tracking
|
|
82
|
+
const last = interruptMessages[interruptMessages.length - 1];
|
|
83
|
+
agentState.lastMessageChannel = last.channel;
|
|
84
|
+
agentState.lastMessageSender = last.sender;
|
|
85
|
+
// Log the interrupt
|
|
86
|
+
for (const msg of interruptMessages) {
|
|
87
|
+
logger.log("info", "incoming", `[interrupt] [${msg.channel}] ${msg.sender}: ${msg.text}`, {
|
|
88
|
+
channel: msg.channel,
|
|
89
|
+
sender: msg.sender,
|
|
90
|
+
text: msg.text,
|
|
91
|
+
id: msg.id,
|
|
92
|
+
interrupt: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Build interrupt context and inject into session
|
|
96
|
+
const interruptBlocks = buildInterruptContext({ messages: interruptMessages });
|
|
97
|
+
const interruptContent = interruptBlocks;
|
|
98
|
+
session.injectMessage(interruptContent);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
logger.system("Agent loop started. Waiting for messages or heartbeat...");
|
|
103
|
+
// --- Main loop ---
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop
|
|
105
|
+
while (true) {
|
|
106
|
+
agentState.busy = false;
|
|
107
|
+
// Sleep until heartbeat or new message
|
|
108
|
+
await new Promise((resolve) => {
|
|
109
|
+
wakeResolver = resolve;
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
if (wakeResolver === resolve) {
|
|
112
|
+
wakeResolver = null;
|
|
113
|
+
resolve();
|
|
114
|
+
}
|
|
115
|
+
}, config.heartbeatIntervalMs);
|
|
116
|
+
if (!messageQueue.isEmpty()) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
wakeResolver = null;
|
|
119
|
+
resolve();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
agentState.busy = true;
|
|
123
|
+
// Determine trigger
|
|
124
|
+
const hasMessages = !messageQueue.isEmpty();
|
|
125
|
+
const trigger = hasMessages ? "message" : "heartbeat";
|
|
126
|
+
// Dequeue all pending messages
|
|
127
|
+
const messages = [];
|
|
128
|
+
while (!messageQueue.isEmpty()) {
|
|
129
|
+
const msg = messageQueue.dequeue();
|
|
130
|
+
if (msg)
|
|
131
|
+
messages.push(msg);
|
|
132
|
+
}
|
|
133
|
+
// Track last message for notify_user
|
|
134
|
+
if (messages.length > 0) {
|
|
135
|
+
const last = messages[messages.length - 1];
|
|
136
|
+
agentState.lastMessageChannel = last.channel;
|
|
137
|
+
agentState.lastMessageSender = last.sender;
|
|
138
|
+
}
|
|
139
|
+
// Log the wake event
|
|
140
|
+
logger.wake(trigger, messages.length);
|
|
141
|
+
// Log each incoming message
|
|
142
|
+
for (const msg of messages) {
|
|
143
|
+
logger.log("info", "incoming", `[${msg.channel}] ${msg.sender}: ${msg.text}`, {
|
|
144
|
+
channel: msg.channel,
|
|
145
|
+
sender: msg.sender,
|
|
146
|
+
text: msg.text,
|
|
147
|
+
id: msg.id,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// --- Gather context ---
|
|
151
|
+
// Gather screenshot and calendar in parallel
|
|
152
|
+
const [screenshotResult, calendarResult] = await Promise.allSettled([
|
|
153
|
+
(async () => {
|
|
154
|
+
const t0 = Date.now();
|
|
155
|
+
const base64 = await takeScreenshot();
|
|
156
|
+
logger.debug(`Screenshot captured in ${Date.now() - t0}ms`);
|
|
157
|
+
const t1 = Date.now();
|
|
158
|
+
const url = await uploadScreenshot(base64);
|
|
159
|
+
logger.debug(`Screenshot uploaded in ${Date.now() - t1}ms`);
|
|
160
|
+
return url ? { url } : { base64 };
|
|
161
|
+
})(),
|
|
162
|
+
(async () => {
|
|
163
|
+
const t0 = Date.now();
|
|
164
|
+
const summary = await getCalendarSummary(config);
|
|
165
|
+
logger.debug(`Calendar fetched in ${Date.now() - t0}ms`);
|
|
166
|
+
return summary;
|
|
167
|
+
})(),
|
|
168
|
+
]);
|
|
169
|
+
const screenshot = screenshotResult.status === "fulfilled" ? screenshotResult.value : undefined;
|
|
170
|
+
if (screenshotResult.status === "rejected") {
|
|
171
|
+
logger.warn(`Screenshot failed: ${screenshotResult.reason instanceof Error ? screenshotResult.reason.message : String(screenshotResult.reason)}`);
|
|
172
|
+
}
|
|
173
|
+
const calendarSummary = calendarResult.status === "fulfilled" ? calendarResult.value : "";
|
|
174
|
+
if (calendarResult.status === "rejected") {
|
|
175
|
+
logger.warn(`Calendar fetch failed: ${calendarResult.reason instanceof Error ? calendarResult.reason.message : String(calendarResult.reason)}`);
|
|
176
|
+
}
|
|
177
|
+
// Build multimodal content blocks
|
|
178
|
+
const contentBlocks = buildWakeContext({
|
|
179
|
+
config,
|
|
180
|
+
trigger,
|
|
181
|
+
messages,
|
|
182
|
+
calendarSummary,
|
|
183
|
+
screenshot,
|
|
184
|
+
});
|
|
185
|
+
// Map tool_use_id -> tool name for correlating results (per wake cycle)
|
|
186
|
+
const toolNameById = new Map();
|
|
187
|
+
// Log the multimodal message being sent (text + image summary)
|
|
188
|
+
logger.sendMultimodal(contentBlocks);
|
|
189
|
+
try {
|
|
190
|
+
// Send the multimodal content and stream the response.
|
|
191
|
+
// Cast through unknown because our ContentBlock type may not
|
|
192
|
+
// exactly match MessageParam["content"].
|
|
193
|
+
const messageContent = contentBlocks;
|
|
194
|
+
const queryStream = session.sendAndStream(messageContent);
|
|
195
|
+
// Process the response stream.
|
|
196
|
+
// We don't break on finish -- breaking calls generator.return() which
|
|
197
|
+
// can hang waiting for the SDK child process to exit. Instead we
|
|
198
|
+
// drain the stream naturally and skip logging after finish.
|
|
199
|
+
let finishCalled = false;
|
|
200
|
+
for await (const msg of queryStream) {
|
|
201
|
+
// Always capture session ID
|
|
202
|
+
if ("session_id" in msg && msg.session_id) {
|
|
203
|
+
session.captureSessionId(msg.session_id);
|
|
204
|
+
}
|
|
205
|
+
// After finish, only log the result message and skip everything else
|
|
206
|
+
if (finishCalled) {
|
|
207
|
+
if (msg.type === "result") {
|
|
208
|
+
logResult(msg);
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// Handle different message types
|
|
213
|
+
switch (msg.type) {
|
|
214
|
+
case "user": {
|
|
215
|
+
// In Claude Agent SDK v2, tool execution results are surfaced back
|
|
216
|
+
// into the stream as "user" messages (not as a separate tool_result
|
|
217
|
+
// message type). Capture and log them for OBS.
|
|
218
|
+
const um = msg;
|
|
219
|
+
// Tool results in this SDK are encoded as user message content blocks:
|
|
220
|
+
// [{ type: "tool_result", tool_use_id, content: [...] }]
|
|
221
|
+
const blocks = um.message?.content;
|
|
222
|
+
if (Array.isArray(blocks)) {
|
|
223
|
+
for (const b of blocks) {
|
|
224
|
+
const block = b;
|
|
225
|
+
if (block.type !== "tool_result" || !block.tool_use_id)
|
|
226
|
+
continue;
|
|
227
|
+
const out = block.content;
|
|
228
|
+
const summary = out === undefined
|
|
229
|
+
? ""
|
|
230
|
+
: typeof out === "string"
|
|
231
|
+
? out
|
|
232
|
+
: JSON.stringify(out);
|
|
233
|
+
const resolvedName = toolNameById.get(block.tool_use_id) ?? "tool";
|
|
234
|
+
logger.toolResult(resolvedName, summary, {
|
|
235
|
+
tool_use_id: block.tool_use_id,
|
|
236
|
+
is_error: block.is_error,
|
|
237
|
+
source: "sdk_user_message_block",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "assistant": {
|
|
244
|
+
const assistantMsg = msg;
|
|
245
|
+
const content = assistantMsg.message.content;
|
|
246
|
+
for (const block of content) {
|
|
247
|
+
switch (block.type) {
|
|
248
|
+
case "text":
|
|
249
|
+
if (block.text) {
|
|
250
|
+
logger.assistant(block.text);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case "thinking":
|
|
254
|
+
logger.debug(`[thinking] ${block.thinking.substring(0, 200)}${block.thinking.length > 200 ? "..." : ""}`);
|
|
255
|
+
break;
|
|
256
|
+
case "tool_use":
|
|
257
|
+
{
|
|
258
|
+
const toolUseId = block.id ??
|
|
259
|
+
block.tool_use_id;
|
|
260
|
+
if (toolUseId) {
|
|
261
|
+
toolNameById.set(toolUseId, block.name);
|
|
262
|
+
}
|
|
263
|
+
logger.toolCall(block.name, block.input, {
|
|
264
|
+
tool_use_id: toolUseId,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (block.name === "finish" || block.name === "mcp__visionclaw__finish") {
|
|
268
|
+
finishCalled = true;
|
|
269
|
+
session.closeInput();
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
// Log other block types (server_tool_use, mcp_tool_use, etc.)
|
|
274
|
+
logger.debug(`Assistant block: ${block.type}`);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Log assistant-level errors
|
|
279
|
+
if (assistantMsg.error) {
|
|
280
|
+
logger.err(`Assistant error: ${assistantMsg.error}`);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "result":
|
|
285
|
+
logResult(msg);
|
|
286
|
+
break;
|
|
287
|
+
case "system":
|
|
288
|
+
logger.debug(`System message: ${JSON.stringify(msg)}`, {
|
|
289
|
+
subtype: msg.subtype,
|
|
290
|
+
});
|
|
291
|
+
break;
|
|
292
|
+
default:
|
|
293
|
+
// stream_event, status, task_notification, etc. — skip silently
|
|
294
|
+
// Also handle tool_result even if the SDK type union doesn't include it.
|
|
295
|
+
if (msg.type === "tool_result") {
|
|
296
|
+
const tr = msg;
|
|
297
|
+
const name = tr.name ?? "tool";
|
|
298
|
+
const summary = tr.content === undefined
|
|
299
|
+
? ""
|
|
300
|
+
: typeof tr.content === "string"
|
|
301
|
+
? tr.content
|
|
302
|
+
: JSON.stringify(tr.content);
|
|
303
|
+
logger.toolResult(name, summary, {
|
|
304
|
+
tool_use_id: tr.tool_use_id,
|
|
305
|
+
is_error: tr.is_error,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Ensure generator is closed after stream ends
|
|
312
|
+
session.closeInput();
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
// Ensure generator is closed on error too
|
|
316
|
+
session.closeInput();
|
|
317
|
+
logger.err(`Agent loop error: ${err instanceof Error ? err.message : String(err)}`, { stack: err instanceof Error ? err.stack : undefined });
|
|
318
|
+
// Wait a bit before retrying to avoid tight error loops
|
|
319
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=loop.js.map
|