@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.
Files changed (173) hide show
  1. package/README.md +3 -2
  2. package/context/airtable.md +35 -0
  3. package/context/asana.md +37 -0
  4. package/context/bluesky.md +26 -91
  5. package/context/calendar.md +63 -0
  6. package/context/country-info.md +13 -0
  7. package/context/create-skill.md +128 -0
  8. package/context/discord.md +30 -0
  9. package/context/docs.md +29 -0
  10. package/context/drive.md +49 -0
  11. package/context/dropbox.md +39 -0
  12. package/context/facebook.md +47 -0
  13. package/context/fetch-form-api.md +16 -0
  14. package/context/figma.md +30 -0
  15. package/context/github.md +58 -0
  16. package/context/gmail.md +52 -0
  17. package/context/google.md +28 -0
  18. package/context/hellohub.md +29 -0
  19. package/context/jira.md +46 -0
  20. package/context/linear.md +40 -0
  21. package/context/notion.md +45 -0
  22. package/context/portal-integrations.md +42 -0
  23. package/context/post-to-x.md +50 -0
  24. package/context/sheets.md +47 -0
  25. package/context/slack.md +48 -0
  26. package/context/slides.md +35 -0
  27. package/context/stripe.md +38 -0
  28. package/context/tes.md +7 -0
  29. package/context/test.md +7 -0
  30. package/context/zoom.md +28 -0
  31. package/dist/agent/google/calendar.d.ts +2 -0
  32. package/dist/agent/google/calendar.d.ts.map +1 -0
  33. package/dist/agent/google/calendar.js +119 -0
  34. package/dist/agent/google/calendar.js.map +1 -0
  35. package/dist/agent/google/drive.d.ts +2 -0
  36. package/dist/agent/google/drive.d.ts.map +1 -0
  37. package/dist/agent/google/drive.js +51 -0
  38. package/dist/agent/google/drive.js.map +1 -0
  39. package/dist/agent/google/get-token.d.ts +7 -0
  40. package/dist/agent/google/get-token.d.ts.map +1 -0
  41. package/dist/agent/google/get-token.js +37 -0
  42. package/dist/agent/google/get-token.js.map +1 -0
  43. package/dist/agent/google/gmail.d.ts +2 -0
  44. package/dist/agent/google/gmail.d.ts.map +1 -0
  45. package/dist/agent/google/gmail.js +138 -0
  46. package/dist/agent/google/gmail.js.map +1 -0
  47. package/dist/agent/google/index.d.ts +2 -0
  48. package/dist/agent/google/index.d.ts.map +1 -0
  49. package/dist/agent/google/index.js +13 -0
  50. package/dist/agent/google/index.js.map +1 -0
  51. package/dist/agent/loop.d.ts +8 -0
  52. package/dist/agent/loop.d.ts.map +1 -1
  53. package/dist/agent/loop.js +226 -40
  54. package/dist/agent/loop.js.map +1 -1
  55. package/dist/agent/memory.d.ts +21 -0
  56. package/dist/agent/memory.d.ts.map +1 -0
  57. package/dist/agent/memory.js +33 -0
  58. package/dist/agent/memory.js.map +1 -0
  59. package/dist/agent/pending-actions.d.ts +21 -0
  60. package/dist/agent/pending-actions.d.ts.map +1 -0
  61. package/dist/agent/pending-actions.js +65 -0
  62. package/dist/agent/pending-actions.js.map +1 -0
  63. package/dist/agent/pi-runner.d.ts +27 -0
  64. package/dist/agent/pi-runner.d.ts.map +1 -0
  65. package/dist/agent/pi-runner.js +300 -0
  66. package/dist/agent/pi-runner.js.map +1 -0
  67. package/dist/agent/skill-generate.d.ts +63 -0
  68. package/dist/agent/skill-generate.d.ts.map +1 -0
  69. package/dist/agent/skill-generate.js +128 -0
  70. package/dist/agent/skill-generate.js.map +1 -0
  71. package/dist/agent/skill-install.d.ts.map +1 -1
  72. package/dist/agent/skill-install.js +80 -31
  73. package/dist/agent/skill-install.js.map +1 -1
  74. package/dist/agent/skill-templates.d.ts +17 -0
  75. package/dist/agent/skill-templates.d.ts.map +1 -0
  76. package/dist/agent/skill-templates.js +26 -0
  77. package/dist/agent/skill-templates.js.map +1 -0
  78. package/dist/agent/skills-config.d.ts +24 -2
  79. package/dist/agent/skills-config.d.ts.map +1 -1
  80. package/dist/agent/skills-config.js +107 -8
  81. package/dist/agent/skills-config.js.map +1 -1
  82. package/dist/agent/skills-watcher.js +1 -1
  83. package/dist/agent/skills.d.ts +9 -3
  84. package/dist/agent/skills.d.ts.map +1 -1
  85. package/dist/agent/skills.js +104 -9
  86. package/dist/agent/skills.js.map +1 -1
  87. package/dist/agent/tools.d.ts +25 -3
  88. package/dist/agent/tools.d.ts.map +1 -1
  89. package/dist/agent/tools.integrations.test.d.ts +2 -0
  90. package/dist/agent/tools.integrations.test.d.ts.map +1 -0
  91. package/dist/agent/tools.integrations.test.js +269 -0
  92. package/dist/agent/tools.integrations.test.js.map +1 -0
  93. package/dist/agent/tools.js +692 -39
  94. package/dist/agent/tools.js.map +1 -1
  95. package/dist/ai/orchestrator.d.ts +4 -1
  96. package/dist/ai/orchestrator.d.ts.map +1 -1
  97. package/dist/ai/orchestrator.js +246 -14
  98. package/dist/ai/orchestrator.js.map +1 -1
  99. package/dist/ai/pricing.d.ts +6 -0
  100. package/dist/ai/pricing.d.ts.map +1 -0
  101. package/dist/ai/pricing.js +39 -0
  102. package/dist/ai/pricing.js.map +1 -0
  103. package/dist/channels/discord.d.ts +15 -0
  104. package/dist/channels/discord.d.ts.map +1 -0
  105. package/dist/channels/discord.js +55 -0
  106. package/dist/channels/discord.js.map +1 -0
  107. package/dist/channels/stripe.d.ts +15 -0
  108. package/dist/channels/stripe.d.ts.map +1 -0
  109. package/dist/channels/stripe.js +58 -0
  110. package/dist/channels/stripe.js.map +1 -0
  111. package/dist/channels/telegram.d.ts +60 -0
  112. package/dist/channels/telegram.d.ts.map +1 -0
  113. package/dist/channels/telegram.js +562 -0
  114. package/dist/channels/telegram.js.map +1 -0
  115. package/dist/cli.js +66 -8
  116. package/dist/cli.js.map +1 -1
  117. package/dist/config.d.ts +14 -0
  118. package/dist/config.d.ts.map +1 -1
  119. package/dist/config.js +85 -1
  120. package/dist/config.js.map +1 -1
  121. package/dist/db/index.d.ts +83 -0
  122. package/dist/db/index.d.ts.map +1 -1
  123. package/dist/db/index.js +174 -2
  124. package/dist/db/index.js.map +1 -1
  125. package/dist/db/schema.sql +35 -0
  126. package/dist/gateway/server.d.ts.map +1 -1
  127. package/dist/gateway/server.js +1219 -27
  128. package/dist/gateway/server.js.map +1 -1
  129. package/dist/index.js +149 -6
  130. package/dist/index.js.map +1 -1
  131. package/dist/ollama-setup.d.ts +27 -0
  132. package/dist/ollama-setup.d.ts.map +1 -0
  133. package/dist/ollama-setup.js +191 -0
  134. package/dist/ollama-setup.js.map +1 -0
  135. package/dist/onboard-env.d.ts +1 -1
  136. package/dist/onboard-env.d.ts.map +1 -1
  137. package/dist/onboard-env.js +2 -0
  138. package/dist/onboard-env.js.map +1 -1
  139. package/dist/onboard.d.ts +3 -1
  140. package/dist/onboard.d.ts.map +1 -1
  141. package/dist/onboard.js +7 -2
  142. package/dist/onboard.js.map +1 -1
  143. package/dist/plugins/index.d.ts +10 -0
  144. package/dist/plugins/index.d.ts.map +1 -1
  145. package/dist/plugins/index.js +32 -0
  146. package/dist/plugins/index.js.map +1 -1
  147. package/dist/redact.d.ts +15 -0
  148. package/dist/redact.d.ts.map +1 -0
  149. package/dist/redact.js +56 -0
  150. package/dist/redact.js.map +1 -0
  151. package/dist/scheduler/cron.d.ts +21 -0
  152. package/dist/scheduler/cron.d.ts.map +1 -1
  153. package/dist/scheduler/cron.js +60 -0
  154. package/dist/scheduler/cron.js.map +1 -1
  155. package/dist/system-capabilities.d.ts +11 -0
  156. package/dist/system-capabilities.d.ts.map +1 -0
  157. package/dist/system-capabilities.js +109 -0
  158. package/dist/system-capabilities.js.map +1 -0
  159. package/dist/types.d.ts +62 -3
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/watcher/index.d.ts +2 -0
  162. package/dist/watcher/index.d.ts.map +1 -1
  163. package/dist/watcher/index.js +31 -1
  164. package/dist/watcher/index.js.map +1 -1
  165. package/dist/workspace-automations.d.ts +16 -0
  166. package/dist/workspace-automations.d.ts.map +1 -0
  167. package/dist/workspace-automations.js +133 -0
  168. package/dist/workspace-automations.js.map +1 -0
  169. package/package.json +19 -3
  170. package/registry/bluesky.md +12 -89
  171. package/registry/skills-registry.json +6 -0
  172. package/src/db/schema.sql +35 -0
  173. 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.6",
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": ["dist", "bin", "src/db/schema.sql", "registry", "context", "dashboard/dist"],
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": { "access": "public" },
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",
@@ -1,111 +1,34 @@
1
1
  ---
