@rubytech/taskmaster 1.0.106 → 1.0.108

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 (62) hide show
  1. package/dist/agents/skills-status.js +23 -3
  2. package/dist/agents/skills.js +1 -0
  3. package/dist/agents/system-prompt.js +3 -0
  4. package/dist/agents/taskmaster-tools.js +5 -0
  5. package/dist/agents/tool-policy.js +2 -1
  6. package/dist/agents/tools/authorize-admin-tool.js +1 -1
  7. package/dist/agents/tools/memory-tool.js +2 -1
  8. package/dist/agents/tools/software-update-tool.js +114 -0
  9. package/dist/auto-reply/reply/commands-status.js +5 -9
  10. package/dist/auto-reply/reply/get-reply-run.js +1 -1
  11. package/dist/auto-reply/reply/get-reply.js +1 -1
  12. package/dist/auto-reply/reply/model-selection.js +1 -1
  13. package/dist/browser/routes/screencast.js +1 -1
  14. package/dist/browser/screencast.js +1 -1
  15. package/dist/build-info.json +3 -3
  16. package/dist/commands/agent.js +2 -2
  17. package/dist/config/zod-schema.js +12 -1
  18. package/dist/control-ui/assets/index-B2FEGOCu.css +1 -0
  19. package/dist/control-ui/assets/index-nLVF-pVT.js +3762 -0
  20. package/dist/control-ui/assets/index-nLVF-pVT.js.map +1 -0
  21. package/dist/control-ui/index.html +2 -2
  22. package/dist/control-ui/maxy-icon.png +0 -0
  23. package/dist/cron/isolated-agent/recipients.js +70 -0
  24. package/dist/cron/isolated-agent/run.js +43 -13
  25. package/dist/gateway/config-reload.js +1 -0
  26. package/dist/gateway/control-ui.js +111 -5
  27. package/dist/gateway/protocol/index.js +6 -1
  28. package/dist/gateway/protocol/schema/agents-models-skills.js +23 -0
  29. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -1
  30. package/dist/gateway/server-http.js +6 -1
  31. package/dist/gateway/server-methods/access.js +3 -3
  32. package/dist/gateway/server-methods/brand.js +160 -0
  33. package/dist/gateway/server-methods/browser-screencast.js +3 -3
  34. package/dist/gateway/server-methods/skills.js +159 -3
  35. package/dist/gateway/server-methods/workspaces.js +7 -7
  36. package/dist/gateway/server-methods-list.js +5 -0
  37. package/dist/gateway/server-methods.js +2 -0
  38. package/dist/gateway/server.impl.js +1 -1
  39. package/dist/infra/heartbeat-runner.js +17 -0
  40. package/dist/infra/heartbeat-update-notify.js +120 -0
  41. package/dist/infra/tunnel.js +1 -1
  42. package/dist/memory/embeddings.js +0 -4
  43. package/dist/memory/manager.js +15 -6
  44. package/dist/web/inbound/media.js +1 -1
  45. package/dist/web/login-qr.js +0 -23
  46. package/dist/web/providers/cloud/receive.js +1 -1
  47. package/dist/web/providers/cloud/webhook.js +1 -1
  48. package/package.json +1 -1
  49. package/skills/skill-builder/SKILL.md +97 -0
  50. package/skills/skill-builder/references/lean-pattern.md +118 -0
  51. package/skills/zero-to-prototype/SKILL.md +35 -0
  52. package/skills/zero-to-prototype/references/discovery.md +64 -0
  53. package/skills/zero-to-prototype/references/prd.md +83 -0
  54. package/skills/zero-to-prototype/references/validation.md +67 -0
  55. package/taskmaster-docs/USER-GUIDE.md +65 -31
  56. package/templates/customer/agents/public/AGENTS.md +3 -10
  57. package/templates/taskmaster/agents/public/SOUL.md +0 -4
  58. package/templates/tradesupport/agents/public/AGENTS.md +3 -10
  59. package/dist/control-ui/assets/index-DjhCZlZd.css +0 -1
  60. package/dist/control-ui/assets/index-DtuDNTAC.js +0 -3539
  61. package/dist/control-ui/assets/index-DtuDNTAC.js.map +0 -1
  62. package/skills/taskmaster/SKILL.md +0 -164
