zubo 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.
Files changed (222) hide show
  1. package/.github/workflows/ci.yml +35 -0
  2. package/README.md +149 -0
  3. package/bun.lock +216 -0
  4. package/desktop/README.md +57 -0
  5. package/desktop/package.json +12 -0
  6. package/desktop/src-tauri/Cargo.toml +25 -0
  7. package/desktop/src-tauri/build.rs +3 -0
  8. package/desktop/src-tauri/icons/README.md +17 -0
  9. package/desktop/src-tauri/icons/icon.png +0 -0
  10. package/desktop/src-tauri/src/main.rs +189 -0
  11. package/desktop/src-tauri/tauri.conf.json +68 -0
  12. package/docs/ROADMAP.md +490 -0
  13. package/migrations/001_init.sql +9 -0
  14. package/migrations/002_memory.sql +33 -0
  15. package/migrations/003_cron.sql +24 -0
  16. package/migrations/004_usage.sql +12 -0
  17. package/migrations/005_secrets.sql +8 -0
  18. package/migrations/006_agents.sql +1 -0
  19. package/migrations/007_workflows.sql +22 -0
  20. package/migrations/008_proactive.sql +24 -0
  21. package/migrations/009_uploads.sql +9 -0
  22. package/migrations/010_observability.sql +22 -0
  23. package/migrations/011_api_keys.sql +7 -0
  24. package/migrations/012_indexes.sql +5 -0
  25. package/migrations/013_budget.sql +11 -0
  26. package/migrations/014_usage_session_idx.sql +2 -0
  27. package/package.json +39 -0
  28. package/site/404.html +156 -0
  29. package/site/CNAME +1 -0
  30. package/site/docs/agents.html +294 -0
  31. package/site/docs/api.html +446 -0
  32. package/site/docs/channels.html +345 -0
  33. package/site/docs/cli.html +238 -0
  34. package/site/docs/config.html +1034 -0
  35. package/site/docs/index.html +433 -0
  36. package/site/docs/integrations.html +381 -0
  37. package/site/docs/memory.html +254 -0
  38. package/site/docs/security.html +375 -0
  39. package/site/docs/skills.html +322 -0
  40. package/site/docs.css +412 -0
  41. package/site/index.html +638 -0
  42. package/site/install.sh +98 -0
  43. package/site/logo.svg +1 -0
  44. package/site/og-image.png +0 -0
  45. package/site/robots.txt +4 -0
  46. package/site/script.js +361 -0
  47. package/site/sitemap.xml +63 -0
  48. package/site/skills.html +532 -0
  49. package/site/style.css +1686 -0
  50. package/src/agent/agents.ts +159 -0
  51. package/src/agent/compaction.ts +53 -0
  52. package/src/agent/context.ts +18 -0
  53. package/src/agent/delegate.ts +118 -0
  54. package/src/agent/loop.ts +318 -0
  55. package/src/agent/prompts.ts +111 -0
  56. package/src/agent/session.ts +87 -0
  57. package/src/agent/teams.ts +116 -0
  58. package/src/agent/workflow-executor.ts +192 -0
  59. package/src/agent/workflow.ts +175 -0
  60. package/src/channels/adapter.ts +21 -0
  61. package/src/channels/dashboard.html.ts +2969 -0
  62. package/src/channels/discord.ts +137 -0
  63. package/src/channels/optional-deps.d.ts +17 -0
  64. package/src/channels/router.ts +199 -0
  65. package/src/channels/signal.ts +133 -0
  66. package/src/channels/slack.ts +101 -0
  67. package/src/channels/telegram.ts +102 -0
  68. package/src/channels/utils.ts +18 -0
  69. package/src/channels/webchat.ts +1797 -0
  70. package/src/channels/whatsapp.ts +119 -0
  71. package/src/config/loader.ts +22 -0
  72. package/src/config/paths.ts +43 -0
  73. package/src/config/schema.ts +121 -0
  74. package/src/db/connection.ts +20 -0
  75. package/src/db/export.ts +148 -0
  76. package/src/db/migrations.ts +42 -0
  77. package/src/index.ts +261 -0
  78. package/src/llm/claude.ts +193 -0
  79. package/src/llm/factory.ts +115 -0
  80. package/src/llm/failover.ts +101 -0
  81. package/src/llm/openai-compat.ts +409 -0
  82. package/src/llm/provider.ts +83 -0
  83. package/src/llm/smart-router.ts +241 -0
  84. package/src/logs.ts +53 -0
  85. package/src/memory/chunker.ts +58 -0
  86. package/src/memory/document-parser.ts +115 -0
  87. package/src/memory/embedder.ts +235 -0
  88. package/src/memory/engine.ts +170 -0
  89. package/src/memory/fts-index.ts +55 -0
  90. package/src/memory/hybrid-search.ts +72 -0
  91. package/src/memory/store.ts +56 -0
  92. package/src/memory/vector-index.ts +72 -0
  93. package/src/model.ts +118 -0
  94. package/src/registry/cli.ts +43 -0
  95. package/src/registry/client.ts +54 -0
  96. package/src/registry/installer.ts +67 -0
  97. package/src/scheduler/briefing.ts +71 -0
  98. package/src/scheduler/cron.ts +258 -0
  99. package/src/scheduler/heartbeat.ts +58 -0
  100. package/src/scheduler/memory-triggers.ts +100 -0
  101. package/src/scheduler/natural-cron.ts +163 -0
  102. package/src/scheduler/proactive.ts +25 -0
  103. package/src/scheduler/recipes.ts +110 -0
  104. package/src/secrets/store.ts +64 -0
  105. package/src/setup.ts +413 -0
  106. package/src/skills.ts +293 -0
  107. package/src/start.ts +373 -0
  108. package/src/status.ts +165 -0
  109. package/src/tools/builtin/connect-service.ts +205 -0
  110. package/src/tools/builtin/cron.ts +126 -0
  111. package/src/tools/builtin/datetime.ts +36 -0
  112. package/src/tools/builtin/delegate-task.ts +81 -0
  113. package/src/tools/builtin/delegate.ts +42 -0
  114. package/src/tools/builtin/diagnose.ts +41 -0
  115. package/src/tools/builtin/google-oauth.ts +379 -0
  116. package/src/tools/builtin/manage-agents.ts +149 -0
  117. package/src/tools/builtin/manage-skills.ts +294 -0
  118. package/src/tools/builtin/manage-teams.ts +89 -0
  119. package/src/tools/builtin/manage-triggers.ts +94 -0
  120. package/src/tools/builtin/manage-workflows.ts +119 -0
  121. package/src/tools/builtin/memory-search.ts +38 -0
  122. package/src/tools/builtin/memory-write.ts +30 -0
  123. package/src/tools/builtin/run-workflow.ts +36 -0
  124. package/src/tools/builtin/secrets.ts +122 -0
  125. package/src/tools/builtin/skill-registry.ts +75 -0
  126. package/src/tools/builtin-integrations/api-helpers.ts +26 -0
  127. package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
  128. package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
  129. package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
  130. package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
  131. package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
  132. package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
  133. package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
  134. package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
  135. package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
  136. package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
  137. package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
  138. package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
  139. package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
  140. package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
  141. package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
  142. package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
  143. package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
  144. package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
  145. package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
  146. package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
  147. package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
  148. package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
  149. package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
  150. package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
  151. package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
  152. package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
  153. package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
  154. package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
  155. package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
  156. package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
  157. package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
  158. package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
  159. package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
  160. package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
  161. package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
  162. package/src/tools/builtin-skills/file-read/handler.ts +66 -0
  163. package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
  164. package/src/tools/builtin-skills/file-write/handler.ts +64 -0
  165. package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
  166. package/src/tools/builtin-skills/http-request/handler.ts +87 -0
  167. package/src/tools/builtin-skills/shell/SKILL.md +26 -0
  168. package/src/tools/builtin-skills/shell/handler.ts +96 -0
  169. package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
  170. package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
  171. package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
  172. package/src/tools/builtin-skills/web-search/handler.ts +50 -0
  173. package/src/tools/executor.ts +205 -0
  174. package/src/tools/integration-installer.ts +106 -0
  175. package/src/tools/permissions.ts +45 -0
  176. package/src/tools/registry.ts +39 -0
  177. package/src/tools/sandbox-runner.ts +56 -0
  178. package/src/tools/sandbox.ts +82 -0
  179. package/src/tools/skill-installer.ts +52 -0
  180. package/src/tools/skill-loader.ts +259 -0
  181. package/src/types/optional-deps.d.ts +23 -0
  182. package/src/util/auth.ts +121 -0
  183. package/src/util/costs.ts +59 -0
  184. package/src/util/error-buffer.ts +32 -0
  185. package/src/util/google-tokens.ts +180 -0
  186. package/src/util/logger.ts +73 -0
  187. package/src/util/perf-collector.ts +35 -0
  188. package/src/util/rate-limiter.ts +70 -0
  189. package/src/util/tokens.ts +17 -0
  190. package/src/voice/stt.ts +57 -0
  191. package/src/voice/tts.ts +103 -0
  192. package/tests/agent/session.test.ts +109 -0
  193. package/tests/agent-loop.test.ts +54 -0
  194. package/tests/auth.test.ts +89 -0
  195. package/tests/channels.test.ts +67 -0
  196. package/tests/compaction.test.ts +44 -0
  197. package/tests/config.test.ts +51 -0
  198. package/tests/costs.test.ts +19 -0
  199. package/tests/cron.test.ts +55 -0
  200. package/tests/db/export.test.ts +219 -0
  201. package/tests/executor.test.ts +144 -0
  202. package/tests/export.test.ts +137 -0
  203. package/tests/helpers/mock-llm.ts +34 -0
  204. package/tests/helpers/test-db.ts +74 -0
  205. package/tests/integration/chat-flow.test.ts +48 -0
  206. package/tests/integrations.test.ts +97 -0
  207. package/tests/memory/engine.test.ts +114 -0
  208. package/tests/memory-engine.test.ts +57 -0
  209. package/tests/permissions.test.ts +21 -0
  210. package/tests/rate-limiter.test.ts +70 -0
  211. package/tests/registry.test.ts +67 -0
  212. package/tests/router.test.ts +36 -0
  213. package/tests/session.test.ts +58 -0
  214. package/tests/skill-loader.test.ts +44 -0
  215. package/tests/tokens.test.ts +30 -0
  216. package/tests/tools/executor.test.ts +130 -0
  217. package/tests/util/auth.test.ts +75 -0
  218. package/tests/util/rate-limiter.test.ts +73 -0
  219. package/tests/voice.test.ts +60 -0
  220. package/tests/webchat.test.ts +88 -0
  221. package/tests/workflow.test.ts +38 -0
  222. package/tsconfig.json +16 -0