2
2
  name: bluesky
3
- description: Post to Bluesky (AT Protocol). Use when the user asks to post a tweet/thread to Bluesky, share content on Bluesky, or post news/headlines from a source (URL or text) to Bluesky.
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", "python3", "sh"], "env": ["BSKY_HANDLE", "BSKY_APP_PASSWORD"] },
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. Use **run_command** with `curl`, `python3`, and `sh`. Add `curl`, `python3`, and `sh` to ALLOWED_BINARIES.
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
- Requires `BSKY_HANDLE` (e.g. `your.bsky.social`) and `BSKY_APP_PASSWORD` (app password from bsky.app → Settings → App Passwords). Set them in `.env` or in the skill config (`~/.sulala/config.json` or `.sulala/config.json` in the project; see Skill config in the dashboard). Config is re-read when the file changes; enable/disable takes effect without restart.
18
+ ## How to post
20
19
 
21
- **IMPORTANT:** `run_command` does not run a shell — `$BSKY_HANDLE` and `$BSKY_APP_PASSWORD` will not expand if you call curl directly. Always use `binary: "sh"` and `args: ["-c", "curl ... $BSKY_HANDLE ... $BSKY_APP_PASSWORD ..."]` so the env vars expand.
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
- ## When to Use
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
- ## Notes
31
+ ## Requirements
108
32
 
109
- - Use an **app password**, not the main account password. Create at bsky.app Settings → App Passwords.
110
- - `bsky.social` is the default PDS; custom PDS users would need a different host.
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 PortalAPI 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 { scheduleCron } from './scheduler/cron.js';
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
- startWatcher();
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
- setTaskHandler(async (task) => {
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
- scheduleCron('* * * * *', 'heartbeat', { ts: Date.now() });
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
  }