zubo 0.1.11 → 0.1.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zubo",
3
- "version": "0.1.11",
3
+ "version": "0.1.14",
4
4
  "description": "Your AI agent that never forgets. Persistent memory, 25+ tools, 7 channels, 11+ LLM providers — runs entirely on your machine.",
5
5
  "license": "MIT",
6
6
  "author": "thomaskanze",
@@ -1978,6 +1978,7 @@ export function createWebChatAdapter(
1978
1978
  server = Bun.serve({
1979
1979
  port,
1980
1980
  hostname: "127.0.0.1", // Bind to localhost only — prevents network exposure
1981
+ idleTimeout: 120,
1981
1982
  async fetch(req) {
1982
1983
  const response = await handleRequest(req, router, sessionKey, chatLimiter, uploadLimiter, server, port);
1983
1984
  return addSecurityHeaders(response);
@@ -16,9 +16,9 @@ export function registerGoogleOAuthTool(): void {
16
16
  name: "google_oauth",
17
17
  description:
18
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. " +
19
+ "Use action 'start' to begin or re-start the OAuth flow. If credentials are already stored in secrets, " +
20
+ "you can call 'start' with NO parameters it will automatically use the saved google_client_id and google_client_secret. " +
21
+ "Only ask the user for client_id and client_secret if this tool returns an error saying they are missing. " +
22
22
  "Use 'complete' to finish the flow by providing the authorization code the user copied from the browser. " +
23
23
  "Use 'status' to check connection state. Use 'disconnect' to remove all stored Google credentials.",
24
24
  input_schema: {
@@ -32,12 +32,12 @@ export function registerGoogleOAuthTool(): void {
32
32
  client_id: {
33
33
  type: "string",
34
34
  description:
35
- "Google OAuth client ID (looks like 123456789-xxxx.apps.googleusercontent.com). Required for 'start'.",
35
+ "Google OAuth client ID. Optional if omitted, uses the value already saved in secrets.",
36
36
  },
37
37
  client_secret: {
38
38
  type: "string",
39
39
  description:
40
- "Google OAuth client secret (starts with GOCSPX-). Required for 'start'.",
40
+ "Google OAuth client secret. Optional if omitted, uses the value already saved in secrets.",
41
41
  },
42
42
  code: {
43
43
  type: "string",
@@ -124,19 +124,11 @@ async function handleStart(
124
124
  setSecret("google_client_secret", clientSecret, "google");
125
125
 
126
126
  // Start a temporary local HTTP server to receive the OAuth callback
127
- let resolveCallback: (code: string) => void;
128
- let rejectCallback: (reason: Error) => void;
129
-
130
- const codePromise = new Promise<string>((resolve, reject) => {
131
- resolveCallback = resolve;
132
- rejectCallback = reject;
133
- });
134
-
135
127
  let server: ReturnType<typeof Bun.serve>;
136
128
  try {
137
129
  server = Bun.serve({
138
130
  port: 0,
139
- idleTimeout: 120, // OAuth flow can take a while
131
+ idleTimeout: 120,
140
132
  fetch(req) {
141
133
  const url = new URL(req.url);
142
134
  if (url.pathname === "/oauth/callback") {
@@ -144,7 +136,7 @@ async function handleStart(
144
136
  const error = url.searchParams.get("error");
145
137
 
146
138
  if (error) {
147
- rejectCallback(new Error(`Google OAuth error: ${error}`));
139
+ logger.error(`Google OAuth error: ${error}`);
148
140
  return new Response(
149
141
  "<html><body><h2>Authorization failed</h2><p>You can close this window.</p></body></html>",
150
142
  { headers: { "Content-Type": "text/html" } }
@@ -152,7 +144,19 @@ async function handleStart(
152
144
  }
153
145
 
154
146
  if (code) {
155
- resolveCallback(code);
147
+ // Exchange code in the background — don't block the callback response
148
+ const storedRedirectUri = getSecret("google_redirect_uri") || `http://localhost:${server.port}/oauth/callback`;
149
+ exchangeGoogleCode(code, storedRedirectUri)
150
+ .then(() => {
151
+ logger.info("[OAuth] Google connected successfully via auto-callback");
152
+ // Auto-shutdown the callback server after success
153
+ setTimeout(() => server.stop(), 1000);
154
+ })
155
+ .catch((err) => {
156
+ logger.error("[OAuth] Auto-callback code exchange failed", { error: err.message });
157
+ setTimeout(() => server.stop(), 1000);
158
+ });
159
+
156
160
  return new Response(
157
161
  "<html><body><h2>Google connected successfully!</h2><p>You can close this window and return to Zubo.</p></body></html>",
158
162
  { headers: { "Content-Type": "text/html" } }
@@ -166,14 +170,12 @@ async function handleStart(
166
170
  },
167
171
  });
168
172
  } catch (err: any) {
169
- // If local server fails (e.g. running in a container), fall back to manual flow
170
173
  logger.warn("Could not start local OAuth callback server", { error: err.message });
171
174
  return handleStartManual();
172
175
  }
173
176
 
174
177
  const port = server.port;
175
178
  const redirectUri = `http://localhost:${port}/oauth/callback`;
176
- // Store redirect URI so 'complete' action can use it if auto-callback fails
177
179
  setSecret("google_redirect_uri", redirectUri, "google");
178
180
 
179
181
  let authUrl: string;
@@ -185,7 +187,6 @@ async function handleStart(
185
187
  }
186
188
 
187
189
  // Try to open the authorization URL in the user's default browser
188
- let browserOpened = false;
189
190
  try {
190
191
  const cmd =
191
192
  process.platform === "darwin"
@@ -194,66 +195,29 @@ async function handleStart(
194
195
  ? ["cmd", "/c", "start", authUrl]
195
196
  : ["xdg-open", authUrl];
196
197
  Bun.spawn(cmd, { stdio: ["ignore", "ignore", "ignore"] });
197
- browserOpened = true;
198
198
  } catch (err: any) {
199
- logger.warn("Failed to open browser for Google OAuth", {
200
- error: err.message,
201
- });
199
+ logger.warn("Failed to open browser for Google OAuth", { error: err.message });
202
200
  }
203
201
 
204
- // Wait for the callback with a 120-second timeout
205
- const TIMEOUT_MS = 120_000;
206
- let timeoutId: ReturnType<typeof setTimeout>;
207
- const timeoutPromise = new Promise<never>((_resolve, reject) => {
208
- timeoutId = setTimeout(() => {
209
- reject(
210
- new Error("TIMEOUT")
211
- );
212
- }, TIMEOUT_MS);
213
- });
214
-
215
- try {
216
- const code = await Promise.race([codePromise, timeoutPromise]);
217
- clearTimeout(timeoutId!);
218
-
219
- await exchangeGoogleCode(code, redirectUri);
220
- server.stop();
221
-
222
- return JSON.stringify({
223
- success: true,
224
- message:
225
- "Google connected successfully! The following services are now available: Gmail, Google Calendar, Google Sheets, Google Docs, Google Drive.",
226
- services: [
227
- "gmail",
228
- "google_calendar",
229
- "google_sheets",
230
- "google_docs",
231
- "google_drive",
232
- ],
233
- });
234
- } catch (err: any) {
235
- clearTimeout(timeoutId!);
236
- server.stop();
202
+ // Auto-shutdown the callback server after 3 minutes if no callback received
203
+ setTimeout(() => {
204
+ try { server.stop(); } catch {}
205
+ }, 180_000);
237
206
 
238
- // If timed out, it likely means the user is remote (Telegram) and can't use localhost callback.
239
- // Return the auth URL so they can open it manually and paste back the code.
240
- if (err.message === "TIMEOUT") {
241
- logger.info("OAuth auto-callback timed out, providing manual flow URL");
242
- return JSON.stringify({
243
- error: "The automatic OAuth callback did not complete in time.",
244
- manual_flow: true,
245
- auth_url: authUrl,
246
- instructions:
247
- "Send the user this link to open in their browser. After they authorize, " +
248
- "the browser will redirect to a localhost URL. The URL will contain a 'code' parameter. " +
249
- "Ask the user to copy the FULL URL from their browser address bar after authorizing, " +
250
- "then use google_oauth with action 'complete' and pass the code parameter value.",
251
- });
252
- }
253
-
254
- logger.error("Google OAuth flow failed", { error: err.message });
255
- return JSON.stringify({ error: err.message });
256
- }
207
+ // Return immediately don't block waiting for the callback.
208
+ // If the user is local, the browser will handle it automatically in the background.
209
+ // If the user is remote (Telegram, Discord), they can open the link manually.
210
+ return JSON.stringify({
211
+ auth_url: authUrl,
212
+ instructions:
213
+ "Send this authorization link to the user. Tell them to open it in their browser and authorize Google. " +
214
+ "If they are on this machine, a browser window should have opened automatically. " +
215
+ "After authorizing, the connection will be set up automatically. " +
216
+ "If the automatic callback fails (e.g. remote user), ask the user to copy the URL from their browser " +
217
+ "after authorizing (it will show a localhost URL), extract the 'code' parameter, and call google_oauth " +
218
+ "with action 'complete' and that code.",
219
+ message: "A browser window should open shortly. If not, use the link above to authorize Google.",
220
+ });
257
221
  }
258
222
 
259
223
  /**
@@ -1,5 +1,5 @@
1
1
  import { registerTool } from "../registry";
2
- import { setSecret, listSecrets, deleteSecret } from "../../secrets/store";
2
+ import { setSecret, getSecret, listSecrets, deleteSecret } from "../../secrets/store";
3
3
 
4
4
  export function registerSecretTools() {
5
5
  registerTool({
@@ -52,6 +52,37 @@ export function registerSecretTools() {
52
52
  },
53
53
  });
54
54
 
55
+ registerTool({
56
+ definition: {
57
+ name: "secret_get",
58
+ description:
59
+ "Retrieve the value of a stored secret. Use this to access API keys, tokens, and credentials needed for tool calls and integrations.",
60
+ input_schema: {
61
+ type: "object",
62
+ properties: {
63
+ name: {
64
+ type: "string",
65
+ description: "The name of the secret to retrieve (e.g., 'github_token', 'google_client_id')",
66
+ },
67
+ },
68
+ required: ["name"],
69
+ },
70
+ },
71
+ execute: async (input) => {
72
+ const { name } = input as { name: string };
73
+ if (!name) {
74
+ return JSON.stringify({ error: "Secret name is required." });
75
+ }
76
+
77
+ const value = getSecret(name);
78
+ if (value === null) {
79
+ return JSON.stringify({ error: `No secret found with name "${name}".` });
80
+ }
81
+
82
+ return JSON.stringify({ name, value });
83
+ },
84
+ });
85
+
55
86
  registerTool({
56
87
  definition: {
57
88
  name: "secret_list",
@@ -41,9 +41,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
41
41
  } catch (err: any) {
42
42
  return JSON.stringify({
43
43
  error: err.message,
44
- action_required: "Google is not connected. The user needs to set up Google OAuth. " +
45
- "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
46
- "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
44
+ action_required: "Google is not connected. Call google_oauth with action 'start' (no other parameters needed — it will use stored credentials automatically). Only ask the user for credentials if google_oauth returns an error saying they are missing.",
47
45
  });
48
46
  }
49
47
 
@@ -24,9 +24,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
24
24
  } catch (err: any) {
25
25
  return JSON.stringify({
26
26
  error: err.message,
27
- action_required: "Google is not connected. The user needs to set up Google OAuth. " +
28
- "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
29
- "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
27
+ action_required: "Google is not connected. Call google_oauth with action 'start' (no other parameters needed — it will use stored credentials automatically). Only ask the user for credentials if google_oauth returns an error saying they are missing.",
30
28
  });
31
29
  }
32
30
 
@@ -24,9 +24,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
24
24
  } catch (err: any) {
25
25
  return JSON.stringify({
26
26
  error: err.message,
27
- action_required: "Google is not connected. The user needs to set up Google OAuth. " +
28
- "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
29
- "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
27
+ action_required: "Google is not connected. Call google_oauth with action 'start' (no other parameters needed — it will use stored credentials automatically). Only ask the user for credentials if google_oauth returns an error saying they are missing.",
30
28
  });
31
29
  }
32
30
 
@@ -24,9 +24,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
24
24
  } catch (err: any) {
25
25
  return JSON.stringify({
26
26
  error: err.message,
27
- action_required: "Google is not connected. The user needs to set up Google OAuth. " +
28
- "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
29
- "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
27
+ action_required: "Google is not connected. Call google_oauth with action 'start' (no other parameters needed — it will use stored credentials automatically). Only ask the user for credentials if google_oauth returns an error saying they are missing.",
30
28
  });
31
29
  }
32
30
 
@@ -24,9 +24,7 @@ export default async function (input: Record<string, unknown>): Promise<string>
24
24
  } catch (err: any) {
25
25
  return JSON.stringify({
26
26
  error: err.message,
27
- action_required: "Google is not connected. The user needs to set up Google OAuth. " +
28
- "Ask them for their Google OAuth client_id (ends with .apps.googleusercontent.com) " +
29
- "and client_secret (starts with GOCSPX-), then use google_oauth tool with action 'start'.",
27
+ action_required: "Google is not connected. Call google_oauth with action 'start' (no other parameters needed — it will use stored credentials automatically). Only ask the user for credentials if google_oauth returns an error saying they are missing.",
30
28
  });
31
29
  }
32
30
 
@@ -9,8 +9,9 @@ const DEFAULT_PERMISSIONS: Record<string, ToolPermission> = {
9
9
  reminder_set: "auto",
10
10
  diagnose: "auto",
11
11
 
12
- // Secrets — set/list are safe, delete requires confirmation
12
+ // Secrets — set/list/get are safe, delete requires confirmation
13
13
  secret_set: "auto",
14
+ secret_get: "auto",
14
15
  secret_list: "auto",
15
16
  secret_delete: "confirm",
16
17