zubo 0.1.27 → 0.1.28
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
package/src/agent/prompts.ts
CHANGED
|
@@ -45,11 +45,16 @@ const DEFAULT_PERSONALITY = `You are Zubo, a personal AI agent. You are friendly
|
|
|
45
45
|
- When the user says "remind me", "ping me", "follow up" — create a reminder.
|
|
46
46
|
- Use follow_ups to schedule check-ins: "follow up about the dentist tomorrow", "check in about the project in 3 days".
|
|
47
47
|
|
|
48
|
-
## Todos & notes
|
|
49
|
-
|
|
50
|
-
- Use the todos tool when the user asks to track tasks, create to-do lists, or manage action items. "Add buy groceries to my list" → todos with action "add".
|
|
51
|
-
- Use the notes tool to save, search, and organize information. "Save this recipe" → notes with action "save". "Find my notes about React" → notes with action "search".
|
|
52
|
-
- When the user mentions something they need to do, proactively offer to add it as a todo.
|
|
48
|
+
## Todos & notes
|
|
49
|
+
|
|
50
|
+
- Use the todos tool when the user asks to track tasks, create to-do lists, or manage action items. "Add buy groceries to my list" → todos with action "add".
|
|
51
|
+
- Use the notes tool to save, search, and organize information. "Save this recipe" → notes with action "save". "Find my notes about React" → notes with action "search".
|
|
52
|
+
- When the user mentions something they need to do, proactively offer to add it as a todo.
|
|
53
|
+
|
|
54
|
+
## Email actions
|
|
55
|
+
|
|
56
|
+
- If the user asks you to send/write an email to someone, you must actually send it using a tool ("email_send" or "gmail"). Do not only draft text unless the user explicitly asks for a draft.
|
|
57
|
+
- If required fields are missing, ask only for what's missing ("to", "subject", or "body") and send immediately once provided.
|
|
53
58
|
|
|
54
59
|
## Preferences
|
|
55
60
|
|
package/src/channels/email.ts
CHANGED
|
@@ -186,11 +186,27 @@ class ImapClient {
|
|
|
186
186
|
|
|
187
187
|
// --- Minimal SMTP client ---
|
|
188
188
|
|
|
189
|
-
class SmtpClient {
|
|
189
|
+
class SmtpClient {
|
|
190
190
|
private socket: Socket | tls.TLSSocket | null = null;
|
|
191
191
|
private buffer = "";
|
|
192
192
|
|
|
193
|
-
constructor(private config: EmailConfig["smtp"], private fromName?: string) {}
|
|
193
|
+
constructor(private config: EmailConfig["smtp"], private fromName?: string) {}
|
|
194
|
+
|
|
195
|
+
private encodeMimeHeader(value: string): string {
|
|
196
|
+
const sanitized = value.replace(/[\r\n]+/g, " ").trim();
|
|
197
|
+
if (!sanitized) return "";
|
|
198
|
+
// RFC 2047 encoded-word for non-ASCII header values (emoji, accents, etc.).
|
|
199
|
+
if (/^[\x00-\x7F]*$/.test(sanitized)) return sanitized;
|
|
200
|
+
return `=?UTF-8?B?${Buffer.from(sanitized, "utf8").toString("base64")}?=`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private toCrlfBody(body: string): string {
|
|
204
|
+
const normalized = body.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
205
|
+
return normalized
|
|
206
|
+
.split("\n")
|
|
207
|
+
.map((line) => (line.startsWith(".") ? `.${line}` : line))
|
|
208
|
+
.join("\r\n");
|
|
209
|
+
}
|
|
194
210
|
|
|
195
211
|
private async connectRaw(): Promise<Socket | tls.TLSSocket> {
|
|
196
212
|
return new Promise((resolve, reject) => {
|
|
@@ -237,7 +253,7 @@ class SmtpClient {
|
|
|
237
253
|
});
|
|
238
254
|
}
|
|
239
255
|
|
|
240
|
-
async sendEmail(to: string, subject: string, body: string, from?: string): Promise<void> {
|
|
256
|
+
async sendEmail(to: string, subject: string, body: string, from?: string): Promise<void> {
|
|
241
257
|
const rawSocket = await this.connectRaw();
|
|
242
258
|
let socket: Socket | tls.TLSSocket = rawSocket;
|
|
243
259
|
|
|
@@ -282,17 +298,21 @@ class SmtpClient {
|
|
|
282
298
|
await this.sendCmd(socket, "DATA");
|
|
283
299
|
|
|
284
300
|
// Message content — write directly to socket, not via sendCmd
|
|
285
|
-
const displayName = this.fromName || "Zubo";
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
`
|
|
291
|
-
`
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
const displayName = this.fromName || "Zubo";
|
|
302
|
+
const encodedFromName = this.encodeMimeHeader(displayName);
|
|
303
|
+
const encodedSubject = this.encodeMimeHeader(subject);
|
|
304
|
+
const safeBody = this.toCrlfBody(body || "");
|
|
305
|
+
const message = [
|
|
306
|
+
`From: ${encodedFromName} <${fromAddr}>`,
|
|
307
|
+
`To: ${to}`,
|
|
308
|
+
`Subject: ${encodedSubject}`,
|
|
309
|
+
`Date: ${new Date().toUTCString()}`,
|
|
310
|
+
`MIME-Version: 1.0`,
|
|
311
|
+
`Content-Type: text/plain; charset=utf-8`,
|
|
312
|
+
`Content-Transfer-Encoding: 8bit`,
|
|
313
|
+
``,
|
|
314
|
+
safeBody,
|
|
315
|
+
].join("\r\n");
|
|
296
316
|
|
|
297
317
|
// Send body then terminator, wait for 250 OK
|
|
298
318
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -312,7 +332,19 @@ class SmtpClient {
|
|
|
312
332
|
socket.destroy();
|
|
313
333
|
}
|
|
314
334
|
}
|
|
315
|
-
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function sendSmtpEmail(
|
|
338
|
+
config: EmailConfig["smtp"],
|
|
339
|
+
to: string,
|
|
340
|
+
subject: string,
|
|
341
|
+
body: string,
|
|
342
|
+
fromName?: string,
|
|
343
|
+
from?: string,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
const smtp = new SmtpClient(config, fromName);
|
|
346
|
+
await smtp.sendEmail(to, subject, body, from);
|
|
347
|
+
}
|
|
316
348
|
|
|
317
349
|
// --- Email channel adapter ---
|
|
318
350
|
|
package/src/start.ts
CHANGED
|
@@ -30,8 +30,9 @@ import { initMemory } from "./memory/engine";
|
|
|
30
30
|
import { registerTodosTool } from "./tools/builtin/todos";
|
|
31
31
|
import { registerNotesTool } from "./tools/builtin/notes";
|
|
32
32
|
import { registerPreferencesTool } from "./tools/builtin/preferences";
|
|
33
|
-
import { registerTopicsTool } from "./tools/builtin/topics";
|
|
34
|
-
import {
|
|
33
|
+
import { registerTopicsTool } from "./tools/builtin/topics";
|
|
34
|
+
import { registerEmailSendTool } from "./tools/builtin/email-send";
|
|
35
|
+
import { logger, enableFileLogging } from "./util/logger";
|
|
35
36
|
|
|
36
37
|
function openBrowser(url: string) {
|
|
37
38
|
try {
|
|
@@ -234,12 +235,13 @@ export async function startZubo(isDaemon = false) {
|
|
|
234
235
|
registerManageTriggersTool();
|
|
235
236
|
|
|
236
237
|
// Register personal agent tools
|
|
237
|
-
registerTodosTool();
|
|
238
|
-
registerNotesTool();
|
|
239
|
-
registerPreferencesTool();
|
|
240
|
-
registerTopicsTool();
|
|
241
|
-
|
|
242
|
-
registerFollowUpsTool
|
|
238
|
+
registerTodosTool();
|
|
239
|
+
registerNotesTool();
|
|
240
|
+
registerPreferencesTool();
|
|
241
|
+
registerTopicsTool();
|
|
242
|
+
registerEmailSendTool();
|
|
243
|
+
const { registerFollowUpsTool } = await import("./tools/builtin/follow-ups");
|
|
244
|
+
registerFollowUpsTool(db, router, config, llm);
|
|
243
245
|
|
|
244
246
|
// Register code interpreter tool
|
|
245
247
|
if (config.codeInterpreter?.enabled !== false) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { registerTool } from "../registry";
|
|
3
|
+
import { paths } from "../../config/paths";
|
|
4
|
+
import { sendSmtpEmail, type EmailConfig } from "../../channels/email";
|
|
5
|
+
|
|
6
|
+
function isValidEmail(value: string): boolean {
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function registerEmailSendTool(): void {
|
|
12
|
+
registerTool({
|
|
13
|
+
definition: {
|
|
14
|
+
name: "email_send",
|
|
15
|
+
description:
|
|
16
|
+
"Send an email using the configured SMTP account in channels.email.smtp. " +
|
|
17
|
+
"Use this when the user asks to send or write an email to someone.",
|
|
18
|
+
input_schema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
to: { type: "string", description: "Recipient email address" },
|
|
22
|
+
subject: { type: "string", description: "Email subject" },
|
|
23
|
+
body: { type: "string", description: "Email body content" },
|
|
24
|
+
},
|
|
25
|
+
required: ["to", "subject", "body"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
execute: async (input) => {
|
|
29
|
+
const to = String(input.to ?? "").trim();
|
|
30
|
+
const subject = String(input.subject ?? "").trim();
|
|
31
|
+
const body = String(input.body ?? "");
|
|
32
|
+
|
|
33
|
+
if (!to || !isValidEmail(to)) {
|
|
34
|
+
return "Invalid recipient email address. Please provide a valid `to` address.";
|
|
35
|
+
}
|
|
36
|
+
if (!subject) return "Subject is required.";
|
|
37
|
+
if (!body.trim()) return "Body is required.";
|
|
38
|
+
|
|
39
|
+
let parsed: any;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(readFileSync(paths.config, "utf-8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return "Could not read config. Run `zubo setup` and configure Email first.";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const emailCfg = parsed?.channels?.email as EmailConfig | undefined;
|
|
47
|
+
const smtpCfg = emailCfg?.smtp;
|
|
48
|
+
if (!smtpCfg?.host || !smtpCfg?.user || !smtpCfg?.password) {
|
|
49
|
+
return (
|
|
50
|
+
"Email is not configured yet. Configure `channels.email.smtp` in Settings > Channels > Email " +
|
|
51
|
+
"or run `zubo setup`."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await sendSmtpEmail(smtpCfg, to, subject, body, emailCfg?.fromName);
|
|
57
|
+
return `Email sent to ${to} with subject "${subject}".`;
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
return `Failed to send email: ${err?.message ?? String(err)}`;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
const API = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
1
|
+
const API = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
2
|
+
|
|
3
|
+
function encodeMimeHeader(value: string): string {
|
|
4
|
+
const sanitized = String(value || "").replace(/[\r\n]+/g, " ").trim();
|
|
5
|
+
if (!sanitized) return "";
|
|
6
|
+
if (/^[\x00-\x7F]*$/.test(sanitized)) return sanitized;
|
|
7
|
+
return `=?UTF-8?B?${Buffer.from(sanitized, "utf8").toString("base64")}?=`;
|
|
8
|
+
}
|
|
2
9
|
|
|
3
10
|
async function getToken(): Promise<string> {
|
|
4
11
|
// Try the new multi-provider OAuth system first
|
|
@@ -82,13 +89,15 @@ export default async function (input: Record<string, unknown>): Promise<string>
|
|
|
82
89
|
}
|
|
83
90
|
return JSON.stringify({ id: data.id, subject: getHeader("Subject"), from: getHeader("From"), date: getHeader("Date"), body: emailBody });
|
|
84
91
|
}
|
|
85
|
-
case "send": {
|
|
86
|
-
if (!to) return JSON.stringify({ error: "to is required" });
|
|
87
|
-
if (!subject) return JSON.stringify({ error: "subject is required" });
|
|
88
|
-
const raw = Buffer.from(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
case "send": {
|
|
93
|
+
if (!to) return JSON.stringify({ error: "to is required" });
|
|
94
|
+
if (!subject) return JSON.stringify({ error: "subject is required" });
|
|
95
|
+
const raw = Buffer.from(
|
|
96
|
+
`MIME-Version: 1.0\r\nTo: ${to}\r\nSubject: ${encodeMimeHeader(subject)}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body || ""}`
|
|
97
|
+
).toString("base64url");
|
|
98
|
+
const res = await fetch(`${API}/messages/send`, {
|
|
99
|
+
method: "POST", headers,
|
|
100
|
+
body: JSON.stringify({ raw }),
|
|
92
101
|
});
|
|
93
102
|
if (!res.ok) return await safeApiErr(res, "Gmail");
|
|
94
103
|
const data = (await res.json()) as any;
|
|
@@ -107,14 +116,16 @@ export default async function (input: Record<string, unknown>): Promise<string>
|
|
|
107
116
|
const orig = await fetch(`${API}/messages/${message_id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Message-ID`, { headers });
|
|
108
117
|
if (!orig.ok) return await safeApiErr(orig, "Gmail");
|
|
109
118
|
const origData = (await orig.json()) as any;
|
|
110
|
-
const getHeader = (name: string) => origData.payload?.headers?.find((h: any) => h.name === name)?.value ?? "";
|
|
111
|
-
const replyTo = getHeader("From");
|
|
112
|
-
const subj = getHeader("Subject").startsWith("Re:") ? getHeader("Subject") : `Re: ${getHeader("Subject")}`;
|
|
113
|
-
const msgId = getHeader("Message-ID");
|
|
114
|
-
const raw = Buffer.from(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
const getHeader = (name: string) => origData.payload?.headers?.find((h: any) => h.name === name)?.value ?? "";
|
|
120
|
+
const replyTo = getHeader("From");
|
|
121
|
+
const subj = getHeader("Subject").startsWith("Re:") ? getHeader("Subject") : `Re: ${getHeader("Subject")}`;
|
|
122
|
+
const msgId = getHeader("Message-ID");
|
|
123
|
+
const raw = Buffer.from(
|
|
124
|
+
`MIME-Version: 1.0\r\nTo: ${replyTo}\r\nSubject: ${encodeMimeHeader(subj)}\r\nIn-Reply-To: ${msgId}\r\nReferences: ${msgId}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body}`
|
|
125
|
+
).toString("base64url");
|
|
126
|
+
const res = await fetch(`${API}/messages/send`, {
|
|
127
|
+
method: "POST", headers,
|
|
128
|
+
body: JSON.stringify({ raw, threadId: origData.threadId }),
|
|
118
129
|
});
|
|
119
130
|
if (!res.ok) return await safeApiErr(res, "Gmail");
|
|
120
131
|
const data = (await res.json()) as any;
|
package/src/tools/permissions.ts
CHANGED
|
@@ -53,8 +53,9 @@ const DEFAULT_PERMISSIONS: Record<string, ToolPermission> = {
|
|
|
53
53
|
todos: "auto",
|
|
54
54
|
notes: "auto",
|
|
55
55
|
preferences: "auto",
|
|
56
|
-
topics: "auto",
|
|
57
|
-
follow_ups: "auto",
|
|
56
|
+
topics: "auto",
|
|
57
|
+
follow_ups: "auto",
|
|
58
|
+
email_send: "confirm",
|
|
58
59
|
|
|
59
60
|
// Built-in skills — safe (read-only or low risk)
|
|
60
61
|
web_search: "auto",
|
|
@@ -145,6 +146,7 @@ const TOOL_SCOPES: Record<string, ToolScope[]> = {
|
|
|
145
146
|
preferences: ["memory"],
|
|
146
147
|
topics: ["memory"],
|
|
147
148
|
follow_ups: ["memory"],
|
|
149
|
+
email_send: ["network_write"],
|
|
148
150
|
web_search: ["network_read"],
|
|
149
151
|
url_fetch: ["network_read"],
|
|
150
152
|
file_read: ["filesystem_read"],
|