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,106 @@
1
+ const API = "https://www.googleapis.com/drive/v3";
2
+
3
+ async function getToken(): Promise<string> {
4
+ const Zubo = (globalThis as any).Zubo;
5
+ if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
+ throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
7
+ }
8
+
9
+ function safeApiErr(status: number, statusText: string, service: string): string {
10
+ return JSON.stringify({ error: `${service} API error: ${status} ${statusText}` });
11
+ }
12
+
13
+ export default async function (input: Record<string, unknown>): Promise<string> {
14
+ let token: string;
15
+ try {
16
+ token = await getToken();
17
+ } catch (err: any) {
18
+ return JSON.stringify({
19
+ error: err.message,
20
+ action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
21
+ "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
22
+ "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
23
+ });
24
+ }
25
+
26
+ const { action, file_id, name, parent_id, query, page_size } = input as {
27
+ action: string;
28
+ file_id?: string;
29
+ name?: string;
30
+ parent_id?: string;
31
+ query?: string;
32
+ page_size?: number;
33
+ };
34
+
35
+ const headers: Record<string, string> = {
36
+ Authorization: `Bearer ${token}`,
37
+ "Content-Type": "application/json",
38
+ };
39
+
40
+ try {
41
+ switch (action) {
42
+ case "list": {
43
+ const qs = new URLSearchParams({
44
+ pageSize: String(page_size || 20),
45
+ fields: "files(id,name,mimeType,size,modifiedTime,webViewLink)",
46
+ });
47
+ if (query) qs.set("q", query);
48
+ const res = await fetch(`${API}/files?${qs}`, { headers });
49
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Drive");
50
+ const data = (await res.json()) as any;
51
+ return JSON.stringify(
52
+ (data.files || []).map((f: any) => ({
53
+ id: f.id,
54
+ name: f.name,
55
+ type: f.mimeType,
56
+ size: f.size,
57
+ modified: f.modifiedTime,
58
+ url: f.webViewLink,
59
+ }))
60
+ );
61
+ }
62
+
63
+ case "create_folder": {
64
+ if (!name) return JSON.stringify({ error: "name is required for create_folder" });
65
+ const metadata: any = {
66
+ name,
67
+ mimeType: "application/vnd.google-apps.folder",
68
+ };
69
+ if (parent_id) metadata.parents = [parent_id];
70
+ const res = await fetch(`${API}/files`, {
71
+ method: "POST",
72
+ headers,
73
+ body: JSON.stringify(metadata),
74
+ });
75
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Drive");
76
+ const folder = (await res.json()) as any;
77
+ return JSON.stringify({ id: folder.id, name: folder.name });
78
+ }
79
+
80
+ case "get": {
81
+ if (!file_id) return JSON.stringify({ error: "file_id is required for get" });
82
+ const qs = new URLSearchParams({
83
+ fields: "id,name,mimeType,size,modifiedTime,webViewLink,description",
84
+ });
85
+ const res = await fetch(`${API}/files/${file_id}?${qs}`, { headers });
86
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Drive");
87
+ const f = (await res.json()) as any;
88
+ return JSON.stringify({
89
+ id: f.id,
90
+ name: f.name,
91
+ type: f.mimeType,
92
+ size: f.size,
93
+ modified: f.modifiedTime,
94
+ description: f.description,
95
+ url: f.webViewLink,
96
+ });
97
+ }
98
+
99
+ default:
100
+ return JSON.stringify({ error: `Unknown action: ${action}` });
101
+ }
102
+ } catch (err: any) {
103
+ console.error(`[Google Drive] Request failed: ${err.message}`);
104
+ return JSON.stringify({ error: "Google Drive request failed. Check logs for details." });
105
+ }
106
+ }
@@ -0,0 +1,36 @@
1
+ # google_sheets
2
+
3
+ Manage Google Sheets: create, read, append rows, and update cells. Requires a Google API key stored as `google_api_key`.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "action": {
12
+ "type": "string",
13
+ "enum": ["create", "read", "append", "update"],
14
+ "description": "The action to perform"
15
+ },
16
+ "spreadsheet_id": {
17
+ "type": "string",
18
+ "description": "The spreadsheet ID (required for read, append, update)"
19
+ },
20
+ "title": {
21
+ "type": "string",
22
+ "description": "Spreadsheet title (required for create)"
23
+ },
24
+ "range": {
25
+ "type": "string",
26
+ "description": "Cell range in A1 notation, e.g., 'Sheet1!A1:D10' (required for read, append, update)"
27
+ },
28
+ "values": {
29
+ "type": "array",
30
+ "items": { "type": "array", "items": {} },
31
+ "description": "2D array of values to write (required for append and update)"
32
+ }
33
+ },
34
+ "required": ["action"]
35
+ }
36
+ ```
@@ -0,0 +1,116 @@
1
+ const API = "https://sheets.googleapis.com/v4/spreadsheets";
2
+
3
+ async function getToken(): Promise<string> {
4
+ const Zubo = (globalThis as any).Zubo;
5
+ if (Zubo?.getGoogleToken) return Zubo.getGoogleToken();
6
+ throw new Error("Google is NOT connected. Use google_oauth with action 'start' to set up Google.");
7
+ }
8
+
9
+ function safeApiErr(status: number, statusText: string, service: string): string {
10
+ return JSON.stringify({ error: `${service} API error: ${status} ${statusText}` });
11
+ }
12
+
13
+ export default async function (input: Record<string, unknown>): Promise<string> {
14
+ let token: string;
15
+ try {
16
+ token = await getToken();
17
+ } catch (err: any) {
18
+ return JSON.stringify({
19
+ error: err.message,
20
+ action_required: "Google is NOT connected. The user needs to complete the OAuth 2.0 flow. " +
21
+ "Ask the user for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
22
+ "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
23
+ });
24
+ }
25
+
26
+ const { action, spreadsheet_id, title, range, values } = input as {
27
+ action: string;
28
+ spreadsheet_id?: string;
29
+ title?: string;
30
+ range?: string;
31
+ values?: unknown[][];
32
+ };
33
+
34
+ const headers: Record<string, string> = {
35
+ Authorization: `Bearer ${token}`,
36
+ "Content-Type": "application/json",
37
+ };
38
+
39
+ try {
40
+ switch (action) {
41
+ case "create": {
42
+ if (!title) return JSON.stringify({ error: "title is required for create" });
43
+ const res = await fetch(API, {
44
+ method: "POST",
45
+ headers,
46
+ body: JSON.stringify({ properties: { title } }),
47
+ });
48
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Sheets");
49
+ const sheet = (await res.json()) as any;
50
+ return JSON.stringify({
51
+ spreadsheet_id: sheet.spreadsheetId,
52
+ title: sheet.properties?.title,
53
+ url: sheet.spreadsheetUrl,
54
+ });
55
+ }
56
+
57
+ case "read": {
58
+ if (!spreadsheet_id) return JSON.stringify({ error: "spreadsheet_id is required for read" });
59
+ if (!range) return JSON.stringify({ error: "range is required for read" });
60
+ const encodedRange = encodeURIComponent(range);
61
+ const res = await fetch(`${API}/${spreadsheet_id}/values/${encodedRange}`, { headers });
62
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Sheets");
63
+ const data = (await res.json()) as any;
64
+ return JSON.stringify({ range: data.range, values: data.values || [] });
65
+ }
66
+
67
+ case "append": {
68
+ if (!spreadsheet_id) return JSON.stringify({ error: "spreadsheet_id is required for append" });
69
+ if (!range) return JSON.stringify({ error: "range is required for append" });
70
+ if (!values) return JSON.stringify({ error: "values are required for append" });
71
+ const encodedRange = encodeURIComponent(range);
72
+ const res = await fetch(
73
+ `${API}/${spreadsheet_id}/values/${encodedRange}:append?valueInputOption=USER_ENTERED`,
74
+ {
75
+ method: "POST",
76
+ headers,
77
+ body: JSON.stringify({ values }),
78
+ }
79
+ );
80
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Sheets");
81
+ const result = (await res.json()) as any;
82
+ return JSON.stringify({
83
+ updated_range: result.updates?.updatedRange,
84
+ updated_rows: result.updates?.updatedRows,
85
+ });
86
+ }
87
+
88
+ case "update": {
89
+ if (!spreadsheet_id) return JSON.stringify({ error: "spreadsheet_id is required for update" });
90
+ if (!range) return JSON.stringify({ error: "range is required for update" });
91
+ if (!values) return JSON.stringify({ error: "values are required for update" });
92
+ const encodedRange = encodeURIComponent(range);
93
+ const res = await fetch(
94
+ `${API}/${spreadsheet_id}/values/${encodedRange}?valueInputOption=USER_ENTERED`,
95
+ {
96
+ method: "PUT",
97
+ headers,
98
+ body: JSON.stringify({ values }),
99
+ }
100
+ );
101
+ if (!res.ok) return safeApiErr(res.status, res.statusText, "Google Sheets");
102
+ const result = (await res.json()) as any;
103
+ return JSON.stringify({
104
+ updated_range: result.updatedRange,
105
+ updated_cells: result.updatedCells,
106
+ });
107
+ }
108
+
109
+ default:
110
+ return JSON.stringify({ error: `Unknown action: ${action}` });
111
+ }
112
+ } catch (err: any) {
113
+ console.error(`[Google Sheets] Request failed: ${err.message}`);
114
+ return JSON.stringify({ error: "Google Sheets request failed. Check logs for details." });
115
+ }
116
+ }
@@ -0,0 +1,21 @@
1
+ # jira_boards
2
+
3
+ List Jira boards and sprints. Requires `jira_token`, `jira_email`, and `jira_url` secrets.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "action": { "type": "string", "enum": ["list_boards", "list_sprints"], "description": "Action to perform" },
12
+ "board_id": { "type": "number", "description": "Board ID (for list_sprints)" }
13
+ },
14
+ "required": ["action"]
15
+ }
16
+ ```
17
+
18
+ ## Usage Hints
19
+
20
+ - Use "list_boards" to see all boards.
21
+ - Use "list_sprints" with board_id to see sprints.
@@ -0,0 +1,74 @@
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
+ export default async function (input: Record<string, unknown>): Promise<string> {
13
+ const token = (globalThis as any).Zubo?.getSecret?.("jira_token");
14
+ const email = (globalThis as any).Zubo?.getSecret?.("jira_email");
15
+ const baseUrl = (globalThis as any).Zubo?.getSecret?.("jira_url");
16
+ if (!token || !email || !baseUrl) {
17
+ return JSON.stringify({ error: "Jira not configured. Set jira_token, jira_email, and jira_url secrets." });
18
+ }
19
+
20
+ // Validate jira_url to prevent SSRF
21
+ let parsedUrl: URL;
22
+ try {
23
+ parsedUrl = new URL(baseUrl);
24
+ } catch {
25
+ return JSON.stringify({ error: "Invalid jira_url format" });
26
+ }
27
+ if (parsedUrl.protocol !== "https:") {
28
+ return JSON.stringify({ error: "jira_url must use HTTPS" });
29
+ }
30
+ const hostname = parsedUrl.hostname;
31
+ const is172Private = hostname.startsWith("172.") && (() => {
32
+ const secondOctet = parseInt(hostname.split(".")[1], 10);
33
+ return secondOctet >= 16 && secondOctet <= 31;
34
+ })();
35
+ if (hostname === "localhost" || hostname.startsWith("127.") || hostname.startsWith("10.") ||
36
+ hostname.startsWith("192.168.") || is172Private || hostname === "[::1]" ||
37
+ hostname.endsWith(".local") || hostname.endsWith(".internal")) {
38
+ return JSON.stringify({ error: "jira_url must not point to internal/private addresses" });
39
+ }
40
+
41
+ const { action, board_id } = input as { action: string; board_id?: number };
42
+ // Validate board_id is a positive integer to prevent injection
43
+ if (board_id !== undefined && (!Number.isInteger(board_id) || board_id <= 0)) {
44
+ return JSON.stringify({ error: "board_id must be a positive integer" });
45
+ }
46
+ const api = parsedUrl.origin + parsedUrl.pathname.replace(/\/+$/, "") + "/rest/agile/1.0";
47
+ const auth = Buffer.from(`${email}:${token}`).toString("base64");
48
+ const headers: Record<string, string> = {
49
+ Authorization: `Basic ${auth}`,
50
+ Accept: "application/json",
51
+ };
52
+
53
+ try {
54
+ switch (action) {
55
+ case "list_boards": {
56
+ const res = await fetch(`${api}/board?maxResults=50`, { headers });
57
+ if (!res.ok) return await safeApiError(res, "Jira");
58
+ const data = (await res.json()) as any;
59
+ return JSON.stringify(data.values?.map((b: any) => ({ id: b.id, name: b.name, type: b.type })) ?? []);
60
+ }
61
+ case "list_sprints": {
62
+ if (!board_id) return JSON.stringify({ error: "board_id required" });
63
+ const res = await fetch(`${api}/board/${board_id}/sprint?maxResults=20`, { headers });
64
+ if (!res.ok) return await safeApiError(res, "Jira");
65
+ const data = (await res.json()) as any;
66
+ return JSON.stringify(data.values?.map((s: any) => ({ id: s.id, name: s.name, state: s.state, startDate: s.startDate, endDate: s.endDate })) ?? []);
67
+ }
68
+ default:
69
+ return JSON.stringify({ error: `Unknown action: ${action}` });
70
+ }
71
+ } catch (err: any) {
72
+ return safeExceptionError(err, "Jira");
73
+ }
74
+ }
@@ -0,0 +1,28 @@
1
+ # jira_issues
2
+
3
+ Manage Jira issues: list, create, get, update, search, transition. Requires `jira_token`, `jira_email`, and `jira_url` secrets.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "action": { "type": "string", "enum": ["list", "create", "get", "update", "search", "transition"], "description": "Action to perform" },
12
+ "issue_key": { "type": "string", "description": "Issue key e.g. PROJ-123" },
13
+ "project_key": { "type": "string", "description": "Project key (for create, list)" },
14
+ "summary": { "type": "string", "description": "Issue summary" },
15
+ "description": { "type": "string", "description": "Issue description" },
16
+ "issue_type": { "type": "string", "description": "Issue type (Bug, Task, Story)" },
17
+ "jql": { "type": "string", "description": "JQL query (for search)" },
18
+ "transition_id": { "type": "string", "description": "Transition ID (for transition)" }
19
+ },
20
+ "required": ["action"]
21
+ }
22
+ ```
23
+
24
+ ## Usage Hints
25
+
26
+ - Use "search" with JQL for flexible queries.
27
+ - Use "list" with project_key for recent issues.
28
+ - Use "transition" to move issues through workflow stages.
@@ -0,0 +1,140 @@
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
+ export default async function (input: Record<string, unknown>): Promise<string> {
13
+ const token = (globalThis as any).Zubo?.getSecret?.("jira_token");
14
+ const email = (globalThis as any).Zubo?.getSecret?.("jira_email");
15
+ const baseUrl = (globalThis as any).Zubo?.getSecret?.("jira_url");
16
+ if (!token || !email || !baseUrl) {
17
+ return JSON.stringify({ error: "Jira not configured. Set jira_token, jira_email, and jira_url secrets." });
18
+ }
19
+
20
+ // Validate jira_url to prevent SSRF — must be HTTPS and a valid Jira host
21
+ let parsedUrl: URL;
22
+ try {
23
+ parsedUrl = new URL(baseUrl);
24
+ } catch {
25
+ return JSON.stringify({ error: "Invalid jira_url format" });
26
+ }
27
+ if (parsedUrl.protocol !== "https:") {
28
+ return JSON.stringify({ error: "jira_url must use HTTPS" });
29
+ }
30
+ // Block private/internal IP ranges
31
+ const hostname = parsedUrl.hostname;
32
+ const is172Private = hostname.startsWith("172.") && (() => {
33
+ const secondOctet = parseInt(hostname.split(".")[1], 10);
34
+ return secondOctet >= 16 && secondOctet <= 31;
35
+ })();
36
+ if (hostname === "localhost" || hostname.startsWith("127.") || hostname.startsWith("10.") ||
37
+ hostname.startsWith("192.168.") || is172Private || hostname === "[::1]" ||
38
+ hostname.endsWith(".local") || hostname.endsWith(".internal")) {
39
+ return JSON.stringify({ error: "jira_url must not point to internal/private addresses" });
40
+ }
41
+
42
+ const { action, issue_key, project_key, summary, description, issue_type, jql, transition_id } = input as {
43
+ action: string; issue_key?: string; project_key?: string; summary?: string;
44
+ description?: string; issue_type?: string; jql?: string; transition_id?: string;
45
+ };
46
+
47
+ // Validate issue_key format to prevent path traversal (e.g. "PROJ-123")
48
+ if (issue_key && !/^[A-Z][A-Z0-9_]+-\d+$/i.test(issue_key)) {
49
+ return JSON.stringify({ error: "Invalid issue_key format. Expected format: PROJ-123" });
50
+ }
51
+ // Validate project_key format
52
+ if (project_key && !/^[A-Z][A-Z0-9_]*$/i.test(project_key)) {
53
+ return JSON.stringify({ error: "Invalid project_key format. Expected format: PROJ" });
54
+ }
55
+
56
+ const api = parsedUrl.origin + parsedUrl.pathname.replace(/\/+$/, "") + "/rest/api/3";
57
+ const auth = Buffer.from(`${email}:${token}`).toString("base64");
58
+ const headers: Record<string, string> = {
59
+ Authorization: `Basic ${auth}`,
60
+ "Content-Type": "application/json",
61
+ Accept: "application/json",
62
+ };
63
+
64
+ try {
65
+ switch (action) {
66
+ case "list": {
67
+ const q = project_key ? `project=${project_key} ORDER BY created DESC` : "ORDER BY created DESC";
68
+ const res = await fetch(`${api}/search?jql=${encodeURIComponent(q)}&maxResults=20`, { headers });
69
+ if (!res.ok) return await safeApiError(res, "Jira");
70
+ const data = (await res.json()) as any;
71
+ return JSON.stringify(data.issues?.map((i: any) => ({
72
+ key: i.key, summary: i.fields.summary, status: i.fields.status?.name,
73
+ assignee: i.fields.assignee?.displayName, priority: i.fields.priority?.name,
74
+ })) ?? []);
75
+ }
76
+ case "create": {
77
+ if (!project_key || !summary) return JSON.stringify({ error: "project_key and summary required" });
78
+ const res = await fetch(`${api}/issue`, {
79
+ method: "POST", headers,
80
+ body: JSON.stringify({
81
+ fields: {
82
+ project: { key: project_key },
83
+ summary,
84
+ description: description ? { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: description }] }] } : undefined,
85
+ issuetype: { name: issue_type || "Task" },
86
+ },
87
+ }),
88
+ });
89
+ if (!res.ok) return await safeApiError(res, "Jira");
90
+ const data = (await res.json()) as any;
91
+ return JSON.stringify({ key: data.key, id: data.id, self: data.self });
92
+ }
93
+ case "get": {
94
+ if (!issue_key) return JSON.stringify({ error: "issue_key required" });
95
+ const res = await fetch(`${api}/issue/${issue_key}`, { headers });
96
+ if (!res.ok) return await safeApiError(res, "Jira");
97
+ const i = (await res.json()) as any;
98
+ return JSON.stringify({
99
+ key: i.key, summary: i.fields.summary, status: i.fields.status?.name,
100
+ assignee: i.fields.assignee?.displayName, priority: i.fields.priority?.name,
101
+ description: i.fields.description, created: i.fields.created,
102
+ });
103
+ }
104
+ case "update": {
105
+ if (!issue_key) return JSON.stringify({ error: "issue_key required" });
106
+ const fields: any = {};
107
+ if (summary) fields.summary = summary;
108
+ if (description) fields.description = { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: description }] }] };
109
+ const res = await fetch(`${api}/issue/${issue_key}`, {
110
+ method: "PUT", headers,
111
+ body: JSON.stringify({ fields }),
112
+ });
113
+ if (!res.ok) return await safeApiError(res, "Jira");
114
+ return JSON.stringify({ updated: true, key: issue_key });
115
+ }
116
+ case "search": {
117
+ if (!jql) return JSON.stringify({ error: "jql required" });
118
+ const res = await fetch(`${api}/search?jql=${encodeURIComponent(jql)}&maxResults=20`, { headers });
119
+ if (!res.ok) return await safeApiError(res, "Jira");
120
+ const data = (await res.json()) as any;
121
+ return JSON.stringify(data.issues?.map((i: any) => ({
122
+ key: i.key, summary: i.fields.summary, status: i.fields.status?.name,
123
+ })) ?? []);
124
+ }
125
+ case "transition": {
126
+ if (!issue_key || !transition_id) return JSON.stringify({ error: "issue_key and transition_id required" });
127
+ const res = await fetch(`${api}/issue/${issue_key}/transitions`, {
128
+ method: "POST", headers,
129
+ body: JSON.stringify({ transition: { id: transition_id } }),
130
+ });
131
+ if (!res.ok) return await safeApiError(res, "Jira");
132
+ return JSON.stringify({ transitioned: true, key: issue_key });
133
+ }
134
+ default:
135
+ return JSON.stringify({ error: `Unknown action: ${action}` });
136
+ }
137
+ } catch (err: any) {
138
+ return safeExceptionError(err, "Jira");
139
+ }
140
+ }
@@ -0,0 +1,30 @@
1
+ # linear_issues
2
+
3
+ Manage Linear issues: list, create, get, update, search. Requires `linear_token`.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "action": { "type": "string", "enum": ["list", "create", "get", "update", "search"], "description": "Action to perform" },
12
+ "issue_id": { "type": "string", "description": "Issue ID (for get, update)" },
13
+ "title": { "type": "string", "description": "Issue title (for create)" },
14
+ "description": { "type": "string", "description": "Issue description" },
15
+ "team_id": { "type": "string", "description": "Team ID (for create, list)" },
16
+ "state": { "type": "string", "description": "State name filter" },
17
+ "query": { "type": "string", "description": "Search query" },
18
+ "assignee_id": { "type": "string", "description": "Assignee ID" },
19
+ "priority": { "type": "number", "description": "Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)" }
20
+ },
21
+ "required": ["action"]
22
+ }
23
+ ```
24
+
25
+ ## Usage Hints
26
+
27
+ - Linear uses GraphQL. This tool wraps common operations.
28
+ - Use "list" with optional team_id to see issues.
29
+ - Use "create" with title and team_id.
30
+ - Use "search" with a text query.
@@ -0,0 +1,75 @@
1
+ function safeExceptionError(err: any, service: string): string {
2
+ console.error(`[${service}] Request failed: ${err.message}`);
3
+ return JSON.stringify({ error: `${service} request failed. Check logs for details.` });
4
+ }
5
+
6
+ const API = "https://api.linear.app/graphql";
7
+
8
+ async function gql(token: string, query: string, variables?: any): Promise<any> {
9
+ const res = await fetch(API, {
10
+ method: "POST",
11
+ headers: {
12
+ Authorization: token,
13
+ "Content-Type": "application/json",
14
+ },
15
+ body: JSON.stringify({ query, variables }),
16
+ });
17
+ if (!res.ok) throw new Error(`Linear API error: ${res.status}`);
18
+ const data = (await res.json()) as any;
19
+ if (data.errors?.length) throw new Error(data.errors[0].message);
20
+ return data.data;
21
+ }
22
+
23
+ export default async function (input: Record<string, unknown>): Promise<string> {
24
+ const token = (globalThis as any).Zubo?.getSecret?.("linear_token");
25
+ if (!token) return JSON.stringify({ error: "Linear token not configured. Use secret_set to store 'linear_token'." });
26
+
27
+ const { action, issue_id, title, description, team_id, query, assignee_id, priority } = input as {
28
+ action: string; issue_id?: string; title?: string; description?: string;
29
+ team_id?: string; query?: string; assignee_id?: string; priority?: number;
30
+ };
31
+
32
+ try {
33
+ switch (action) {
34
+ case "list": {
35
+ const data = team_id
36
+ ? await gql(token, `query($teamId: String!) { issues(first: 25, filter: { team: { id: { eq: $teamId } } }) { nodes { id identifier title state { name } assignee { name } priority createdAt } } }`, { teamId: team_id })
37
+ : await gql(token, `{ issues(first: 25) { nodes { id identifier title state { name } assignee { name } priority createdAt } } }`);
38
+ return JSON.stringify(data.issues.nodes);
39
+ }
40
+ case "create": {
41
+ if (!title || !team_id) return JSON.stringify({ error: "title and team_id required" });
42
+ const vars: any = { title, teamId: team_id };
43
+ if (description) vars.description = description;
44
+ if (assignee_id) vars.assigneeId = assignee_id;
45
+ if (priority !== undefined) vars.priority = priority;
46
+ const data = await gql(token, `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier title url } } }`, { input: vars });
47
+ return JSON.stringify(data.issueCreate.issue);
48
+ }
49
+ case "get": {
50
+ if (!issue_id) return JSON.stringify({ error: "issue_id required" });
51
+ const data = await gql(token, `query($id: String!) { issue(id: $id) { id identifier title description state { name } assignee { name } priority labels { nodes { name } } createdAt url } }`, { id: issue_id });
52
+ return JSON.stringify(data.issue);
53
+ }
54
+ case "update": {
55
+ if (!issue_id) return JSON.stringify({ error: "issue_id required" });
56
+ const upd: any = {};
57
+ if (title) upd.title = title;
58
+ if (description) upd.description = description;
59
+ if (assignee_id) upd.assigneeId = assignee_id;
60
+ if (priority !== undefined) upd.priority = priority;
61
+ const data = await gql(token, `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id identifier title } } }`, { id: issue_id, input: upd });
62
+ return JSON.stringify(data.issueUpdate.issue);
63
+ }
64
+ case "search": {
65
+ if (!query) return JSON.stringify({ error: "query required" });
66
+ const data = await gql(token, `query($q: String!) { searchIssues(query: $q, first: 20) { nodes { id identifier title state { name } } } }`, { q: query });
67
+ return JSON.stringify(data.searchIssues.nodes);
68
+ }
69
+ default:
70
+ return JSON.stringify({ error: `Unknown action: ${action}` });
71
+ }
72
+ } catch (err: any) {
73
+ return safeExceptionError(err, "Linear");
74
+ }
75
+ }
@@ -0,0 +1,21 @@
1
+ # linear_projects
2
+
3
+ List and view Linear projects. Requires `linear_token`.
4
+
5
+ ## Input Schema
6
+
7
+ ```json
8
+ {
9
+ "type": "object",
10
+ "properties": {
11
+ "action": { "type": "string", "enum": ["list", "get"], "description": "Action to perform" },
12
+ "project_id": { "type": "string", "description": "Project ID (for get)" }
13
+ },
14
+ "required": ["action"]
15
+ }
16
+ ```
17
+
18
+ ## Usage Hints
19
+
20
+ - Use "list" to see all projects.
21
+ - Use "get" with project_id for details.