@@ -61,7 +61,7 @@ export const browserScreencastHandlers = {
61
61
  respond(true, result);
62
62
  }
63
63
  catch (err) {
64
- log.error(`screencast.start failed: ${err}`);
64
+ log.error(`screencast.start failed: ${String(err)}`);
65
65
  respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
66
66
  }
67
67
  },
@@ -84,7 +84,7 @@ export const browserScreencastHandlers = {
84
84
  respond(true, { ok: true });
85
85
  }
86
86
  catch (err) {
87
- log.error(`screencast.stop failed: ${err}`);
87
+ log.error(`screencast.stop failed: ${String(err)}`);
88
88
  respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
89
89
  }
90
90
  },
@@ -169,7 +169,7 @@ async function resolveBridgeUrl(context) {
169
169
  log.warn(`browser config resolved but not usable: enabled=${resolved.enabled} controlUrl=${resolved.controlUrl ?? "null"}`);
170
170
  }
171
171
  catch (err) {
172
- log.error(`failed to resolve browser config: ${err}`);
172
+ log.error(`failed to resolve browser config: ${String(err)}`);
173
173
  }
174
174
  return null;
175
175
  }
@@ -1,10 +1,13 @@
1
- import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveDefaultAgentId } from "../../agents/agent-scope.js";
2
4
  import { installSkill } from "../../agents/skills-install.js";
3
5
  import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
