@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.
- package/dist/agents/taskmaster-tools.js +5 -3
- package/dist/agents/tool-policy.js +1 -1
- package/dist/agents/tools/authorize-admin-tool.js +58 -17
- package/dist/agents/tools/bootstrap-tool.js +51 -0
- package/dist/agents/tools/cron-tool.js +20 -7
- package/dist/build-info.json +3 -3
- package/dist/commands/agents.config.js +1 -0
- package/dist/config/zod-schema.js +18 -0
- package/dist/control-ui/assets/{index-DpMaqt-b.js → index-CP9IoaZp.js} +110 -97
- package/dist/control-ui/assets/index-CP9IoaZp.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/control-ui.js +24 -9
- package/dist/gateway/protocol/schema/cron.js +8 -0
- package/dist/gateway/public-chat/deliver-email.js +5 -5
- package/dist/gateway/public-chat/deliver-otp.js +3 -3
- package/dist/gateway/public-chat/deliver-sms.js +5 -1
- package/dist/gateway/public-chat-api.js +26 -11
- package/dist/gateway/server-channels.js +6 -2
- package/dist/gateway/server-methods/cron.js +32 -0
- package/dist/gateway/server-methods/public-chat.js +4 -2
- package/dist/gateway/server-methods/tailscale.js +9 -0
- package/dist/gateway/server-methods/workspaces.js +52 -0
- package/dist/web/auto-reply/monitor.js +3 -0
- package/package.json +1 -1
- package/skills/tailscale/SKILL.md +37 -9
- package/templates/beagle-taxi/agents/admin/AGENTS.md +25 -0
- package/templates/beagle-taxi/agents/admin/IDENTITY.md +9 -0
- package/templates/beagle-taxi/agents/admin/SOUL.md +31 -0
- package/templates/beagle-taxi/agents/public/AGENTS.md +54 -0
- package/templates/beagle-taxi/agents/public/IDENTITY.md +10 -0
- package/templates/beagle-taxi/agents/public/SOUL.md +32 -0
- package/templates/beagle-taxi/memory/public/knowledge-base.md +177 -0
- package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +44 -0
- package/templates/customer/agents/admin/BOOTSTRAP.md +2 -2
- package/templates/education-hero/agents/admin/BOOTSTRAP.md +2 -2
- package/templates/maxy/agents/admin/BOOTSTRAP.md +2 -3
- package/templates/taskmaster/agents/admin/BOOTSTRAP.md +2 -2
- package/templates/tradesupport/agents/admin/BOOTSTRAP.md +2 -2
- 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-
|
|
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
|
|
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:"
|
|
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.
|
|
601
|
+
if(opts&&opts.color) cfg.btnColor=opts.color;
|
|
595
602
|
if(opts&&opts.bgColor) cfg.bgColor=opts.bgColor;
|
|
596
|
-
|
|
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.
|
|
604
|
-
"box-shadow:0 2px 8px rgba(0,0,0,.3);z-index:999999;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
759
|
-
const verifyMethods = normalizeVerifyMethods(cfg
|
|
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:
|
|
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:
|
|
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?.
|
|
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: tailscale
|
|
3
|
-
description: Guide users through Tailscale remote access setup — installing the app,
|
|
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
|
-
##
|
|
18
|
+
## Critical: what NOT to tell users
|
|
18
19
|
|
|
19
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
-
|
|
32
|
-
-
|
|
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
|
-
- **
|
|
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 |
|