@rubytech/taskmaster 1.10.0 → 1.11.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.
@@ -6,8 +6,8 @@
6
6
  <title>Taskmaster Control</title>
7
7
  <meta name="color-scheme" content="dark light" />
8
8
  <link rel="icon" type="image/png" href="./favicon.png" />
9
- <script type="module" crossorigin src="./assets/index-CTFFQkyj.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-n_TWBBTg.css">
9
+ <script type="module" crossorigin src="./assets/index-YAjVyXqJ.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-CpaEIgQy.css">
11
11
  </head>
12
12
  <body>
13
13
  <taskmaster-app></taskmaster-app>
@@ -0,0 +1,92 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_CRON_DIR } from "./store.js";
4
+ export const DEFAULT_SEED_TRACKER_PATH = path.join(DEFAULT_CRON_DIR, "seeded.json");
5
+ export async function loadSeedTracker(trackerPath) {
6
+ try {
7
+ const raw = await fs.promises.readFile(trackerPath, "utf-8");
8
+ const parsed = JSON.parse(raw);
9
+ return {
10
+ version: 1,
11
+ seeded: Array.isArray(parsed?.seeded) ? parsed.seeded : [],
12
+ };
13
+ }
14
+ catch {
15
+ return { version: 1, seeded: [] };
16
+ }
17
+ }
18
+ export async function saveSeedTracker(trackerPath, tracker) {
19
+ await fs.promises.mkdir(path.dirname(trackerPath), { recursive: true });
20
+ const json = JSON.stringify(tracker, null, 2);
21
+ await fs.promises.writeFile(trackerPath, json, "utf-8");
22
+ }
23
+ export function isTemplateSeeded(tracker, templateId) {
24
+ return tracker.seeded.some((e) => e.templateId === templateId);
25
+ }
26
+ export function markTemplateSeeded(tracker, templateId, jobId) {
27
+ tracker.seeded.push({ templateId, jobId, seededAtMs: Date.now() });
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // Template loader — scan bundled skills for cron-template.json files
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * Scan the bundled skills directory for cron-template.json files.
34
+ * Returns validated templates. Skips files that can't be parsed or lack a templateId.
35
+ */
36
+ export function loadCronTemplatesFromBundledSkills(bundledSkillsDir) {
37
+ const templates = [];
38
+ let entries;
39
+ try {
40
+ entries = fs.readdirSync(bundledSkillsDir, { withFileTypes: true });
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory())
47
+ continue;
48
+ const templatePath = path.join(bundledSkillsDir, entry.name, "cron-template.json");
49
+ try {
50
+ const raw = fs.readFileSync(templatePath, "utf-8");
51
+ const parsed = JSON.parse(raw);
52
+ if (typeof parsed.templateId === "string" && parsed.templateId.trim()) {
53
+ templates.push(parsed);
54
+ }
55
+ }
56
+ catch {
57
+ // No template or invalid JSON — skip silently.
58
+ }
59
+ }
60
+ return templates;
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Seeding function — create cron jobs from un-seeded templates
64
+ // ---------------------------------------------------------------------------
65
+ export async function seedPreloadedCronJobs(params) {
66
+ const { bundledSkillsDir, trackerPath, cronService, defaultAccountId } = params;
67
+ const templates = loadCronTemplatesFromBundledSkills(bundledSkillsDir);
68
+ if (templates.length === 0)
69
+ return 0;
70
+ const tracker = await loadSeedTracker(trackerPath);
71
+ let seeded = 0;
72
+ for (const template of templates) {
73
+ if (isTemplateSeeded(tracker, template.templateId))
74
+ continue;
75
+ const job = await cronService.add({
76
+ name: template.name,
77
+ description: template.description,
78
+ enabled: template.enabled,
79
+ agentId: template.agentId,
80
+ accountId: defaultAccountId,
81
+ schedule: template.schedule,
82
+ sessionTarget: template.sessionTarget,
83
+ wakeMode: template.wakeMode,
84
+ payload: template.payload,
85
+ isolation: template.isolation,
86
+ });
87
+ markTemplateSeeded(tracker, template.templateId, job.id);
88
+ await saveSeedTracker(trackerPath, tracker);
89
+ seeded++;
90
+ }
91
+ return seeded;
92
+ }
@@ -1,39 +1,91 @@
1
1
  /**
2
- * Lightweight Resend email sender — single HTTP POST, no SDK dependency.
2
+ * Lightweight Brevo email sender — single HTTP POST, no SDK dependency.
3
+ *
4
+ * The "from" address is auto-detected from Brevo's senders API when not
5
+ * explicitly set in config. Users only need to provide an API key.
3
6
  */
4
7
  import { loadConfig } from "../../config/config.js";
5
8
  /**
6
- * Read email credentials from `publicChat.email` in config.
7
- * Returns null if any required field is missing.
9
+ * Sync check: is a Brevo API key configured?
10
+ * Used by detectOtpChannels() for capability reporting does not resolve the
11
+ * "from" address, just confirms the key exists.
8
12
  */
9
- export function resolveEmailCredentials() {
13
+ export function hasEmailApiKey() {
14
+ const cfg = loadConfig();
15
+ return !!cfg.publicChat?.email?.apiKey;
16
+ }
17
+ /** Cached verified sender — avoids hitting the API on every email. */
18
+ let cachedSender = null;
19
+ /**
20
+ * Query Brevo for the first active sender address.
21
+ * Returns the sender email or null if none exist.
22
+ */
23
+ async function fetchVerifiedSender(apiKey) {
24
+ try {
25
+ const res = await fetch("https://api.brevo.com/v3/senders", {
26
+ headers: {
27
+ "api-key": apiKey,
28
+ Accept: "application/json",
29
+ },
30
+ });
31
+ if (!res.ok)
32
+ return null;
33
+ const json = (await res.json());
34
+ const active = json.senders?.find((s) => s.active && s.email);
35
+ return active?.email ?? json.senders?.[0]?.email ?? null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ /**
42
+ * Resolve email credentials from config.
43
+ *
44
+ * Only `publicChat.email.apiKey` is required. The "from" address is resolved
45
+ * in order: explicit config value → cached sender → live Brevo API lookup.
46
+ * Returns null if no API key is configured.
47
+ */
48
+ export async function resolveEmailCredentials() {
10
49
  const cfg = loadConfig();
11
50
  const email = cfg.publicChat?.email;
12
- if (!email?.apiKey || !email.from)
51
+ if (!email?.apiKey)
52
+ return null;
53
+ // Explicit from address in config — use it directly
54
+ if (email.from)
55
+ return { apiKey: email.apiKey, from: email.from };
56
+ // Cached sender for this API key
57
+ if (cachedSender && cachedSender.apiKey === email.apiKey) {
58
+ return { apiKey: email.apiKey, from: cachedSender.from };
59
+ }
60
+ // Auto-detect from Brevo senders API
61
+ const sender = await fetchVerifiedSender(email.apiKey);
62
+ if (!sender)
13
63
  return null;
14
- return { apiKey: email.apiKey, from: email.from };
64
+ cachedSender = { apiKey: email.apiKey, from: sender };
65
+ return { apiKey: email.apiKey, from: sender };
15
66
  }
16
67
  /**
17
- * Send an email via the Resend REST API.
68
+ * Send an email via the Brevo transactional email API.
18
69
  */
19
70
  export async function sendEmail(to, subject, body, creds) {
20
- const res = await fetch("https://api.resend.com/emails", {
71
+ const res = await fetch("https://api.brevo.com/v3/smtp/email", {
21
72
  method: "POST",
22
73
  headers: {
23
- Authorization: `Bearer ${creds.apiKey}`,
74
+ "api-key": creds.apiKey,
24
75
  "Content-Type": "application/json",
76
+ Accept: "application/json",
25
77
  },
26
78
  body: JSON.stringify({
27
- from: creds.from,
28
- to: [to],
79
+ sender: { email: creds.from },
80
+ to: [{ email: to }],
29
81
  subject,
30
- text: body,
82
+ textContent: body,
31
83
  }),
32
84
  });
33
85
  if (!res.ok) {
34
86
  const text = await res.text().catch(() => "");
35
- throw new Error(`Resend email failed (${res.status}): ${text}`);
87
+ throw new Error(`Brevo email failed (${res.status}): ${text}`);
36
88
  }
37
89
  const json = (await res.json());
38
- return { id: json.id ?? "unknown" };
90
+ return { id: json.messageId ?? "unknown" };
39
91
  }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Deliver OTP verification codes via WhatsApp (primary), SMS (fallback), or email (Resend).
2
+ * Deliver OTP verification codes via WhatsApp (primary), SMS (fallback), or email (Brevo).
3
3
  */
4
4
  import { getActiveWebListener } from "../../web/active-listener.js";
5
5
  import { sendMessageWhatsApp } from "../../web/outbound.js";
6
6
  import { resolveSmsCredentials, sendSms } from "./deliver-sms.js";
7
- import { resolveEmailCredentials, sendEmail } from "./deliver-email.js";
7
+ import { hasEmailApiKey, resolveEmailCredentials, sendEmail } from "./deliver-email.js";
8
8
  /** Detect whether an identifier is an email address. */
9
9
  function isEmail(identifier) {
10
10
  return identifier.includes("@");
@@ -19,23 +19,23 @@ export function detectOtpChannels(whatsappAccountId) {
19
19
  channels.push("whatsapp");
20
20
  if (resolveSmsCredentials())
21
21
  channels.push("sms");
22
- if (resolveEmailCredentials())
22
+ if (hasEmailApiKey())
23
23
  channels.push("email");
24
24
  return channels;
25
25
  }
26
26
  /**
27
27
  * Deliver a verification code to the given identifier.
28
28
  *
29
- * Email identifiers (containing @) are sent via Resend.
29
+ * Email identifiers (containing @) are sent via Brevo.
30
30
  * Phone identifiers use the existing WhatsApp -> SMS fallback chain.
31
31
  */
32
32
  export async function deliverOtp(identifier, code, accountId) {
33
33
  const message = `Your verification code is: ${code}`;
34
- // Email identifiers — deliver via Resend only
34
+ // Email identifiers — deliver via Brevo only
35
35
  if (isEmail(identifier)) {
36
- const emailCreds = resolveEmailCredentials();
36
+ const emailCreds = await resolveEmailCredentials();
37
37
  if (!emailCreds) {
38
- throw new Error("Email verification is not configured. Set publicChat.email credentials.");
38
+ throw new Error("Email verification is not configured. Add a Brevo API key and verify a sender in the Brevo console.");
39
39
  }
40
40
  await sendEmail(identifier, "Your verification code", message, emailCreds);
41
41
  return { channel: "email" };
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Endpoints:
7
7
  * POST /session — create an anonymous session (returns sessionKey)
8
- * POST /otp/request — request an OTP code (phone via WhatsApp/SMS, or email via Resend)
8
+ * POST /otp/request — request an OTP code (phone via WhatsApp/SMS, or email via Brevo)
9
9
  * POST /otp/verify — verify OTP and get a verified session (returns sessionKey)
10
10
  * POST /chat — send a message, receive the agent reply (sync or SSE stream)
11
11
  * GET /chat/history — retrieve past messages for a session
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Authentication mirrors the public chat widget: anonymous sessions use a
16
16
  * client-provided identity string; verified sessions use OTP sent via phone
17
- * (WhatsApp with SMS fallback) or email (Resend). The sessionKey returned
17
+ * (WhatsApp with SMS fallback) or email (Brevo). The sessionKey returned
18
18
  * from /session or /otp/verify is passed via the X-Session-Key header on
19
19
  * subsequent requests.
20
20
  *
@@ -26,7 +26,7 @@ const PROVIDER_CATALOG = [
26
26
  { id: "hume", name: "Hume", category: "Voice" },
27
27
  { id: "brave", name: "Brave", category: "Web Search" },
28
28
  { id: "elevenlabs", name: "ElevenLabs", category: "Voice" },
29
- { id: "resend", name: "Resend", category: "Email" },
29
+ { id: "brevo", name: "Brevo", category: "Email" },
30
30
  ];
31
31
  const VALID_PROVIDER_IDS = new Set(PROVIDER_CATALOG.map((p) => p.id));
32
32
  export const apikeysHandlers = {
@@ -31,7 +31,7 @@ function validateAccountId(raw) {
31
31
  export const publicChatHandlers = {
32
32
  /**
33
33
  * Request an OTP code — sends a 6-digit code via WhatsApp/SMS (phone) or
34
- * Resend (email).
34
+ * Brevo (email).
35
35
  * Params: { identifier: string } or { phone: string } (backward compat)
36
36
  */
37
37
  "public.otp.request": async ({ params, respond, context }) => {
@@ -0,0 +1,19 @@
1
+ import { ErrorCodes, errorShape } from "../protocol/index.js";
2
+ import { renderQrPngBase64 } from "../../web/qr-image.js";
3
+ export const qrHandlers = {
4
+ "qr.generate": async ({ params, respond, context }) => {
5
+ const url = typeof params.url === "string" ? params.url : "";
6
+ if (!url) {
7
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "missing url param"));
8
+ return;
9
+ }
10
+ try {
11
+ const base64 = await renderQrPngBase64(url, { scale: 6, marginModules: 2 });
12
+ respond(true, { dataUrl: `data:image/png;base64,${base64}` });
13
+ }
14
+ catch (err) {
15
+ context.logGateway.warn(`qr.generate failed: ${err instanceof Error ? err.message : String(err)}`);
16
+ respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, "failed to generate QR code"));
17
+ }
18
+ },
19
+ };
@@ -33,6 +33,7 @@ import { voicewakeHandlers } from "./server-methods/voicewake.js";
33
33
  import { webHandlers } from "./server-methods/web.js";
34
34
  import { wizardHandlers } from "./server-methods/wizard.js";
35
35
  import { publicChatHandlers } from "./server-methods/public-chat.js";
36
+ import { qrHandlers } from "./server-methods/qr.js";
36
37
  import { tailscaleHandlers } from "./server-methods/tailscale.js";
37
38
  import { networkHandlers } from "./server-methods/network.js";
38
39
  import { wifiHandlers } from "./server-methods/wifi.js";
@@ -105,6 +106,7 @@ const READ_METHODS = new Set([
105
106
  "workspaces.scan",
106
107
  "memory.status",
107
108
  "memory.audit",
109
+ "qr.generate",
108
110
  ]);
109
111
  const WRITE_METHODS = new Set([
110
112
  "send",
@@ -240,6 +242,7 @@ export const coreGatewayHandlers = {
240
242
  ...workspacesHandlers,
241
243
  ...brandHandlers,
242
244
  ...publicChatHandlers,
245
+ ...qrHandlers,
243
246
  ...networkHandlers,
244
247
  ...tailscaleHandlers,
245
248
  ...wifiHandlers,
@@ -34,6 +34,9 @@ import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js
34
34
  import { createChannelManager } from "./server-channels.js";
35
35
  import { createAgentEventHandler } from "./server-chat.js";
36
36
  import { createGatewayCloseHandler } from "./server-close.js";
37
+ import { resolveBundledSkillsDir } from "../agents/skills/bundled-dir.js";
38
+ import { DEFAULT_SEED_TRACKER_PATH, seedPreloadedCronJobs } from "../cron/preloaded.js";
39
+ import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js";
37
40
  import { buildGatewayCronService } from "./server-cron.js";
38
41
  import { applyGatewayLaneConcurrency } from "./server-lanes.js";
39
42
  import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
@@ -395,7 +398,29 @@ export async function startGatewayServer(port = 18789, opts = {}) {
395
398
  broadcast("notification", evt);
396
399
  });
397
400
  let heartbeatRunner = startHeartbeatRunner({ cfg: cfgAtStart });
398
- void cron.start().catch((err) => logCron.error(`failed to start: ${String(err)}`));
401
+ // Start cron, then seed preloaded cron jobs from bundled skills (one-time per template).
402
+ void cron
403
+ .start()
404
+ .then(async () => {
405
+ const bundledSkillsDir = resolveBundledSkillsDir();
406
+ if (!bundledSkillsDir)
407
+ return;
408
+ try {
409
+ const seeded = await seedPreloadedCronJobs({
410
+ bundledSkillsDir,
411
+ trackerPath: DEFAULT_SEED_TRACKER_PATH,
412
+ cronService: cron,
413
+ defaultAccountId: DEFAULT_ACCOUNT_ID,
414
+ });
415
+ if (seeded > 0) {
416
+ logCron.info(`cron: seeded ${seeded} preloaded job(s)`);
417
+ }
418
+ }
419
+ catch (err) {
420
+ logCron.error(`cron: failed to seed preloaded jobs: ${String(err)}`);
421
+ }
422
+ })
423
+ .catch((err) => logCron.error(`failed to start: ${String(err)}`));
399
424
  const execApprovalManager = new ExecApprovalManager();
400
425
  const execApprovalForwarder = createExecApprovalForwarder();
401
426
  const execApprovalHandlers = createExecApprovalHandlers(execApprovalManager, {
@@ -346,6 +346,13 @@ export async function runOnboardingWizard(opts, runtime = defaultRuntime, prompt
346
346
  }
347
347
  // Setup hooks (session memory on /new)
348
348
  nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
349
+ // Inform user about the preloaded Daily Strategic Review cron job.
350
+ await prompter.note([
351
+ "A Daily Strategic Review is pre-configured (paused).",
352
+ "It scans your conversations, audits your materials, and suggests improvements.",
353
+ "",
354
+ "To enable it: Control Panel → Advanced → Events → enable 🔭 Daily Strategic Review.",
355
+ ].join("\n"), "Daily Strategic Review");
349
356
  nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
350
357
  await writeConfigFile(nextConfig);
351
358
  await finalizeOnboardingWizard({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -84,8 +84,6 @@
84
84
  "prepack": "pnpm build && pnpm ui:build",
85
85
  "docs:list": "node scripts/docs-list.js",
86
86
  "docs:bin": "node scripts/build-docs-list.mjs",
87
- "docs:dev": "cd docs && mint dev",
88
- "docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
89
87
  "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
90
88
  "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
91
89
  "release:check": "node --import tsx scripts/release-check.ts",
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: brevo
3
+ description: Guide users through getting a free Brevo API key for email OTP verification.
4
+ metadata: {"taskmaster":{"emoji":"🔑"}}
5
+ ---
6
+
7
+ # Brevo Setup
8
+
9
+ Walks users through creating a Brevo account, verifying a sender email address, and obtaining an API key. Brevo uses single sender verification — the user verifies one email address by entering a 6-digit code sent to it. No DNS records or domain ownership required.
10
+
11
+ ## When to activate
12
+
13
+ - User enables Email OTP or Visitor Chooses mode for Public Chat and email is not configured
14
+ - User asks about email verification setup or Brevo
15
+ - BOOTSTRAP detects missing Brevo API key when email verification is enabled
16
+
17
+ ## What it unlocks
18
+
19
+ - Email OTP verification for public chat visitors
20
+ - Verification codes delivered via email to any address
21
+ - Free tier: 300 emails per day, no credit card required
22
+
23
+ ## References
24
+
25
+ | Task | When to use | Reference |
26
+ |------|-------------|-----------|
27
+ | Guided setup | User wants help getting the key | `references/browser-setup.md` |
28
+
29
+ Load the reference and follow its instructions.
@@ -0,0 +1,113 @@
1
+ # Brevo Email — Guided Setup
2
+
3
+ Walk the user through getting Brevo credentials. Brevo uses single sender verification — the user verifies an email address by entering a 6-digit code sent to it. No DNS records required. Guide with clear instructions.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ - User has an email address they want OTP emails to come from (e.g. their business email)
10
+ - Chat channel for sending instructions
11
+
12
+ ---
13
+
14
+ ## Step 1: Explain
15
+
16
+ Tell the user what Brevo is and why they need it:
17
+
18
+ > "To send verification codes by email, we need a Brevo account. It's completely free — you get 300 emails a day, which is way more than enough for verification codes. No credit card required.
19
+ >
20
+ > I'll walk you through the setup step by step. The whole thing takes about 5 minutes."
21
+
22
+ ## Step 2: Create a Brevo account
23
+
24
+ > "Go to **brevo.com** in your browser and click **Sign up free**.
25
+ >
26
+ > You'll need to enter:
27
+ > - Your email address
28
+ > - A password
29
+ >
30
+ > Brevo will send a confirmation email. Click the link in it to verify your account. Let me know when you're logged in."
31
+
32
+ Wait for the user to confirm they've completed signup.
33
+
34
+ If the user is asked onboarding questions (company name, team size, etc.), tell them:
35
+
36
+ > "Just fill in your business name and pick whatever options feel right — those are for Brevo's records and don't affect anything."
37
+
38
+ ## Step 3: Verify a sender email address
39
+
40
+ This determines what email address OTP codes come from. Brevo sends a 6-digit code to the email — no DNS records needed.
41
+
42
+ > "Now we need to verify the email address that verification codes will be sent from. This is what your visitors will see in their inbox.
43
+ >
44
+ > 1. In the Brevo dashboard, click your **name/icon** in the top-right
45
+ > 2. Click **Senders, Domains & Dedicated IPs** (or go to **Settings → Senders**)
46
+ > 3. Click **Add a sender**
47
+ > 4. Fill in:
48
+ > - **From Name** — your business name (e.g. "Acme Plumbing")
49
+ > - **From Email** — the email you want codes to come from (your business email works)
50
+ > 5. Click **Save**
51
+ >
52
+ > Brevo will send a **6-digit code** to that email address. Check your inbox (and spam folder), enter the code, and click **Verify**. Let me know when it's done."
53
+
54
+ Wait for the user to confirm.
55
+
56
+ ## Step 4: Get an API key
57
+
58
+ > "Almost done. Now we need an API key:
59
+ >
60
+ > 1. Click your **name/icon** in the top-right again
61
+ > 2. Click **SMTP & API**
62
+ > 3. Click the **API Keys** tab
63
+ > 4. Click **Generate a new API key**
64
+ > 5. Give it a name like **Taskmaster**
65
+ > 6. Click **Generate**
66
+ >
67
+ > You'll see the API key — it starts with **xkeysib-** and is quite long. **Copy it now**. Send it to me."
68
+
69
+ Wait for the user to send the API key.
70
+
71
+ Verify it looks right (starts with `xkeysib-`, usually 60+ characters). If not:
72
+
73
+ > "That doesn't look like a Brevo API key — it should start with **xkeysib-** and be about 64 characters long. Can you check you copied the full key?"
74
+
75
+ ## Step 5: Save to Taskmaster
76
+
77
+ Once you have the API key, store it using the `api_keys` tool:
78
+
79
+ ```
80
+ api_keys({ action: "set", provider: "brevo", apiKey: "<the key>" })
81
+ ```
82
+
83
+ The key is applied immediately — no restart needed. Confirm to the user:
84
+
85
+ > "Done — I've saved your Brevo API key. Email verification is now enabled. When visitors use your public chat, they can verify their identity with an email code. The code will come from the address you verified in Brevo."
86
+
87
+ That's it. No further configuration is needed in Taskmaster — the verified sender address is detected automatically from Brevo.
88
+
89
+ ---
90
+
91
+ ## If the user already has a Brevo account
92
+
93
+ Check if they have a verified sender:
94
+
95
+ > "Do you already have a verified sender in Brevo? If so, I just need your API key (starts with **xkeysib-**)."
96
+
97
+ If yes, skip to Step 4. If they're unsure:
98
+
99
+ > "Go to **Settings → Senders** in Brevo and check if there's a verified sender listed. If not, we'll set one up."
100
+
101
+ ---
102
+
103
+ ## Common questions
104
+
105
+ | Question | Answer |
106
+ |----------|--------|
107
+ | "Is it free?" | Yes — 300 emails per day, forever. No credit card required. For verification codes, that's more than enough. |
108
+ | "Can I use my Gmail/Outlook address?" | Yes, as long as you verify it by entering the 6-digit code Brevo sends to it. |
109
+ | "Will emails go to spam?" | OTP emails are solicited (the user just clicked 'send me a code'), so they almost always reach the inbox. If a visitor doesn't see it, tell them to check spam. |
110
+ | "I lost my API key" | Go to SMTP & API → API Keys in Brevo and generate a new one. |
111
+ | "Do I need to set up DNS records?" | No. Single sender verification works without DNS. Brevo will recommend domain authentication for better deliverability, but it's not required. |
112
+ | "Can I change the sender address later?" | Yes — add and verify a new sender in Brevo. Taskmaster auto-detects the verified sender, so no changes needed on this end. |
113
+ | "Where does Taskmaster get the sender address?" | Automatically from the Brevo API. When you verify a sender in Brevo, Taskmaster detects it — you don't need to enter it anywhere else. |
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: strategic-proactivity
3
+ description: "Periodic strategic review — scans recent conversations, audits materials against reality, identifies gaps and opportunities, drafts improvements, and sends a prioritised summary to admins. Triggered by cron job or on-demand."
4
+ metadata: {"taskmaster":{"emoji":"🔭"}}
5
+ ---
6
+
7
+ # Strategic Proactivity
8
+
9
+ ## When to Activate
10
+
11
+ - Triggered by a scheduled review (cron job)
12
+ - Admin asks to review conversations, materials, opportunities, or gaps
13
+ - Admin asks "what needs my attention?", "what should we be working on?", or similar strategic questions
14
+
15
+ ## Behaviour
16
+
17
+ Load `references/daily-review.md` for the full review protocol and action framework.
18
+
19
+ **Core principle:** Surface insights and draft improvements — don't just report. Always send findings to admin for approval before publishing anything.
20
+
21
+ ### Rules
22
+
23
+ 1. **Scan recent conversations** — all channels, all agent sessions (public, admin, group)
24
+ 2. **Compare against existing materials** — search memory for current documents, then check whether they still reflect reality
25
+ 3. **Identify gaps and opportunities** — questions that couldn't be answered, recurring themes, new verticals, unaddressed objections
26
+ 4. **Draft improvements** — don't just flag problems, write the fix
27
+ 5. **Track opportunities** — leads, partnership signals, market intel, feature requests
28
+ 6. **Send a concise summary to admins** — bullet points, prioritised by impact, with drafts attached where relevant
29
+
30
+ ### Output Format
31
+
32
+ The summary should follow this structure:
33
+
34
+ - **New Insights** — what emerged from conversations
35
+ - **Drafts Ready** — what you've written or updated (saved to memory for review)
36
+ - **Opportunities** — leads, partnerships, verticals worth pursuing
37
+ - **Gaps Found** — materials that need updating or creating
38
+ - **No Action Needed** — if genuinely nothing surfaced, say so briefly
39
+
40
+ ### Constraints
41
+
42
+ - Keep total execution under 2 minutes
43
+ - Don't publish anything to shared/ or public/ without admin approval
44
+ - Save all drafts to memory for admin review before acting on them
45
+ - Be honest — if nothing meaningful surfaced, say "quiet period" rather than padding the report
@@ -0,0 +1,21 @@
1
+ {
2
+ "templateId": "strategic-proactivity-daily-review",
3
+ "name": "🔭 Daily Strategic Review",
4
+ "description": "Scans recent conversations, audits materials, identifies opportunities and gaps",
5
+ "enabled": false,
6
+ "agentId": "admin",
7
+ "schedule": { "kind": "cron", "expr": "0 7 * * *" },
8
+ "sessionTarget": "isolated",
9
+ "wakeMode": "next-heartbeat",
10
+ "payload": {
11
+ "kind": "agentTurn",
12
+ "message": "Run the daily strategic proactivity review. Use the strategic-proactivity skill.",
13
+ "deliver": true,
14
+ "to": "admins",
15
+ "bestEffortDeliver": true
16
+ },
17
+ "isolation": {
18
+ "postToMainMode": "summary",
19
+ "postToMainPrefix": "🔭 Daily Review"
20
+ }
21
+ }