@sulala/agent 0.1.6 → 0.1.7
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 +3 -2
- package/context/airtable.md +35 -0
- package/context/asana.md +37 -0
- package/context/bluesky.md +26 -91
- package/context/calendar.md +63 -0
- package/context/country-info.md +13 -0
- package/context/create-skill.md +128 -0
- package/context/discord.md +30 -0
- package/context/docs.md +29 -0
- package/context/drive.md +49 -0
- package/context/dropbox.md +39 -0
- package/context/facebook.md +47 -0
- package/context/fetch-form-api.md +16 -0
- package/context/figma.md +30 -0
- package/context/github.md +58 -0
- package/context/gmail.md +52 -0
- package/context/google.md +28 -0
- package/context/hellohub.md +29 -0
- package/context/jira.md +46 -0
- package/context/linear.md +40 -0
- package/context/notion.md +45 -0
- package/context/portal-integrations.md +42 -0
- package/context/post-to-x.md +50 -0
- package/context/sheets.md +47 -0
- package/context/slack.md +48 -0
- package/context/slides.md +35 -0
- package/context/stripe.md +38 -0
- package/context/tes.md +7 -0
- package/context/test.md +7 -0
- package/context/zoom.md +28 -0
- package/dist/agent/google/calendar.d.ts +2 -0
- package/dist/agent/google/calendar.d.ts.map +1 -0
- package/dist/agent/google/calendar.js +119 -0
- package/dist/agent/google/calendar.js.map +1 -0
- package/dist/agent/google/drive.d.ts +2 -0
- package/dist/agent/google/drive.d.ts.map +1 -0
- package/dist/agent/google/drive.js +51 -0
- package/dist/agent/google/drive.js.map +1 -0
- package/dist/agent/google/get-token.d.ts +7 -0
- package/dist/agent/google/get-token.d.ts.map +1 -0
- package/dist/agent/google/get-token.js +37 -0
- package/dist/agent/google/get-token.js.map +1 -0
- package/dist/agent/google/gmail.d.ts +2 -0
- package/dist/agent/google/gmail.d.ts.map +1 -0
- package/dist/agent/google/gmail.js +138 -0
- package/dist/agent/google/gmail.js.map +1 -0
- package/dist/agent/google/index.d.ts +2 -0
- package/dist/agent/google/index.d.ts.map +1 -0
- package/dist/agent/google/index.js +13 -0
- package/dist/agent/google/index.js.map +1 -0
- package/dist/agent/loop.d.ts +8 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +226 -40
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory.d.ts +21 -0
- package/dist/agent/memory.d.ts.map +1 -0
- package/dist/agent/memory.js +33 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/pending-actions.d.ts +21 -0
- package/dist/agent/pending-actions.d.ts.map +1 -0
- package/dist/agent/pending-actions.js +65 -0
- package/dist/agent/pending-actions.js.map +1 -0
- package/dist/agent/pi-runner.d.ts +27 -0
- package/dist/agent/pi-runner.d.ts.map +1 -0
- package/dist/agent/pi-runner.js +300 -0
- package/dist/agent/pi-runner.js.map +1 -0
- package/dist/agent/skill-generate.d.ts +63 -0
- package/dist/agent/skill-generate.d.ts.map +1 -0
- package/dist/agent/skill-generate.js +128 -0
- package/dist/agent/skill-generate.js.map +1 -0
- package/dist/agent/skill-install.d.ts.map +1 -1
- package/dist/agent/skill-install.js +80 -31
- package/dist/agent/skill-install.js.map +1 -1
- package/dist/agent/skill-templates.d.ts +17 -0
- package/dist/agent/skill-templates.d.ts.map +1 -0
- package/dist/agent/skill-templates.js +26 -0
- package/dist/agent/skill-templates.js.map +1 -0
- package/dist/agent/skills-config.d.ts +24 -2
- package/dist/agent/skills-config.d.ts.map +1 -1
- package/dist/agent/skills-config.js +107 -8
- package/dist/agent/skills-config.js.map +1 -1
- package/dist/agent/skills-watcher.js +1 -1
- package/dist/agent/skills.d.ts +9 -3
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +104 -9
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/tools.d.ts +25 -3
- package/dist/agent/tools.d.ts.map +1 -1
- package/dist/agent/tools.integrations.test.d.ts +2 -0
- package/dist/agent/tools.integrations.test.d.ts.map +1 -0
- package/dist/agent/tools.integrations.test.js +269 -0
- package/dist/agent/tools.integrations.test.js.map +1 -0
- package/dist/agent/tools.js +692 -39
- package/dist/agent/tools.js.map +1 -1
- package/dist/ai/orchestrator.d.ts +4 -1
- package/dist/ai/orchestrator.d.ts.map +1 -1
- package/dist/ai/orchestrator.js +246 -14
- package/dist/ai/orchestrator.js.map +1 -1
- package/dist/ai/pricing.d.ts +6 -0
- package/dist/ai/pricing.d.ts.map +1 -0
- package/dist/ai/pricing.js +39 -0
- package/dist/ai/pricing.js.map +1 -0
- package/dist/channels/discord.d.ts +15 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +55 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/stripe.d.ts +15 -0
- package/dist/channels/stripe.d.ts.map +1 -0
- package/dist/channels/stripe.js +58 -0
- package/dist/channels/stripe.js.map +1 -0
- package/dist/channels/telegram.d.ts +60 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +562 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/cli.js +66 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +85 -1
- package/dist/config.js.map +1 -1
- package/dist/db/index.d.ts +83 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +174 -2
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.sql +35 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +1219 -27
- package/dist/gateway/server.js.map +1 -1
- package/dist/index.js +149 -6
- package/dist/index.js.map +1 -1
- package/dist/ollama-setup.d.ts +27 -0
- package/dist/ollama-setup.d.ts.map +1 -0
- package/dist/ollama-setup.js +191 -0
- package/dist/ollama-setup.js.map +1 -0
- package/dist/onboard-env.d.ts +1 -1
- package/dist/onboard-env.d.ts.map +1 -1
- package/dist/onboard-env.js +2 -0
- package/dist/onboard-env.js.map +1 -1
- package/dist/onboard.d.ts +3 -1
- package/dist/onboard.d.ts.map +1 -1
- package/dist/onboard.js +7 -2
- package/dist/onboard.js.map +1 -1
- package/dist/plugins/index.d.ts +10 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +32 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/redact.d.ts +15 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +56 -0
- package/dist/redact.js.map +1 -0
- package/dist/scheduler/cron.d.ts +21 -0
- package/dist/scheduler/cron.d.ts.map +1 -1
- package/dist/scheduler/cron.js +60 -0
- package/dist/scheduler/cron.js.map +1 -1
- package/dist/system-capabilities.d.ts +11 -0
- package/dist/system-capabilities.d.ts.map +1 -0
- package/dist/system-capabilities.js +109 -0
- package/dist/system-capabilities.js.map +1 -0
- package/dist/types.d.ts +62 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/watcher/index.d.ts +2 -0
- package/dist/watcher/index.d.ts.map +1 -1
- package/dist/watcher/index.js +31 -1
- package/dist/watcher/index.js.map +1 -1
- package/dist/workspace-automations.d.ts +16 -0
- package/dist/workspace-automations.d.ts.map +1 -0
- package/dist/workspace-automations.js +133 -0
- package/dist/workspace-automations.js.map +1 -0
- package/package.json +19 -3
- package/registry/bluesky.md +12 -89
- package/registry/skills-registry.json +6 -0
- package/src/db/schema.sql +35 -0
- package/src/index.ts +159 -6
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sulala/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Local AI orchestration platform — file watcher, task scheduler, AI layer, plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"start": "tsx src/index.ts",
|
|
9
|
+
"install-ollama": "node install-ollama.cjs",
|
|
9
10
|
"gateway": "tsx src/gateway/server.ts",
|
|
10
11
|
"dashboard": "cd dashboard && npm run dev",
|
|
11
12
|
"dashboard:build": "cd dashboard && npm run build",
|
|
@@ -28,9 +29,18 @@
|
|
|
28
29
|
"local"
|
|
29
30
|
],
|
|
30
31
|
"license": "MIT",
|
|
31
|
-
"files": [
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"bin",
|
|
35
|
+
"src/db/schema.sql",
|
|
36
|
+
"registry",
|
|
37
|
+
"context",
|
|
38
|
+
"dashboard/dist"
|
|
39
|
+
],
|
|
32
40
|
"prepublishOnly": "pnpm run build && pnpm run dashboard:build",
|
|
33
|
-
"publishConfig": {
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
34
44
|
"bin": {
|
|
35
45
|
"sulala": "bin/sulala.mjs"
|
|
36
46
|
},
|
|
@@ -40,10 +50,16 @@
|
|
|
40
50
|
"cors": "^2.8.5",
|
|
41
51
|
"dotenv": "^16.4.5",
|
|
42
52
|
"express": "^4.21.1",
|
|
53
|
+
"grammy": "^1.40.1",
|
|
43
54
|
"node-cron": "^3.0.3",
|
|
44
55
|
"openai": "^6.25.0",
|
|
45
56
|
"ws": "^8.18.0"
|
|
46
57
|
},
|
|
58
|
+
"optionalDependencies": {
|
|
59
|
+
"@mariozechner/pi-agent-core": "0.54.0",
|
|
60
|
+
"@mariozechner/pi-ai": "0.54.0",
|
|
61
|
+
"@mariozechner/pi-coding-agent": "0.54.0"
|
|
62
|
+
},
|
|
47
63
|
"devDependencies": {
|
|
48
64
|
"@eslint/js": "^10.0.1",
|
|
49
65
|
"@types/better-sqlite3": "^7.6.13",
|
package/registry/bluesky.md
CHANGED
|
@@ -1,111 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bluesky
|
|
3
|
-
description: Post to Bluesky (AT Protocol). Use when the user asks to post
|
|
3
|
+
description: Post to Bluesky (AT Protocol). Use when the user asks to post to Bluesky or share content on Bluesky. Uses Portal OAuth connection via integrations.
|
|
4
4
|
homepage: https://bsky.app
|
|
5
5
|
metadata:
|
|
6
6
|
{
|
|
7
7
|
"sulala": {
|
|
8
8
|
"emoji": "🦋",
|
|
9
|
-
"requires": { "bins": ["curl"
|
|
10
|
-
"primaryEnv": "BSKY_APP_PASSWORD"
|
|
9
|
+
"requires": { "bins": ["curl"] }
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
12
|
---
|
|
14
13
|
|
|
15
14
|
# Bluesky Posting
|
|
16
15
|
|
|
17
|
-
Post to Bluesky via the AT Protocol.
|
|
16
|
+
Post to Bluesky via the AT Protocol. Uses the **Portal OAuth connection** (integrations). User connects Bluesky in the Portal; no app password needed.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
## How to post
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
1. **list_integrations_connections** with `provider: "bluesky"` → get `connection_id`.
|
|
21
|
+
2. **bluesky_post** with that `connection_id` and the post text (max 300 characters).
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Use **bluesky_post**; do not use run_command (curl) for Bluesky.
|
|
24
|
+
|
|
25
|
+
## When to use
|
|
24
26
|
|
|
25
27
|
- "Post this to Bluesky"
|
|
26
28
|
- "Share [content] on Bluesky"
|
|
27
29
|
- "Post news from [URL] to Bluesky"
|
|
28
|
-
- "Post a headline about [topic]"
|
|
29
|
-
|
|
30
|
-
## Post Flow (non-interactive)
|
|
31
|
-
|
|
32
|
-
### 1. Ensure credentials
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
BSKY_HANDLE="${BSKY_HANDLE:?Set BSKY_HANDLE}"
|
|
36
|
-
BSKY_APP_PASSWORD="${BSKY_APP_PASSWORD:?Set BSKY_APP_PASSWORD}"
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
If empty, read from Sulala config:
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
CONFIG_PATH="${SULALA_CONFIG_PATH:-$HOME/.sulala/config.json}"
|
|
43
|
-
# If using workspace config, use: CONFIG_PATH=".sulala/config.json" (when run from project root)
|
|
44
|
-
BSKY_HANDLE=$(cat "$CONFIG_PATH" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('skills',{}).get('entries',{}).get('bluesky',{}); print(e.get('handle','') or e.get('apiKey',''))" 2>/dev/null)
|
|
45
|
-
BSKY_APP_PASSWORD=$(cat "$CONFIG_PATH" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('skills',{}).get('entries',{}).get('bluesky',{}); print(e.get('apiKey','') or e.get('password',''))" 2>/dev/null)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
(Config may use `handle` + `apiKey` for Bluesky. Adjust field names if your config differs.)
|
|
49
|
-
|
|
50
|
-
### 2. Create session (login)
|
|
51
|
-
|
|
52
|
-
**Use `run_command` with `binary: "sh"` and `args: ["-c", "..."]`** so `$BSKY_HANDLE` and `$BSKY_APP_PASSWORD` expand from the environment. Do NOT call curl directly.
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
sh -c 'SESSION=$(curl -s -X POST "https://bsky.social/xrpc/com.atproto.server.createSession" -H "Content-Type: application/json" -d "{\"identifier\": \"$BSKY_HANDLE\", \"password\": \"$BSKY_APP_PASSWORD\"}"); echo "$SESSION"'
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Then parse the JSON output with `python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('accessJwt',''), d.get('did',''))"` to get `ACCESS_TOKEN` and `DID`.
|
|
59
|
-
|
|
60
|
-
If the session response contains `"error"` or `"AuthenticationRequired"`, report: "Bluesky login failed. Check BSKY_HANDLE and BSKY_APP_PASSWORD in .env."
|
|
61
|
-
|
|
62
|
-
### 3. Create post
|
|
63
|
-
|
|
64
|
-
Text must be JSON-escaped. Use python to build the payload. **Use `run_command` with `binary: "sh"`** so `$ACCESS_TOKEN`, `$DID`, and other vars expand.
|
|
65
|
-
|
|
66
|
-
Run a single `sh -c` that chains steps 2 and 3 so SESSION, ACCESS_TOKEN, DID persist in the same shell:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
sh -c '
|
|
70
|
-
SESSION=$(curl -s -X POST "https://bsky.social/xrpc/com.atproto.server.createSession" \
|
|
71
|
-
-H "Content-Type: application/json" \
|
|
72
|
-
-d "{\"identifier\": \"$BSKY_HANDLE\", \"password\": \"$BSKY_APP_PASSWORD\"}");
|
|
73
|
-
ACCESS_TOKEN=$(echo "$SESSION" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get(\"accessJwt\",\"\"))");
|
|
74
|
-
DID=$(echo "$SESSION" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get(\"did\",\"\"))");
|
|
75
|
-
if [ -z "$ACCESS_TOKEN" ]; then echo "Login failed"; exit 1; fi;
|
|
76
|
-
POST_TEXT="Your post content here";
|
|
77
|
-
POST_JSON=$(python3 -c "import sys,json; print(json.dumps(sys.argv[1]))" "$POST_TEXT");
|
|
78
|
-
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z");
|
|
79
|
-
curl -s -X POST "https://bsky.social/xrpc/com.atproto.repo.createRecord" \
|
|
80
|
-
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
|
81
|
-
-H "Content-Type: application/json" \
|
|
82
|
-
-d "{\"repo\": \"$DID\", \"collection\": \"app.bsky.feed.post\", \"record\": {\"\\$type\": \"app.bsky.feed.post\", \"text\": $POST_JSON, \"createdAt\": \"$NOW\"}}"
|
|
83
|
-
'
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Replace `Your post content here` with the user's post text.
|
|
87
|
-
|
|
88
|
-
If the response contains `"uri"`, the post succeeded. Otherwise report the error.
|
|
89
|
-
|
|
90
|
-
## Posting from a news source
|
|
91
|
-
|
|
92
|
-
When the user wants to post **news** or **content from a URL**:
|
|
93
|
-
|
|
94
|
-
1. **Fetch the content** (e.g. with `curl -s <URL>` or read a file).
|
|
95
|
-
2. **Summarize** if the content is long — Bluesky posts are max 300 characters. Write a short headline or summary.
|
|
96
|
-
3. **Post** using the flow above. Optionally append the source URL if it fits within 300 chars.
|
|
97
|
-
|
|
98
|
-
Example for "post news from https://example.com/article":
|
|
99
|
-
- Fetch the page, extract title/lead.
|
|
100
|
-
- Build post: "Headline here — https://example.com/article"
|
|
101
|
-
- Post via the createRecord flow above.
|
|
102
|
-
|
|
103
|
-
## Character limit
|
|
104
|
-
|
|
105
|
-
Bluesky posts are limited to 300 characters. If the user's text or your summary exceeds that, truncate or split into multiple posts (thread). For a thread, create each post separately; the API does not natively support threads — you would need to reference the previous post's URI if linking them (advanced).
|
|
106
30
|
|
|
107
|
-
##
|
|
31
|
+
## Requirements
|
|
108
32
|
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
- Do not embed real credentials in the skill or in replies — use env or config.
|
|
33
|
+
- **PORTAL_GATEWAY_URL** and **PORTAL_API_KEY** (from Portal → API Keys).
|
|
34
|
+
- User must have connected Bluesky in the Portal (Connections).
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"name": "news",
|
|
36
36
|
"description": "Fetch news and articles via the Perigon API. Use when the user asks for news, headlines, or articles on a topic.",
|
|
37
37
|
"version": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"slug": "portal-integrations",
|
|
41
|
+
"name": "portal-integrations",
|
|
42
|
+
"description": "Use connected apps (Gmail, Calendar, Zoom, Slack, GitHub, etc.) via the Portal. List connections with list_integrations_connections, then use connection_id in provider tools.",
|
|
43
|
+
"version": "1.0.0"
|
|
38
44
|
}
|
|
39
45
|
]
|
|
40
46
|
}
|
package/src/db/schema.sql
CHANGED
|
@@ -68,7 +68,42 @@ CREATE TABLE IF NOT EXISTS agent_messages (
|
|
|
68
68
|
|
|
69
69
|
CREATE INDEX IF NOT EXISTS idx_agent_messages_session ON agent_messages(session_id);
|
|
70
70
|
|
|
71
|
+
-- Agent memory: session-scoped and shared (cross-session) durable notes
|
|
72
|
+
CREATE TABLE IF NOT EXISTS agent_memory (
|
|
73
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
|
+
scope TEXT NOT NULL,
|
|
75
|
+
scope_key TEXT NOT NULL,
|
|
76
|
+
content TEXT NOT NULL,
|
|
77
|
+
created_at INTEGER NOT NULL
|
|
78
|
+
);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_agent_memory_scope_key ON agent_memory(scope, scope_key);
|
|
80
|
+
|
|
71
81
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
72
82
|
CREATE INDEX IF NOT EXISTS idx_tasks_scheduled ON tasks(scheduled_at);
|
|
73
83
|
CREATE INDEX IF NOT EXISTS idx_logs_created ON logs(created_at);
|
|
74
84
|
CREATE INDEX IF NOT EXISTS idx_ai_results_created ON ai_results(created_at);
|
|
85
|
+
|
|
86
|
+
-- Channel config (Telegram, etc.) — set from dashboard
|
|
87
|
+
CREATE TABLE IF NOT EXISTS channel_config (
|
|
88
|
+
channel TEXT PRIMARY KEY,
|
|
89
|
+
config TEXT NOT NULL,
|
|
90
|
+
updated_at INTEGER NOT NULL
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
-- Scheduled jobs: legacy (task_type + payload) or agent jobs (prompt + delivery)
|
|
94
|
+
CREATE TABLE IF NOT EXISTS scheduled_jobs (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
name TEXT NOT NULL DEFAULT '',
|
|
97
|
+
description TEXT NOT NULL DEFAULT '',
|
|
98
|
+
cron_expression TEXT NOT NULL,
|
|
99
|
+
task_type TEXT NOT NULL DEFAULT 'agent_job',
|
|
100
|
+
payload TEXT,
|
|
101
|
+
prompt TEXT,
|
|
102
|
+
delivery TEXT,
|
|
103
|
+
provider TEXT,
|
|
104
|
+
model TEXT,
|
|
105
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
106
|
+
created_at INTEGER NOT NULL,
|
|
107
|
+
updated_at INTEGER NOT NULL
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_enabled ON scheduled_jobs(enabled);
|
package/src/index.ts
CHANGED
|
@@ -1,20 +1,100 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { createServer } from 'http';
|
|
3
|
+
import {
|
|
4
|
+
getWatchFoldersFromAutomations,
|
|
5
|
+
runMatchingAutomations,
|
|
6
|
+
} from './workspace-automations.js';
|
|
3
7
|
import { loadFullConfig } from './agent/skills-config.js';
|
|
4
8
|
import { config } from './config.js';
|
|
5
|
-
import { initDb, log } from './db/index.js';
|
|
9
|
+
import { initDb, log, getChannelConfig } from './db/index.js';
|
|
6
10
|
import { createGateway, attachWebSocket } from './gateway/server.js';
|
|
7
11
|
import { startWatcher, setEventCallback, enqueueTaskOnEvent } from './watcher/index.js';
|
|
8
12
|
import { setTaskHandler, loadPendingFromDb, enqueue } from './scheduler/queue.js';
|
|
9
|
-
import {
|
|
13
|
+
import { scheduleCronEphemeral, scheduleCronById } from './scheduler/cron.js';
|
|
14
|
+
import { loadSchedulesConfig } from './config.js';
|
|
15
|
+
import { listScheduledJobs } from './db/index.js';
|
|
10
16
|
import { loadAllPlugins, onFileEvent, onTask } from './plugins/index.js';
|
|
11
17
|
import { fireWebhooks } from './webhooks.js';
|
|
12
18
|
import { registerBuiltInTools } from './agent/tools.js';
|
|
13
19
|
import { startSkillsWatcher } from './agent/skills-watcher.js';
|
|
20
|
+
import { ensureOllamaInstalled } from './ollama-setup.js';
|
|
21
|
+
import { getOrCreateAgentSession } from './db/index.js';
|
|
22
|
+
import { runAgentTurn } from './agent/loop.js';
|
|
23
|
+
import { sendTelegramNotification, startTelegramChannel, resolveDefaultProviderAndModel } from './channels/telegram.js';
|
|
14
24
|
import type { AppLocals } from './types.js';
|
|
15
25
|
|
|
26
|
+
type DeliveryTarget = { channel: string; target?: string };
|
|
27
|
+
|
|
28
|
+
const JOB_DEFAULT_CHANNEL_KEY = 'job_default';
|
|
29
|
+
|
|
30
|
+
function parseJobDefaultConfig(raw: string | null): { defaultProvider?: string; defaultModel?: string } | null {
|
|
31
|
+
if (!raw?.trim()) return null;
|
|
32
|
+
try {
|
|
33
|
+
const o = JSON.parse(raw) as Record<string, unknown>;
|
|
34
|
+
const defaultProvider = typeof o.defaultProvider === 'string' ? o.defaultProvider.trim() || undefined : undefined;
|
|
35
|
+
const defaultModel = typeof o.defaultModel === 'string' ? o.defaultModel.trim() || undefined : undefined;
|
|
36
|
+
if (!defaultProvider && !defaultModel) return null;
|
|
37
|
+
return { defaultProvider, defaultModel };
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runAgentJobAndNotify(payload: unknown): Promise<void> {
|
|
44
|
+
const p = payload as { jobId?: string; name?: string; prompt?: string; delivery?: DeliveryTarget[]; provider?: string; model?: string } | null;
|
|
45
|
+
if (!p?.prompt?.trim()) {
|
|
46
|
+
log('worker', 'warn', 'agent_job missing prompt', { payload: p });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const jobName = p.name?.trim() || p.jobId || 'Scheduled job';
|
|
50
|
+
const deliveryList = Array.isArray(p.delivery) ? p.delivery : [];
|
|
51
|
+
const sessionKey = `job_${p.jobId ?? 'run'}_${Date.now()}`;
|
|
52
|
+
const session = getOrCreateAgentSession(sessionKey);
|
|
53
|
+
const jobProvider = typeof p.provider === 'string' ? p.provider.trim() || undefined : undefined;
|
|
54
|
+
const jobModel = typeof p.model === 'string' ? p.model.trim() || undefined : undefined;
|
|
55
|
+
const jobDefaultFromDb = parseJobDefaultConfig(getChannelConfig(JOB_DEFAULT_CHANNEL_KEY));
|
|
56
|
+
const { provider, model } = resolveDefaultProviderAndModel({
|
|
57
|
+
provider: jobProvider ?? jobDefaultFromDb?.defaultProvider,
|
|
58
|
+
model: jobModel ?? jobDefaultFromDb?.defaultModel,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const sendNotification = async (text: string): Promise<void> => {
|
|
62
|
+
for (const d of deliveryList) {
|
|
63
|
+
if (d.channel === 'telegram') {
|
|
64
|
+
await sendTelegramNotification(text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await runAgentTurn({
|
|
71
|
+
sessionId: session.id,
|
|
72
|
+
userMessage: p.prompt.trim(),
|
|
73
|
+
provider,
|
|
74
|
+
model,
|
|
75
|
+
skipToolApproval: true,
|
|
76
|
+
});
|
|
77
|
+
const summary = result.finalContent?.trim()?.slice(0, 2000) || '(No output)';
|
|
78
|
+
const message = `✅ Job «${jobName}» completed.\n\n${summary}`;
|
|
79
|
+
await sendNotification(message);
|
|
80
|
+
log('worker', 'info', `Agent job completed: ${jobName}`, { jobId: p.jobId });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
const message = `❌ Job «${jobName}» failed.\n\n${errorMsg.slice(0, 1500)}`;
|
|
84
|
+
await sendNotification(message);
|
|
85
|
+
log('worker', 'error', `Agent job failed: ${jobName}`, { jobId: p.jobId, error: errorMsg });
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
16
90
|
async function main(): Promise<void> {
|
|
17
91
|
initDb(config.dbPath);
|
|
92
|
+
if (process.env.SULALA_OLLAMA_AUTO_INSTALL === '1') {
|
|
93
|
+
ensureOllamaInstalled();
|
|
94
|
+
} else {
|
|
95
|
+
// Don't auto-install Ollama on startup; user must confirm via UI
|
|
96
|
+
console.log('[Ollama] Auto-install disabled. Install from Settings or ollama.com when needed.');
|
|
97
|
+
}
|
|
18
98
|
// Inject skill config into process.env so run_command (e.g. curl with $PERIGON_API_KEY) sees them
|
|
19
99
|
const full = loadFullConfig();
|
|
20
100
|
const entries = full.skills?.entries;
|
|
@@ -27,6 +107,13 @@ async function main(): Promise<void> {
|
|
|
27
107
|
}
|
|
28
108
|
}
|
|
29
109
|
}
|
|
110
|
+
// Merge allowedBinaries from config into ALLOWED_BINARIES (skills using curl, git, etc.)
|
|
111
|
+
const configBins = full.allowedBinaries;
|
|
112
|
+
if (Array.isArray(configBins) && configBins.length > 0) {
|
|
113
|
+
const envBins = (process.env.ALLOWED_BINARIES || '').split(',').map((b) => b.trim()).filter(Boolean);
|
|
114
|
+
const merged = [...new Set([...envBins, ...configBins.map((b) => String(b).trim().toLowerCase()).filter(Boolean)])];
|
|
115
|
+
process.env.ALLOWED_BINARIES = merged.join(',');
|
|
116
|
+
}
|
|
30
117
|
log('main', 'info', 'Starting Sulala Agent', { port: config.port });
|
|
31
118
|
|
|
32
119
|
registerBuiltInTools(enqueue);
|
|
@@ -58,21 +145,87 @@ async function main(): Promise<void> {
|
|
|
58
145
|
broadcast({ type: 'file_event', payload });
|
|
59
146
|
fireWebhooks('file_event', payload);
|
|
60
147
|
});
|
|
61
|
-
|
|
148
|
+
const automationFolders = getWatchFoldersFromAutomations();
|
|
149
|
+
const allWatchFolders = [...new Set([...config.watchFolders, ...automationFolders])];
|
|
150
|
+
startWatcher(allWatchFolders.length ? allWatchFolders : null);
|
|
62
151
|
startSkillsWatcher(config, (ev) => broadcast(ev));
|
|
63
152
|
|
|
64
|
-
|
|
153
|
+
const taskHandler = async (task: {
|
|
154
|
+
id: string;
|
|
155
|
+
type: string;
|
|
156
|
+
payload: unknown;
|
|
157
|
+
retry_count: number;
|
|
158
|
+
max_retries: number;
|
|
159
|
+
}) => {
|
|
65
160
|
const handled = await onTask(task);
|
|
66
161
|
if (handled) return;
|
|
162
|
+
if (task.type === 'agent_job') {
|
|
163
|
+
try {
|
|
164
|
+
await runAgentJobAndNotify(task.payload);
|
|
165
|
+
} catch {
|
|
166
|
+
// already logged and notified; rethrow so queue marks task failed
|
|
167
|
+
throw new Error('Agent job failed');
|
|
168
|
+
}
|
|
169
|
+
broadcast({ type: 'task_done', taskId: task.id });
|
|
170
|
+
fireWebhooks('task_done', { taskId: task.id, type: task.type });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
67
173
|
if (task.type === 'file_event') {
|
|
174
|
+
const payload = task.payload as { event?: string; path?: string } | null;
|
|
68
175
|
log('worker', 'info', 'File event task', (task.payload ?? null) as Record<string, unknown> | null);
|
|
176
|
+
if (payload?.event && payload?.path) {
|
|
177
|
+
runMatchingAutomations(payload.event, payload.path);
|
|
178
|
+
}
|
|
69
179
|
}
|
|
70
180
|
broadcast({ type: 'task_done', taskId: task.id });
|
|
71
181
|
fireWebhooks('task_done', { taskId: task.id, type: task.type });
|
|
72
|
-
}
|
|
182
|
+
};
|
|
183
|
+
setTaskHandler(taskHandler);
|
|
73
184
|
|
|
74
185
|
loadPendingFromDb();
|
|
75
|
-
|
|
186
|
+
// Heartbeat runs every minute; ephemeral = no DB row to avoid filling tasks table
|
|
187
|
+
scheduleCronEphemeral('* * * * *', 'heartbeat', () => ({ ts: Date.now() }), taskHandler);
|
|
188
|
+
|
|
189
|
+
// Config-driven schedules (config/schedules.json)
|
|
190
|
+
const configSchedules = loadSchedulesConfig();
|
|
191
|
+
configSchedules.forEach((entry, i) => {
|
|
192
|
+
try {
|
|
193
|
+
scheduleCronById(`config_${i}`, entry.cron, entry.type, entry.payload ?? null);
|
|
194
|
+
log('main', 'info', `Scheduled job from config: ${entry.type}`, { cron: entry.cron });
|
|
195
|
+
} catch (e) {
|
|
196
|
+
log('main', 'error', `Invalid schedule config at index ${i}: ${(e as Error).message}`, { cron: entry.cron, type: entry.type });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// DB-driven schedules (dashboard Jobs): agent jobs (prompt) or legacy (task_type + payload)
|
|
201
|
+
const dbSchedules = listScheduledJobs(true);
|
|
202
|
+
dbSchedules.forEach((row) => {
|
|
203
|
+
try {
|
|
204
|
+
if (row.prompt?.trim()) {
|
|
205
|
+
const delivery = row.delivery?.trim() ? JSON.parse(row.delivery) as DeliveryTarget[] : [];
|
|
206
|
+
const r = row as { provider?: string | null; model?: string | null };
|
|
207
|
+
const payload = {
|
|
208
|
+
jobId: row.id,
|
|
209
|
+
name: row.name || row.id,
|
|
210
|
+
prompt: row.prompt,
|
|
211
|
+
delivery,
|
|
212
|
+
provider: r.provider?.trim() || undefined,
|
|
213
|
+
model: r.model?.trim() || undefined,
|
|
214
|
+
};
|
|
215
|
+
scheduleCronById(row.id, row.cron_expression, 'agent_job', payload);
|
|
216
|
+
log('main', 'info', `Scheduled agent job from DB: ${row.name || row.id}`, { id: row.id });
|
|
217
|
+
} else {
|
|
218
|
+
const payload = row.payload ? JSON.parse(row.payload) : null;
|
|
219
|
+
scheduleCronById(row.id, row.cron_expression, row.task_type, payload);
|
|
220
|
+
log('main', 'info', `Scheduled job from DB: ${row.task_type}`, { id: row.id });
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
log('main', 'error', `Invalid scheduled job ${row.id}: ${(e as Error).message}`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Start Telegram bot if enabled and configured (so it stays connected without opening Settings)
|
|
228
|
+
startTelegramChannel();
|
|
76
229
|
|
|
77
230
|
log('main', 'info', 'Sulala Agent ready');
|
|
78
231
|
}
|