@rubytech/taskmaster 1.12.0 → 1.12.2

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 (39) hide show
  1. package/dist/agents/taskmaster-tools.js +5 -3
  2. package/dist/agents/tool-policy.js +1 -1
  3. package/dist/agents/tools/authorize-admin-tool.js +58 -17
  4. package/dist/agents/tools/bootstrap-tool.js +51 -0
  5. package/dist/agents/tools/cron-tool.js +20 -7
  6. package/dist/build-info.json +3 -3
  7. package/dist/commands/agents.config.js +1 -0
  8. package/dist/config/zod-schema.js +18 -0
  9. package/dist/control-ui/assets/{index-DpMaqt-b.js → index-CP9IoaZp.js} +110 -97
  10. package/dist/control-ui/assets/index-CP9IoaZp.js.map +1 -0
  11. package/dist/control-ui/index.html +1 -1
  12. package/dist/gateway/control-ui.js +24 -9
  13. package/dist/gateway/protocol/schema/cron.js +8 -0
  14. package/dist/gateway/public-chat/deliver-email.js +5 -5
  15. package/dist/gateway/public-chat/deliver-otp.js +3 -3
  16. package/dist/gateway/public-chat/deliver-sms.js +5 -1
  17. package/dist/gateway/public-chat-api.js +26 -11
  18. package/dist/gateway/server-channels.js +6 -2
  19. package/dist/gateway/server-methods/cron.js +32 -0
  20. package/dist/gateway/server-methods/public-chat.js +4 -2
  21. package/dist/gateway/server-methods/tailscale.js +9 -0
  22. package/dist/gateway/server-methods/workspaces.js +52 -0
  23. package/dist/web/auto-reply/monitor.js +3 -0
  24. package/package.json +1 -1
  25. package/skills/tailscale/SKILL.md +37 -9
  26. package/templates/beagle-taxi/agents/admin/AGENTS.md +25 -0
  27. package/templates/beagle-taxi/agents/admin/IDENTITY.md +9 -0
  28. package/templates/beagle-taxi/agents/admin/SOUL.md +31 -0
  29. package/templates/beagle-taxi/agents/public/AGENTS.md +54 -0
  30. package/templates/beagle-taxi/agents/public/IDENTITY.md +10 -0
  31. package/templates/beagle-taxi/agents/public/SOUL.md +32 -0
  32. package/templates/beagle-taxi/memory/public/knowledge-base.md +177 -0
  33. package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +44 -0
  34. package/templates/customer/agents/admin/BOOTSTRAP.md +2 -2
  35. package/templates/education-hero/agents/admin/BOOTSTRAP.md +2 -2
  36. package/templates/maxy/agents/admin/BOOTSTRAP.md +2 -3
  37. package/templates/taskmaster/agents/admin/BOOTSTRAP.md +2 -2
  38. package/templates/tradesupport/agents/admin/BOOTSTRAP.md +2 -2
  39. package/dist/control-ui/assets/index-DpMaqt-b.js.map +0 -1
@@ -6,7 +6,7 @@
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-DpMaqt-b.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-CP9IoaZp.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="./assets/index-CpaEIgQy.css">
11
11
  </head>
12
12
  <body>
