@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.
@@ -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-Tpr1NFEw.js"></script>
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 (pathname !== "/public/chat" && !pathname.startsWith("/public/chat/")) {
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 && (/^(https?:\/\/|data:image\/|\/)/i.test(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 public-chat globals
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 = injected.indexOf("</head>");
465
+ const headClose = baseInjected.indexOf("</head>");
447
466
  const withPublic = headClose !== -1
448
- ? `${injected.slice(0, headClose)}${publicScript}${injected.slice(headClose)}`
449
- : `${publicScript}${injected}`;
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 the default account.
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 on the first WhatsApp account. This
16
- * guarantees the public chat routes to the exact same agent that handles
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
- const cfg = loadConfigFn();
131
- const publicAgentId = resolvePublicAgentId(cfg);
132
- const prefix = `agent:${publicAgentId}:dm:`;
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() / (1024 ** 3)).toFixed(1)} GB)`);
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);
@@ -515,7 +515,11 @@ export class MemoryIndexManager {
515
515
  const wrappedParams = {
516
516
  ...params,
517
517
  progress: (update) => {
518
- this.syncProgress = { completed: update.completed, total: update.total };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.68",
3
+ "version": "1.0.70",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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 is generous no credit card required and sufficient for small business use.
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 (~640 MB for most devices, larger on devices with more RAM). 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.
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