@rubytech/taskmaster 1.0.68 → 1.0.70
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/memory-search.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.agent-runtime.js +3 -1
- package/dist/control-ui/assets/{index-Tpr1NFEw.js → index-lEyZ7hzB.js} +550 -550
- package/dist/control-ui/assets/index-lEyZ7hzB.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/control-ui.js +31 -11
- package/dist/gateway/public-chat/session.js +6 -9
- package/dist/gateway/server/ws-connection/message-handler.js +1 -0
- package/dist/gateway/server-methods/chat.js +3 -6
- package/dist/gateway/server-methods/public-chat.js +22 -4
- package/dist/memory/embeddings.js +2 -4
- package/dist/memory/manager.js +5 -1
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +2 -2
- package/dist/control-ui/assets/index-Tpr1NFEw.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-lEyZ7hzB.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-BCh3mx9Z.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -387,11 +387,16 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
387
387
|
res.end("Method Not Allowed");
|
|
388
388
|
return true;
|
|
389
389
|
}
|
|
390
|
-
// Only /public/chat (the SPA route)
|
|
391
|
-
if (
|
|
390
|
+
// Only /public/chat/:accountId (the SPA route)
|
|
391
|
+
if (!pathname.startsWith("/public/chat/")) {
|
|
392
392
|
// /public/widget.js is handled separately
|
|
393
393
|
if (pathname === "/public/widget.js")
|
|
394
394
|
return false;
|
|
395
|
+
// Bare /public/chat without accountId → 404
|
|
396
|
+
if (pathname === "/public/chat") {
|
|
397
|
+
respondNotFound(res);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
395
400
|
respondNotFound(res);
|
|
396
401
|
return true;
|
|
397
402
|
}
|
|
@@ -400,6 +405,12 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
400
405
|
respondNotFound(res);
|
|
401
406
|
return true;
|
|
402
407
|
}
|
|
408
|
+
// Extract accountId from /public/chat/:accountId
|
|
409
|
+
const accountId = pathname.slice("/public/chat/".length).split("/")[0]?.trim();
|
|
410
|
+
if (!accountId || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(accountId)) {
|
|
411
|
+
respondNotFound(res);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
403
414
|
const root = resolveControlUiRoot();
|
|
404
415
|
if (!root) {
|
|
405
416
|
res.statusCode = 503;
|
|
@@ -412,7 +423,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
412
423
|
respondNotFound(res);
|
|
413
424
|
return true;
|
|
414
425
|
}
|
|
415
|
-
const publicAgentId = resolvePublicAgentId(config);
|
|
426
|
+
const publicAgentId = resolvePublicAgentId(config, accountId);
|
|
416
427
|
const identity = resolveAssistantIdentity({ cfg: config, agentId: publicAgentId });
|
|
417
428
|
// Only inject avatar if it resolves to an actual image URL/path (not a
|
|
418
429
|
// single-letter fallback like "D" which would render as a broken <img>).
|
|
@@ -421,7 +432,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
421
432
|
agentId: publicAgentId,
|
|
422
433
|
basePath: "",
|
|
423
434
|
});
|
|
424
|
-
const avatarValue = resolvedAvatar &&
|
|
435
|
+
const avatarValue = resolvedAvatar && /^(https?:\/\/|data:image\/|\/)/i.test(resolvedAvatar)
|
|
425
436
|
? resolvedAvatar
|
|
426
437
|
: undefined;
|
|
427
438
|
const authMode = config.publicChat.auth ?? "anonymous";
|
|
@@ -438,15 +449,23 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
438
449
|
brandIconUrl,
|
|
439
450
|
accentColor,
|
|
440
451
|
});
|
|
441
|
-
// Inject
|
|
452
|
+
// Inject <base href="/"> right after <head> so relative asset paths (./assets/...)
|
|
453
|
+
// resolve from root. The URL is /public/chat/:accountId — 3 levels deep, so without
|
|
454
|
+
// <base> the browser would look for /public/chat/assets/... which doesn't exist.
|
|
455
|
+
// The <base> tag MUST appear before any tags that use relative URLs.
|
|
456
|
+
const headOpen = injected.indexOf("<head>");
|
|
457
|
+
const baseInjected = headOpen !== -1
|
|
458
|
+
? `${injected.slice(0, headOpen + 6)}<base href="/">${injected.slice(headOpen + 6)}`
|
|
459
|
+
: injected;
|
|
460
|
+
// Inject public-chat globals before </head>
|
|
442
461
|
const publicScript = `<script>` +
|
|
443
462
|
`window.__TASKMASTER_PUBLIC_CHAT__=true;` +
|
|
444
|
-
`window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ auth: authMode, cookieTtlDays })};` +
|
|
463
|
+
`window.__TASKMASTER_PUBLIC_CHAT_CONFIG__=${JSON.stringify({ accountId, auth: authMode, cookieTtlDays })};` +
|
|
445
464
|
`</script>`;
|
|
446
|
-
const headClose =
|
|
465
|
+
const headClose = baseInjected.indexOf("</head>");
|
|
447
466
|
const withPublic = headClose !== -1
|
|
448
|
-
? `${
|
|
449
|
-
: `${publicScript}${
|
|
467
|
+
? `${baseInjected.slice(0, headClose)}${publicScript}${baseInjected.slice(headClose)}`
|
|
468
|
+
: `${publicScript}${baseInjected}`;
|
|
450
469
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
451
470
|
res.setHeader("Cache-Control", "no-cache");
|
|
452
471
|
res.end(withPublic);
|
|
@@ -455,12 +474,13 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
455
474
|
/** Widget script content — self-contained JS for embedding. */
|
|
456
475
|
const WIDGET_SCRIPT = `(function(){
|
|
457
476
|
"use strict";
|
|
458
|
-
var cfg={server:""};
|
|
477
|
+
var cfg={server:"",accountId:""};
|
|
459
478
|
var isOpen=false;
|
|
460
479
|
var btn,overlay,iframe;
|
|
461
480
|
|
|
462
481
|
function init(opts){
|
|
463
482
|
if(opts&&opts.server) cfg.server=opts.server.replace(/\\/$/,"");
|
|
483
|
+
if(opts&&opts.accountId) cfg.accountId=opts.accountId;
|
|
464
484
|
build();
|
|
465
485
|
}
|
|
466
486
|
|
|
@@ -495,7 +515,7 @@ const WIDGET_SCRIPT = `(function(){
|
|
|
495
515
|
overlay.className="tm-widget-overlay";
|
|
496
516
|
iframe=document.createElement("iframe");
|
|
497
517
|
iframe.className="tm-widget-iframe";
|
|
498
|
-
iframe.src=cfg.server+"/public/chat";
|
|
518
|
+
iframe.src=cfg.server+"/public/chat/"+encodeURIComponent(cfg.accountId);
|
|
499
519
|
overlay.appendChild(iframe);
|
|
500
520
|
document.body.appendChild(overlay);
|
|
501
521
|
}
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve public-chat session keys (anonymous and verified).
|
|
3
3
|
*
|
|
4
|
-
* The public agent is the agent handling WhatsApp DMs for
|
|
4
|
+
* The public agent is the agent handling unknown WhatsApp DMs for a given account.
|
|
5
|
+
* The account is determined by the URL path: /public/chat/:accountId.
|
|
5
6
|
* Anonymous sessions use a cookie-based identifier; verified sessions use the
|
|
6
7
|
* phone number so they share the same DM session as WhatsApp.
|
|
7
8
|
*/
|
|
8
|
-
import { listBoundAccountIds } from "../../routing/bindings.js";
|
|
9
9
|
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
|
10
10
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
|
11
11
|
/**
|
|
12
|
-
* Find the agent that handles public-facing WhatsApp DMs.
|
|
12
|
+
* Find the agent that handles public-facing WhatsApp DMs for the given account.
|
|
13
13
|
*
|
|
14
14
|
* Uses the same routing logic as WhatsApp itself: calls resolveAgentRoute
|
|
15
|
-
* with a synthetic unknown-peer DM
|
|
16
|
-
*
|
|
17
|
-
* unknown WhatsApp DMs.
|
|
15
|
+
* with a synthetic unknown-peer DM. This guarantees the public chat routes
|
|
16
|
+
* to the exact same agent that handles unknown WhatsApp DMs for that account.
|
|
18
17
|
*/
|
|
19
|
-
export function resolvePublicAgentId(cfg) {
|
|
20
|
-
const accountIds = listBoundAccountIds(cfg, "whatsapp");
|
|
21
|
-
const accountId = accountIds[0] ?? "default";
|
|
18
|
+
export function resolvePublicAgentId(cfg, accountId) {
|
|
22
19
|
const route = resolveAgentRoute({
|
|
23
20
|
cfg,
|
|
24
21
|
channel: "whatsapp",
|
|
@@ -660,6 +660,7 @@ export function attachGatewayWsMessageHandler(params) {
|
|
|
660
660
|
version: process.env.TASKMASTER_VERSION ?? process.env.npm_package_version ?? "dev",
|
|
661
661
|
commit: process.env.GIT_COMMIT,
|
|
662
662
|
host: os.hostname(),
|
|
663
|
+
platform: os.platform(),
|
|
663
664
|
connId,
|
|
664
665
|
},
|
|
665
666
|
features: { methods: gatewayMethods, events },
|
|
@@ -11,9 +11,7 @@ import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.j
|
|
|
11
11
|
import { extractShortModelName, } from "../../auto-reply/reply/response-prefix-template.js";
|
|
12
12
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
|
13
13
|
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
|
14
|
-
import { loadConfig as loadConfigFn } from "../../config/config.js";
|
|
15
14
|
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
|
16
|
-
import { resolvePublicAgentId } from "../public-chat/session.js";
|
|
17
15
|
import { abortChatRunById, abortChatRunsForSessionKey, isChatStopCommandText, resolveChatRunExpiresAtMs, } from "../chat-abort.js";
|
|
18
16
|
import { ErrorCodes, errorShape, formatValidationErrors, validateChatAbortParams, validateChatHistoryParams, validateChatInjectParams, validateChatSendParams, } from "../protocol/index.js";
|
|
19
17
|
import { loadSessionEntry, readSessionMessages, resolveSessionModelRef } from "../session-utils.js";
|
|
@@ -127,10 +125,9 @@ function broadcastChatError(params) {
|
|
|
127
125
|
function validatePublicSessionAccess(role, sessionKey) {
|
|
128
126
|
if (role !== "public")
|
|
129
127
|
return null;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!sessionKey.startsWith(prefix)) {
|
|
128
|
+
// Public clients can only access agent DM sessions (issued by public.session / public.otp.verify).
|
|
129
|
+
// Format: agent:{agentId}:dm:{identifier}
|
|
130
|
+
if (!/^agent:[^:]+:dm:/.test(sessionKey)) {
|
|
134
131
|
return errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized session key");
|
|
135
132
|
}
|
|
136
133
|
return null;
|
|
@@ -10,6 +10,14 @@ import { buildPublicSessionKey, resolvePublicAgentId } from "../public-chat/sess
|
|
|
10
10
|
function isValidPhone(phone) {
|
|
11
11
|
return /^\+\d{7,15}$/.test(phone);
|
|
12
12
|
}
|
|
13
|
+
function validateAccountId(raw) {
|
|
14
|
+
if (typeof raw !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
if (!trimmed || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed))
|
|
18
|
+
return null;
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
13
21
|
export const publicChatHandlers = {
|
|
14
22
|
/**
|
|
15
23
|
* Request an OTP code — sends a 6-digit code to the given phone via WhatsApp.
|
|
@@ -43,12 +51,13 @@ export const publicChatHandlers = {
|
|
|
43
51
|
},
|
|
44
52
|
/**
|
|
45
53
|
* Verify an OTP code and return the session key.
|
|
46
|
-
* Params: { phone: string, code: string, name?: string }
|
|
54
|
+
* Params: { phone: string, code: string, accountId: string, name?: string }
|
|
47
55
|
*/
|
|
48
56
|
"public.otp.verify": async ({ params, respond }) => {
|
|
49
57
|
const phone = typeof params.phone === "string" ? params.phone.trim() : "";
|
|
50
58
|
const code = typeof params.code === "string" ? params.code.trim() : "";
|
|
51
59
|
const name = typeof params.name === "string" ? params.name.trim() : undefined;
|
|
60
|
+
const accountId = validateAccountId(params.accountId);
|
|
52
61
|
if (!phone || !isValidPhone(phone)) {
|
|
53
62
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid phone number"));
|
|
54
63
|
return;
|
|
@@ -57,6 +66,10 @@ export const publicChatHandlers = {
|
|
|
57
66
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "code required"));
|
|
58
67
|
return;
|
|
59
68
|
}
|
|
69
|
+
if (!accountId) {
|
|
70
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "accountId required"));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
60
73
|
const cfg = loadConfig();
|
|
61
74
|
if (!cfg.publicChat?.enabled) {
|
|
62
75
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
@@ -73,7 +86,7 @@ export const publicChatHandlers = {
|
|
|
73
86
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, messages[result.error] ?? "verification failed"));
|
|
74
87
|
return;
|
|
75
88
|
}
|
|
76
|
-
const agentId = resolvePublicAgentId(cfg);
|
|
89
|
+
const agentId = resolvePublicAgentId(cfg, accountId);
|
|
77
90
|
const sessionKey = buildPublicSessionKey(agentId, phone);
|
|
78
91
|
respond(true, {
|
|
79
92
|
ok: true,
|
|
@@ -85,20 +98,25 @@ export const publicChatHandlers = {
|
|
|
85
98
|
},
|
|
86
99
|
/**
|
|
87
100
|
* Resolve a session key for anonymous public chat.
|
|
88
|
-
* Params: { cookieId: string }
|
|
101
|
+
* Params: { cookieId: string, accountId: string }
|
|
89
102
|
*/
|
|
90
103
|
"public.session": async ({ params, respond }) => {
|
|
91
104
|
const cookieId = typeof params.cookieId === "string" ? params.cookieId.trim() : "";
|
|
105
|
+
const accountId = validateAccountId(params.accountId);
|
|
92
106
|
if (!cookieId || cookieId.length < 8 || cookieId.length > 128) {
|
|
93
107
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid cookieId"));
|
|
94
108
|
return;
|
|
95
109
|
}
|
|
110
|
+
if (!accountId) {
|
|
111
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "accountId required"));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
96
114
|
const cfg = loadConfig();
|
|
97
115
|
if (!cfg.publicChat?.enabled) {
|
|
98
116
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "public chat disabled"));
|
|
99
117
|
return;
|
|
100
118
|
}
|
|
101
|
-
const agentId = resolvePublicAgentId(cfg);
|
|
119
|
+
const agentId = resolvePublicAgentId(cfg, accountId);
|
|
102
120
|
const identifier = `anon-${cookieId}`;
|
|
103
121
|
const sessionKey = buildPublicSessionKey(agentId, identifier);
|
|
104
122
|
respond(true, {
|
|
@@ -104,7 +104,7 @@ async function createLocalEmbeddingProvider(options) {
|
|
|
104
104
|
else {
|
|
105
105
|
const selected = selectDefaultLocalModel();
|
|
106
106
|
modelPath = selected.model;
|
|
107
|
-
log.info(`selected tier ${selected.label} (system RAM: ${(os.totalmem() /
|
|
107
|
+
log.info(`selected tier ${selected.label} (system RAM: ${(os.totalmem() / 1024 ** 3).toFixed(1)} GB)`);
|
|
108
108
|
}
|
|
109
109
|
const modelCacheDir = options.local?.modelCacheDir?.trim();
|
|
110
110
|
// Lazy-load node-llama-cpp to keep startup light unless local is enabled.
|
|
@@ -213,9 +213,7 @@ export async function createEmbeddingProvider(options) {
|
|
|
213
213
|
catch (err) {
|
|
214
214
|
errors.push(formatLocalSetupError(err));
|
|
215
215
|
}
|
|
216
|
-
throw new Error(errors.length > 0
|
|
217
|
-
? errors.join("\n\n")
|
|
218
|
-
: "No embeddings provider available.");
|
|
216
|
+
throw new Error(errors.length > 0 ? errors.join("\n\n") : "No embeddings provider available.");
|
|
219
217
|
}
|
|
220
218
|
try {
|
|
221
219
|
const primary = await createProvider(requestedProvider);
|
package/dist/memory/manager.js
CHANGED
|
@@ -515,7 +515,11 @@ export class MemoryIndexManager {
|
|
|
515
515
|
const wrappedParams = {
|
|
516
516
|
...params,
|
|
517
517
|
progress: (update) => {
|
|
518
|
-
this.syncProgress = {
|
|
518
|
+
this.syncProgress = {
|
|
519
|
+
completed: update.completed,
|
|
520
|
+
total: update.total,
|
|
521
|
+
label: update.label,
|
|
522
|
+
};
|
|
519
523
|
outerProgress?.(update);
|
|
520
524
|
},
|
|
521
525
|
};
|
package/package.json
CHANGED
|
@@ -514,7 +514,7 @@ You get 1,000 free search credits per month — more than enough for daily busin
|
|
|
514
514
|
4. **Copy the key** (it starts with `AIza`)
|
|
515
515
|
5. Go to your Taskmaster **Setup** page, click the **API Keys** row, find **Google**, paste the key, and click **Save**
|
|
516
516
|
|
|
517
|
-
The free tier
|
|
517
|
+
The free tier works for low-volume use (a handful of voice notes or images per day). For higher volumes, you'll need to enable billing in your Google Cloud account — see [Google AI pricing](https://ai.google.dev/pricing) for details.
|
|
518
518
|
|
|
519
519
|
---
|
|
520
520
|
|
|
@@ -533,7 +533,7 @@ All files are markdown (`.md`) — plain text with simple formatting. You can up
|
|
|
533
533
|
|
|
534
534
|
When you add or change a file, your assistant picks it up automatically — no restart needed. The status light on the Files page turns red when files have changed since the last index, so you can see at a glance whether a re-index is needed.
|
|
535
535
|
|
|
536
|
-
After a fresh install or upgrade, the embedding model downloads automatically (~
|
|
536
|
+
After a fresh install or upgrade, the embedding model downloads automatically (~330 MB). During the download, a full-screen overlay blocks all navigation with a "Downloading embedding model" message — this is a one-time download and typically takes a few minutes. Memory search is unavailable until the download completes.
|
|
537
537
|
|
|
538
538
|
When your assistant writes to **public/** or **shared/**, a shield icon appears in the navigation bar so you can review what was written (see [Data Safety Alert](#data-safety-alert) above).
|
|
539
539
|
|