@@ -527,7 +527,9 @@ export function handlePublicChatHttpRequest(req, res, opts) {
527
527
  const avatarValue = resolvedAvatar && /^(https?:\/\/|data:image\/|\/)/i.test(resolvedAvatar)
528
528
  ? resolvedAvatar
529
529
  : undefined;
530
- const authMode = config.publicChat.auth ?? "anonymous";
530
+ const authMode = (config.publicChat?.accountSettings?.[accountId]?.auth ??
531
+ config.publicChat?.auth ??
532
+ "anonymous");
531
533
  const cookieTtlDays = config.publicChat.cookieTtlDays ?? 30;
532
534
  const brandName = config.ui?.brand?.name;
533
535
  const brandIconUrl = resolveBrandIconUrl(config.ui?.brand?.icon, root);
@@ -570,7 +572,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
570
572
  // Inject public-chat globals before </head>
571
573
  const publicScript = `<script>` +
572
574
  `window.__TASKMASTER_PUBLIC_CHAT__=true;` +
573
- `window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ accountId, auth: authMode, cookieTtlDays, otpChannels, verifyMethods: normalizeVerifyMethods(config.publicChat?.verifyMethods ?? ["whatsapp", "sms"]), greeting: config.publicChat?.greetings?.[accountId] || undefined })};` +
575
+ `window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ accountId, auth: authMode, cookieTtlDays, otpChannels, verifyMethods: normalizeVerifyMethods(config.publicChat?.accountSettings?.[accountId]?.verifyMethods ?? config.publicChat?.verifyMethods ?? ["whatsapp", "sms"]), greeting: config.publicChat?.greetings?.[accountId] || undefined })};` +
574
576
  `</script>`;
575
577
  const headClose = baseInjected.indexOf("</head>");
576
578
  const withPublic = headClose !== -1
@@ -584,24 +586,37 @@ export function handlePublicChatHttpRequest(req, res, opts) {
584
586
  /** Widget script content — self-contained JS for embedding. */
585
587
  const WIDGET_SCRIPT = `(function(){
586
588
  "use strict";
587
- var cfg={server:"",accountId:"",color:"#1a1a2e",bgColor:"#1a1a2e"};
589
+ var cfg={server:"",accountId:"",color:"",bgColor:"#1a1a2e",btnColor:""};
588
590
  var isOpen=false;
589
591
  var btn,overlay,iframe;
592
+ var SVG_NS="http://www.w3.org/2000/svg";
593
+ function svgEl(tag,attrs){var e=document.createElementNS(SVG_NS,tag);for(var k in attrs)e.setAttribute(k,attrs[k]);return e;}
594
+ function makeChatIcon(){var s=svgEl("svg",{width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"});s.appendChild(svgEl("path",{d:"M7.9 20A9 9 0 1 0 4 16.1L2 22Z"}));return s;}
595
+ function makeCloseIcon(){var s=svgEl("svg",{width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2.5","stroke-linecap":"round","stroke-linejoin":"round"});s.appendChild(svgEl("line",{x1:"18",y1:"6",x2:"6",y2:"18"}));s.appendChild(svgEl("line",{x1:"6",y1:"6",x2:"18",y2:"18"}));return s;}
596
+ function setIcon(el,fn){while(el.firstChild)el.removeChild(el.firstChild);el.appendChild(fn());}
590
597
 
591
598
  function init(opts){
592
599
  if(opts&&opts.server) cfg.server=opts.server.replace(/\\/$/,"");
593
600
  if(opts&&opts.accountId) cfg.accountId=opts.accountId;
594
- if(opts&&opts.color) cfg.color=opts.color;
601
+ if(opts&&opts.color) cfg.btnColor=opts.color;
595
602
  if(opts&&opts.bgColor) cfg.bgColor=opts.bgColor;
596
- build();
603
+ if(!cfg.btnColor){
604
+ fetch(cfg.server+"/public/api/v1/"+encodeURIComponent(cfg.accountId)+"/capabilities")
605
+ .then(function(r){return r.json()})
606
+ .then(function(d){if(d.background_color) cfg.btnColor=d.background_color; else if(d.accent_color) cfg.btnColor=d.accent_color; build();})
607
+ .catch(function(){build();});
608
+ } else {
609
+ build();
610
+ }
597
611
  }
598
612
 
599
613
  function build(){
614
+ if(!cfg.btnColor) cfg.btnColor="#1a1a2e";
600
615
  var css=document.createElement("style");
601
616
  css.textContent=[
602
617
  ".tm-widget-btn{position:fixed;bottom:20px;right:20px;width:48px;height:48px;",
603
- "border-radius:50%;background:"+cfg.color+";color:#fff;border:none;cursor:pointer;",
604
- "box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:999999;font-size:22px;",
618
+ "border-radius:50%;background:"+cfg.btnColor+";color:#fff;border:none;cursor:pointer;",
619
+ "box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:999999;",
605
620
  "display:flex;align-items:center;justify-content:center;transition:transform .2s}",
606
621
  ".tm-widget-btn:hover{transform:scale(1.08)}",
607
622
  ".tm-widget-overlay{position:fixed;bottom:78px;right:20px;width:400px;height:600px;",
@@ -618,7 +633,7 @@ const WIDGET_SCRIPT = `(function(){
618
633
 
619
634
  btn=document.createElement("button");
620
635
  btn.className="tm-widget-btn";
621
- btn.textContent="\\uD83D\\uDCAC";
636
+ setIcon(btn,makeChatIcon);
622
637
  btn.setAttribute("aria-label","Chat");
623
638
  btn.onclick=toggle;
624
639
  document.body.appendChild(btn);
@@ -635,7 +650,7 @@ const WIDGET_SCRIPT = `(function(){
635
650
  function toggle(){
636
651
  isOpen=!isOpen;
637
652
  overlay.classList.toggle("open",isOpen);
638
- btn.textContent=isOpen?"\\u2715":"\\uD83D\\uDCAC";
653
+ setIcon(btn,isOpen?makeCloseIcon:makeChatIcon);
639
654
  }
640
655
 
641
656
  window.Taskmaster={init:init};
@@ -117,38 +117,46 @@ export const CronUpdateParamsSchema = Type.Union([
117
117
  Type.Object({
118
118
  id: NonEmptyString,
119
119
  patch: CronJobPatchSchema,
120
+ accountId: Type.Optional(Type.String()),
120
121
  }, { additionalProperties: false }),
121
122
  Type.Object({
122
123
  jobId: NonEmptyString,
123
124
  patch: CronJobPatchSchema,
125
+ accountId: Type.Optional(Type.String()),
124
126
  }, { additionalProperties: false }),
125
127
  ]);
126
128
  export const CronRemoveParamsSchema = Type.Union([
127
129
  Type.Object({
128
130
  id: NonEmptyString,
131
+ accountId: Type.Optional(Type.String()),
129
132
  }, { additionalProperties: false }),
130
133
  Type.Object({
131
134
  jobId: NonEmptyString,
135
+ accountId: Type.Optional(Type.String()),
132
136
  }, { additionalProperties: false }),
133
137
  ]);
134
138
  export const CronRunParamsSchema = Type.Union([
135
139
  Type.Object({
136
140
  id: NonEmptyString,
137
141
  mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])),
142
+ accountId: Type.Optional(Type.String()),
138
143
  }, { additionalProperties: false }),
139
144
  Type.Object({
140
145
  jobId: NonEmptyString,
141
146
  mode: Type.Optional(Type.Union([Type.Literal("due"), Type.Literal("force")])),
147
+ accountId: Type.Optional(Type.String()),
142
148
  }, { additionalProperties: false }),
143
149
  ]);
144
150
  export const CronRunsParamsSchema = Type.Union([
145
151
  Type.Object({
146
152
  id: NonEmptyString,
147
153
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })),
154
+ accountId: Type.Optional(Type.String()),
148
155
  }, { additionalProperties: false }),
149
156
  Type.Object({
150
157
  jobId: NonEmptyString,
151
158
  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })),
159
+ accountId: Type.Optional(Type.String()),
152
160
  }, { additionalProperties: false }),
153
161
  ]);
154
162
  export const CronRunLogEntrySchema = Type.Object({
@@ -45,24 +45,24 @@ async function fetchVerifiedSender(apiKey) {
45
45
  * in order: explicit config value → cached sender → live Brevo API lookup.
46
46
  * Returns null if no API key is configured.
47
47
  */
48
- export async function resolveEmailCredentials() {
48
+ export async function resolveEmailCredentials(fromName) {
49
49
  const cfg = loadConfig();
50
50
  const email = cfg.publicChat?.email;
51
51
  if (!email?.apiKey)
52
52
  return null;
53
53
  // Explicit from address in config — use it directly
54
54
  if (email.from)
55
- return { apiKey: email.apiKey, from: email.from };
55
+ return { apiKey: email.apiKey, from: email.from, fromName };
56
56
  // Cached sender for this API key
57
57
  if (cachedSender && cachedSender.apiKey === email.apiKey) {
58
- return { apiKey: email.apiKey, from: cachedSender.from };
58
+ return { apiKey: email.apiKey, from: cachedSender.from, fromName };
59
59
  }
60
60
  // Auto-detect from Brevo senders API
61
61
  const sender = await fetchVerifiedSender(email.apiKey);
62
62
  if (!sender)
63
63
  return null;
64
64
  cachedSender = { apiKey: email.apiKey, from: sender };
65
- return { apiKey: email.apiKey, from: sender };
65
+ return { apiKey: email.apiKey, from: sender, fromName };
66
66
  }
67
67
  /**
68
68
  * Send an email via the Brevo transactional email API.
@@ -76,7 +76,7 @@ export async function sendEmail(to, subject, body, creds) {
76
76
  Accept: "application/json",
77
77
  },
78
78
  body: JSON.stringify({
79
- sender: { email: creds.from },
79
+ sender: creds.fromName ? { email: creds.from, name: creds.fromName } : { email: creds.from },
80
80
  to: [{ email: to }],
81
81
  subject,
82
82
  textContent: body,
@@ -55,12 +55,12 @@ export function detectOtpChannels(whatsappAccountId) {
55
55
  * Phone identifiers try WhatsApp then SMS, but only channels that
56
56
  * are enabled in `enabledMethods` are attempted.
57
57
  */
58
- export async function deliverOtp(identifier, code, accountId, enabledMethods) {
58
+ export async function deliverOtp(identifier, code, accountId, enabledMethods, brandName) {
59
59
  const message = `Your verification code is: ${code}`;
60
60
  const methods = enabledMethods ?? ["whatsapp", "sms", "email"];
61
61
  // Email identifiers — deliver via Brevo email API
62
62
  if (isEmail(identifier)) {
63
- const emailCreds = await resolveEmailCredentials();
63
+ const emailCreds = await resolveEmailCredentials(brandName);
64
64
  if (!emailCreds) {
65
65
  throw new Error("Email verification is not configured. Add a Brevo API key and verify a sender in the Brevo console.");
66
66
  }
@@ -80,7 +80,7 @@ export async function deliverOtp(identifier, code, accountId, enabledMethods) {
80
80
  }
81
81
  }
82
82
  if (trySms) {
83
- const smsCreds = await resolveSmsCredentials();
83
+ const smsCreds = await resolveSmsCredentials(brandName);
84
84
  if (smsCreds) {
85
85
  await sendSms(identifier, message, smsCreds);
86
86
  return { channel: "sms" };
@@ -72,11 +72,15 @@ export function hasSmsApiKey() {
72
72
  * brand identity. The name is truncated to 11 characters (GSM limit).
73
73
  * Returns null if no API key is configured.
74
74
  */
75
- export async function resolveSmsCredentials() {
75
+ export async function resolveSmsCredentials(brandName) {
76
76
  const cfg = loadConfig();
77
77
  const apiKey = cfg.publicChat?.email?.apiKey;
78
78
  if (!apiKey)
79
79
  return null;
80
+ // Brand name override takes precedence — use it directly (truncated to GSM limit)
81
+ if (brandName) {
82
+ return { apiKey, sender: truncateSender(brandName) };
83
+ }
80
84
  if (cachedSenderName &&
81
85
  cachedSenderName.apiKey === apiKey &&
82
86
  cachedSenderName.name.length <= 11) {
@@ -98,14 +98,23 @@ function isValidEmail(email) {
98
98
  function normalizePhone(raw) {
99
99
  return raw.replace(/[\s\-()]/g, "");
100
100
  }
101
+ /** Resolve the auth mode for a specific account, falling back to the global setting. */
102
+ function resolveAccountAuthMode(cfg, accountId) {
103
+ return cfg.publicChat?.accountSettings?.[accountId]?.auth ?? cfg.publicChat?.auth ?? "anonymous";
104
+ }
105
+ /** Resolve the verify methods for a specific account, falling back to the global setting. */
106
+ function resolveAccountVerifyMethods(cfg, accountId) {
107
+ return (cfg.publicChat?.accountSettings?.[accountId]?.verifyMethods ??
108
+ cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
109
+ }
101
110
  /** Check whether the auth mode allows anonymous sessions. */
102
- function allowsAnonymous(cfg) {
103
- const mode = cfg.publicChat?.auth ?? "anonymous";
111
+ function allowsAnonymous(cfg, accountId) {
112
+ const mode = resolveAccountAuthMode(cfg, accountId);
104
113
  return mode === "anonymous" || mode === "choice";
105
114
  }
106
115
  /** Check whether the auth mode allows OTP verification. */
107
- function allowsVerified(cfg) {
108
- const mode = cfg.publicChat?.auth ?? "anonymous";
116
+ function allowsVerified(cfg, accountId) {
117
+ const mode = resolveAccountAuthMode(cfg, accountId);
109
118
  return mode === "verified" || mode === "choice";
110
119
  }
111
120
  function writeSse(res, data) {
@@ -184,7 +193,7 @@ async function handleSession(req, res, accountId, cfg, maxBodyBytes) {
184
193
  sendMethodNotAllowed(res);
185
194
  return;
186
195
  }
187
- if (!allowsAnonymous(cfg)) {
196
+ if (!allowsAnonymous(cfg, accountId)) {
188
197
  sendForbidden(res, "anonymous sessions are disabled — use OTP verification");
189
198
  return;
190
199
  }
@@ -214,7 +223,7 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
214
223
  sendMethodNotAllowed(res);
215
224
  return;
216
225
  }
217
- if (!allowsVerified(cfg)) {
226
+ if (!allowsVerified(cfg, accountId)) {
218
227
  sendForbidden(res, "OTP verification is disabled for this account");
219
228
  return;
220
229
  }
@@ -229,7 +238,7 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
229
238
  ? payload.phone.trim()
230
239
  : "";
231
240
  const isEmailId = rawIdentifier.includes("@");
232
- const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
241
+ const verifyMethods = normalizeVerifyMethods(resolveAccountVerifyMethods(cfg, accountId));
233
242
  let identifier;
234
243
  if (isEmailId) {
235
244
  if (!verifyMethods.includes("email")) {
@@ -270,8 +279,9 @@ async function handleOtpRequest(req, res, accountId, cfg, maxBodyBytes) {
270
279
  const deliveryMethods = preferredChannel && verifyMethods.includes(preferredChannel)
271
280
  ? [preferredChannel]
272
281
  : verifyMethods;
282
+ const brandName = accountId || undefined;
273
283
  try {
274
- const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods);
284
+ const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods, brandName);
275
285
  sendJson(res, 200, { ok: true, channel: delivery.channel });
276
286
  }
277
287
  catch (err) {
@@ -287,7 +297,7 @@ async function handleOtpVerify(req, res, accountId, cfg, maxBodyBytes) {
287
297
  sendMethodNotAllowed(res);
288
298
  return;
289
299
  }
290
- if (!allowsVerified(cfg)) {
300
+ if (!allowsVerified(cfg, accountId)) {
291
301
  sendForbidden(res, "OTP verification is disabled for this account");
292
302
  return;
293
303
  }
@@ -755,14 +765,19 @@ async function handleCapabilities(req, res, accountId, cfg) {
755
765
  sendMethodNotAllowed(res, "GET");
756
766
  return;
757
767
  }
758
- const authMode = cfg.publicChat?.auth ?? "anonymous";
759
- const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
768
+ const authMode = resolveAccountAuthMode(cfg, accountId);
769
+ const verifyMethods = normalizeVerifyMethods(resolveAccountVerifyMethods(cfg, accountId));
760
770
  const agentId = resolvePublicAgentId(cfg, accountId);
761
771
  const whatsappAccountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined;
762
772
  const channels = detectOtpChannels(whatsappAccountId);
773
+ const wsBrand = cfg.workspaces?.[accountId]?.brand;
774
+ const accentColor = wsBrand?.accentColor ?? cfg.ui?.seamColor ?? null;
775
+ const backgroundColor = wsBrand?.backgroundColor ?? null;
763
776
  sendJson(res, 200, {
764
777
  auth: authMode,
765
778
  verify_methods: verifyMethods,
779
+ accent_color: accentColor,
780
+ background_color: backgroundColor,
766
781
  otp: {
767
782
  available: channels.length > 0,
768
783
  channels,
@@ -65,10 +65,12 @@ export function createChannelManager(opts) {
65
65
  ? plugin.config.isEnabled(account, cfg)
66
66
  : isAccountEnabled(account);
67
67
  if (!enabled) {
68
+ const disabledReason = plugin.config.disabledReason?.(account, cfg) ?? "disabled";
69
+ channelLogs[channelId].info?.(`[${id}] channel not starting: ${disabledReason}`);
68
70
  setRuntime(channelId, id, {
69
71
  accountId: id,
70
72
  running: false,
71
- lastError: plugin.config.disabledReason?.(account, cfg) ?? "disabled",
73
+ lastError: disabledReason,
72
74
  });
73
75
  return;
74
76
  }
@@ -77,10 +79,12 @@ export function createChannelManager(opts) {
77
79
  configured = await plugin.config.isConfigured(account, cfg);
78
80
  }
79
81
  if (!configured) {
82
+ const unconfiguredReason = plugin.config.unconfiguredReason?.(account, cfg) ?? "not configured";
83
+ channelLogs[channelId].warn?.(`[${id}] channel not starting: ${unconfiguredReason}`);
80
84
  setRuntime(channelId, id, {
81
85
  accountId: id,
82
86
  running: false,
83
- lastError: plugin.config.unconfiguredReason?.(account, cfg) ?? "not configured",
87
+ lastError: unconfiguredReason,
84
88
  });
85
89
  return;
86
90
  }
@@ -66,6 +66,14 @@ export const cronHandlers = {
66
66
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cron.update params: missing id"));
67
67
  return;
68
68
  }
69
+ if (p.accountId) {
70
+ const jobs = await context.cron.list({ includeDisabled: true });
71
+ const target = jobs.find((j) => j.id === jobId);
72
+ if (!target || target.accountId !== p.accountId) {
73
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `cron job not found: ${jobId}`));
74
+ return;
75
+ }
76
+ }
69
77
  const job = await context.cron.update(jobId, p.patch);
70
78
  respond(true, job, undefined);
71
79
  },
@@ -80,6 +88,14 @@ export const cronHandlers = {
80
88
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cron.remove params: missing id"));
81
89
  return;
82
90
  }
91
+ if (p.accountId) {
92
+ const jobs = await context.cron.list({ includeDisabled: true });
93
+ const target = jobs.find((j) => j.id === jobId);
94
+ if (!target || target.accountId !== p.accountId) {
95
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `cron job not found: ${jobId}`));
96
+ return;
97
+ }
98
+ }
83
99
  const result = await context.cron.remove(jobId);
84
100
  respond(true, result, undefined);
85
101
  },
@@ -94,6 +110,14 @@ export const cronHandlers = {
94
110
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cron.run params: missing id"));
95
111
  return;
96
112
  }
113
+ if (p.accountId) {
114
+ const jobs = await context.cron.list({ includeDisabled: true });
115
+ const target = jobs.find((j) => j.id === jobId);
116
+ if (!target || target.accountId !== p.accountId) {
117
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `cron job not found: ${jobId}`));
118
+ return;
119
+ }
120
+ }
97
121
  const result = await context.cron.run(jobId, p.mode);
98
122
  respond(true, result, undefined);
99
123
  },
@@ -108,6 +132,14 @@ export const cronHandlers = {
108
132
  respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cron.runs params: missing id"));
109
133
  return;
110
134
  }
135
+ if (p.accountId) {
136
+ const jobs = await context.cron.list({ includeDisabled: true });
137
+ const target = jobs.find((j) => j.id === jobId);
138
+ if (!target || target.accountId !== p.accountId) {
139
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `cron job not found: ${jobId}`));
140
+ return;
141
+ }
142
+ }
111
143
  const logPath = resolveCronRunLogPath({
112
144
  storePath: context.cronStorePath,
113
145
  jobId,
@@ -47,7 +47,8 @@ export const publicChatHandlers = {
47
47
  return;
48
48
  }
49
49
  const isEmailId = rawIdentifier.includes("@");
50
- const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
50
+ const verifyMethods = normalizeVerifyMethods(cfg.publicChat?.accountSettings?.[accountId ?? ""]?.verifyMethods ??
51
+ cfg.publicChat?.verifyMethods ?? ["whatsapp", "sms"]);
51
52
  let identifier;
52
53
  if (isEmailId) {
53
54
  if (!verifyMethods.includes("email")) {
@@ -87,8 +88,9 @@ export const publicChatHandlers = {
87
88
  const deliveryMethods = preferredChannel && verifyMethods.includes(preferredChannel)
88
89
  ? [preferredChannel]
89
90
  : verifyMethods;
91
+ const brandName = accountId || undefined;
90
92
  try {
91
- const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods);
93
+ const delivery = await deliverOtp(identifier, result.code, whatsappAccountId, deliveryMethods, brandName);
92
94
  respond(true, { ok: true, channel: delivery.channel });
93
95
  }
94
96
  catch (err) {
@@ -278,6 +278,15 @@ export const tailscaleHandlers = {
278
278
  catch (serveErr) {
279
279
  const errObj = serveErr;
280
280
  const combined = `${typeof errObj.stdout === "string" ? errObj.stdout : ""}\n${typeof errObj.stderr === "string" ? errObj.stderr : ""}`;
281
+ // Check for "Serve is not enabled" — extract the enable URL
282
+ if (combined.includes("Serve is not enabled") ||
283
+ combined.includes("not enabled on your tailnet")) {
284
+ const enableUrlMatch = combined.match(/https:\/\/login\.tailscale\.com\/f\/serve\S*/);
285
+ const enableUrl = enableUrlMatch?.[0] ?? "https://login.tailscale.com/admin/machines";
286
+ context.logGateway.warn(`tailscale.serve.enable: Serve not enabled on tailnet`);
287
+ respond(false, { enableUrl }, errorShape(ErrorCodes.INVALID_REQUEST, "Serve is not enabled on your Tailscale account. Open the link in the browser to enable it, then try again."));
288
+ return;
289
+ }
281
290
  context.logGateway.warn(`tailscale.serve.enable pre-flight failed: ${combined.slice(0, 500)}`);
282
291
  respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `Serve command failed — ${combined.trim().split("\n")[0] || "unknown error"}`));
283
292
  return;
@@ -12,6 +12,52 @@ import { normalizeAgentId } from "../../routing/session-key.js";
12
12
  import { resolveUserPath } from "../../utils.js";
13
13
  import { ErrorCodes, errorShape } from "../protocol/index.js";
14
14
  // ---------------------------------------------------------------------------
15
+ // Per-account agent tool sets
16
+ // ---------------------------------------------------------------------------
17
+ const ACCOUNT_ADMIN_TOOLS = {
18
+ allow: [
19
+ "skill_read",
20
+ "message",
21
+ "group:memory",
22
+ "group:documents",
23
+ "image",
24
+ "image_generate",
25
+ "web_search",
26
+ "web_fetch",
27
+ "current_time",
28
+ "sessions_list",
29
+ "sessions_history",
30
+ "session_status",
31
+ "cron",
32
+ "authorize_admin",
33
+ "revoke_admin",
34
+ "list_admins",
35
+ "bootstrap_complete",
36
+ "relay_message",
37
+ "browser",
38
+ "group:contacts",
39
+ "document_to_pdf",
40
+ "skill_draft_save",
41
+ "group:control-panel",
42
+ "api_keys",
43
+ "software_update",
44
+ ],
45
+ deny: ["exec", "group:fs", "group:runtime", "canvas"],
46
+ };
47
+ const ACCOUNT_PUBLIC_TOOLS = {
48
+ allow: [
49
+ "message",
50
+ "memory_search",
51
+ "memory_get",
52
+ "memory_write",
53
+ "memory_save_media",
54
+ "web_search",
55
+ "web_fetch",
56
+ "current_time",
57
+ ],
58
+ deny: ["exec", "group:fs", "group:runtime", "group:sessions", "group:automation", "browser"],
59
+ };
60
+ // ---------------------------------------------------------------------------
15
61
  // Helpers
16
62
  // ---------------------------------------------------------------------------
17
63
  function requireBaseHash(params, snapshot, respond) {
@@ -396,10 +442,16 @@ export const workspacesHandlers = {
396
442
  // Each agent's workspace points to its agent subdirectory, not the root.
397
443
  for (const agentName of scan.agents) {
398
444
  const agentId = normalizeAgentId(`${name}-${agentName}`);
445
+ const tools = agentName === "admin"
446
+ ? ACCOUNT_ADMIN_TOOLS
447
+ : agentName === "public"
448
+ ? ACCOUNT_PUBLIC_TOOLS
449
+ : undefined;
399
450
  cfg = applyAgentConfig(cfg, {
400
451
  agentId,
401
452
  name: `${name} ${agentName}`,
402
453
  workspace: path.join(workspaceDir, "agents", agentName),
454
+ tools,
403
455
  });
404
456
  agentIds.push(agentId);
405
457
  }
@@ -223,6 +223,7 @@ export async function monitorWebChannel(verbose, listenerFactory = monitorWebInb
223
223
  emitStatus();
224
224
  // Surface a concise connection event for the next main-session turn/heartbeat.
225
225
  const { e164: selfE164 } = readWebSelfId(account.authDir);
226
+ reconnectLogger.info({ accountId: account.accountId, selfE164: selfE164 ?? null, reconnectAttempts }, "WhatsApp connected");
226
227
  const connectRoute = resolveAgentRoute({
227
228
  cfg,
228
229
  channel: "whatsapp",
@@ -273,6 +274,8 @@ export async function monitorWebChannel(verbose, listenerFactory = monitorWebInb
273
274
  : null;
274
275
  const logData = {
275
276
  connectionId,
277
+ accountId: account.accountId,
278
+ selfE164: selfE164 ?? null,
276
279
  reconnectAttempts,
277
280
  messagesHandled: handledMessages,
278
281
  lastMessageAt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.12.0",
3
+ "version": "1.12.2",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tailscale
3
- description: Guide users through Tailscale remote access setup — installing the app, creating an account, and connecting their device.
3
+ description: Guide users through Tailscale remote access setup — installing the app, connecting their device, and enabling Serve for remote access to the control panel.
4
4
  metadata: {"taskmaster":{"emoji":"🔐"}}
5
5
  ---
6
6
 
@@ -13,10 +13,18 @@ Helps users set up remote access to their Taskmaster device using Tailscale. Tai
13
13
  - User asks about remote access, accessing their device from outside the local network, or connecting while away from home
14
14
  - User encounters issues during the Tailscale connection flow on the setup page
15
15
  - User sees a QR code on the setup page and doesn't know what to do
16
+ - User asks about enabling Serve or making the control panel accessible remotely
16
17
 
17
- ## Recommended path
18
+ ## Critical: what NOT to tell users
18
19
 
19
- The simplest setup path for non-technical users:
20
+ - **Never suggest CLI commands** — Taskmaster handles all Tailscale operations through the setup page. Users do not need to SSH into the Pi, open a terminal, or run any `tailscale` commands.
21
+ - **Never reference `tailscale serve`, `tailscale up`, `sudo`, or any shell commands** — the gateway runs these internally when the user clicks buttons in the UI.
22
+ - **Never suggest port numbers, `--bg` flags, or `--https` flags** — these are implementation details the user never sees.
23
+ - **Never suggest the Tailscale admin console as the primary path** — the setup page is the primary interface. The admin console link is only relevant when Serve or Funnel needs one-time tailnet-level enablement (the UI provides the link automatically).
24
+
25
+ ## Setup flow (from the setup page)
26
+
27
+ ### Step 1: Connect to Tailscale
20
28
 
21
29
  1. **Install the Tailscale app on your phone** — available on iOS App Store and Google Play Store. Search for "Tailscale" and install the official app.
22
30
  2. **Create a Tailscale account** — open the app and sign up. You can use Google, Microsoft, Apple, or email to create your account.
@@ -24,24 +32,44 @@ The simplest setup path for non-technical users:
24
32
  4. **Click "Connect" next to Remote Access** — a modal will appear showing progress while the connection starts.
25
33
  5. **Scan the QR code with your phone camera** — when the QR code appears, point your phone camera at it. This links your Taskmaster device to your Tailscale account.
26
34
  6. **Approve the device in the Tailscale app** — you may be asked to confirm the new device. Tap approve/allow.
27
- 7. **Done** — the modal will show a success message and close automatically. Your device is now accessible from anywhere you have Tailscale installed.
35
+ 7. **Done** — the modal will show a success message and close automatically.
36
+
37
+ ### Step 2: Enable Remote Access (Serve)
38
+
39
+ After connecting, the setup page shows Remote Access as "Connected (not enabled)." The next step:
40
+
41
+ 1. **Click the power button** next to Remote Access on the setup page.
42
+ 2. **If it succeeds** — remote access is now active. The setup page shows a green light and "Active."
43
+ 3. **If a link appears** — Serve must be enabled on your Tailscale account first. This is a one-time step. Click the link that appears (it opens the Tailscale admin page). Enable Serve, then come back and click the power button again.
44
+
45
+ Once enabled, remote access persists across restarts.
46
+
47
+ ### Step 3: Access from other devices
48
+
49
+ - Install Tailscale on any other device (laptop, tablet, phone) where you want to access the control panel.
50
+ - Sign in with the **same Tailscale account**.
51
+ - Your Taskmaster device will appear in the Tailscale app — access it using the Tailscale URL shown on the setup page.
52
+
53
+ ## Internet Access (Funnel) — optional
28
54
 
29
- ## After connecting
55
+ Funnel makes the control panel accessible to anyone on the internet, not just devices on your Tailscale network. This is optional and most users don't need it.
30
56
 
31
- - Install Tailscale on any other device (laptop, tablet) where you want to access the control panel
32
- - Sign in with the same Tailscale account
33
- - Your Taskmaster device will appear in the Tailscale network — access it using the Tailscale URL shown on the setup page
57
+ - After Remote Access is active, a globe icon appears on the setup page.
58
+ - Clicking it enables Funnel. If Funnel isn't enabled on your tailnet, a link appears — same one-click flow as Serve.
34
59
 
35
60
  ## Troubleshooting
36
61
 
37
62
  - **QR code doesn't appear**: The Tailscale service may not be running on the device. Check the setup page for error details. Restarting the gateway may help.
38
63
  - **Authentication times out**: The QR code is valid for a limited time. Click "Connect" again to generate a new one.
39
64
  - **"Tailscale not installed" message**: Tailscale needs to be installed on the Taskmaster device itself (the server). This is separate from installing it on your phone.
65
+ - **"Serve must be enabled" with a link**: Click the link to open Tailscale's admin page. Enable Serve for this device, then retry from the setup page.
40
66
  - **Connection drops**: Make sure both devices (phone/laptop and Taskmaster) have Tailscale running and are signed into the same account.
67
+ - **Remote Access shows "Enabled but not active"**: Tailscale may have disconnected. Try disabling and re-enabling from the setup page, or check that the device is still connected in the Tailscale app.
41
68
 
42
69
  ## Key concepts
43
70
 
44
71
  - **Tailscale** is a mesh VPN — devices connect directly to each other through encrypted tunnels
45
72
  - **Free for personal use** — up to 100 devices on the free plan
46
73
  - **No port forwarding needed** — works through firewalls and NAT automatically
47
- - **Internet Access** (Funnel) — an optional feature that gives your device a public URL reachable by anyone, not just Tailscale users. Enable this from the setup page after remote access is connected.
74
+ - **Serve** (Remote Access) — makes the control panel accessible over HTTPS to devices on the same Tailscale account. Private to your tailnet.
75
+ - **Funnel** (Internet Access) — makes the control panel accessible to anyone with the URL. Public.
@@ -0,0 +1,25 @@
1
+ # AGENTS.md — Admin Agent (Beagle Corporate Admin)
2
+
3
+ You help the operator manage the Beagle corporate site. You handle inbound interest, knowledge base maintenance, and operational oversight of the public-facing agent.
4
+
5
+ ## First-Run Check
6
+
7
+ **If BOOTSTRAP.md is present in your context, STOP and follow its instructions.** BOOTSTRAP.md means this workspace hasn't been set up yet. Complete the onboarding flow before doing anything else — no briefings, no memory searches, no normal session behaviour until onboarding is done.
8
+
9
+ ## Every Session
10
+
11
+ Before doing anything else:
12
+ 1. Read `SOUL.md` — this is who you are
13
+ 2. Read `IDENTITY.md` — your role and boundaries
14
+ 3. Check memory for recent inbound interest and any pending follow-ups
15
+
16
+ ## Tools
17
+
18
+ | Tool | Use |
19
+ |------|-----|
20
+ | `memory_search` | Find stored interest, enquiries, knowledge base content |
21
+ | `memory_get` | Read specific files |
22
+ | `memory_write` | Update knowledge base, store notes, flag follow-ups |
23
+ | `sessions_list` | Review recent conversations the public agent has had |
24
+ | `sessions_history` | Read specific past sessions for context |
25
+ | `current_time` | Timestamps for notes and follow-up scheduling |