4
- import { loadWorkspaceSkillEntries } from "../../agents/skills.js";
6
+ import { loadWorkspaceSkillEntries, resolveBundledSkillsDir } from "../../agents/skills.js";
7
+ import { bumpSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
5
8
  import { loadConfig, writeConfigFile } from "../../config/config.js";
6
9
  import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
7
- import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsInstallParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
10
+ import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsCreateParams, validateSkillsDeleteDraftParams, validateSkillsDeleteParams, validateSkillsDraftsParams, validateSkillsInstallParams, validateSkillsReadParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
8
11
  function listWorkspaceDirs(cfg) {
9
12
  const dirs = new Set();
10
13
  const list = cfg.agents?.list;
@@ -45,6 +48,21 @@ function collectSkillBins(entries) {
45
48
  }
46
49
  return [...bins].sort();
47
50
  }
51
+ function resolveWorkspaceRoot(cfg) {
52
+ return resolveAgentWorkspaceRoot(cfg, resolveDefaultAgentId(cfg));
53
+ }
54
+ function isPreloadedSkill(skillKey) {
55
+ const dir = resolveBundledSkillsDir();
56
+ if (!dir)
57
+ return false;
58
+ try {
59
+ return fs.existsSync(`${dir}/${skillKey}`) &&
60
+ fs.statSync(`${dir}/${skillKey}`).isDirectory();
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
48
66
  export const skillsHandlers = {
49
67
  "skills.status": ({ params, respond }) => {
50
68
  if (!validateSkillsStatusParams(params)) {
@@ -97,6 +115,10 @@ export const skillsHandlers = {
97
115
  return;
98
116
  }
99
117
  const p = params;
118
+ if (p.enabled === false && isPreloadedSkill(p.skillKey)) {
119
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "preloaded skills cannot be disabled"));
120
+ return;
121
+ }
100
122
  const cfg = loadConfig();
101
123
  const skills = cfg.skills ? { ...cfg.skills } : {};
102
124
  const entries = skills.entries ? { ...skills.entries } : {};
@@ -134,4 +156,138 @@ export const skillsHandlers = {
134
156
  await writeConfigFile(nextConfig);
135
157
  respond(true, { ok: true, skillKey: p.skillKey, config: current }, undefined);
136
158
  },
159
+ "skills.read": ({ params, respond }) => {
160
+ if (!validateSkillsReadParams(params)) {
161
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.read params: ${formatValidationErrors(validateSkillsReadParams.errors)}`));
162
+ return;
163
+ }
164
+ const p = params;
165
+ const cfg = loadConfig();
166
+ const root = resolveWorkspaceRoot(cfg);
167
+ const skillDir = path.join(root, "skills", p.name);
168
+ const skillFile = path.join(skillDir, "SKILL.md");
169
+ if (!fs.existsSync(skillFile)) {
170
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `skill "${p.name}" not found`));
171
+ return;
172
+ }
173
+ const content = fs.readFileSync(skillFile, "utf-8");
174
+ const references = [];
175
+ const refsDir = path.join(skillDir, "references");
176
+ if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
177
+ for (const entry of fs.readdirSync(refsDir, { withFileTypes: true })) {
178
+ if (entry.isFile() && entry.name.endsWith(".md")) {
179
+ references.push({
180
+ name: entry.name,
181
+ content: fs.readFileSync(path.join(refsDir, entry.name), "utf-8"),
182
+ });
183
+ }
184
+ }
185
+ references.sort((a, b) => a.name.localeCompare(b.name));
186
+ }
187
+ respond(true, { name: p.name, content, references }, undefined);
188
+ },
189
+ "skills.create": async ({ params, respond }) => {
190
+ if (!validateSkillsCreateParams(params)) {
191
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.create params: ${formatValidationErrors(validateSkillsCreateParams.errors)}`));
192
+ return;
193
+ }
194
+ const p = params;
195
+ if (isPreloadedSkill(p.name)) {
196
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "cannot overwrite a preloaded skill"));
197
+ return;
198
+ }
199
+ const cfg = loadConfig();
200
+ const root = resolveWorkspaceRoot(cfg);
201
+ const skillDir = path.join(root, "skills", p.name);
202
+ // If the skill already exists (user skill), remove it first so we get a clean replace
203
+ if (fs.existsSync(skillDir)) {
204
+ fs.rmSync(skillDir, { recursive: true, force: true });
205
+ }
206
+ fs.mkdirSync(skillDir, { recursive: true });
207
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), p.skillContent, "utf-8");
208
+ if (p.references && p.references.length > 0) {
209
+ const refsDir = path.join(skillDir, "references");
210
+ fs.mkdirSync(refsDir, { recursive: true });
211
+ for (const ref of p.references) {
212
+ const safeName = ref.name.replace(/[^a-zA-Z0-9._-]/g, "-");
213
+ fs.writeFileSync(path.join(refsDir, safeName), ref.content, "utf-8");
214
+ }
215
+ }
216
+ bumpSkillsSnapshotVersion();
217
+ respond(true, { ok: true, name: p.name }, undefined);
218
+ },
219
+ "skills.delete": async ({ params, respond }) => {
220
+ if (!validateSkillsDeleteParams(params)) {
221
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.delete params: ${formatValidationErrors(validateSkillsDeleteParams.errors)}`));
222
+ return;
223
+ }
224
+ const p = params;
225
+ if (isPreloadedSkill(p.name)) {
226
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "preloaded skills cannot be deleted"));
227
+ return;
228
+ }
229
+ const cfg = loadConfig();
230
+ const root = resolveWorkspaceRoot(cfg);
231
+ const skillDir = path.join(root, "skills", p.name);
232
+ if (!fs.existsSync(skillDir)) {
233
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `skill "${p.name}" not found`));
234
+ return;
235
+ }
236
+ fs.rmSync(skillDir, { recursive: true, force: true });
237
+ bumpSkillsSnapshotVersion();
238
+ respond(true, { ok: true, name: p.name }, undefined);
239
+ },
240
+ "skills.drafts": ({ params, respond }) => {
241
+ if (!validateSkillsDraftsParams(params)) {
242
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.drafts params: ${formatValidationErrors(validateSkillsDraftsParams.errors)}`));
243
+ return;
244
+ }
245
+ const cfg = loadConfig();
246
+ const root = resolveWorkspaceRoot(cfg);
247
+ const draftsDir = path.join(root, ".skill-drafts");
248
+ if (!fs.existsSync(draftsDir)) {
249
+ respond(true, { drafts: [] }, undefined);
250
+ return;
251
+ }
252
+ const drafts = [];
253
+ for (const entry of fs.readdirSync(draftsDir, { withFileTypes: true })) {
254
+ if (!entry.isDirectory())
255
+ continue;
256
+ const skillFile = path.join(draftsDir, entry.name, "SKILL.md");
257
+ if (!fs.existsSync(skillFile))
258
+ continue;
259
+ const skillContent = fs.readFileSync(skillFile, "utf-8");
260
+ const references = [];
261
+ const refsDir = path.join(draftsDir, entry.name, "references");
262
+ if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
263
+ for (const ref of fs.readdirSync(refsDir, { withFileTypes: true })) {
264
+ if (ref.isFile() && ref.name.endsWith(".md")) {
265
+ references.push({
266
+ name: ref.name,
267
+ content: fs.readFileSync(path.join(refsDir, ref.name), "utf-8"),
268
+ });
269
+ }
270
+ }
271
+ references.sort((a, b) => a.name.localeCompare(b.name));
272
+ }
273
+ drafts.push({ name: entry.name, skillContent, references });
274
+ }
275
+ respond(true, { drafts }, undefined);
276
+ },
277
+ "skills.deleteDraft": ({ params, respond }) => {
278
+ if (!validateSkillsDeleteDraftParams(params)) {
279
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.deleteDraft params: ${formatValidationErrors(validateSkillsDeleteDraftParams.errors)}`));
280
+ return;
281
+ }
282
+ const p = params;
283
+ const cfg = loadConfig();
284
+ const root = resolveWorkspaceRoot(cfg);
285
+ const draftDir = path.join(root, ".skill-drafts", p.name);
286
+ if (!fs.existsSync(draftDir)) {
287
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `draft "${p.name}" not found`));
288
+ return;
289
+ }
290
+ fs.rmSync(draftDir, { recursive: true, force: true });
291
+ respond(true, { ok: true, name: p.name }, undefined);
292
+ },
137
293
  };
@@ -56,7 +56,7 @@ function sanitiseName(raw) {
56
56
  * or the agent subdirectory (e.g. ~/taskmaster/agents/public).
57
57
  * We normalise to the root by stripping a trailing /agents/{name} suffix.
58
58
  */
59
- function resolveWorkspaceRoot(agentWorkspaceDir, agentId) {
59
+ function resolveWorkspaceRoot(agentWorkspaceDir, _agentId) {
60
60
  const normalised = agentWorkspaceDir.replace(/\/+$/, "");
61
61
  // Check if the path ends with /agents/{something} — that's the agent subdir, not the workspace root
62
62
  const agentsMatch = normalised.match(/^(.+)\/agents\/[^/]+$/);
@@ -424,9 +424,9 @@ export const workspacesHandlers = {
424
424
  cfg = {
425
425
  ...cfg,
426
426
  channels: {
427
- ...(cfg.channels ?? {}),
427
+ ...cfg.channels,
428
428
  whatsapp: {
429
- ...(cfg.channels?.whatsapp ?? {}),
429
+ ...cfg.channels?.whatsapp,
430
430
  accounts: {
431
431
  ...existingAccounts,
432
432
  [whatsappAccountId]: {
@@ -467,7 +467,7 @@ export const workspacesHandlers = {
467
467
  if (typeof rawName === "string" && rawName.trim() && rawName.trim() !== name) {
468
468
  cfg = {
469
469
  ...cfg,
470
- workspaces: { ...(cfg.workspaces ?? {}), [name]: { displayName: rawName.trim() } },
470
+ workspaces: { ...cfg.workspaces, [name]: { displayName: rawName.trim() } },
471
471
  };
472
472
  }
473
473
  // Write config and schedule restart
@@ -582,12 +582,12 @@ export const workspacesHandlers = {
582
582
  }
583
583
  // Clean up empty accounts object
584
584
  if (Object.keys(whatsappAccounts).length === 0) {
585
- const whatsappConfig = { ...(cfg.channels?.whatsapp ?? {}) };
585
+ const whatsappConfig = { ...cfg.channels?.whatsapp };
586
586
  delete whatsappConfig.accounts;
587
587
  cfg = {
588
588
  ...cfg,
589
589
  channels: {
590
- ...(cfg.channels ?? {}),
590
+ ...cfg.channels,
591
591
  whatsapp: whatsappConfig,
592
592
  },
593
593
  };
@@ -640,7 +640,7 @@ export const workspacesHandlers = {
640
640
  if (!requireBaseHash(params, snapshot, respond))
641
641
  return;
642
642
  let cfg = snapshot.config;
643
- cfg = { ...cfg, workspaces: { ...(cfg.workspaces ?? {}), [name]: { displayName } } };
643
+ cfg = { ...cfg, workspaces: { ...cfg.workspaces, [name]: { displayName } } };
644
644
  try {
645
645
  await writeConfigFile(cfg);
646
646
  }
@@ -41,6 +41,11 @@ const BASE_METHODS = [
41
41
  "skills.bins",
42
42
  "skills.install",
43
43
  "skills.update",
44
+ "skills.read",
45
+ "skills.create",
46
+ "skills.delete",
47
+ "skills.drafts",
48
+ "skills.deleteDraft",
44
49
  "update.status",
45
50
  "update.run",
46
51
  "voicewake.get",
@@ -36,6 +36,7 @@ import { publicChatHandlers } from "./server-methods/public-chat.js";
36
36
  import { tailscaleHandlers } from "./server-methods/tailscale.js";
37
37
  import { wifiHandlers } from "./server-methods/wifi.js";
38
38
  import { workspacesHandlers } from "./server-methods/workspaces.js";
39
+ import { brandHandlers } from "./server-methods/brand.js";
39
40
  const ADMIN_SCOPE = "operator.admin";
40
41
  const READ_SCOPE = "operator.read";
41
42
  const WRITE_SCOPE = "operator.write";
@@ -236,6 +237,7 @@ export const coreGatewayHandlers = {
236
237
  ...memoryHandlers,
237
238
  ...recordsHandlers,
238
239
  ...workspacesHandlers,
240
+ ...brandHandlers,
239
241
  ...publicChatHandlers,
240
242
  ...tailscaleHandlers,
241
243
  ...wifiHandlers,
@@ -428,7 +428,7 @@ export async function startGatewayServer(port = 18789, opts = {}) {
428
428
  const logMemory = log.child("memory");
429
429
  const agentIds = listAgentIds(cfgAtStart);
430
430
  for (const agentId of agentIds) {
431
- getMemorySearchManager({ cfg: cfgAtStart, agentId }).then(async (result) => {
431
+ void getMemorySearchManager({ cfg: cfgAtStart, agentId }).then(async (result) => {
432
432
  if (result.manager) {
433
433
  logMemory.info(`initialized for agent: ${agentId}`);
434
434
  // Sync memory index on startup
@@ -25,6 +25,7 @@ import { resolveHeartbeatVisibility } from "./heartbeat-visibility.js";
25
25
  import { requestHeartbeatNow, setHeartbeatWakeHandler, } from "./heartbeat-wake.js";
26
26
  import { deliverOutboundPayloads } from "./outbound/deliver.js";
27
27
  import { resolveHeartbeatDeliveryTarget, resolveHeartbeatSenderContext, } from "./outbound/targets.js";
28
+ import { maybeNotifyUpdateAvailable } from "./heartbeat-update-notify.js";
28
29
  const log = createSubsystemLogger("gateway/heartbeat");
29
30
  let heartbeatsEnabled = true;
30
31
  export function setHeartbeatsEnabled(enabled) {
@@ -621,6 +622,14 @@ export async function runHeartbeatOnce(opts) {
621
622
  return { status: "failed", reason };
622
623
  }
623
624
  }
625
+ async function checkAndNotifyUpdate(cfg, agent, deps) {
626
+ const agentId = agent.agentId;
627
+ const heartbeat = agent.heartbeat;
628
+ const { entry } = resolveHeartbeatSession(cfg, agentId, heartbeat);
629
+ const bindingAccountId = resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined;
630
+ const delivery = resolveHeartbeatDeliveryTarget({ cfg, entry, heartbeat, bindingAccountId });
631
+ await maybeNotifyUpdateAvailable({ cfg, delivery, deps });
632
+ }
624
633
  export function startHeartbeatRunner(opts) {
625
634
  const runtime = opts.runtime ?? defaultRuntime;
626
635
  const runOnce = opts.runOnce ?? runHeartbeatOnce;
@@ -742,6 +751,14 @@ export function startHeartbeatRunner(opts) {
742
751
  if (res.status === "ran")
743
752
  ran = true;
744
753
  }
754
+ // After heartbeat cycle: check for software updates and notify admin.
755
+ // Uses the first agent's delivery target. Non-blocking — never delays the next heartbeat.
756
+ if (ran) {
757
+ const firstAgent = state.agents.values().next().value;
758
+ if (firstAgent) {
759
+ void checkAndNotifyUpdate(state.cfg, firstAgent, { runtime: state.runtime }).catch(() => { });
760
+ }
761
+ }
745
762
  scheduleNext();
746
763
  if (ran)
747
764
  return { status: "ran", durationMs: Date.now() - startedAt };
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getChannelPlugin } from "../channels/plugins/index.js";
4
+ import { resolveStateDir } from "../config/paths.js";
5
+ import { createSubsystemLogger } from "../logging/subsystem.js";
6
+ import { VERSION } from "../version.js";
7
+ import { compareSemverStrings, resolveNpmChannelTag } from "./update-check.js";
8
+ import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js";
9
+ import { deliverOutboundPayloads } from "./outbound/deliver.js";
10
+ const log = createSubsystemLogger("gateway/heartbeat-update-notify");
11
+ // Registry check interval during heartbeat cycles.
12
+ // Shorter than the 24h startup check — the admin may publish frequently.
13
+ const REGISTRY_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
14
+ // In-memory: last time we checked the registry.
15
+ let lastRegistryCheckMs = 0;
16
+ // State file stores lastNotifiedVersion so we don't re-notify after restart
17
+ // for a version we already told the admin about.
18
+ const STATE_FILENAME = "update-check.json";
19
+ async function readState() {
20
+ try {
21
+ const statePath = path.join(resolveStateDir(), STATE_FILENAME);
22
+ const raw = await fs.readFile(statePath, "utf-8");
23
+ const parsed = JSON.parse(raw);
24
+ return parsed && typeof parsed === "object" ? parsed : {};
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
30
+ async function writeState(state) {
31
+ const statePath = path.join(resolveStateDir(), STATE_FILENAME);
32
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
33
+ await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
34
+ }
35
+ /**
36
+ * Check for available software updates and notify the admin via the heartbeat
37
+ * delivery channel. Designed to run after each heartbeat cycle.
38
+ *
39
+ * - Checks the NPM registry at most once per REGISTRY_CHECK_INTERVAL_MS.
40
+ * - Notifies the admin once per new version (persisted across restarts).
41
+ * - Never throws — errors are logged and swallowed.
42
+ */
43
+ export async function maybeNotifyUpdateAvailable(params) {
44
+ try {
45
+ const { cfg, delivery, deps } = params;
46
+ const nowMs = params.nowMs ?? Date.now();
47
+ // Skip if no delivery target
48
+ if (delivery.channel === "none" || !delivery.to)
49
+ return false;
50
+ // Skip if disabled via config
51
+ if (cfg.update?.checkOnStart === false)
52
+ return false;
53
+ // Rate-limit registry checks
54
+ if (nowMs - lastRegistryCheckMs < REGISTRY_CHECK_INTERVAL_MS)
55
+ return false;
56
+ lastRegistryCheckMs = nowMs;
57
+ // Resolve update channel and fetch latest version
58
+ const channel = normalizeUpdateChannel(cfg.update?.channel) ?? DEFAULT_PACKAGE_CHANNEL;
59
+ const resolved = await resolveNpmChannelTag({ channel, timeoutMs: 3500 });
60
+ if (!resolved.version)
61
+ return false;
62
+ // Compare versions
63
+ const cmp = compareSemverStrings(VERSION, resolved.version);
64
+ if (cmp === null || cmp >= 0)
65
+ return false; // up to date or parse failure
66
+ // Check if we already notified for this version
67
+ const state = await readState();
68
+ if (state.lastNotifiedVersion === resolved.version)
69
+ return false;
70
+ // Check channel readiness
71
+ const plugin = getChannelPlugin(delivery.channel);
72
+ if (plugin?.heartbeat?.checkReady) {
73
+ const readiness = await plugin.heartbeat.checkReady({
74
+ cfg,
75
+ accountId: delivery.accountId,
76
+ deps,
77
+ });
78
+ if (!readiness.ok) {
79
+ log.debug("update notification skipped: channel not ready", {
80
+ reason: readiness.reason,
81
+ });
82
+ return false;
83
+ }
84
+ }
85
+ // Send the notification
86
+ const message = `Taskmaster v${resolved.version} is available (you're on v${VERSION}). ` +
87
+ `Update from Setup → Software Update, or say "update software".`;
88
+ await deliverOutboundPayloads({
89
+ cfg,
90
+ channel: delivery.channel,
91
+ to: delivery.to,
92
+ accountId: delivery.accountId,
93
+ payloads: [{ text: message }],
94
+ deps,
95
+ });
96
+ // Persist so we don't re-notify after restart
97
+ await writeState({
98
+ ...state,
99
+ lastNotifiedVersion: resolved.version,
100
+ lastNotifiedTag: resolved.tag,
101
+ lastCheckedAt: new Date(nowMs).toISOString(),
102
+ });
103
+ log.info("update notification sent", {
104
+ current: VERSION,
105
+ latest: resolved.version,
106
+ to: delivery.to,
107
+ });
108
+ return true;
109
+ }
110
+ catch (err) {
111
+ log.error("update notification failed", {
112
+ error: err instanceof Error ? err.message : String(err),
113
+ });
114
+ return false;
115
+ }
116
+ }
117
+ /** Reset in-memory check timer. Exposed for testing. */
118
+ export function resetUpdateCheckTimer() {
119
+ lastRegistryCheckMs = 0;
120
+ }
@@ -79,7 +79,7 @@ export class CloudflareTunnel {
79
79
  const urlTimeout = setTimeout(() => {
80
80
  if (!urlFound) {
81
81
  reject(new Error("Timeout waiting for tunnel URL"));
82
- this.stop();
82
+ void this.stop();
83
83
  }
84
84
  }, 30000);
85
85
  this.process.stdout?.on("data", (data) => {
@@ -33,10 +33,6 @@ function canAutoSelectLocal(options) {
33
33
  return false;
34
34
  }
35
35
  }
36
- function isMissingApiKeyError(err) {
37
- const message = formatError(err);
38
- return message.includes("No API key found for provider");
39
- }
40
36
  /**
41
37
  * Deduplicates concurrent model downloads. When multiple agents resolve the
42
38
  * same model path, only one download runs; the rest await the same promise.
@@ -74,6 +74,15 @@ function expandPathTemplate(pattern, ctx) {
74
74
  result = result.replaceAll("{agentId}", ctx.agentId);
75
75
  return result;
76
76
  }
77
+ /**
78
+ * Ensure phone numbers in memory/users/ paths include the canonical + prefix.
79
+ * AI agents sometimes omit the + when constructing paths (e.g. "memory/users/447734875155/...")
80
+ * but the session peer and filesystem convention always use + (e.g. "+447734875155").
81
+ * Without this, scope patterns like memory/users/{peer}/** won't match the path.
82
+ */
83
+ function normalizePhoneInMemoryPath(relPath) {
84
+ return relPath.replace(/^(memory\/users\/)(\d)/i, "$1+$2");
85
+ }
77
86
  /**
78
87
  * Simple glob pattern matcher supporting * and **.
79
88
  * - * matches any characters except /
@@ -85,7 +94,7 @@ function matchGlobPattern(pattern, filePath) {
85
94
  // Replace ** with a placeholder, then * with [^/]*, then placeholder with .*
86
95
  regexStr = regexStr.replace(/\*\*/g, "\0");
87
96
  regexStr = regexStr.replace(/\*/g, "[^/]*");
88
- regexStr = regexStr.replace(/\0/g, ".*");
97
+ regexStr = regexStr.replaceAll("\0", ".*");
89
98
  const regex = new RegExp(`^${regexStr}$`, "i");
90
99
  return regex.test(filePath);
91
100
  }
@@ -517,7 +526,7 @@ export class MemoryIndexManager {
517
526
  return this.syncing;
518
527
  }
519
528
  async readFile(params) {
520
- const relPath = normalizeRelPath(params.relPath);
529
+ const relPath = normalizePhoneInMemoryPath(normalizeRelPath(params.relPath));
521
530
  if (!relPath || !isMemoryPath(relPath)) {
522
531
  throw new Error("path required");
523
532
  }
@@ -552,7 +561,7 @@ export class MemoryIndexManager {
552
561
  * matching the session's scope configuration.
553
562
  */
554
563
  async writeFile(params) {
555
- const relPath = normalizeRelPath(params.relPath);
564
+ const relPath = normalizePhoneInMemoryPath(normalizeRelPath(params.relPath));
556
565
  if (!relPath || !isMemoryPath(relPath)) {
557
566
  throw new Error("path required (must be in memory/ directory)");
558
567
  }
@@ -573,7 +582,7 @@ export class MemoryIndexManager {
573
582
  }
574
583
  // Ensure parent directory exists
575
584
  const parentDir = path.dirname(absPath);
576
- await ensureDir(parentDir);
585
+ ensureDir(parentDir);
577
586
  // Write or append content
578
587
  const mode = params.mode ?? "overwrite";
579
588
  if (mode === "append") {
@@ -598,7 +607,7 @@ export class MemoryIndexManager {
598
607
  let relPath;
599
608
  if (params.destFolder) {
600
609
  // Explicit folder — use as-is (scope checking enforces access)
601
- relPath = `${params.destFolder}/${params.destFilename}`;
610
+ relPath = normalizePhoneInMemoryPath(`${params.destFolder}/${params.destFilename}`);
602
611
  }
603
612
  else {
604
613
  // Default: memory/users/{peer}/media/{filename}
@@ -629,7 +638,7 @@ export class MemoryIndexManager {
629
638
  }
630
639
  // Ensure parent directory exists
631
640
  const parentDir = path.dirname(absPath);
632
- await ensureDir(parentDir);
641
+ ensureDir(parentDir);
633
642
  // Copy the file
634
643
  await fs.copyFile(params.sourcePath, absPath);
635
644
  return { path: relPath, bytesWritten: sourceStat.size };
@@ -55,7 +55,7 @@ export async function downloadInboundMedia(msg, sock) {
55
55
  log.info(`audioMessage: ptt=${audio.ptt}, url=${audio.url ? "present" : "missing"}, ` +
56
56
  `directPath=${audio.directPath ? "present" : "missing"}, ` +
57
57
  `mediaKey=${audio.mediaKey ? "present" : "missing"}, ` +
58
- `fileLength=${audio.fileLength}, seconds=${audio.seconds}`);
58
+ `fileLength=${String(audio.fileLength)}, seconds=${String(audio.seconds)}`);
59
59
  }
60
60
  try {
61
61
  // Try standard download first
@@ -44,29 +44,6 @@ function attachLoginWaiter(accountId, login) {
44
44
  current.errorStatus = getStatusCode(err);
45
45
  });
46
46
  }
47
- async function restartLoginSocket(login, runtime) {
48
- if (login.restartAttempted)
49
- return false;
50
- login.restartAttempted = true;
51
- runtime.log(info("WhatsApp asked for a restart after pairing (code 515); retrying connection once…"));
52
- closeSocket(login.sock);
53
- try {
54
- const sock = await createWaSocket(false, login.verbose, {
55
- authDir: login.authDir,
56
- });
57
- login.sock = sock;
58
- login.connected = false;
59
- login.error = undefined;
60
- login.errorStatus = undefined;
61
- attachLoginWaiter(login.accountId, login);
62
- return true;
63
- }
64
- catch (err) {
65
- login.error = formatError(err);
66
- login.errorStatus = getStatusCode(err);
67
- return false;
68
- }
69
- }
70
47
  export async function startWebLoginWithQr(opts = {}) {
71
48
  const runtime = opts.runtime ?? defaultRuntime;
72
49
  const cfg = loadConfig();
@@ -59,7 +59,7 @@ function extractMessageText(message) {
59
59
  // Reactions are handled separately
60
60
  return "";
61
61
  default:
62
- return `<${message.type}>`;
62
+ return `<${String(message.type)}>`;
63
63
  }
64
64
  }
65
65
  /**
@@ -67,7 +67,7 @@ export function createWebhookHandlers(options) {
67
67
  try {
68
68
  const payload = req.body;
69
69
  if (payload.object !== "whatsapp_business_account") {
70
- log.warn(`Unexpected webhook object type: ${payload.object}`);
70
+ log.warn(`Unexpected webhook object type: ${String(payload.object)}`);
71
71
  return;
72
72
  }
73
73
  for (const entry of payload.entry) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.106",
3
+ "version": "1.0.108",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"