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,133 @@
1
+ async function safeApiError(res: Response, service: string): Promise<string> {
2
+ const body = await res.text().catch(() => "");
3
+ console.error(`[${service}] API error ${res.status}: ${body.slice(0, 500)}`);
4
+ return JSON.stringify({ error: `${service} API error: ${res.status} ${res.statusText}` });
5
+ }
6
+
7
+ function safeExceptionError(err: any, service: string): string {
8
+ console.error(`[${service}] Request failed: ${err.message}`);
9
+ return JSON.stringify({ error: `${service} request failed. Check logs for details.` });
10
+ }
11
+
12
+ const API = "https://api.twitter.com/2";
13
+
14
+ export default async function (input: Record<string, unknown>): Promise<string> {
15
+ const bearer = (globalThis as any).Zubo?.getSecret?.("twitter_bearer_token");
16
+ if (!bearer) {
17
+ return JSON.stringify({ error: "Twitter bearer token not configured. Use secret_set to store 'twitter_bearer_token'." });
18
+ }
19
+
20
+ const { action, text, query, tweet_id, max_results } = input as {
21
+ action: string; text?: string; query?: string; tweet_id?: string; max_results?: number;
22
+ };
23
+
24
+ const readHeaders: Record<string, string> = {
25
+ Authorization: `Bearer ${bearer}`,
26
+ };
27
+
28
+ try {
29
+ switch (action) {
30
+ case "timeline": {
31
+ // Get authenticated user's timeline (home timeline requires user context)
32
+ const meRes = await fetch(`${API}/users/me`, { headers: readHeaders });
33
+ if (!meRes.ok) return await safeApiError(meRes, "Twitter");
34
+ const me = (await meRes.json()) as any;
35
+ const res = await fetch(`${API}/users/${me.data.id}/tweets?max_results=${max_results || 10}&tweet.fields=created_at,public_metrics`, { headers: readHeaders });
36
+ if (!res.ok) return await safeApiError(res, "Twitter");
37
+ const data = (await res.json()) as any;
38
+ return JSON.stringify(data.data?.map((t: any) => ({ id: t.id, text: t.text, created_at: t.created_at, metrics: t.public_metrics })) ?? []);
39
+ }
40
+ case "search": {
41
+ if (!query) return JSON.stringify({ error: "query required" });
42
+ const res = await fetch(`${API}/tweets/search/recent?query=${encodeURIComponent(query)}&max_results=${max_results || 10}&tweet.fields=created_at,author_id,public_metrics`, { headers: readHeaders });
43
+ if (!res.ok) return await safeApiError(res, "Twitter");
44
+ const data = (await res.json()) as any;
45
+ return JSON.stringify(data.data?.map((t: any) => ({ id: t.id, text: t.text, author_id: t.author_id, created_at: t.created_at })) ?? []);
46
+ }
47
+ case "post": {
48
+ if (!text) return JSON.stringify({ error: "text required" });
49
+ const apiKey = (globalThis as any).Zubo?.getSecret?.("twitter_api_key");
50
+ const apiSecret = (globalThis as any).Zubo?.getSecret?.("twitter_api_secret");
51
+ const accessToken = (globalThis as any).Zubo?.getSecret?.("twitter_access_token");
52
+ const accessSecret = (globalThis as any).Zubo?.getSecret?.("twitter_access_secret");
53
+ if (!apiKey || !apiSecret || !accessToken || !accessSecret) {
54
+ return JSON.stringify({ error: "Twitter OAuth credentials not configured. Need twitter_api_key, twitter_api_secret, twitter_access_token, twitter_access_secret." });
55
+ }
56
+ // Use OAuth 1.0a -- simplified via fetch with pre-computed header
57
+ const oauthHeader = computeOAuth1Header("POST", `${API}/tweets`, {}, apiKey, apiSecret, accessToken, accessSecret);
58
+ const res = await fetch(`${API}/tweets`, {
59
+ method: "POST",
60
+ headers: { Authorization: oauthHeader, "Content-Type": "application/json" },
61
+ body: JSON.stringify({ text }),
62
+ });
63
+ if (!res.ok) return await safeApiError(res, "Twitter");
64
+ const data = (await res.json()) as any;
65
+ return JSON.stringify({ posted: true, id: data.data?.id, text: data.data?.text });
66
+ }
67
+ case "reply": {
68
+ if (!text || !tweet_id) return JSON.stringify({ error: "text and tweet_id required" });
69
+ const apiKey = (globalThis as any).Zubo?.getSecret?.("twitter_api_key");
70
+ const apiSecret = (globalThis as any).Zubo?.getSecret?.("twitter_api_secret");
71
+ const accessToken = (globalThis as any).Zubo?.getSecret?.("twitter_access_token");
72
+ const accessSecret = (globalThis as any).Zubo?.getSecret?.("twitter_access_secret");
73
+ if (!apiKey || !apiSecret || !accessToken || !accessSecret) {
74
+ return JSON.stringify({ error: "Twitter OAuth credentials not configured." });
75
+ }
76
+ const oauthHeader = computeOAuth1Header("POST", `${API}/tweets`, {}, apiKey, apiSecret, accessToken, accessSecret);
77
+ const res = await fetch(`${API}/tweets`, {
78
+ method: "POST",
79
+ headers: { Authorization: oauthHeader, "Content-Type": "application/json" },
80
+ body: JSON.stringify({ text, reply: { in_reply_to_tweet_id: tweet_id } }),
81
+ });
82
+ if (!res.ok) return await safeApiError(res, "Twitter");
83
+ const data = (await res.json()) as any;
84
+ return JSON.stringify({ replied: true, id: data.data?.id });
85
+ }
86
+ default:
87
+ return JSON.stringify({ error: `Unknown action: ${action}` });
88
+ }
89
+ } catch (err: any) {
90
+ return safeExceptionError(err, "Twitter");
91
+ }
92
+ }
93
+
94
+ // Minimal OAuth 1.0a header computation
95
+ function computeOAuth1Header(
96
+ method: string, url: string, params: Record<string, string>,
97
+ consumerKey: string, consumerSecret: string,
98
+ tokenKey: string, tokenSecret: string
99
+ ): string {
100
+ const timestamp = Math.floor(Date.now() / 1000).toString();
101
+ const nonce = crypto.randomUUID().replace(/-/g, "");
102
+
103
+ const oauthParams: Record<string, string> = {
104
+ oauth_consumer_key: consumerKey,
105
+ oauth_nonce: nonce,
106
+ oauth_signature_method: "HMAC-SHA1",
107
+ oauth_timestamp: timestamp,
108
+ oauth_token: tokenKey,
109
+ oauth_version: "1.0",
110
+ };
111
+
112
+ const allParams = { ...params, ...oauthParams };
113
+ const sortedKeys = Object.keys(allParams).sort();
114
+ const paramString = sortedKeys.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(allParams[k])}`).join("&");
115
+
116
+ const baseString = `${method}&${encodeURIComponent(url)}&${encodeURIComponent(paramString)}`;
117
+ const signingKey = `${encodeURIComponent(consumerSecret)}&${encodeURIComponent(tokenSecret)}`;
118
+
119
+ // Use Web Crypto for HMAC-SHA1
120
+ const encoder = new TextEncoder();
121
+ const keyData = encoder.encode(signingKey);
122
+ const msgData = encoder.encode(baseString);
123
+
124
+ // Sync HMAC-SHA1 using Bun's crypto
125
+ const hmac = new Bun.CryptoHasher("sha1", keyData);
126
+ hmac.update(msgData);
127
+ const signature = Buffer.from(hmac.digest()).toString("base64");
128
+
129
+ oauthParams.oauth_signature = signature;
130
+
131
+ const headerParts = Object.keys(oauthParams).sort().map(k => `${encodeURIComponent(k)}="${encodeURIComponent(oauthParams[k])}"`);
132
+ return `OAuth ${headerParts.join(", ")}`;
133
+ }
@@ -0,0 +1,26 @@
1
+ # file_read
2
+
3
+ Read the contents of a file from the local filesystem.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "path": {
12
+ "type": "string",
13
+ "description": "Absolute or ~ path to the file to read."
14
+ },
15
+ "maxLength": {
16
+ "type": "number",
17
+ "description": "Maximum characters to return (default 50000)."
18
+ }
19
+ },
20
+ "required": ["path"]
21
+ }
22
+ ```
23
+
24
+ ## Usage Hints
25
+
26
+ Use this tool when the user asks you to read, view, or check the contents of a file on their machine. Supports text files. Use ~ for home directory paths.
@@ -0,0 +1,66 @@
1
+ import { homedir } from "os";
2
+ import { resolve } from "path";
3
+
4
+ const BLOCKED_PATHS = [
5
+ "/.ssh/",
6
+ "/.gnupg/",
7
+ "/.aws/credentials",
8
+ "/.zubo/config.json",
9
+ "/.zubo/zubo.db",
10
+ "/id_rsa",
11
+ "/id_ed25519",
12
+ "/.env",
13
+ "/.npmrc",
14
+ "/.netrc",
15
+ "/.docker/config.json",
16
+ ];
17
+
18
+ function validatePath(rawPath: string): string {
19
+ let p = rawPath;
20
+
21
+ // Expand ~
22
+ if (p.startsWith("~/")) {
23
+ p = p.replace("~", homedir());
24
+ }
25
+
26
+ // Resolve to absolute, eliminating .. traversals
27
+ p = resolve(p);
28
+
29
+ // Must be under home directory
30
+ const home = homedir();
31
+ if (!p.startsWith(home + "/") && p !== home) {
32
+ throw new Error(`Access denied: reads are restricted to your home directory`);
33
+ }
34
+
35
+ // Block sensitive paths
36
+ for (const blocked of BLOCKED_PATHS) {
37
+ if (p.includes(blocked)) {
38
+ throw new Error(`Access denied: cannot read sensitive path (${blocked})`);
39
+ }
40
+ }
41
+
42
+ return p;
43
+ }
44
+
45
+ export default async function (input: Record<string, unknown>): Promise<string> {
46
+ const rawPath = input.path as string;
47
+ const maxLength = (input.maxLength as number) || 50000;
48
+
49
+ const path = validatePath(rawPath);
50
+
51
+ const file = Bun.file(path);
52
+ const exists = await file.exists();
53
+
54
+ if (!exists) {
55
+ throw new Error(`File not found: ${path}`);
56
+ }
57
+
58
+ let content = await file.text();
59
+ const size = content.length;
60
+
61
+ if (content.length > maxLength) {
62
+ content = content.slice(0, maxLength) + "\n\n[Truncated]";
63
+ }
64
+
65
+ return JSON.stringify({ path, size, contentLength: content.length, content });
66
+ }
@@ -0,0 +1,30 @@
1
+ # file_write
2
+
3
+ Write content to a file on the local filesystem.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "path": {
12
+ "type": "string",
13
+ "description": "Absolute or ~ path to the file to write."
14
+ },
15
+ "content": {
16
+ "type": "string",
17
+ "description": "The content to write to the file."
18
+ },
19
+ "append": {
20
+ "type": "boolean",
21
+ "description": "If true, append to file instead of overwriting (default false)."
22
+ }
23
+ },
24
+ "required": ["path", "content"]
25
+ }
26
+ ```
27
+
28
+ ## Usage Hints
29
+
30
+ Use this tool when the user asks you to create, write, or save content to a file. Use ~ for home directory paths. Set append=true to add to existing files.
@@ -0,0 +1,64 @@
1
+ import { homedir } from "os";
2
+ import { resolve } from "path";
3
+ import { appendFileSync } from "fs";
4
+
5
+ const BLOCKED_PATHS = [
6
+ "/.ssh/",
7
+ "/.gnupg/",
8
+ "/.aws/credentials",
9
+ "/.zubo/config.json",
10
+ "/id_rsa",
11
+ "/id_ed25519",
12
+ "/.env",
13
+ "/.bashrc",
14
+ "/.bash_profile",
15
+ "/.zshrc",
16
+ "/.profile",
17
+ ];
18
+
19
+ function validatePath(rawPath: string): string {
20
+ let p = rawPath;
21
+
22
+ // Expand ~
23
+ if (p.startsWith("~/")) {
24
+ p = p.replace("~", homedir());
25
+ }
26
+
27
+ // Resolve to absolute, eliminating .. traversals
28
+ p = resolve(p);
29
+
30
+ // Must be under home directory
31
+ const home = homedir();
32
+ if (!p.startsWith(home + "/") && p !== home) {
33
+ throw new Error(`Access denied: writes are restricted to your home directory`);
34
+ }
35
+
36
+ // Block sensitive paths
37
+ for (const blocked of BLOCKED_PATHS) {
38
+ if (p.includes(blocked)) {
39
+ throw new Error(`Access denied: cannot write to sensitive path (${blocked})`);
40
+ }
41
+ }
42
+
43
+ return p;
44
+ }
45
+
46
+ export default async function (input: Record<string, unknown>): Promise<string> {
47
+ const rawPath = input.path as string;
48
+ const content = input.content as string;
49
+ const append = (input.append as boolean) || false;
50
+
51
+ const path = validatePath(rawPath);
52
+
53
+ if (append) {
54
+ appendFileSync(path, content);
55
+ } else {
56
+ await Bun.write(path, content);
57
+ }
58
+
59
+ return JSON.stringify({
60
+ path,
61
+ bytesWritten: content.length,
62
+ mode: append ? "append" : "write",
63
+ });
64
+ }
@@ -0,0 +1,34 @@
1
+ # http_request
2
+
3
+ Make an HTTP request to any URL with full control over method, headers, and body.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "url": {
12
+ "type": "string",
13
+ "description": "The URL to send the request to."
14
+ },
15
+ "method": {
16
+ "type": "string",
17
+ "description": "HTTP method (GET, POST, PUT, DELETE, PATCH). Default GET."
18
+ },
19
+ "headers": {
20
+ "type": "object",
21
+ "description": "Key-value pairs for request headers."
22
+ },
23
+ "body": {
24
+ "type": "string",
25
+ "description": "Request body (for POST, PUT, PATCH)."
26
+ }
27
+ },
28
+ "required": ["url"]
29
+ }
30
+ ```
31
+
32
+ ## Usage Hints
33
+
34
+ Use this tool when the user needs to call an API, make HTTP requests, or interact with web services. More flexible than url_fetch — supports all methods and custom headers.
@@ -0,0 +1,87 @@
1
+ const BLOCKED_HOST_PATTERNS = [
2
+ /^localhost$/i,
3
+ /^127\.\d+\.\d+\.\d+$/,
4
+ /^10\.\d+\.\d+\.\d+$/,
5
+ /^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
6
+ /^192\.168\.\d+\.\d+$/,
7
+ /^169\.254\.\d+\.\d+$/,
8
+ /^\[?::1\]?$/,
9
+ /^0\.0\.0\.0$/,
10
+ /^metadata\.google\.internal$/i,
11
+ /^metadata\.azure\.com$/i,
12
+ /^fe80:/i,
13
+ /^fc00:/i,
14
+ ];
15
+
16
+ function isBlockedHost(hostname: string): boolean {
17
+ const normalized = hostname.replace(/^\[|\]$/g, "");
18
+ for (const pattern of BLOCKED_HOST_PATTERNS) {
19
+ if (pattern.test(normalized)) return true;
20
+ }
21
+ return false;
22
+ }
23
+
24
+ function validateUrl(raw: string): void {
25
+ let parsed: URL;
26
+ try {
27
+ parsed = new URL(raw);
28
+ } catch {
29
+ throw new Error(`Invalid URL: ${raw}`);
30
+ }
31
+
32
+ if (isBlockedHost(parsed.hostname)) {
33
+ throw new Error(`Blocked: requests to internal/private addresses are not allowed`);
34
+ }
35
+ }
36
+
37
+ export default async function (input: Record<string, unknown>): Promise<string> {
38
+ const url = input.url as string;
39
+ const method = ((input.method as string) || "GET").toUpperCase();
40
+ const headers = (input.headers as Record<string, string>) || {};
41
+ const body = input.body as string | undefined;
42
+
43
+ validateUrl(url);
44
+
45
+ const opts: RequestInit = {
46
+ method,
47
+ headers: {
48
+ "User-Agent": "Zubo/1.0",
49
+ ...headers,
50
+ },
51
+ redirect: "manual",
52
+ signal: AbortSignal.timeout(30000),
53
+ };
54
+
55
+ if (body && ["POST", "PUT", "PATCH"].includes(method)) {
56
+ opts.body = body;
57
+ if (!headers["Content-Type"] && !headers["content-type"]) {
58
+ (opts.headers as Record<string, string>)["Content-Type"] = "application/json";
59
+ }
60
+ }
61
+
62
+ let res = await fetch(url, opts);
63
+ let redirectCount = 0;
64
+ const MAX_REDIRECTS = 5;
65
+
66
+ while (res.status >= 300 && res.status < 400 && redirectCount < MAX_REDIRECTS) {
67
+ const location = res.headers.get("location");
68
+ if (!location) break;
69
+
70
+ const redirectUrl = new URL(location, url);
71
+ if (isBlockedHost(redirectUrl.hostname)) {
72
+ return JSON.stringify({ error: "Redirect to blocked host", status: 0 });
73
+ }
74
+
75
+ res = await fetch(redirectUrl.toString(), { ...opts, redirect: "manual" });
76
+ redirectCount++;
77
+ }
78
+
79
+ const responseText = await res.text();
80
+
81
+ return JSON.stringify({
82
+ status: res.status,
83
+ statusText: res.statusText,
84
+ headers: Object.fromEntries(res.headers.entries()),
85
+ body: responseText.slice(0, 50000),
86
+ });
87
+ }
@@ -0,0 +1,26 @@
1
+ # shell
2
+
3
+ Execute a shell command and return its output.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "command": {
12
+ "type": "string",
13
+ "description": "The shell command to execute."
14
+ },
15
+ "timeout": {
16
+ "type": "number",
17
+ "description": "Timeout in milliseconds (default 30000)."
18
+ }
19
+ },
20
+ "required": ["command"]
21
+ }
22
+ ```
23
+
24
+ ## Usage Hints
25
+
26
+ Use this tool when the user asks you to run a command, check system info, list files, or perform any shell operation. Be cautious with destructive commands.
@@ -0,0 +1,96 @@
1
+ const BLOCKED_PATTERNS = [
2
+ /\brm\s+(-[a-zA-Z]*)?.*\s+\/\s*$/, // rm -rf /
3
+ /\bmkfs\b/,
4
+ /\bdd\s+.*of=\/dev\//,
5
+ />\s*\/dev\/sd/,
6
+ /\bshutdown\b/,
7
+ /\breboot\b/,
8
+ /\b:(){ :\|:& };:/, // fork bomb
9
+ /\bkill\s+(-9\s+)?-1\b/, // kill all processes
10
+ /\bchmod\s+0{3}\s+\//, // chmod 000 /
11
+ /\bchown\s+.*\s+\/\s*$/, // chown ... /
12
+ /\bfind\s+\/\s+.*-delete\b/, // find / -delete
13
+ /\bcurl\s+.*\|\s*sh\b/, // curl pipe to sh
14
+ /\bwget\s+.*\|\s*sh\b/, // wget pipe to sh
15
+ /bash\s+-i\s+>&\s*\/dev\/tcp\//, // reverse shell
16
+ /\b>\s*\/dev\/[sv]d/, // overwrite disk devices
17
+ /\bnc\s+(-[a-zA-Z]*\s+)*-e\b/, // netcat reverse shell
18
+ ];
19
+
20
+ const SENSITIVE_ENV_KEYS = [
21
+ "ANTHROPIC_API_KEY",
22
+ "OPENAI_API_KEY",
23
+ "AWS_SECRET_ACCESS_KEY",
24
+ "GITHUB_TOKEN",
25
+ "NPM_TOKEN",
26
+ "DATABASE_URL",
27
+ "IFS",
28
+ "PS4",
29
+ "BASH_ENV",
30
+ "ENV",
31
+ "SHELLOPTS",
32
+ "BASHOPTS",
33
+ "CDPATH",
34
+ "GLOBIGNORE",
35
+ "LD_PRELOAD",
36
+ "LD_LIBRARY_PATH",
37
+ ];
38
+
39
+ function sanitizeEnv(): Record<string, string | undefined> {
40
+ const env = { ...process.env };
41
+ for (const key of SENSITIVE_ENV_KEYS) {
42
+ delete env[key];
43
+ }
44
+ // Also strip any key containing SECRET, TOKEN, PASSWORD, CREDENTIAL
45
+ for (const key of Object.keys(env)) {
46
+ if (/SECRET|TOKEN|PASSWORD|CREDENTIAL|PRIVATE_KEY/i.test(key)) {
47
+ delete env[key];
48
+ }
49
+ }
50
+ return env;
51
+ }
52
+
53
+ export default async function (input: Record<string, unknown>): Promise<string> {
54
+ const command = input.command as string;
55
+ const timeout = (input.timeout as number) || 30000;
56
+
57
+ // Block obviously destructive commands
58
+ for (const pattern of BLOCKED_PATTERNS) {
59
+ if (pattern.test(command)) {
60
+ throw new Error(`Blocked: command matches dangerous pattern`);
61
+ }
62
+ }
63
+
64
+ const proc = Bun.spawn(["sh", "-c", command], {
65
+ stdout: "pipe",
66
+ stderr: "pipe",
67
+ env: sanitizeEnv(),
68
+ });
69
+
70
+ // Set up timeout that kills the process
71
+ let timedOut = false;
72
+ const timer = setTimeout(() => {
73
+ timedOut = true;
74
+ try {
75
+ proc.kill();
76
+ } catch (err: any) {
77
+ // Process may have already exited
78
+ }
79
+ }, timeout);
80
+
81
+ // Read stdout and stderr in parallel to avoid deadlock
82
+ const [stdout, stderr] = await Promise.all([
83
+ new Response(proc.stdout).text(),
84
+ new Response(proc.stderr).text(),
85
+ ]);
86
+ const exitCode = await proc.exited;
87
+
88
+ clearTimeout(timer);
89
+
90
+ return JSON.stringify({
91
+ exitCode,
92
+ timedOut,
93
+ stdout: stdout.slice(0, 50000),
94
+ stderr: stderr.slice(0, 10000),
95
+ });
96
+ }
@@ -0,0 +1,26 @@
1
+ # url_fetch
2
+
3
+ Fetch the content of a URL and return the text body.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "url": {
12
+ "type": "string",
13
+ "description": "The URL to fetch."
14
+ },
15
+ "maxLength": {
16
+ "type": "number",
17
+ "description": "Maximum characters to return (default 10000)."
18
+ }
19
+ },
20
+ "required": ["url"]
21
+ }
22
+ ```
23
+
24
+ ## Usage Hints
25
+
26
+ Use this tool when the user asks you to read, fetch, or summarize a web page. Returns raw text content — useful for reading articles, docs, or API responses.
@@ -0,0 +1,37 @@
1
+ export default async function (input: Record<string, unknown>): Promise<string> {
2
+ const url = input.url as string;
3
+ const maxLength = (input.maxLength as number) || 10000;
4
+
5
+ const res = await fetch(url, {
6
+ headers: {
7
+ "User-Agent": "Zubo/1.0",
8
+ Accept: "text/html, application/json, text/plain, */*",
9
+ },
10
+ redirect: "follow",
11
+ signal: AbortSignal.timeout(30000),
12
+ });
13
+
14
+ if (!res.ok) {
15
+ throw new Error(`Fetch failed: ${res.status} ${res.statusText}`);
16
+ }
17
+
18
+ const contentType = res.headers.get("content-type") || "";
19
+ let text = await res.text();
20
+
21
+ // Strip HTML tags for HTML content
22
+ if (contentType.includes("text/html")) {
23
+ // Remove script and style blocks
24
+ text = text.replace(/<script[\s\S]*?<\/script>/gi, "");
25
+ text = text.replace(/<style[\s\S]*?<\/style>/gi, "");
26
+ // Remove all tags
27
+ text = text.replace(/<[^>]+>/g, " ");
28
+ // Collapse whitespace
29
+ text = text.replace(/\s+/g, " ").trim();
30
+ }
31
+
32
+ if (text.length > maxLength) {
33
+ text = text.slice(0, maxLength) + "\n\n[Truncated]";
34
+ }
35
+
36
+ return JSON.stringify({ url, contentType, length: text.length, content: text });
37
+ }
@@ -0,0 +1,26 @@
1
+ # web_search
2
+
3
+ Search the web using DuckDuckGo and return relevant results.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "query": {
12
+ "type": "string",
13
+ "description": "The search query."
14
+ },
15
+ "maxResults": {
16
+ "type": "number",
17
+ "description": "Maximum number of results to return (default 5)."
18
+ }
19
+ },
20
+ "required": ["query"]
21
+ }
22
+ ```
23
+
24
+ ## Usage Hints
25
+
26
+ Use this tool when the user asks you to search the web, look something up online, or find current information. Good for news, facts, documentation, and general knowledge queries.