@rubytech/taskmaster 1.29.2 → 1.30.1

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 (52) hide show
  1. package/dist/agents/model-fallback.js +9 -9
  2. package/dist/agents/subagent-announce.js +10 -2
  3. package/dist/agents/tools/cron-tool.js +10 -3
  4. package/dist/agents/tools/skill-read-tool.js +18 -1
  5. package/dist/agents/workspace-migrations.js +48 -0
  6. package/dist/build-info.json +3 -3
  7. package/dist/control-ui/assets/{index-C-AxOsfW.js → index-CkGA_2Sq.js} +494 -423
  8. package/dist/control-ui/assets/index-CkGA_2Sq.js.map +1 -0
  9. package/dist/control-ui/assets/{index-BwiQeiEa.css → index-CxVLKv6r.css} +1 -1
  10. package/dist/control-ui/index.html +2 -2
  11. package/dist/cron/normalize.js +8 -4
  12. package/dist/cron/parse.js +48 -7
  13. package/dist/cron/service/jobs.js +4 -1
  14. package/dist/cron/service/ops.js +16 -0
  15. package/dist/gateway/server-methods/whatsapp-conversations.js +41 -7
  16. package/dist/web/auto-reply/monitor/group-activation.js +1 -1
  17. package/dist/web/inbound/monitor.js +34 -3
  18. package/extensions/.npmignore +1 -0
  19. package/package.json +52 -62
  20. package/scripts/install.sh +0 -0
  21. package/skills/event-management/references/events.md +16 -6
  22. package/skills/whatsapp-business/SKILL.md +47 -0
  23. package/skills/whatsapp-business/references/setup-guide.md +258 -0
  24. package/templates/.DS_Store +0 -0
  25. package/templates/beagle-taxi/.DS_Store +0 -0
  26. package/templates/beagle-taxi/agents/.DS_Store +0 -0
  27. package/templates/beagle-taxi/memory/.DS_Store +0 -0
  28. package/templates/beagle-taxi/skills/.DS_Store +0 -0
  29. package/templates/beagle-zanzibar/.DS_Store +0 -0
  30. package/templates/beagle-zanzibar/agents/.DS_Store +0 -0
  31. package/templates/beagle-zanzibar/memory/.DS_Store +0 -0
  32. package/templates/beagle-zanzibar/skills/.DS_Store +0 -0
  33. package/templates/customer/.DS_Store +0 -0
  34. package/templates/customer/agents/.DS_Store +0 -0
  35. package/templates/education-hero/.DS_Store +0 -0
  36. package/templates/education-hero/agents/.DS_Store +0 -0
  37. package/templates/education-hero/agents/admin/.DS_Store +0 -0
  38. package/templates/education-hero/skills/.DS_Store +0 -0
  39. package/templates/education-hero/skills/education-hero/.DS_Store +0 -0
  40. package/templates/maxy/.DS_Store +0 -0
  41. package/templates/maxy/.gitignore +1 -0
  42. package/templates/maxy/agents/.DS_Store +0 -0
  43. package/templates/maxy/agents/admin/.DS_Store +0 -0
  44. package/templates/maxy/memory/.DS_Store +0 -0
  45. package/templates/maxy/skills/.DS_Store +0 -0
  46. package/templates/real-agent/.DS_Store +0 -0
  47. package/templates/real-agent/skills/.DS_Store +0 -0
  48. package/templates/taskmaster/.DS_Store +0 -0
  49. package/templates/taskmaster/.gitignore +1 -0
  50. package/templates/taskmaster/agents/public/AGENTS.md +16 -0
  51. package/templates/taskmaster/skills/.DS_Store +0 -0
  52. package/dist/control-ui/assets/index-C-AxOsfW.js.map +0 -1
@@ -6,8 +6,8 @@
6
6
  <title>Taskmaster Control</title>
7
7
  <meta name="color-scheme" content="dark light" />
