@rubytech/taskmaster 1.0.107 → 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.
- package/dist/agents/system-prompt.js +2 -0
- package/dist/agents/taskmaster-tools.js +5 -0
- package/dist/agents/tool-policy.js +2 -1
- package/dist/agents/tools/authorize-admin-tool.js +1 -1
- package/dist/agents/tools/software-update-tool.js +114 -0
- package/dist/auto-reply/reply/commands-status.js +5 -9
- package/dist/auto-reply/reply/get-reply-run.js +1 -1
- package/dist/auto-reply/reply/get-reply.js +1 -1
- package/dist/auto-reply/reply/model-selection.js +1 -1
- package/dist/browser/routes/screencast.js +1 -1
- package/dist/browser/screencast.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/commands/agent.js +2 -2
- package/dist/control-ui/assets/index-B2FEGOCu.css +1 -0
- package/dist/control-ui/assets/{index-B_zHmTQU.js → index-nLVF-pVT.js} +414 -369
- package/dist/control-ui/assets/index-nLVF-pVT.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/cron/isolated-agent/recipients.js +70 -0
- package/dist/cron/isolated-agent/run.js +43 -13
- package/dist/gateway/server-methods/access.js +3 -3
- package/dist/gateway/server-methods/browser-screencast.js +3 -3
- package/dist/gateway/server-methods/workspaces.js +7 -7
- package/dist/gateway/server.impl.js +1 -1
- package/dist/infra/heartbeat-runner.js +17 -0
- package/dist/infra/heartbeat-update-notify.js +120 -0
- package/dist/infra/tunnel.js +1 -1
- package/dist/memory/embeddings.js +0 -4
- package/dist/memory/manager.js +3 -3
- package/dist/web/inbound/media.js +1 -1
- package/dist/web/login-qr.js +0 -23
- package/dist/web/providers/cloud/receive.js +1 -1
- package/dist/web/providers/cloud/webhook.js +1 -1
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +17 -39
- package/dist/control-ui/assets/index-2XyxmiR6.css +0 -1
- package/dist/control-ui/assets/index-B_zHmTQU.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-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-nLVF-pVT.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B2FEGOCu.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<taskmaster-app></taskmaster-app>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract admin phone numbers from config bindings.
|
|
3
|
+
* Admin phones are bindings where agentId is "admin" (or legacy "management"),
|
|
4
|
+
* channel is "whatsapp", and peer.kind is "dm".
|
|
5
|
+
*/
|
|
6
|
+
export function resolveAdminBindingPhones(cfg, accountId) {
|
|
7
|
+
const bindings = Array.isArray(cfg.bindings) ? cfg.bindings : [];
|
|
8
|
+
const phones = [];
|
|
9
|
+
for (const binding of bindings) {
|
|
10
|
+
if (!binding || typeof binding !== "object")
|
|
11
|
+
continue;
|
|
12
|
+
const agentId = binding.agentId;
|
|
13
|
+
if (agentId !== "admin" && agentId !== "management")
|
|
14
|
+
continue;
|
|
15
|
+
const match = binding.match;
|
|
16
|
+
if (!match || typeof match !== "object")
|
|
17
|
+
continue;
|
|
18
|
+
const channel = typeof match.channel === "string" ? match.channel.toLowerCase() : "";
|
|
19
|
+
if (channel !== "whatsapp")
|
|
20
|
+
continue;
|
|
21
|
+
// Filter by accountId if specified
|
|
22
|
+
if (accountId && typeof match.accountId === "string" && match.accountId !== accountId)
|
|
23
|
+
continue;
|
|
24
|
+
const peer = match.peer;
|
|
25
|
+
if (!peer || typeof peer !== "object")
|
|
26
|
+
continue;
|
|
27
|
+
if (peer.kind !== "dm")
|
|
28
|
+
continue;
|
|
29
|
+
const id = typeof peer.id === "string" ? peer.id.trim() : "";
|
|
30
|
+
if (id)
|
|
31
|
+
phones.push(id);
|
|
32
|
+
}
|
|
33
|
+
return phones;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Expand the `to` field from a cron payload into individual recipient addresses.
|
|
37
|
+
*
|
|
38
|
+
* Supports:
|
|
39
|
+
* - Single phone: "+15551234567"
|
|
40
|
+
* - Comma-separated phones: "+15551234567,+15559876543"
|
|
41
|
+
* - "admins" token: resolves to all admin binding phones for the account
|
|
42
|
+
* - Mixed: "admins,+15559999999"
|
|
43
|
+
*
|
|
44
|
+
* Returns deduplicated list of phone numbers/addresses.
|
|
45
|
+
* Returns empty array if `to` is empty/undefined.
|
|
46
|
+
*/
|
|
47
|
+
export function expandCronRecipients(cfg, to, accountId) {
|
|
48
|
+
if (!to?.trim())
|
|
49
|
+
return [];
|
|
50
|
+
const parts = to.split(",").map((s) => s.trim()).filter(Boolean);
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const result = [];
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
if (part.toLowerCase() === "admins") {
|
|
55
|
+
for (const phone of resolveAdminBindingPhones(cfg, accountId)) {
|
|
56
|
+
if (!seen.has(phone)) {
|
|
57
|
+
seen.add(phone);
|
|
58
|
+
result.push(phone);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (!seen.has(part)) {
|
|
64
|
+
seen.add(part);
|
|
65
|
+
result.push(part);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -18,10 +18,12 @@ import { createOutboundSendDeps } from "../../cli/outbound-send-deps.js";
|
|
|
18
18
|
import { resolveSessionTranscriptPath, updateSessionStore } from "../../config/sessions.js";
|
|
19
19
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
|
20
20
|
import { deliverOutboundPayloads } from "../../infra/outbound/deliver.js";
|
|
21
|
+
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
|
21
22
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
|
22
23
|
import { buildAgentMainSessionKey, normalizeAgentId } from "../../routing/session-key.js";
|
|
23
24
|
import { resolveAgentBoundAccountId } from "../../routing/bindings.js";
|
|
24
25
|
import { resolveDeliveryTarget } from "./delivery-target.js";
|
|
26
|
+
import { expandCronRecipients } from "./recipients.js";
|
|
25
27
|
import { isHeartbeatOnlyResponse, pickLastNonEmptyTextFromPayloads, pickSummaryFromOutput, pickSummaryFromPayloads, resolveHeartbeatAckMaxChars, } from "./helpers.js";
|
|
26
28
|
import { resolveCronSession } from "./session.js";
|
|
27
29
|
function matchesMessagingToolDeliveryTarget(target, delivery) {
|
|
@@ -308,7 +310,15 @@ export async function runCronIsolatedAgentTurn(params) {
|
|
|
308
310
|
accountId: resolvedDelivery.accountId,
|
|
309
311
|
}));
|
|
310
312
|
if (deliveryRequested && !skipHeartbeatDelivery && !skipMessagingToolDelivery) {
|
|
311
|
-
|
|
313
|
+
// Expand multi-recipient targets (comma-separated, "admins" token).
|
|
314
|
+
const expandedRecipients = expandCronRecipients(cfgWithAgentDefaults, agentPayload?.to, effectiveAccountId);
|
|
315
|
+
// Use expanded list if multi-target, otherwise fall back to the resolved single target.
|
|
316
|
+
const deliveryTargets = expandedRecipients.length > 1
|
|
317
|
+
? expandedRecipients
|
|
318
|
+
: resolvedDelivery.to
|
|
319
|
+
? [resolvedDelivery.to]
|
|
320
|
+
: [];
|
|
321
|
+
if (deliveryTargets.length === 0) {
|
|
312
322
|
const reason = resolvedDelivery.error?.message ?? "Cron delivery requires a recipient (--to).";
|
|
313
323
|
if (!bestEffortDeliver) {
|
|
314
324
|
return {
|
|
@@ -324,22 +334,42 @@ export async function runCronIsolatedAgentTurn(params) {
|
|
|
324
334
|
outputText,
|
|
325
335
|
};
|
|
326
336
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
337
|
+
const errors = [];
|
|
338
|
+
for (const targetTo of deliveryTargets) {
|
|
339
|
+
// Validate each target against the channel.
|
|
340
|
+
const docked = resolveOutboundTarget({
|
|
330
341
|
channel: resolvedDelivery.channel,
|
|
331
|
-
to:
|
|
342
|
+
to: targetTo,
|
|
343
|
+
cfg: cfgWithAgentDefaults,
|
|
332
344
|
accountId: resolvedDelivery.accountId,
|
|
333
|
-
|
|
334
|
-
bestEffort: bestEffortDeliver,
|
|
335
|
-
deps: createOutboundSendDeps(params.deps),
|
|
345
|
+
mode: "explicit",
|
|
336
346
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
347
|
+
if (!docked.ok) {
|
|
348
|
+
errors.push(`${targetTo}: ${docked.error.message}`);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await deliverOutboundPayloads({
|
|
353
|
+
cfg: cfgWithAgentDefaults,
|
|
354
|
+
channel: resolvedDelivery.channel,
|
|
355
|
+
to: docked.to,
|
|
356
|
+
accountId: resolvedDelivery.accountId,
|
|
357
|
+
payloads,
|
|
358
|
+
bestEffort: bestEffortDeliver,
|
|
359
|
+
deps: createOutboundSendDeps(params.deps),
|
|
360
|
+
});
|
|
341
361
|
}
|
|
342
|
-
|
|
362
|
+
catch (err) {
|
|
363
|
+
errors.push(`${targetTo}: ${String(err)}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (errors.length > 0 && !bestEffortDeliver) {
|
|
367
|
+
return {
|
|
368
|
+
status: "error",
|
|
369
|
+
summary,
|
|
370
|
+
outputText,
|
|
371
|
+
error: errors.join("; "),
|
|
372
|
+
};
|
|
343
373
|
}
|
|
344
374
|
}
|
|
345
375
|
return { status: "ok", summary, outputText };
|
|
@@ -176,7 +176,7 @@ export const accessHandlers = {
|
|
|
176
176
|
await writeConfigFile({
|
|
177
177
|
...config,
|
|
178
178
|
access: {
|
|
179
|
-
...
|
|
179
|
+
...config.access,
|
|
180
180
|
masterPin: pin,
|
|
181
181
|
},
|
|
182
182
|
});
|
|
@@ -210,9 +210,9 @@ export const accessHandlers = {
|
|
|
210
210
|
await writeConfigFile({
|
|
211
211
|
...config,
|
|
212
212
|
access: {
|
|
213
|
-
...
|
|
213
|
+
...config.access,
|
|
214
214
|
pins: {
|
|
215
|
-
...
|
|
215
|
+
...config.access?.pins,
|
|
216
216
|
[workspace]: pin,
|
|
217
217
|
},
|
|
218
218
|
},
|
|
@@ -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
|
}
|
|
@@ -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,
|
|
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
|
-
...
|
|
427
|
+
...cfg.channels,
|
|
428
428
|
whatsapp: {
|
|
429
|
-
...
|
|
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: { ...
|
|
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 = { ...
|
|
585
|
+
const whatsappConfig = { ...cfg.channels?.whatsapp };
|
|
586
586
|
delete whatsappConfig.accounts;
|
|
587
587
|
cfg = {
|
|
588
588
|
...cfg,
|
|
589
589
|
channels: {
|
|
590
|
-
...
|
|
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: { ...
|
|
643
|
+
cfg = { ...cfg, workspaces: { ...cfg.workspaces, [name]: { displayName } } };
|
|
644
644
|
try {
|
|
645
645
|
await writeConfigFile(cfg);
|
|
646
646
|
}
|
|
@@ -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
|
+
}
|
package/dist/infra/tunnel.js
CHANGED
|
@@ -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.
|
package/dist/memory/manager.js
CHANGED
|
@@ -94,7 +94,7 @@ function matchGlobPattern(pattern, filePath) {
|
|
|
94
94
|
// Replace ** with a placeholder, then * with [^/]*, then placeholder with .*
|
|
95
95
|
regexStr = regexStr.replace(/\*\*/g, "\0");
|
|
96
96
|
regexStr = regexStr.replace(/\*/g, "[^/]*");
|
|
97
|
-
regexStr = regexStr.
|
|
97
|
+
regexStr = regexStr.replaceAll("\0", ".*");
|
|
98
98
|
const regex = new RegExp(`^${regexStr}$`, "i");
|
|
99
99
|
return regex.test(filePath);
|
|
100
100
|
}
|
|
@@ -582,7 +582,7 @@ export class MemoryIndexManager {
|
|
|
582
582
|
}
|
|
583
583
|
// Ensure parent directory exists
|
|
584
584
|
const parentDir = path.dirname(absPath);
|
|
585
|
-
|
|
585
|
+
ensureDir(parentDir);
|
|
586
586
|
// Write or append content
|
|
587
587
|
const mode = params.mode ?? "overwrite";
|
|
588
588
|
if (mode === "append") {
|
|
@@ -638,7 +638,7 @@ export class MemoryIndexManager {
|
|
|
638
638
|
}
|
|
639
639
|
// Ensure parent directory exists
|
|
640
640
|
const parentDir = path.dirname(absPath);
|
|
641
|
-
|
|
641
|
+
ensureDir(parentDir);
|
|
642
642
|
// Copy the file
|
|
643
643
|
await fs.copyFile(params.sourcePath, absPath);
|
|
644
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
|
package/dist/web/login-qr.js
CHANGED
|
@@ -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();
|
|
@@ -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) {
|