@@ -0,0 +1,205 @@
1
+ import { registerTool } from "../registry";
2
+ import { listAvailableIntegrations, installIntegration } from "../integration-installer";
3
+ import { setSecret } from "../../secrets/store";
4
+ import { loadSkills } from "../skill-loader";
5
+ import { paths } from "../../config/paths";
6
+ import { logger } from "../../util/logger";
7
+
8
+ export function registerConnectServiceTool() {
9
+ registerTool({
10
+ definition: {
11
+ name: "connect_service",
12
+ description:
13
+ "Connect an external service (GitHub, Google, Notion, etc.) by storing credentials and installing pre-built integration skills. Use action 'list' to see available integrations, or 'connect' to set up a service.",
14
+ input_schema: {
15
+ type: "object",
16
+ properties: {
17
+ action: {
18
+ type: "string",
19
+ enum: ["list", "connect"],
20
+ description: "The action to perform",
21
+ },
22
+ service: {
23
+ type: "string",
24
+ description:
25
+ "Service name to connect (e.g., 'github', 'google', 'notion'). Required for connect.",
26
+ },
27
+ credentials: {
28
+ type: "object",
29
+ description:
30
+ "Key-value pairs of credential names and values (e.g., { \"github_token\": \"ghp_...\" }). Required for connect.",
31
+ },
32
+ },
33
+ required: ["action"],
34
+ },
35
+ },
36
+ execute: async (input) => {
37
+ const { action, service, credentials } = input as {
38
+ action: string;
39
+ service?: string;
40
+ credentials?: Record<string, string>;
41
+ };
42
+
43
+ switch (action) {
44
+ case "list": {
45
+ const integrations = listAvailableIntegrations();
46
+ if (integrations.length === 0) {
47
+ return "No integrations available.";
48
+ }
49
+ return JSON.stringify({
50
+ integrations: integrations.map((i) => ({
51
+ service: i.service,
52
+ skills: i.skills,
53
+ required_secret: i.secret_name,
54
+ })),
55
+ });
56
+ }
57
+
58
+ case "connect": {
59
+ if (!service) {
60
+ return JSON.stringify({ error: "service is required for connect" });
61
+ }
62
+
63
+ // Validate service name to prevent path traversal
64
+ if (!/^[a-z0-9_-]+$/.test(service)) {
65
+ return JSON.stringify({
66
+ error: "Invalid service name. Must contain only lowercase letters, numbers, hyphens, and underscores.",
67
+ });
68
+ }
69
+
70
+ // Google uses OAuth 2.0 -- redirect to the google_oauth tool
71
+ if (service === "google") {
72
+ // If client_id and client_secret are provided, store them for the OAuth flow
73
+ if (credentials) {
74
+ if (credentials.client_id) {
75
+ setSecret("google_client_id", credentials.client_id, "google");
76
+ }
77
+ if (credentials.client_secret) {
78
+ setSecret("google_client_secret", credentials.client_secret, "google");
79
+ }
80
+ }
81
+
82
+ // Install the integration skill templates
83
+ const installed = installIntegration(paths.skills, service);
84
+ if (installed.length > 0) {
85
+ try {
86
+ await loadSkills(paths.skills);
87
+ logger.info(`Google integration skills installed: ${installed.join(", ")}`);
88
+ } catch (err: any) {
89
+ logger.error(`Failed to load Google integration skills: ${err.message}`);
90
+ }
91
+ }
92
+
93
+ return JSON.stringify({
94
+ success: true,
95
+ service: "google",
96
+ skills_installed: installed,
97
+ message:
98
+ "Google integration skills installed. Google requires OAuth 2.0 to authenticate. " +
99
+ "You need TWO credentials from the user: " +
100
+ "(1) client_id (ends with .apps.googleusercontent.com) and " +
101
+ "(2) client_secret (starts with GOCSPX-). " +
102
+ "These are different values — ask for both separately.",
103
+ next_step:
104
+ "Ask the user for both their Google OAuth client_id AND client_secret, " +
105
+ "then call google_oauth with action 'start' passing both values.",
106
+ });
107
+ }
108
+
109
+ // Validate required credentials per service
110
+ const REQUIRED_SECRETS: Record<string, { keys: string[]; guide: string }> = {
111
+ github: {
112
+ keys: ["github_token"],
113
+ guide: "Requires a GitHub Personal Access Token (PAT). Get one at: GitHub > Settings > Developer settings > Personal access tokens. Needs scopes: repo, read:org.",
114
+ },
115
+ notion: {
116
+ keys: ["notion_token"],
117
+ guide: "Requires a Notion Internal Integration Token. Create one at: notion.so/my-integrations. Then share your pages/databases with the integration.",
118
+ },
119
+ linear: {
120
+ keys: ["linear_token"],
121
+ guide: "Requires a Linear Personal API Key. Get one at: Linear > Settings > API > Personal API keys.",
122
+ },
123
+ slack: {
124
+ keys: ["slack_token"],
125
+ guide: "Requires a Slack Bot Token (xoxb-...). Create a Slack app at api.slack.com/apps, add Bot Token Scopes (channels:read, channels:history, chat:write, search:read), then install to workspace.",
126
+ },
127
+ jira: {
128
+ keys: ["jira_token", "jira_email", "jira_url"],
129
+ guide: "Requires 3 credentials: jira_email (your Atlassian email), jira_token (API token from id.atlassian.com/manage-profile/security/api-tokens), and jira_url (e.g. https://yourteam.atlassian.net).",
130
+ },
131
+ twitter: {
132
+ keys: ["twitter_bearer_token"],
133
+ guide: "For reading: provide twitter_bearer_token (from developer.twitter.com > App > Keys and tokens). For posting: also need twitter_api_key, twitter_api_secret, twitter_access_token, twitter_access_secret.",
134
+ },
135
+ };
136
+
137
+ const serviceInfo = REQUIRED_SECRETS[service];
138
+
139
+ // If no credentials provided, return guidance
140
+ if (!credentials || Object.keys(credentials).length === 0) {
141
+ if (serviceInfo) {
142
+ return JSON.stringify({
143
+ error: "No credentials provided.",
144
+ required_secrets: serviceInfo.keys,
145
+ guide: serviceInfo.guide,
146
+ });
147
+ }
148
+ }
149
+
150
+ // Store credentials
151
+ if (credentials) {
152
+ for (const [name, value] of Object.entries(credentials)) {
153
+ setSecret(name, value, service);
154
+ }
155
+ }
156
+
157
+ // Check if all required keys were provided
158
+ let missingKeys: string[] = [];
159
+ if (serviceInfo) {
160
+ for (const key of serviceInfo.keys) {
161
+ if (!credentials || !credentials[key]) {
162
+ missingKeys.push(key);
163
+ }
164
+ }
165
+ }
166
+
167
+ // Install integration skills
168
+ const installed = installIntegration(paths.skills, service);
169
+ if (installed.length === 0) {
170
+ return JSON.stringify({
171
+ error: `No integration templates found for service "${service}". Credentials were stored if provided.`,
172
+ });
173
+ }
174
+
175
+ // Hot-load newly installed skills
176
+ try {
177
+ const loaded = await loadSkills(paths.skills);
178
+ logger.info(
179
+ `Integration "${service}" connected. Skills: ${installed.join(", ")}`
180
+ );
181
+ } catch (err: any) {
182
+ logger.error(`Failed to load integration skills: ${err.message}`);
183
+ }
184
+
185
+ const result: Record<string, unknown> = {
186
+ success: true,
187
+ service,
188
+ skills_installed: installed,
189
+ message: `Service "${service}" connected. ${installed.length} skill(s) installed and ready: ${installed.join(", ")}`,
190
+ };
191
+
192
+ if (missingKeys.length > 0) {
193
+ result.warning = `Missing credentials: ${missingKeys.join(", ")}. Some features may not work.`;
194
+ if (serviceInfo) result.guide = serviceInfo.guide;
195
+ }
196
+
197
+ return JSON.stringify(result);
198
+ }
199
+
200
+ default:
201
+ return JSON.stringify({ error: `Unknown action: ${action}` });
202
+ }
203
+ },
204
+ });
205
+ }
@@ -0,0 +1,126 @@
1
+ import { registerTool } from "../registry";
2
+ import { addCronJob, removeCronJob, listCronJobs } from "../../scheduler/cron";
3
+ import { parseNaturalSchedule } from "../../scheduler/natural-cron";
4
+ import type { MessageRouter } from "../../channels/router";
5
+ import type { ZuboConfig } from "../../config/schema";
6
+ import type { LlmProvider } from "../../llm/provider";
7
+ import type { Database } from "bun:sqlite";
8
+
9
+ export function registerCronTools(
10
+ db: Database,
11
+ router: MessageRouter,
12
+ config: ZuboConfig,
13
+ llm?: LlmProvider
14
+ ) {
15
+ registerTool({
16
+ definition: {
17
+ name: "cron_create",
18
+ description:
19
+ "Create a scheduled task that runs on a cron schedule. Schedule can be a cron expression (e.g. '0 9 * * 1-5') or natural language (e.g. 'every weekday at 9am', 'every 30 minutes', 'daily at noon'). The task is a natural language instruction that the agent will execute at the scheduled time. Optionally assign to a specific sub-agent.",
20
+ input_schema: {
21
+ type: "object",
22
+ properties: {
23
+ name: {
24
+ type: "string",
25
+ description:
26
+ "A unique name for this task (e.g., 'morning-briefing', 'pr-review-reminder')",
27
+ },
28
+ schedule: {
29
+ type: "string",
30
+ description:
31
+ "Cron expression (e.g., '0 9 * * 1-5') or natural language (e.g., 'every weekday at 9am', 'every monday and friday at noon', 'every 30 minutes', 'daily at 8:30am')",
32
+ },
33
+ task: {
34
+ type: "string",
35
+ description:
36
+ "The task to perform, as a natural language instruction (e.g., 'Send a morning briefing with weather and calendar summary')",
37
+ },
38
+ agent: {
39
+ type: "string",
40
+ description:
41
+ "Optional: name of a sub-agent to delegate this task to. The agent must be created with manage_agents first.",
42
+ },
43
+ },
44
+ required: ["name", "schedule", "task"],
45
+ },
46
+ },
47
+ execute: async (input) => {
48
+ const { name, schedule, task, agent } = input as {
49
+ name: string;
50
+ schedule: string;
51
+ task: string;
52
+ agent?: string;
53
+ };
54
+ try {
55
+ let cronExpr = schedule;
56
+ let naturalParseError = "";
57
+ try {
58
+ cronExpr = parseNaturalSchedule(schedule);
59
+ } catch (parseErr: any) {
60
+ naturalParseError = parseErr.message;
61
+ // Only fall through if it looks like raw cron (5 space-separated fields)
62
+ if (!/^[\d*\/,\-]+(\s+[\d*\/,\-]+){4}$/.test(schedule.trim())) {
63
+ throw new Error(naturalParseError);
64
+ }
65
+ }
66
+ addCronJob(db, name, cronExpr, task, router, config, agent, llm);
67
+ let msg = `Scheduled task "${name}" created.\nSchedule: ${cronExpr}`;
68
+ if (cronExpr !== schedule) msg += ` (parsed from "${schedule}")`;
69
+ msg += `\nTask: ${task}`;
70
+ if (agent) msg += `\nAgent: ${agent}`;
71
+ return msg;
72
+ } catch (err: any) {
73
+ if (err.message?.includes("UNIQUE constraint")) {
74
+ return `Error: A task named "${name}" already exists. Use a different name or delete the existing one first.`;
75
+ }
76
+ throw err;
77
+ }
78
+ },
79
+ });
80
+
81
+ registerTool({
82
+ definition: {
83
+ name: "cron_list",
84
+ description:
85
+ "List all scheduled tasks with their status, schedule, and last run time.",
86
+ input_schema: {
87
+ type: "object",
88
+ properties: {},
89
+ },
90
+ },
91
+ execute: async () => {
92
+ const jobs = listCronJobs(db);
93
+ if (jobs.length === 0) return "No scheduled tasks found.";
94
+
95
+ return jobs
96
+ .map(
97
+ (j) =>
98
+ `- ${j.name} [${j.enabled ? "active" : "disabled"}]\n Schedule: ${j.schedule}\n Task: ${j.task}${j.agent ? `\n Agent: ${j.agent}` : ""}\n Last run: ${j.last_run ?? "never"}`
99
+ )
100
+ .join("\n\n");
101
+ },
102
+ });
103
+
104
+ registerTool({
105
+ definition: {
106
+ name: "cron_delete",
107
+ description: "Delete a scheduled task by name.",
108
+ input_schema: {
109
+ type: "object",
110
+ properties: {
111
+ name: {
112
+ type: "string",
113
+ description: "The name of the scheduled task to delete",
114
+ },
115
+ },
116
+ required: ["name"],
117
+ },
118
+ },
119
+ execute: async (input) => {
120
+ const { name } = input as { name: string };
121
+ const removed = removeCronJob(db, name);
122
+ if (removed) return `Scheduled task "${name}" has been deleted.`;
123
+ return `No scheduled task found with name "${name}".`;
124
+ },
125
+ });
126
+ }
@@ -0,0 +1,36 @@
1
+ import { registerTool } from "../registry";
2
+
3
+ export function registerDatetimeTool() {
4
+ registerTool({
5
+ definition: {
6
+ name: "get_current_datetime",
7
+ description:
8
+ "Get the current date and time in ISO format, with optional timezone.",
9
+ input_schema: {
10
+ type: "object",
11
+ properties: {
12
+ timezone: {
13
+ type: "string",
14
+ description:
15
+ "IANA timezone (e.g. 'Europe/Berlin'). Defaults to UTC.",
16
+ },
17
+ },
18
+ required: [],
19
+ },
20
+ },
21
+ execute: async (input) => {
22
+ const tz = (input.timezone as string) || "UTC";
23
+ const now = new Date();
24
+ const formatted = now.toLocaleString("en-US", {
25
+ timeZone: tz,
26
+ dateStyle: "full",
27
+ timeStyle: "long",
28
+ });
29
+ return JSON.stringify({
30
+ iso: now.toISOString(),
31
+ formatted,
32
+ timezone: tz,
33
+ });
34
+ },
35
+ });
36
+ }
@@ -0,0 +1,81 @@
1
+ import { registerTool } from "../registry";
2
+ import type { LlmProvider } from "../../llm/provider";
3
+ import { logger } from "../../util/logger";
4
+
5
+ export function registerDelegateTaskTool(llm: LlmProvider): void {
6
+ registerTool({
7
+ definition: {
8
+ name: "delegate_task",
9
+ description:
10
+ "Delegate a subtask to an ad-hoc sub-agent. The sub-agent runs a fresh agent loop with its own context. Use for complex tasks that benefit from focused, independent processing. Unlike 'delegate', this does not require a pre-registered agent.",
11
+ input_schema: {
12
+ type: "object",
13
+ properties: {
14
+ task: {
15
+ type: "string",
16
+ description: "Clear description of the subtask to delegate",
17
+ },
18
+ context: {
19
+ type: "string",
20
+ description:
21
+ "Optional additional context or constraints for the sub-agent",
22
+ },
23
+ },
24
+ required: ["task"],
25
+ },
26
+ },
27
+ execute: async (input) => {
28
+ const task = input.task as string;
29
+ const context = input.context as string | undefined;
30
+
31
+ if (!task) {
32
+ return JSON.stringify({ error: "task is required" });
33
+ }
34
+
35
+ // Dynamic imports to avoid circular dependencies
36
+ const { agentLoop } = await import("../../agent/loop");
37
+ const crypto = await import("crypto");
38
+
39
+ const subSessionId = "delegate-task-" + crypto.randomUUID().slice(0, 8);
40
+
41
+ const message = context
42
+ ? `${task}\n\nAdditional context: ${context}`
43
+ : task;
44
+
45
+ logger.info("Delegating subtask to ad-hoc sub-agent", {
46
+ task: task.slice(0, 100),
47
+ sessionId: subSessionId,
48
+ });
49
+
50
+ try {
51
+ // Exclude delegation tools to prevent recursive delegation
52
+ const { getAllToolDefs } = await import("../registry");
53
+ const allowedTools = getAllToolDefs()
54
+ .map((t) => t.name)
55
+ .filter((n) => n !== "delegate_task" && n !== "delegate");
56
+
57
+ const result = await agentLoop(llm, subSessionId, message, {
58
+ systemPromptOverride:
59
+ "You are a focused sub-agent. Complete the given task concisely and return the result. Do not ask follow-up questions.",
60
+ maxRounds: 5,
61
+ allowedTools,
62
+ });
63
+
64
+ logger.info("Sub-agent completed", {
65
+ sessionId: subSessionId,
66
+ toolCalls: result.toolCalls,
67
+ });
68
+
69
+ return result.reply;
70
+ } catch (err: unknown) {
71
+ const errorMessage =
72
+ err instanceof Error ? err.message : String(err);
73
+ logger.error("Sub-agent failed", {
74
+ error: errorMessage,
75
+ sessionId: subSessionId,
76
+ });
77
+ return "Delegation failed: " + errorMessage;
78
+ }
79
+ },
80
+ });
81
+ }
@@ -0,0 +1,42 @@
1
+ import { registerTool } from "../registry";
2
+ import type { LlmProvider } from "../../llm/provider";
3
+
4
+ export function registerDelegateTool(llm: LlmProvider) {
5
+ registerTool({
6
+ definition: {
7
+ name: "delegate",
8
+ description:
9
+ "Delegate a task to a named sub-agent. The sub-agent runs with its own system prompt and scoped tools, shares memory, but keeps separate conversation history. Use manage_agents to create agents first.",
10
+ input_schema: {
11
+ type: "object",
12
+ properties: {
13
+ agent: {
14
+ type: "string",
15
+ description: "The name of the agent to delegate to",
16
+ },
17
+ task: {
18
+ type: "string",
19
+ description:
20
+ "The task to delegate, as a natural language instruction",
21
+ },
22
+ },
23
+ required: ["agent", "task"],
24
+ },
25
+ },
26
+ execute: async (input) => {
27
+ const { agent, task } = input as { agent: string; task: string };
28
+
29
+ if (!agent) {
30
+ return JSON.stringify({ error: "agent name is required" });
31
+ }
32
+ if (!task) {
33
+ return JSON.stringify({ error: "task is required" });
34
+ }
35
+
36
+ // Dynamic import to avoid circular dependency
37
+ const { delegateToAgent } = await import("../../agent/delegate");
38
+ const result = await delegateToAgent(llm, agent, task);
39
+ return result;
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,41 @@
1
+ import { registerTool } from "../registry";
2
+ import { getRecentErrors } from "../../util/error-buffer";
3
+
4
+ export function registerDiagnoseTool(): void {
5
+ registerTool({
6
+ definition: {
7
+ name: "diagnose",
8
+ description:
9
+ "Check recent errors and system health. Use when something seems wrong or the user reports an issue.",
10
+ input_schema: {
11
+ type: "object",
12
+ properties: {
13
+ category: {
14
+ type: "string",
15
+ description:
16
+ "Optional error category filter (e.g. 'agent-loop', 'tool:shell')",
17
+ },
18
+ },
19
+ required: [],
20
+ },
21
+ },
22
+ execute: async (input) => {
23
+ const category = input.category as string | undefined;
24
+ const errors = getRecentErrors();
25
+
26
+ const filtered = category
27
+ ? errors.filter((e) => e.source.includes(category))
28
+ : errors;
29
+
30
+ if (filtered.length === 0) {
31
+ return "No recent errors found. System appears healthy.";
32
+ }
33
+
34
+ const summary = filtered
35
+ .map((e) => `[${e.timestamp}] ${e.source}: ${e.message}`)
36
+ .join("\n");
37
+
38
+ return `Found ${filtered.length} recent error(s):\n${summary}`;
39
+ },
40
+ });
41
+ }