8
8
  <link rel="icon" type="image/png" href="./favicon.png" />
9
- <script type="module" crossorigin src="./assets/index-C-AxOsfW.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-BwiQeiEa.css">
9
+ <script type="module" crossorigin src="./assets/index-CkGA_2Sq.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-CxVLKv6r.css">
11
11
  </head>
12
12
  <body>
13
13
  <taskmaster-app></taskmaster-app>
@@ -7,15 +7,19 @@ const DEFAULT_OPTIONS = {
7
7
  function isRecord(value) {
8
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
9
  }
10
- function coerceSchedule(schedule) {
10
+ function coerceSchedule(schedule, opts) {
11
11
  const next = { ...schedule };
12
12
  const kind = typeof schedule.kind === "string" ? schedule.kind : undefined;
13
13
  const atMsRaw = schedule.atMs;
14
14
  const atRaw = schedule.at;
15
+ // When the agent passes a timezone-naive ISO string (e.g. "2026-03-11T09:00:00"),
16
+ // interpret it in the user's configured timezone — not UTC. A schedule-level
17
+ // `tz` field takes priority over the account default.
18
+ const tz = (typeof schedule.tz === "string" ? schedule.tz.trim() : undefined) || opts?.userTimezone;
15
19
  const parsedAtMs = typeof atMsRaw === "string"
16
- ? parseAbsoluteTimeMs(atMsRaw)
20
+ ? parseAbsoluteTimeMs(atMsRaw, tz)
17
21
  : typeof atRaw === "string"
18
- ? parseAbsoluteTimeMs(atRaw)
22
+ ? parseAbsoluteTimeMs(atRaw, tz)
19
23
  : null;
20
24
  if (!kind) {
21
25
  if (typeof schedule.atMs === "number" ||
@@ -92,7 +96,7 @@ export function normalizeCronJobInput(raw, options = DEFAULT_OPTIONS) {
92
96
  }
93
97
  }
94
98
  if (isRecord(base.schedule)) {
95
- next.schedule = coerceSchedule(base.schedule);
99
+ next.schedule = coerceSchedule(base.schedule, { userTimezone: options.userTimezone });
96
100
  }
97
101
  if (isRecord(base.payload)) {
98
102
  next.payload = coercePayload(base.payload);
@@ -1,16 +1,57 @@
1
1
  const ISO_TZ_RE = /(Z|[+-]\d{2}:?\d{2})$/i;
2
2
  const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
3
3
  const ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T/;
4
- function normalizeUtcIso(raw) {
4
+ /**
5
+ * Resolve the UTC offset string (e.g. "+01:00") for a given IANA timezone at
6
+ * a specific instant. Returns undefined if the timezone is invalid.
7
+ */
8
+ function offsetForTimezone(tz, date) {
9
+ try {
10
+ // Format in en-US with timeZoneName to extract the UTC offset.
11
+ const parts = new Intl.DateTimeFormat("en-US", {
12
+ timeZone: tz,
13
+ timeZoneName: "longOffset",
14
+ }).formatToParts(date);
15
+ const tzPart = parts.find((p) => p.type === "timeZoneName")?.value ?? "";
16
+ // Intl returns "GMT" for UTC, "GMT+1:00", "GMT-5:00", etc.
17
+ if (tzPart === "GMT")
18
+ return "+00:00";
19
+ const match = tzPart.match(/GMT([+-]\d{1,2}):?(\d{2})?/);
20
+ if (!match)
21
+ return undefined;
22
+ const hours = match[1].padStart(3, match[1][0] === "-" ? "-" : "+");
23
+ const minutes = match[2] ?? "00";
24
+ return `${hours}:${minutes}`;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ function normalizeIso(raw, tz) {
5
31
  if (ISO_TZ_RE.test(raw))
6
32
  return raw;
7
- if (ISO_DATE_RE.test(raw))
8
- return `${raw}T00:00:00Z`;
9
- if (ISO_DATE_TIME_RE.test(raw))
10
- return `${raw}Z`;
33
+ if (ISO_DATE_RE.test(raw)) {
34
+ const suffix = tz ? resolveOffsetSuffix(`${raw}T00:00:00`, tz) : "Z";
35
+ return `${raw}T00:00:00${suffix}`;
36
+ }
37
+ if (ISO_DATE_TIME_RE.test(raw)) {
38
+ const suffix = tz ? resolveOffsetSuffix(raw, tz) : "Z";
39
+ return `${raw}${suffix}`;
40
+ }
11
41
  return raw;
12
42
  }
13
- export function parseAbsoluteTimeMs(input) {
43
+ /**
44
+ * Compute the UTC offset suffix for a timezone-naive ISO string interpreted in
45
+ * the given IANA timezone. We do a two-pass approach: first parse as UTC to
46
+ * get a rough Date, then look up the real offset for that instant.
47
+ */
48
+ function resolveOffsetSuffix(naiveIso, tz) {
49
+ const rough = new Date(`${naiveIso}Z`);
50
+ if (Number.isNaN(rough.getTime()))
51
+ return "Z";
52
+ return offsetForTimezone(tz, rough) ?? "Z";
53
+ }
54
+ export function parseAbsoluteTimeMs(input, tz) {
14
55
  const raw = input.trim();
15
56
  if (!raw)
16
57
  return null;
@@ -19,6 +60,6 @@ export function parseAbsoluteTimeMs(input) {
19
60
  if (Number.isFinite(n) && n > 0)
20
61
  return Math.floor(n);
21
62
  }
22
- const parsed = Date.parse(normalizeUtcIso(raw));
63
+ const parsed = Date.parse(normalizeIso(raw, tz));
23
64
  return Number.isFinite(parsed) ? parsed : null;
24
65
  }
@@ -23,7 +23,10 @@ export function computeJobNextRunAtMs(job, nowMs) {
23
23
  // One-shot jobs stay due until they successfully finish.
24
24
  if (job.state.lastStatus === "ok" && job.state.lastRunAtMs)
25
25
  return undefined;
26
- return job.schedule.atMs;
26
+ // Guard: if atMs is in the past, treat as expired — never fire immediately.
27
+ // A past atMs typically means the agent computed the timestamp incorrectly
28
+ // (e.g. timezone-naive ISO string interpreted as UTC).
29
+ return job.schedule.atMs > nowMs ? job.schedule.atMs : undefined;
27
30
  }
28
31
  // For "every" schedules without an explicit anchor, use the job's creation
29
32
  // time so the schedule stays grid-aligned across daemon restarts. Without
@@ -65,6 +65,22 @@ export async function add(state, input) {
65
65
  warnIfDisabled(state, "add");
66
66
  await ensureLoaded(state);
67
67
  const job = createJob(state, input);
68
+ // If a one-shot "at" job resolved to no nextRunAtMs, the atMs was in the
69
+ // past. Reject it outright so the agent gets a clear error instead of a
70
+ // silently disabled job that never fires.
71
+ if (job.schedule.kind === "at" &&
72
+ job.enabled &&
73
+ job.state.nextRunAtMs == null &&
74
+ !job.state.lastRunAtMs) {
75
+ const nowMs = state.deps.nowMs();
76
+ const delta = nowMs - job.schedule.atMs;
77
+ const nowIso = new Date(nowMs).toISOString();
78
+ const atIso = new Date(job.schedule.atMs).toISOString();
79
+ state.deps.log.warn({ atMs: job.schedule.atMs, atIso, nowIso, deltaMs: delta }, "cron.add rejected: atMs is in the past — reminder would fire immediately");
80
+ throw new Error(`schedule.atMs resolved to ${atIso} which is ${Math.round(delta / 1000)}s in the past ` +
81
+ `(current time: ${nowIso}). The reminder would fire immediately. ` +
82
+ "Call current_time to confirm the current date and year, then retry with a future timestamp.");
83
+ }
68
84
  state.store?.jobs.push(job);
69
85
  await persist(state);
70
86
  armTimer(state);
@@ -4,7 +4,7 @@ import { updateSessionStore } from "../../config/sessions.js";
4
4
  import { listRecords } from "../../records/records-manager.js";
5
5
  import { normalizeAccountId } from "../../routing/session-key.js";
6
6
  import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
7
- import { requireActiveWebListener } from "../../web/active-listener.js";
7
+ import { getActiveWebListener, requireActiveWebListener } from "../../web/active-listener.js";
8
8
  import { stripEnvelope } from "../chat-sanitize.js";
9
9
  import { ErrorCodes, errorShape, formatValidationErrors, validateWhatsAppConversationsParams, validateWhatsAppGroupInfoParams, validateWhatsAppMessagesParams, validateWhatsAppSendMessageParams, validateWhatsAppSetActivationParams, } from "../protocol/index.js";
10
10
  import { loadCombinedSessionStoreForGateway, parseGroupKey } from "../session-utils.js";
@@ -239,9 +239,9 @@ export const whatsappConversationsHandlers = {
239
239
  respond(true, { messages: [] });
240
240
  return;
241
241
  }
242
- // Read transcript and transform to WhatsApp message format
242
+ // --- Session transcript messages (agent-processed only) ---
243
243
  const rawMessages = readSessionMessages(matchedSessionId, storePath, matchedSessionFile);
244
- const messages = [];
244
+ const transcriptMessages = [];
245
245
  for (const raw of rawMessages) {
246
246
  const msg = raw;
247
247
  if (!msg.role || (msg.role !== "user" && msg.role !== "assistant"))
@@ -263,8 +263,8 @@ export const whatsappConversationsHandlers = {
263
263
  }
264
264
  if (!text.trim())
265
265
  continue;
266
- messages.push({
267
- id: `${matchedSessionId}-${messages.length}`,
266
+ transcriptMessages.push({
267
+ id: `transcript-${matchedSessionId}-${transcriptMessages.length}`,
268
268
  sender,
269
269
  senderName,
270
270
  body: text,
@@ -272,8 +272,42 @@ export const whatsappConversationsHandlers = {
272
272
  fromMe,
273
273
  });
274
274
  }
275
- // Return the most recent N messages
276
- const limited = messages.slice(-limit);
275
+ // --- Baileys in-memory store (all messages seen since gateway started) ---
276
+ const listener = getActiveWebListener(accountId);
277
+ const baileysMessages = listener?.getMessages
278
+ ? await listener.getMessages(jid, limit * 4)
279
+ : [];
280
+ // Merge: use Baileys as the primary source for inbound messages.
281
+ // Keep transcript assistant (fromMe) messages that Baileys doesn't have,
282
+ // and deduplicate inbound messages that appear in both sources.
283
+ let merged;
284
+ if (baileysMessages.length > 0) {
285
+ // Build a dedup set from Baileys entries: timestamp (±5s window) + body prefix
286
+ const baileysSignatures = new Set();
287
+ for (const m of baileysMessages) {
288
+ // Use 5-second timestamp buckets so minor clock drift doesn't cause duplicates
289
+ const bucket = Math.floor(m.timestamp / 5);
290
+ baileysSignatures.add(`${bucket}:${m.body.slice(0, 60)}`);
291
+ }
292
+ // From transcript, keep only assistant messages not already in Baileys
293
+ const transcriptOnly = transcriptMessages.filter((m) => {
294
+ if (!m.fromMe) {
295
+ const bucket = Math.floor(m.timestamp / 5);
296
+ return !baileysSignatures.has(`${bucket}:${m.body.slice(0, 60)}`);
297
+ }
298
+ // Always include assistant messages — Baileys store has no agent responses
299
+ return true;
300
+ });
301
+ merged = [...baileysMessages, ...transcriptOnly];
302
+ }
303
+ else {
304
+ // Baileys store is empty (gateway just started or account not connected) —
305
+ // fall back to session transcript only.
306
+ merged = transcriptMessages;
307
+ }
308
+ // Sort by timestamp, then return the most recent N
309
+ merged.sort((a, b) => a.timestamp - b.timestamp);
310
+ const limited = merged.slice(-limit);
277
311
  respond(true, { messages: limited });
278
312
  }
279
313
  catch (err) {
@@ -35,5 +35,5 @@ export function resolveGroupActivationFor(params) {
35
35
  channel: "whatsapp",
36
36
  groupId: groupId ?? params.conversationId,
37
37
  });
38
- return configActivation ?? "mention";
38
+ return configActivation ?? "off";
39
39
  }
@@ -88,6 +88,18 @@ export async function monitorWebInbox(options) {
88
88
  inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
89
89
  },
90
90
  });
91
+ // In-memory message store: captures all inbound messages per JID (including
92
+ // history catch-up / append type) so the UI can show a complete conversation view.
93
+ const messageStore = new Map();
94
+ const MESSAGE_STORE_MAX = 500; // max entries per JID
95
+ const storeMessageEntry = (jid, entry) => {
96
+ const entries = messageStore.get(jid) ?? [];
97
+ entries.push(entry);
98
+ if (entries.length > MESSAGE_STORE_MAX) {
99
+ entries.splice(0, entries.length - MESSAGE_STORE_MAX);
100
+ }
101
+ messageStore.set(jid, entries);
102
+ };
91
103
  const groupMetaCache = new Map();
92
104
  const GROUP_META_TTL_MS = 5 * 60 * 1000; // 5 minutes
93
105
  const lidLookup = sock.signalRepository?.lidMapping;
@@ -177,6 +189,25 @@ export async function monitorWebInbox(options) {
177
189
  const messageTimestampMs = msg.messageTimestamp
178
190
  ? Number(msg.messageTimestamp) * 1000
179
191
  : undefined;
192
+ // Store in the in-memory conversation store so the UI sees all messages,
193
+ // including history catch-up (append type) and messages from participants
194
+ // that don't trigger the agent. Runs before access control.
195
+ const storeBody = extractText(msg.message ?? undefined);
196
+ if (storeBody) {
197
+ const fromMe = Boolean(msg.key?.fromMe);
198
+ const replyCtx = describeReplyContext(msg.message);
199
+ storeMessageEntry(remoteJid, {
200
+ id: id ?? `${remoteJid}-${messageTimestampMs ?? 0}`,
201
+ sender: senderE164 ?? participantJid ?? (fromMe ? (selfE164 ?? "") : ""),
202
+ senderName: msg.pushName ?? undefined,
203
+ body: storeBody,
204
+ timestamp: messageTimestampMs ? Math.floor(messageTimestampMs / 1000) : 0,
205
+ fromMe,
206
+ quoted: replyCtx
207
+ ? { id: replyCtx.id ?? "", body: replyCtx.body, sender: replyCtx.sender }
208
+ : undefined,
209
+ });
210
+ }
180
211
  const access = await checkInboundAccessControl({
181
212
  accountId: options.accountId,
182
213
  from,
@@ -414,9 +445,9 @@ export async function monitorWebInbox(options) {
414
445
  }
415
446
  return entries;
416
447
  },
417
- getMessages: async (_jid, _limit) => {
418
- // No in-memory message store — Baileys makeInMemoryStore is not used.
419
- return [];
448
+ getMessages: async (jid, limit) => {
449
+ const entries = messageStore.get(jid) ?? [];
450
+ return limit ? entries.slice(-limit) : entries;
420
451
  },
421
452
  getGroupMetadata: async (jid) => {
422
453
  const meta = await sock.groupMetadata(jid);
@@ -0,0 +1 @@
1
+ node_modules/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.29.2",
3
+ "version": "1.30.1",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -79,64 +79,12 @@
79
79
  "dist/license/**",
80
80
  "dist/suggestions/**"
81
81
  ],
82
- "scripts": {
83
- "dev": "node scripts/run-node.mjs",
84
- "postinstall": "node scripts/postinstall.js",
85
- "prepack": "pnpm build && pnpm ui:build",
86
- "docs:list": "node scripts/docs-list.js",
87
- "docs:bin": "node scripts/build-docs-list.mjs",
88
- "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
89
- "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
90
- "release:check": "node --import tsx scripts/release-check.ts",
91
- "ui:install": "node scripts/ui.js install",
92
- "ui:dev": "node scripts/ui.js dev",
93
- "ui:build": "node scripts/ui.js build",
94
- "start": "node scripts/run-node.mjs",
95
- "taskmaster": "node scripts/run-node.mjs",
96
- "gateway:watch": "node scripts/watch-node.mjs gateway --force",
97
- "logs": "npx tsx scripts/session-viewer.ts",
98
- "gateway:dev": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
99
- "gateway:dev:reset": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
100
- "tui": "node scripts/run-node.mjs tui",
101
- "tui:dev": "TASKMASTER_PROFILE=dev node scripts/run-node.mjs tui",
102
- "taskmaster:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
103
- "lint": "oxlint --type-aware src test",
104
- "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test",
105
- "format": "oxfmt --check src test",
106
- "format:fix": "oxfmt --write src test",
107
- "test": "node scripts/test-parallel.mjs",
108
- "test:watch": "vitest",
109
- "test:ui": "pnpm --dir ui test",
110
- "test:force": "node --import tsx scripts/test-force.ts",
111
- "test:coverage": "vitest run --coverage",
112
- "test:e2e": "vitest run --config vitest.e2e.config.ts",
113
- "test:live": "TASKMASTER_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
114
- "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
115
- "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
116
- "test:docker:live-models": "bash scripts/test-live-models-docker.sh",
117
- "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
118
- "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
119
- "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
120
- "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
121
- "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
122
- "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
123
- "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
124
- "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
125
- "test:install:smoke": "bash scripts/test-install-sh-docker.sh",
126
- "test:install:e2e:openai": "TASKMASTER_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
127
- "test:install:e2e:anthropic": "TASKMASTER_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
128
- "protocol:gen": "node --import tsx scripts/protocol-gen.ts",
129
- "protocol:check": "pnpm protocol:gen && git diff --exit-code -- dist/protocol.schema.json",
130
- "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
131
- "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
132
- },
133
82
  "keywords": [],
134
83
  "author": "",
135
84
  "license": "MIT",
136
85
  "engines": {
137
86
  "node": ">=22.12.0"
138
87
  },
139
- "packageManager": "pnpm@10.23.0",
140
88
  "dependencies": {
141
89
  "@agentclientprotocol/sdk": "0.13.1",
142
90
  "@aws-sdk/client-bedrock": "^3.975.0",
@@ -229,14 +177,6 @@
229
177
  "vitest": "^4.0.18",
230
178
  "wireit": "^0.14.12"
231
179
  },
232
- "pnpm": {
233
- "minimumReleaseAge": 2880,
234
- "overrides": {
235
- "@sinclair/typebox": "0.34.47",
236
- "hono": "4.11.4",
237
- "tar": "7.5.4"
238
- }
239
- },
240
180
  "vitest": {
241
181
  "coverage": {
242
182
  "provider": "v8",
@@ -265,5 +205,55 @@
265
205
  "**/vendor/**",
266
206
  "dist/Taskmaster.app/**"
267
207
  ]
208
+ },
209
+ "scripts": {
210
+ "dev": "node scripts/run-node.mjs",
211
+ "postinstall": "node scripts/postinstall.js",
212
+ "docs:list": "node scripts/docs-list.js",
213
+ "docs:bin": "node scripts/build-docs-list.mjs",
214
+ "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
215
+ "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
216
+ "release:check": "node --import tsx scripts/release-check.ts",
217
+ "ui:install": "node scripts/ui.js install",
218
+ "ui:dev": "node scripts/ui.js dev",
219
+ "ui:build": "node scripts/ui.js build",
220
+ "start": "node scripts/run-node.mjs",
221
+ "taskmaster": "node scripts/run-node.mjs",
222
+ "gateway:watch": "node scripts/watch-node.mjs gateway --force",
223
+ "logs": "npx tsx scripts/session-viewer.ts",
224
+ "gateway:dev": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
225
+ "gateway:dev:reset": "TASKMASTER_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
226
+ "tui": "node scripts/run-node.mjs tui",
227
+ "tui:dev": "TASKMASTER_PROFILE=dev node scripts/run-node.mjs tui",
228
+ "taskmaster:rpc": "node scripts/run-node.mjs agent --mode rpc --json",
229
+ "lint": "oxlint --type-aware src test",
230
+ "lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test",
231
+ "format": "oxfmt --check src test",
232
+ "format:fix": "oxfmt --write src test",
233
+ "test": "node scripts/test-parallel.mjs",
234
+ "test:watch": "vitest",
235
+ "test:ui": "pnpm --dir ui test",
236
+ "test:force": "node --import tsx scripts/test-force.ts",
237
+ "test:coverage": "vitest run --coverage",
238
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
239
+ "test:live": "TASKMASTER_LIVE_TEST=1 vitest run --config vitest.live.config.ts",
240
+ "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh",
241
+ "test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
242
+ "test:docker:live-models": "bash scripts/test-live-models-docker.sh",
243
+ "test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",
244
+ "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
245
+ "test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
246
+ "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh",
247
+ "test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
248
+ "test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
249
+ "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
250
+ "test:install:e2e": "bash scripts/test-install-sh-e2e-docker.sh",
251
+ "test:install:smoke": "bash scripts/test-install-sh-docker.sh",
252
+ "test:install:e2e:openai": "TASKMASTER_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh",
253
+ "test:install:e2e:anthropic": "TASKMASTER_E2E_MODELS=anthropic bash scripts/test-install-sh-e2e-docker.sh",
254
+ "protocol:gen": "node --import tsx scripts/protocol-gen.ts",
255
+ "protocol:check": "pnpm protocol:gen && git diff --exit-code -- dist/protocol.schema.json",
256
+ "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
257
+ "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500"
268
258
  }
269
- }
259
+ }
File without changes
@@ -105,14 +105,24 @@ The reminder's payload text should reference the event clearly so the recipient
105
105
 
106
106
  ### Computing the cron timestamp safely
107
107
 
108
- The `cron` tool takes `atMs` a Unix timestamp in **milliseconds**. Errors here cause the job to fire immediately (if the time is in the past) with no warning.
108
+ The `cron` tool accepts `atMs` as either a numeric Unix timestamp in milliseconds or an **ISO 8601 datetime string**. The ISO string approach is strongly preferred because it avoids epoch arithmetic errors.
109
109
 
110
- **Always call `current_time` before scheduling a cron job.** Use the returned `unix` value (seconds since epoch) as your anchor:
110
+ **Always call `current_time` before scheduling a cron job** to confirm the current date and year.
111
111
 
112
- 1. Get `unix` from `current_time` this is the current time in seconds.
113
- 2. Calculate how many seconds until the reminder should fire (days × 86400 + hours × 3600 + minutes × 60).
114
- 3. Add to `unix`, then multiply by 1000 to get `atMs`.
115
- 4. Before submitting: verify your computed `atMs` is strictly greater than `unix × 1000`. If it is not, the job will fire immediately recalculate.
112
+ **Preferred approachISO string:**
113
+
114
+ 1. Call `current_time` to confirm today's date and year.
115
+ 2. Pass `atMs` as an ISO 8601 string: `"2026-03-11T09:00:00"` (no timezone suffix needed the system uses the user's configured timezone automatically).
116
+ 3. Double-check that the year in your string matches the year from `current_time`.
117
+
118
+ The system rejects timestamps that resolve to the past — you will get a clear error with the current time if this happens.
119
+
120
+ **Fallback approach — numeric timestamp:**
121
+
122
+ 1. Get `unix` from `current_time`.
123
+ 2. Calculate offset in seconds (days × 86400 + hours × 3600 + minutes × 60).
124
+ 3. Compute: `atMs = (unix + offset) × 1000`.
125
+ 4. Verify `atMs > unix × 1000` before submitting.
116
126
 
117
127
  Never compute a Unix timestamp from a fixed epoch date (e.g. "January 1, 2026 = X"). Epoch arithmetic done from scratch is the most common source of year-off errors.
118
128
 
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: whatsapp-business
3
+ description: WhatsApp Business Cloud API setup — guides the user through creating a Meta Business Portfolio, registering a phone number, generating a permanent System User access token, configuring the webhook, and entering credentials into Taskmaster. Use when a user wants to connect WhatsApp via Meta's official Cloud API (verified badge, no QR dependency).
4
+ metadata: {"taskmaster":{"emoji":"📱"}}
5
+ ---
6
+
7
+ # WhatsApp Business Cloud API
8
+
9
+ Handles the full Meta Cloud API setup. This is the official WhatsApp Business Platform — messages arrive via Meta webhooks instead of a QR-linked device. It unlocks a verified green checkmark, message templates for proactive outreach, and eliminates the phone dependency entirely.
10
+
11
+ ## When to activate
12
+
13
+ - User asks about connecting WhatsApp via Meta's official API or Business Platform
14
+ - User mentions "Cloud API", "WABA", "Meta Business Manager", "verified badge", or "green checkmark"
15
+ - User wants WhatsApp without needing a physical phone linked
16
+ - User has a dedicated number for a business WhatsApp account
17
+ - User asks why they need a separate number for Cloud API
18
+
19
+ ## What it unlocks
20
+
21
+ - **Verified business profile** — Meta's green checkmark badge builds customer trust
22
+ - **No phone dependency** — no linked device, no QR scan every 14 days
23
+ - **Message templates** — send proactive outreach messages to opted-in customers
24
+ - **Higher reliability** — webhooks from Meta's infrastructure, not a linked device session
25
+
26
+ ## Important: Number exclusivity
27
+
28
+ A phone number can only be on **one** WhatsApp connection at a time. A number registered with Meta Cloud API cannot also be used with Baileys (QR). The two providers can coexist on one Taskmaster instance but must use different phone numbers.
29
+
30
+ ## Key Storage
31
+
32
+ Cloud API credentials are stored directly in the Taskmaster config, not in API key slots. Use the Setup page (WhatsApp → gear icon → Add Cloud API Account) to enter them. The four required values are:
33
+
34
+ | Field | What it is | Where to find it |
35
+ |-------|-----------|-----------------|
36
+ | Phone Number ID | Numeric ID for the registered number | WhatsApp Manager → Phone Numbers tab |
37
+ | Business Account ID | Your WABA (WhatsApp Business Account) ID | WhatsApp Manager → Business Account section |
38
+ | Access Token | Permanent System User token | Meta Business Settings → System Users |
39
+ | Webhook Verify Token | A string you define (shared secret) | You generate this; enter same value in Taskmaster and Meta |
40
+
41
+ ## References
42
+
43
+ | Task | When to use | Reference |
44
+ |------|-------------|-----------|
45
+ | Full Meta Cloud API setup | Credentials not yet entered or user starting from scratch | `references/setup-guide.md` |
46
+
47
+ Load the reference and follow it step by step.