@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.
- package/dist/agents/skills-status.js +23 -3
- package/dist/agents/skills.js +1 -0
- package/dist/agents/system-prompt.js +3 -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/memory-tool.js +2 -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/config/zod-schema.js +12 -1
- package/dist/control-ui/assets/index-B2FEGOCu.css +1 -0
- package/dist/control-ui/assets/index-nLVF-pVT.js +3762 -0
- package/dist/control-ui/assets/index-nLVF-pVT.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/control-ui/maxy-icon.png +0 -0
- package/dist/cron/isolated-agent/recipients.js +70 -0
- package/dist/cron/isolated-agent/run.js +43 -13
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/control-ui.js +111 -5
- package/dist/gateway/protocol/index.js +6 -1
- package/dist/gateway/protocol/schema/agents-models-skills.js +23 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +6 -1
- package/dist/gateway/server-http.js +6 -1
- package/dist/gateway/server-methods/access.js +3 -3
- package/dist/gateway/server-methods/brand.js +160 -0
- package/dist/gateway/server-methods/browser-screencast.js +3 -3
- package/dist/gateway/server-methods/skills.js +159 -3
- package/dist/gateway/server-methods/workspaces.js +7 -7
- package/dist/gateway/server-methods-list.js +5 -0
- package/dist/gateway/server-methods.js +2 -0
- 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 +15 -6
- 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/skills/skill-builder/SKILL.md +97 -0
- package/skills/skill-builder/references/lean-pattern.md +118 -0
- package/skills/zero-to-prototype/SKILL.md +35 -0
- package/skills/zero-to-prototype/references/discovery.md +64 -0
- package/skills/zero-to-prototype/references/prd.md +83 -0
- package/skills/zero-to-prototype/references/validation.md +67 -0
- package/taskmaster-docs/USER-GUIDE.md +65 -31
- package/templates/customer/agents/public/AGENTS.md +3 -10
- package/templates/taskmaster/agents/public/SOUL.md +0 -4
- package/templates/tradesupport/agents/public/AGENTS.md +3 -10
- package/dist/control-ui/assets/index-DjhCZlZd.css +0 -1
- package/dist/control-ui/assets/index-DtuDNTAC.js +0 -3539
- package/dist/control-ui/assets/index-DtuDNTAC.js.map +0 -1
- 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
|
|
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,
|
|
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
|
}
|
|
@@ -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
|
+
}
|
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
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
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) {
|