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.
- package/.github/workflows/ci.yml +35 -0
- package/README.md +149 -0
- package/bun.lock +216 -0
- package/desktop/README.md +57 -0
- package/desktop/package.json +12 -0
- package/desktop/src-tauri/Cargo.toml +25 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/icons/README.md +17 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/main.rs +189 -0
- package/desktop/src-tauri/tauri.conf.json +68 -0
- package/docs/ROADMAP.md +490 -0
- package/migrations/001_init.sql +9 -0
- package/migrations/002_memory.sql +33 -0
- package/migrations/003_cron.sql +24 -0
- package/migrations/004_usage.sql +12 -0
- package/migrations/005_secrets.sql +8 -0
- package/migrations/006_agents.sql +1 -0
- package/migrations/007_workflows.sql +22 -0
- package/migrations/008_proactive.sql +24 -0
- package/migrations/009_uploads.sql +9 -0
- package/migrations/010_observability.sql +22 -0
- package/migrations/011_api_keys.sql +7 -0
- package/migrations/012_indexes.sql +5 -0
- package/migrations/013_budget.sql +11 -0
- package/migrations/014_usage_session_idx.sql +2 -0
- package/package.json +39 -0
- package/site/404.html +156 -0
- package/site/CNAME +1 -0
- package/site/docs/agents.html +294 -0
- package/site/docs/api.html +446 -0
- package/site/docs/channels.html +345 -0
- package/site/docs/cli.html +238 -0
- package/site/docs/config.html +1034 -0
- package/site/docs/index.html +433 -0
- package/site/docs/integrations.html +381 -0
- package/site/docs/memory.html +254 -0
- package/site/docs/security.html +375 -0
- package/site/docs/skills.html +322 -0
- package/site/docs.css +412 -0
- package/site/index.html +638 -0
- package/site/install.sh +98 -0
- package/site/logo.svg +1 -0
- package/site/og-image.png +0 -0
- package/site/robots.txt +4 -0
- package/site/script.js +361 -0
- package/site/sitemap.xml +63 -0
- package/site/skills.html +532 -0
- package/site/style.css +1686 -0
- package/src/agent/agents.ts +159 -0
- package/src/agent/compaction.ts +53 -0
- package/src/agent/context.ts +18 -0
- package/src/agent/delegate.ts +118 -0
- package/src/agent/loop.ts +318 -0
- package/src/agent/prompts.ts +111 -0
- package/src/agent/session.ts +87 -0
- package/src/agent/teams.ts +116 -0
- package/src/agent/workflow-executor.ts +192 -0
- package/src/agent/workflow.ts +175 -0
- package/src/channels/adapter.ts +21 -0
- package/src/channels/dashboard.html.ts +2969 -0
- package/src/channels/discord.ts +137 -0
- package/src/channels/optional-deps.d.ts +17 -0
- package/src/channels/router.ts +199 -0
- package/src/channels/signal.ts +133 -0
- package/src/channels/slack.ts +101 -0
- package/src/channels/telegram.ts +102 -0
- package/src/channels/utils.ts +18 -0
- package/src/channels/webchat.ts +1797 -0
- package/src/channels/whatsapp.ts +119 -0
- package/src/config/loader.ts +22 -0
- package/src/config/paths.ts +43 -0
- package/src/config/schema.ts +121 -0
- package/src/db/connection.ts +20 -0
- package/src/db/export.ts +148 -0
- package/src/db/migrations.ts +42 -0
- package/src/index.ts +261 -0
- package/src/llm/claude.ts +193 -0
- package/src/llm/factory.ts +115 -0
- package/src/llm/failover.ts +101 -0
- package/src/llm/openai-compat.ts +409 -0
- package/src/llm/provider.ts +83 -0
- package/src/llm/smart-router.ts +241 -0
- package/src/logs.ts +53 -0
- package/src/memory/chunker.ts +58 -0
- package/src/memory/document-parser.ts +115 -0
- package/src/memory/embedder.ts +235 -0
- package/src/memory/engine.ts +170 -0
- package/src/memory/fts-index.ts +55 -0
- package/src/memory/hybrid-search.ts +72 -0
- package/src/memory/store.ts +56 -0
- package/src/memory/vector-index.ts +72 -0
- package/src/model.ts +118 -0
- package/src/registry/cli.ts +43 -0
- package/src/registry/client.ts +54 -0
- package/src/registry/installer.ts +67 -0
- package/src/scheduler/briefing.ts +71 -0
- package/src/scheduler/cron.ts +258 -0
- package/src/scheduler/heartbeat.ts +58 -0
- package/src/scheduler/memory-triggers.ts +100 -0
- package/src/scheduler/natural-cron.ts +163 -0
- package/src/scheduler/proactive.ts +25 -0
- package/src/scheduler/recipes.ts +110 -0
- package/src/secrets/store.ts +64 -0
- package/src/setup.ts +413 -0
- package/src/skills.ts +293 -0
- package/src/start.ts +373 -0
- package/src/status.ts +165 -0
- package/src/tools/builtin/connect-service.ts +205 -0
- package/src/tools/builtin/cron.ts +126 -0
- package/src/tools/builtin/datetime.ts +36 -0
- package/src/tools/builtin/delegate-task.ts +81 -0
- package/src/tools/builtin/delegate.ts +42 -0
- package/src/tools/builtin/diagnose.ts +41 -0
- package/src/tools/builtin/google-oauth.ts +379 -0
- package/src/tools/builtin/manage-agents.ts +149 -0
- package/src/tools/builtin/manage-skills.ts +294 -0
- package/src/tools/builtin/manage-teams.ts +89 -0
- package/src/tools/builtin/manage-triggers.ts +94 -0
- package/src/tools/builtin/manage-workflows.ts +119 -0
- package/src/tools/builtin/memory-search.ts +38 -0
- package/src/tools/builtin/memory-write.ts +30 -0
- package/src/tools/builtin/run-workflow.ts +36 -0
- package/src/tools/builtin/secrets.ts +122 -0
- package/src/tools/builtin/skill-registry.ts +75 -0
- package/src/tools/builtin-integrations/api-helpers.ts +26 -0
- package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
- package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
- package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
- package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
- package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
- package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
- package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
- package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
- package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
- package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
- package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
- package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
- package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
- package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
- package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
- package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
- package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
- package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
- package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
- package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
- package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
- package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
- package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
- package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
- package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
- package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
- package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
- package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
- package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
- package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
- package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
- package/src/tools/builtin-skills/file-read/handler.ts +66 -0
- package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
- package/src/tools/builtin-skills/file-write/handler.ts +64 -0
- package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
- package/src/tools/builtin-skills/http-request/handler.ts +87 -0
- package/src/tools/builtin-skills/shell/SKILL.md +26 -0
- package/src/tools/builtin-skills/shell/handler.ts +96 -0
- package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
- package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
- package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
- package/src/tools/builtin-skills/web-search/handler.ts +50 -0
- package/src/tools/executor.ts +205 -0
- package/src/tools/integration-installer.ts +106 -0
- package/src/tools/permissions.ts +45 -0
- package/src/tools/registry.ts +39 -0
- package/src/tools/sandbox-runner.ts +56 -0
- package/src/tools/sandbox.ts +82 -0
- package/src/tools/skill-installer.ts +52 -0
- package/src/tools/skill-loader.ts +259 -0
- package/src/types/optional-deps.d.ts +23 -0
- package/src/util/auth.ts +121 -0
- package/src/util/costs.ts +59 -0
- package/src/util/error-buffer.ts +32 -0
- package/src/util/google-tokens.ts +180 -0
- package/src/util/logger.ts +73 -0
- package/src/util/perf-collector.ts +35 -0
- package/src/util/rate-limiter.ts +70 -0
- package/src/util/tokens.ts +17 -0
- package/src/voice/stt.ts +57 -0
- package/src/voice/tts.ts +103 -0
- package/tests/agent/session.test.ts +109 -0
- package/tests/agent-loop.test.ts +54 -0
- package/tests/auth.test.ts +89 -0
- package/tests/channels.test.ts +67 -0
- package/tests/compaction.test.ts +44 -0
- package/tests/config.test.ts +51 -0
- package/tests/costs.test.ts +19 -0
- package/tests/cron.test.ts +55 -0
- package/tests/db/export.test.ts +219 -0
- package/tests/executor.test.ts +144 -0
- package/tests/export.test.ts +137 -0
- package/tests/helpers/mock-llm.ts +34 -0
- package/tests/helpers/test-db.ts +74 -0
- package/tests/integration/chat-flow.test.ts +48 -0
- package/tests/integrations.test.ts +97 -0
- package/tests/memory/engine.test.ts +114 -0
- package/tests/memory-engine.test.ts +57 -0
- package/tests/permissions.test.ts +21 -0
- package/tests/rate-limiter.test.ts +70 -0
- package/tests/registry.test.ts +67 -0
- package/tests/router.test.ts +36 -0
- package/tests/session.test.ts +58 -0
- package/tests/skill-loader.test.ts +44 -0
- package/tests/tokens.test.ts +30 -0
- package/tests/tools/executor.test.ts +130 -0
- package/tests/util/auth.test.ts +75 -0
- package/tests/util/rate-limiter.test.ts +73 -0
- package/tests/voice.test.ts +60 -0
- package/tests/webchat.test.ts +88 -0
- package/tests/workflow.test.ts +38 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { registerTool } from "../registry";
|
|
2
|
+
import { setSecret, getSecret, deleteSecret } from "../../secrets/store";
|
|
3
|
+
import {
|
|
4
|
+
getGoogleAuthUrl,
|
|
5
|
+
exchangeGoogleCode,
|
|
6
|
+
} from "../../util/google-tokens";
|
|
7
|
+
import { logger } from "../../util/logger";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registers the `google_oauth` tool that lets the agent (and user) start,
|
|
11
|
+
* check, and tear down the Google OAuth 2.0 connection.
|
|
12
|
+
*/
|
|
13
|
+
export function registerGoogleOAuthTool(): void {
|
|
14
|
+
registerTool({
|
|
15
|
+
definition: {
|
|
16
|
+
name: "google_oauth",
|
|
17
|
+
description:
|
|
18
|
+
"Manage the Google OAuth 2.0 connection used by Gmail, Calendar, Sheets, Docs, and Drive. " +
|
|
19
|
+
"IMPORTANT: You need TWO credentials from the user: client_id (looks like 123456-abc.apps.googleusercontent.com) " +
|
|
20
|
+
"and client_secret (starts with GOCSPX-). Ask for BOTH before calling this tool. " +
|
|
21
|
+
"Use action 'start' to begin the OAuth flow. " +
|
|
22
|
+
"Use 'complete' to finish the flow by providing the authorization code the user copied from the browser. " +
|
|
23
|
+
"Use 'status' to check connection state. Use 'disconnect' to remove all stored Google credentials.",
|
|
24
|
+
input_schema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
action: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["start", "complete", "status", "disconnect"],
|
|
30
|
+
description: "The action to perform.",
|
|
31
|
+
},
|
|
32
|
+
client_id: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description:
|
|
35
|
+
"Google OAuth client ID (looks like 123456789-xxxx.apps.googleusercontent.com). Required for 'start'.",
|
|
36
|
+
},
|
|
37
|
+
client_secret: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description:
|
|
40
|
+
"Google OAuth client secret (starts with GOCSPX-). Required for 'start'.",
|
|
41
|
+
},
|
|
42
|
+
code: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description:
|
|
45
|
+
"The authorization code from the Google OAuth callback URL. Required for 'complete' action.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["action"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
execute: async (input) => {
|
|
52
|
+
const { action, client_id, client_secret, code } = input as {
|
|
53
|
+
action: string;
|
|
54
|
+
client_id?: string;
|
|
55
|
+
client_secret?: string;
|
|
56
|
+
code?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
switch (action) {
|
|
60
|
+
case "start":
|
|
61
|
+
return handleStart(client_id, client_secret);
|
|
62
|
+
case "complete":
|
|
63
|
+
return handleComplete(code);
|
|
64
|
+
case "status":
|
|
65
|
+
return handleStatus();
|
|
66
|
+
case "disconnect":
|
|
67
|
+
return handleDisconnect();
|
|
68
|
+
default:
|
|
69
|
+
return JSON.stringify({ error: `Unknown action: ${action}` });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Action handlers
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
async function handleStart(
|
|
80
|
+
clientId?: string,
|
|
81
|
+
clientSecret?: string
|
|
82
|
+
): Promise<string> {
|
|
83
|
+
if (!clientId || !clientSecret) {
|
|
84
|
+
return JSON.stringify({
|
|
85
|
+
error:
|
|
86
|
+
"Both client_id and client_secret are required. " +
|
|
87
|
+
"Ask the user for BOTH values. " +
|
|
88
|
+
"client_id looks like: 123456789-xxxx.apps.googleusercontent.com. " +
|
|
89
|
+
"client_secret looks like: GOCSPX-xxxxx.",
|
|
90
|
+
setup_guide:
|
|
91
|
+
"The user needs to go to Google Cloud Console > APIs & Services > Credentials > " +
|
|
92
|
+
"Create Credentials > OAuth 2.0 Client ID. Choose 'Desktop app' as the application type. " +
|
|
93
|
+
"Then copy BOTH the Client ID and Client Secret.",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate client_id format
|
|
98
|
+
if (!clientId.endsWith(".apps.googleusercontent.com")) {
|
|
99
|
+
return JSON.stringify({
|
|
100
|
+
error:
|
|
101
|
+
`The client_id "${clientId.slice(0, 20)}..." does not look valid. ` +
|
|
102
|
+
"A Google OAuth client_id ends with '.apps.googleusercontent.com'. " +
|
|
103
|
+
"Make sure you have the Client ID (not the Client Secret). " +
|
|
104
|
+
"It looks like: 123456789-xxxx.apps.googleusercontent.com",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate client_secret format
|
|
109
|
+
if (!clientSecret.startsWith("GOCSPX-")) {
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
error:
|
|
112
|
+
`The client_secret "${clientSecret.slice(0, 10)}..." does not look valid. ` +
|
|
113
|
+
"A Google OAuth client_secret starts with 'GOCSPX-'. " +
|
|
114
|
+
"Make sure you have the Client Secret (not the Client ID).",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Persist client credentials
|
|
119
|
+
setSecret("google_client_id", clientId, "google");
|
|
120
|
+
setSecret("google_client_secret", clientSecret, "google");
|
|
121
|
+
|
|
122
|
+
// Start a temporary local HTTP server to receive the OAuth callback
|
|
123
|
+
let resolveCallback: (code: string) => void;
|
|
124
|
+
let rejectCallback: (reason: Error) => void;
|
|
125
|
+
|
|
126
|
+
const codePromise = new Promise<string>((resolve, reject) => {
|
|
127
|
+
resolveCallback = resolve;
|
|
128
|
+
rejectCallback = reject;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
132
|
+
try {
|
|
133
|
+
server = Bun.serve({
|
|
134
|
+
port: 0,
|
|
135
|
+
idleTimeout: 120, // OAuth flow can take a while
|
|
136
|
+
fetch(req) {
|
|
137
|
+
const url = new URL(req.url);
|
|
138
|
+
if (url.pathname === "/oauth/callback") {
|
|
139
|
+
const code = url.searchParams.get("code");
|
|
140
|
+
const error = url.searchParams.get("error");
|
|
141
|
+
|
|
142
|
+
if (error) {
|
|
143
|
+
rejectCallback(new Error(`Google OAuth error: ${error}`));
|
|
144
|
+
return new Response(
|
|
145
|
+
"<html><body><h2>Authorization failed</h2><p>You can close this window.</p></body></html>",
|
|
146
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (code) {
|
|
151
|
+
resolveCallback(code);
|
|
152
|
+
return new Response(
|
|
153
|
+
"<html><body><h2>Google connected successfully!</h2><p>You can close this window and return to Zubo.</p></body></html>",
|
|
154
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new Response("Missing authorization code.", { status: 400 });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return new Response("Not found", { status: 404 });
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
} catch (err: any) {
|
|
165
|
+
// If local server fails (e.g. running in a container), fall back to manual flow
|
|
166
|
+
logger.warn("Could not start local OAuth callback server", { error: err.message });
|
|
167
|
+
return handleStartManual();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const port = server.port;
|
|
171
|
+
const redirectUri = `http://localhost:${port}/oauth/callback`;
|
|
172
|
+
// Store redirect URI so 'complete' action can use it if auto-callback fails
|
|
173
|
+
setSecret("google_redirect_uri", redirectUri, "google");
|
|
174
|
+
|
|
175
|
+
let authUrl: string;
|
|
176
|
+
try {
|
|
177
|
+
authUrl = getGoogleAuthUrl(redirectUri);
|
|
178
|
+
} catch (err: any) {
|
|
179
|
+
server.stop();
|
|
180
|
+
return JSON.stringify({ error: err.message });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Try to open the authorization URL in the user's default browser
|
|
184
|
+
let browserOpened = false;
|
|
185
|
+
try {
|
|
186
|
+
const cmd =
|
|
187
|
+
process.platform === "darwin"
|
|
188
|
+
? ["open", authUrl]
|
|
189
|
+
: process.platform === "win32"
|
|
190
|
+
? ["cmd", "/c", "start", authUrl]
|
|
191
|
+
: ["xdg-open", authUrl];
|
|
192
|
+
Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] });
|
|
193
|
+
browserOpened = true;
|
|
194
|
+
} catch (err: any) {
|
|
195
|
+
logger.warn("Failed to open browser for Google OAuth", {
|
|
196
|
+
error: err.message,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Wait for the callback with a 120-second timeout
|
|
201
|
+
const TIMEOUT_MS = 120_000;
|
|
202
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
203
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
204
|
+
timeoutId = setTimeout(() => {
|
|
205
|
+
reject(
|
|
206
|
+
new Error("TIMEOUT")
|
|
207
|
+
);
|
|
208
|
+
}, TIMEOUT_MS);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const code = await Promise.race([codePromise, timeoutPromise]);
|
|
213
|
+
clearTimeout(timeoutId!);
|
|
214
|
+
|
|
215
|
+
await exchangeGoogleCode(code, redirectUri);
|
|
216
|
+
server.stop();
|
|
217
|
+
|
|
218
|
+
return JSON.stringify({
|
|
219
|
+
success: true,
|
|
220
|
+
message:
|
|
221
|
+
"Google connected successfully! The following services are now available: Gmail, Google Calendar, Google Sheets, Google Docs, Google Drive.",
|
|
222
|
+
services: [
|
|
223
|
+
"gmail",
|
|
224
|
+
"google_calendar",
|
|
225
|
+
"google_sheets",
|
|
226
|
+
"google_docs",
|
|
227
|
+
"google_drive",
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
} catch (err: any) {
|
|
231
|
+
clearTimeout(timeoutId!);
|
|
232
|
+
server.stop();
|
|
233
|
+
|
|
234
|
+
// If timed out, it likely means the user is remote (Telegram) and can't use localhost callback.
|
|
235
|
+
// Return the auth URL so they can open it manually and paste back the code.
|
|
236
|
+
if (err.message === "TIMEOUT") {
|
|
237
|
+
logger.info("OAuth auto-callback timed out, providing manual flow URL");
|
|
238
|
+
return JSON.stringify({
|
|
239
|
+
error: "The automatic OAuth callback did not complete in time.",
|
|
240
|
+
manual_flow: true,
|
|
241
|
+
auth_url: authUrl,
|
|
242
|
+
instructions:
|
|
243
|
+
"Send the user this link to open in their browser. After they authorize, " +
|
|
244
|
+
"the browser will redirect to a localhost URL. The URL will contain a 'code' parameter. " +
|
|
245
|
+
"Ask the user to copy the FULL URL from their browser address bar after authorizing, " +
|
|
246
|
+
"then use google_oauth with action 'complete' and pass the code parameter value.",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
logger.error("Google OAuth flow failed", { error: err.message });
|
|
251
|
+
return JSON.stringify({ error: err.message });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Manual flow: generates an auth URL for the user to open themselves.
|
|
257
|
+
* Used when the local callback server can't be started or user is remote.
|
|
258
|
+
*/
|
|
259
|
+
function handleStartManual(): string {
|
|
260
|
+
const redirectUri = "http://localhost:8080/oauth/callback";
|
|
261
|
+
setSecret("google_redirect_uri", redirectUri, "google");
|
|
262
|
+
|
|
263
|
+
let authUrl: string;
|
|
264
|
+
try {
|
|
265
|
+
authUrl = getGoogleAuthUrl(redirectUri);
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
return JSON.stringify({ error: err.message });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return JSON.stringify({
|
|
271
|
+
manual_flow: true,
|
|
272
|
+
auth_url: authUrl,
|
|
273
|
+
instructions:
|
|
274
|
+
"Send this link to the user to open in their browser. After they authorize, " +
|
|
275
|
+
"the browser will redirect to a localhost URL that may show an error page (that's OK). " +
|
|
276
|
+
"Ask the user to copy the FULL URL from their browser address bar. " +
|
|
277
|
+
"Look for the 'code' parameter in the URL (after ?code=). " +
|
|
278
|
+
"Then call google_oauth with action 'complete' and pass that code value.",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Complete the OAuth flow by exchanging a manually-provided authorization code.
|
|
284
|
+
*/
|
|
285
|
+
async function handleComplete(code?: string): Promise<string> {
|
|
286
|
+
if (!code) {
|
|
287
|
+
return JSON.stringify({
|
|
288
|
+
error:
|
|
289
|
+
"The 'code' parameter is required. This should be the authorization code " +
|
|
290
|
+
"from the Google OAuth callback URL (the value after ?code= in the redirect URL).",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// URL-decode the code if it was copied from a URL
|
|
295
|
+
const decodedCode = decodeURIComponent(code);
|
|
296
|
+
|
|
297
|
+
// Try to get the redirect URI that was used during 'start'
|
|
298
|
+
const redirectUri = getSecret("google_redirect_uri") || "http://localhost:8080/oauth/callback";
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
await exchangeGoogleCode(decodedCode, redirectUri);
|
|
302
|
+
|
|
303
|
+
return JSON.stringify({
|
|
304
|
+
success: true,
|
|
305
|
+
message:
|
|
306
|
+
"Google connected successfully! The following services are now available: Gmail, Google Calendar, Google Sheets, Google Docs, Google Drive.",
|
|
307
|
+
services: [
|
|
308
|
+
"gmail",
|
|
309
|
+
"google_calendar",
|
|
310
|
+
"google_sheets",
|
|
311
|
+
"google_docs",
|
|
312
|
+
"google_drive",
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
} catch (err: any) {
|
|
316
|
+
logger.error("Google OAuth code exchange failed", { error: err.message });
|
|
317
|
+
return JSON.stringify({
|
|
318
|
+
error: err.message,
|
|
319
|
+
hint:
|
|
320
|
+
"The authorization code may have expired (they are single-use and expire quickly). " +
|
|
321
|
+
"Try starting the OAuth flow again with google_oauth action 'start'.",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function handleStatus(): string {
|
|
327
|
+
const refreshToken = getSecret("google_refresh_token");
|
|
328
|
+
const accessToken = getSecret("google_access_token");
|
|
329
|
+
const expiresAtRaw = getSecret("google_token_expires_at");
|
|
330
|
+
const clientId = getSecret("google_client_id");
|
|
331
|
+
|
|
332
|
+
if (!refreshToken) {
|
|
333
|
+
return JSON.stringify({
|
|
334
|
+
connected: false,
|
|
335
|
+
message:
|
|
336
|
+
"Google is not connected. Use google_oauth with action 'start' to set up the connection.",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const expiresAt = expiresAtRaw ? parseInt(expiresAtRaw, 10) : 0;
|
|
341
|
+
const tokenValid = accessToken && Date.now() < expiresAt;
|
|
342
|
+
|
|
343
|
+
return JSON.stringify({
|
|
344
|
+
connected: true,
|
|
345
|
+
has_client_id: !!clientId,
|
|
346
|
+
has_refresh_token: true,
|
|
347
|
+
access_token_valid: !!tokenValid,
|
|
348
|
+
access_token_expires: expiresAt
|
|
349
|
+
? new Date(expiresAt).toISOString()
|
|
350
|
+
: null,
|
|
351
|
+
services: [
|
|
352
|
+
"gmail",
|
|
353
|
+
"google_calendar",
|
|
354
|
+
"google_sheets",
|
|
355
|
+
"google_docs",
|
|
356
|
+
"google_drive",
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function handleDisconnect(): string {
|
|
362
|
+
const secretNames = [
|
|
363
|
+
"google_client_id",
|
|
364
|
+
"google_client_secret",
|
|
365
|
+
"google_access_token",
|
|
366
|
+
"google_refresh_token",
|
|
367
|
+
"google_token_expires_at",
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
let removed = 0;
|
|
371
|
+
for (const name of secretNames) {
|
|
372
|
+
if (deleteSecret(name)) removed++;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return JSON.stringify({
|
|
376
|
+
success: true,
|
|
377
|
+
message: `Google disconnected. Removed ${removed} stored credential(s). Google services (Gmail, Calendar, Sheets, Docs, Drive) will no longer work until reconnected.`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { registerTool } from "../registry";
|
|
2
|
+
import {
|
|
3
|
+
loadAgentDefinitions,
|
|
4
|
+
getAgentDefinition,
|
|
5
|
+
createAgentDefinition,
|
|
6
|
+
removeAgentDefinition,
|
|
7
|
+
} from "../../agent/agents";
|
|
8
|
+
|
|
9
|
+
export function registerManageAgentsTool() {
|
|
10
|
+
registerTool({
|
|
11
|
+
definition: {
|
|
12
|
+
name: "manage_agents",
|
|
13
|
+
description:
|
|
14
|
+
"Create, list, view, or remove sub-agents. Sub-agents have their own system prompt and scoped tools. Use the delegate tool to assign tasks to created agents.",
|
|
15
|
+
input_schema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
action: {
|
|
19
|
+
type: "string",
|
|
20
|
+
enum: ["create", "list", "view", "remove"],
|
|
21
|
+
description: "The action to perform",
|
|
22
|
+
},
|
|
23
|
+
name: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description:
|
|
26
|
+
"Agent name (lowercase, underscores only). Required for create, view, and remove.",
|
|
27
|
+
},
|
|
28
|
+
description: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "What this agent does. Required for create.",
|
|
31
|
+
},
|
|
32
|
+
system_prompt: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "The agent's system prompt. Required for create.",
|
|
35
|
+
},
|
|
36
|
+
tools: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
description:
|
|
40
|
+
"List of tool names the agent can use (e.g., ['web_search', 'url_fetch', 'memory_write']). Required for create.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["action"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
execute: async (input) => {
|
|
47
|
+
const { action, name, description, system_prompt, tools } = input as {
|
|
48
|
+
action: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
system_prompt?: string;
|
|
52
|
+
tools?: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
switch (action) {
|
|
56
|
+
case "create": {
|
|
57
|
+
if (!name || !/^[a-z0-9_]+$/.test(name)) {
|
|
58
|
+
return JSON.stringify({
|
|
59
|
+
error: "Invalid name. Must match [a-z0-9_]+",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (!description) {
|
|
63
|
+
return JSON.stringify({ error: "description is required" });
|
|
64
|
+
}
|
|
65
|
+
if (!system_prompt) {
|
|
66
|
+
return JSON.stringify({ error: "system_prompt is required" });
|
|
67
|
+
}
|
|
68
|
+
if (!tools || tools.length === 0) {
|
|
69
|
+
return JSON.stringify({
|
|
70
|
+
error: "tools array is required (at least one tool)",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if agent already exists
|
|
75
|
+
if (getAgentDefinition(name)) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
error: `Agent "${name}" already exists. Remove it first to recreate.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const agent = createAgentDefinition(
|
|
82
|
+
name,
|
|
83
|
+
description,
|
|
84
|
+
system_prompt,
|
|
85
|
+
tools
|
|
86
|
+
);
|
|
87
|
+
return JSON.stringify({
|
|
88
|
+
success: true,
|
|
89
|
+
name: agent.name,
|
|
90
|
+
tools: agent.tools,
|
|
91
|
+
message: `Agent "${name}" created. Use delegate to assign tasks to it.`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case "list": {
|
|
96
|
+
const agents = loadAgentDefinitions();
|
|
97
|
+
if (agents.length === 0) {
|
|
98
|
+
return "No agents defined yet. Use manage_agents with action 'create' to create one.";
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
agents: agents.map((a) => ({
|
|
102
|
+
name: a.name,
|
|
103
|
+
description: a.description,
|
|
104
|
+
tools: a.tools,
|
|
105
|
+
})),
|
|
106
|
+
count: agents.length,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case "view": {
|
|
111
|
+
if (!name) {
|
|
112
|
+
return JSON.stringify({ error: "name is required for view" });
|
|
113
|
+
}
|
|
114
|
+
const agent = getAgentDefinition(name);
|
|
115
|
+
if (!agent) {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
error: `Agent "${name}" not found.`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return JSON.stringify({
|
|
121
|
+
name: agent.name,
|
|
122
|
+
description: agent.description,
|
|
123
|
+
system_prompt: agent.systemPrompt,
|
|
124
|
+
tools: agent.tools,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "remove": {
|
|
129
|
+
if (!name) {
|
|
130
|
+
return JSON.stringify({ error: "name is required for remove" });
|
|
131
|
+
}
|
|
132
|
+
const removed = removeAgentDefinition(name);
|
|
133
|
+
if (removed) {
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
success: true,
|
|
136
|
+
message: `Agent "${name}" removed.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
error: `Agent "${name}" not found.`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
default:
|
|
145
|
+
return JSON.stringify({ error: `Unknown action: ${action}` });
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|