@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
|
@@ -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>
|
|
Binary file
|
|
@@ -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 };
|
|
@@ -45,6 +45,7 @@ const BASE_RELOAD_RULES_TAIL = [
|
|
|
45
45
|
{ prefix: "license", kind: "none" },
|
|
46
46
|
{ prefix: "plugins", kind: "restart" },
|
|
47
47
|
{ prefix: "ui", kind: "none" },
|
|
48
|
+
{ prefix: "workspaces", kind: "none" },
|
|
48
49
|
{ prefix: "gateway", kind: "restart" },
|
|
49
50
|
{ prefix: "discovery", kind: "restart" },
|
|
50
51
|
{ prefix: "canvasHost", kind: "restart" },
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { resolveAgentWorkspaceRoot } from "../agents/agent-scope.js";
|
|
5
|
+
import { buildAgentSummaries } from "../commands/agents.config.js";
|
|
4
6
|
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
|
|
5
7
|
import { resolvePublicAgentId } from "./public-chat/session.js";
|
|
8
|
+
import { findBrandLogo } from "./server-methods/brand.js";
|
|
6
9
|
import { buildControlUiAvatarUrl, CONTROL_UI_AVATAR_PREFIX, normalizeControlUiBasePath, resolveAssistantAvatarUrl, } from "./control-ui-shared.js";
|
|
7
10
|
const ROOT_PREFIX = "/";
|
|
8
11
|
function resolveControlUiRoot() {
|
|
@@ -132,7 +135,7 @@ const DEFAULT_BRAND_NAME = "Taskmaster";
|
|
|
132
135
|
const DEFAULT_BRAND_ICON_URL = "/taskmaster-icon.png";
|
|
133
136
|
const DEFAULT_ACCENT_COLOR = "#00d4ff";
|
|
134
137
|
function injectControlUiConfig(html, opts) {
|
|
135
|
-
const { basePath, assistantName, assistantAvatar, brandName, brandIconUrl, accentColor } = opts;
|
|
138
|
+
const { basePath, assistantName, assistantAvatar, brandName, brandIconUrl, accentColor, backgroundColor, workspaceBrands, } = opts;
|
|
136
139
|
const script = `<script>` +
|
|
137
140
|
`window.__TASKMASTER_CONTROL_UI_BASE_PATH__=${JSON.stringify(basePath)};` +
|
|
138
141
|
`window.__TASKMASTER_ASSISTANT_NAME__=${JSON.stringify(assistantName ?? DEFAULT_ASSISTANT_IDENTITY.name)};` +
|
|
@@ -140,6 +143,10 @@ function injectControlUiConfig(html, opts) {
|
|
|
140
143
|
`window.__TASKMASTER_BRAND_NAME__=${JSON.stringify(brandName ?? DEFAULT_BRAND_NAME)};` +
|
|
141
144
|
`window.__TASKMASTER_BRAND_ICON_URL__=${JSON.stringify(brandIconUrl ?? DEFAULT_BRAND_ICON_URL)};` +
|
|
142
145
|
`window.__TASKMASTER_ACCENT_COLOR__=${JSON.stringify(accentColor ?? DEFAULT_ACCENT_COLOR)};` +
|
|
146
|
+
`window.__TASKMASTER_BACKGROUND_COLOR__=${JSON.stringify(backgroundColor ?? "")};` +
|
|
147
|
+
(workspaceBrands
|
|
148
|
+
? `window.__TASKMASTER_WORKSPACE_BRANDS__=${JSON.stringify(workspaceBrands)};`
|
|
149
|
+
: "") +
|
|
143
150
|
`</script>`;
|
|
144
151
|
// Check if already injected
|
|
145
152
|
if (html.includes("__TASKMASTER_ASSISTANT_NAME__"))
|
|
@@ -163,6 +170,37 @@ function resolveBrandIconUrl(iconKey, uiRoot) {
|
|
|
163
170
|
return "/brand-icon.png";
|
|
164
171
|
return DEFAULT_BRAND_ICON_URL;
|
|
165
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Build a map of workspace name → brand config for injection into the client.
|
|
175
|
+
* Includes both config colours and whether a logo file exists on disk.
|
|
176
|
+
*/
|
|
177
|
+
function buildWorkspaceBrands(config) {
|
|
178
|
+
const wsConfig = config.workspaces ?? {};
|
|
179
|
+
const result = {};
|
|
180
|
+
const summaries = buildAgentSummaries(config);
|
|
181
|
+
const seen = new Set();
|
|
182
|
+
for (const summary of summaries) {
|
|
183
|
+
const root = resolveAgentWorkspaceRoot(config, summary.id);
|
|
184
|
+
const name = path.basename(root);
|
|
185
|
+
if (seen.has(name))
|
|
186
|
+
continue;
|
|
187
|
+
seen.add(name);
|
|
188
|
+
const wsBrand = wsConfig[name]?.brand;
|
|
189
|
+
const logoPath = findBrandLogo(root);
|
|
190
|
+
const entry = {};
|
|
191
|
+
if (wsBrand?.accentColor)
|
|
192
|
+
entry.accentColor = wsBrand.accentColor;
|
|
193
|
+
if (wsBrand?.backgroundColor)
|
|
194
|
+
entry.backgroundColor = wsBrand.backgroundColor;
|
|
195
|
+
if (logoPath)
|
|
196
|
+
entry.logoUrl = `/brand-logo/${encodeURIComponent(name)}`;
|
|
197
|
+
// Only include if there's something to inject
|
|
198
|
+
if (Object.keys(entry).length > 0) {
|
|
199
|
+
result[name] = entry;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
166
204
|
function serveIndexHtml(res, indexPath, opts) {
|
|
167
205
|
const { basePath, config, agentId } = opts;
|
|
168
206
|
const identity = config
|
|
@@ -180,6 +218,8 @@ function serveIndexHtml(res, indexPath, opts) {
|
|
|
180
218
|
const brandName = config?.ui?.brand?.name;
|
|
181
219
|
const brandIconUrl = resolveBrandIconUrl(config?.ui?.brand?.icon, uiRoot);
|
|
182
220
|
const accentColor = config?.ui?.seamColor;
|
|
221
|
+
// Build workspace brand map for the control panel
|
|
222
|
+
const workspaceBrands = config ? buildWorkspaceBrands(config) : undefined;
|
|
183
223
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
184
224
|
res.setHeader("Cache-Control", "no-cache");
|
|
185
225
|
const raw = fs.readFileSync(indexPath, "utf8");
|
|
@@ -190,6 +230,7 @@ function serveIndexHtml(res, indexPath, opts) {
|
|
|
190
230
|
brandName,
|
|
191
231
|
brandIconUrl,
|
|
192
232
|
accentColor,
|
|
233
|
+
workspaceBrands,
|
|
193
234
|
}));
|
|
194
235
|
}
|
|
195
236
|
function isSafeRelativePath(relPath) {
|
|
@@ -346,6 +387,55 @@ export function handleBrandIconRequest(req, res, opts) {
|
|
|
346
387
|
serveFile(res, filePath);
|
|
347
388
|
return true;
|
|
348
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Dynamic route for `/brand-logo/:workspace`.
|
|
392
|
+
* Serves the uploaded brand logo from the workspace directory.
|
|
393
|
+
*/
|
|
394
|
+
export function handleBrandLogoRequest(req, res, opts) {
|
|
395
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
396
|
+
if (!url.pathname.startsWith("/brand-logo/"))
|
|
397
|
+
return false;
|
|
398
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
399
|
+
return false;
|
|
400
|
+
const workspace = decodeURIComponent(url.pathname.slice("/brand-logo/".length)).split("/")[0];
|
|
401
|
+
if (!workspace || !/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(workspace)) {
|
|
402
|
+
respondNotFound(res);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
const config = opts?.config;
|
|
406
|
+
if (!config) {
|
|
407
|
+
respondNotFound(res);
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
// Resolve workspace root by name
|
|
411
|
+
let workspaceDir = null;
|
|
412
|
+
const summaries = buildAgentSummaries(config);
|
|
413
|
+
for (const summary of summaries) {
|
|
414
|
+
const root = resolveAgentWorkspaceRoot(config, summary.id);
|
|
415
|
+
if (path.basename(root) === workspace) {
|
|
416
|
+
workspaceDir = root;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!workspaceDir) {
|
|
421
|
+
respondNotFound(res);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
const logoPath = findBrandLogo(workspaceDir);
|
|
425
|
+
if (!logoPath) {
|
|
426
|
+
respondNotFound(res);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
if (req.method === "HEAD") {
|
|
430
|
+
res.statusCode = 200;
|
|
431
|
+
res.setHeader("Content-Type", contentTypeForExt(path.extname(logoPath).toLowerCase()));
|
|
432
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
433
|
+
res.end();
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
serveFile(res, logoPath);
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
349
439
|
/**
|
|
350
440
|
* Serve `/public/chat` — the same SPA but with public-chat flags injected.
|
|
351
441
|
*/
|
|
@@ -439,15 +529,30 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
439
529
|
const cookieTtlDays = config.publicChat.cookieTtlDays ?? 30;
|
|
440
530
|
const brandName = config.ui?.brand?.name;
|
|
441
531
|
const brandIconUrl = resolveBrandIconUrl(config.ui?.brand?.icon, root);
|
|
442
|
-
|
|
532
|
+
// Resolve workspace-level brand — overrides device-level seamColor
|
|
533
|
+
const wsBrand = config.workspaces?.[accountId]?.brand;
|
|
534
|
+
const accentColor = wsBrand?.accentColor ?? config.ui?.seamColor;
|
|
535
|
+
const backgroundColor = wsBrand?.backgroundColor;
|
|
536
|
+
// Resolve workspace logo URL
|
|
537
|
+
const wsLogoUrl = (() => {
|
|
538
|
+
const summaries = buildAgentSummaries(config);
|
|
539
|
+
for (const s of summaries) {
|
|
540
|
+
const wsRoot = resolveAgentWorkspaceRoot(config, s.id);
|
|
541
|
+
if (path.basename(wsRoot) === accountId && findBrandLogo(wsRoot)) {
|
|
542
|
+
return `/brand-logo/${encodeURIComponent(accountId)}`;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
})();
|
|
443
547
|
const raw = fs.readFileSync(indexPath, "utf8");
|
|
444
548
|
const injected = injectControlUiConfig(raw, {
|
|
445
549
|
basePath: "",
|
|
446
550
|
assistantName: identity.name,
|
|
447
551
|
assistantAvatar: avatarValue,
|
|
448
552
|
brandName,
|
|
449
|
-
brandIconUrl,
|
|
553
|
+
brandIconUrl: wsLogoUrl ?? brandIconUrl,
|
|
450
554
|
accentColor,
|
|
555
|
+
backgroundColor,
|
|
451
556
|
});
|
|
452
557
|
// Inject <base href="/"> right after <head> so relative asset paths (./assets/...)
|
|
453
558
|
// resolve from root. The URL is /public/chat/:accountId — 3 levels deep, so without
|
|
@@ -474,7 +579,7 @@ export function handlePublicChatHttpRequest(req, res, opts) {
|
|
|
474
579
|
/** Widget script content — self-contained JS for embedding. */
|
|
475
580
|
const WIDGET_SCRIPT = `(function(){
|
|
476
581
|
"use strict";
|
|
477
|
-
var cfg={server:"",accountId:"",color:"#1a1a2e"};
|
|
582
|
+
var cfg={server:"",accountId:"",color:"#1a1a2e",bgColor:"#1a1a2e"};
|
|
478
583
|
var isOpen=false;
|
|
479
584
|
var btn,overlay,iframe;
|
|
480
585
|
|
|
@@ -482,6 +587,7 @@ const WIDGET_SCRIPT = `(function(){
|
|
|
482
587
|
if(opts&&opts.server) cfg.server=opts.server.replace(/\\/$/,"");
|
|
483
588
|
if(opts&&opts.accountId) cfg.accountId=opts.accountId;
|
|
484
589
|
if(opts&&opts.color) cfg.color=opts.color;
|
|
590
|
+
if(opts&&opts.bgColor) cfg.bgColor=opts.bgColor;
|
|
485
591
|
build();
|
|
486
592
|
}
|
|
487
593
|
|
|
@@ -496,7 +602,7 @@ const WIDGET_SCRIPT = `(function(){
|
|
|
496
602
|
".tm-widget-overlay{position:fixed;bottom:78px;right:20px;width:400px;height:600px;",
|
|
497
603
|
"max-width:calc(100vw - 40px);max-height:calc(100vh - 98px);",
|
|
498
604
|
"border-radius:12px;overflow:hidden;box-shadow:0 8px 30px rgba(0,0,0,.3);",
|
|
499
|
-
"z-index:999998;display:none;background
|
|
605
|
+
"z-index:999998;display:none;background:"+cfg.bgColor+"}",
|
|
500
606
|
".tm-widget-overlay.open{display:block}",
|
|
501
607
|
".tm-widget-iframe{width:100%;height:100%;border:none}",
|
|
502
608
|
"@media(max-width:480px){",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AjvPkg from "ajv";
|
|
2
|
-
import { AgentEventSchema, AgentIdentityParamsSchema, AgentIdentityResultSchema, AgentParamsSchema, AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, AgentWaitParamsSchema, ChannelsLogoutParamsSchema, ChannelsStatusParamsSchema, ChannelsStatusResultSchema, ChatAbortParamsSchema, ChatEventSchema, ChatHistoryParamsSchema, ChatInjectParamsSchema, ChatSendParamsSchema, ConfigApplyParamsSchema, ConfigGetParamsSchema, ConfigPatchParamsSchema, ConfigSchemaParamsSchema, ConfigSchemaResponseSchema, ConfigSetParamsSchema, ConnectParamsSchema, CronAddParamsSchema, CronJobSchema, CronListParamsSchema, CronRemoveParamsSchema, CronRunParamsSchema, CronRunsParamsSchema, CronStatusParamsSchema, CronUpdateParamsSchema, DevicePairApproveParamsSchema, DevicePairListParamsSchema, DevicePairRejectParamsSchema, DeviceTokenRevokeParamsSchema, DeviceTokenRotateParamsSchema, ExecApprovalsGetParamsSchema, ExecApprovalsNodeGetParamsSchema, ExecApprovalsNodeSetParamsSchema, ExecApprovalsSetParamsSchema, ExecApprovalRequestParamsSchema, ExecApprovalResolveParamsSchema, ErrorCodes, ErrorShapeSchema, EventFrameSchema, errorShape, GatewayFrameSchema, HelloOkSchema, LogsTailParamsSchema, LogsTailResultSchema, SessionsTranscriptParamsSchema, SessionsTranscriptResultSchema, ModelsListParamsSchema, NodeDescribeParamsSchema, NodeEventParamsSchema, NodeInvokeParamsSchema, NodeInvokeResultParamsSchema, NodeListParamsSchema, NodePairApproveParamsSchema, NodePairListParamsSchema, NodePairRejectParamsSchema, NodePairRequestParamsSchema, NodePairVerifyParamsSchema, NodeRenameParamsSchema, PollParamsSchema, PROTOCOL_VERSION, PresenceEntrySchema, ProtocolSchemas, RequestFrameSchema, ResponseFrameSchema, SendParamsSchema, SessionsCompactParamsSchema, SessionsDeleteParamsSchema, SessionsListParamsSchema, SessionsPatchParamsSchema, SessionsPreviewParamsSchema, SessionsResetParamsSchema, SessionsResolveParamsSchema, ShutdownEventSchema, SkillsBinsParamsSchema, SkillsInstallParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, SnapshotSchema, StateVersionSchema, TalkModeParamsSchema, TickEventSchema, UpdateRunParamsSchema, WakeParamsSchema, WebLoginStartParamsSchema, WebLoginWaitParamsSchema, WizardCancelParamsSchema, WizardNextParamsSchema, WizardNextResultSchema, WizardStartParamsSchema, WizardStartResultSchema, WizardStatusParamsSchema, WizardStatusResultSchema, WizardStepSchema, } from "./schema.js";
|
|
2
|
+
import { AgentEventSchema, AgentIdentityParamsSchema, AgentIdentityResultSchema, AgentParamsSchema, AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, AgentWaitParamsSchema, ChannelsLogoutParamsSchema, ChannelsStatusParamsSchema, ChannelsStatusResultSchema, ChatAbortParamsSchema, ChatEventSchema, ChatHistoryParamsSchema, ChatInjectParamsSchema, ChatSendParamsSchema, ConfigApplyParamsSchema, ConfigGetParamsSchema, ConfigPatchParamsSchema, ConfigSchemaParamsSchema, ConfigSchemaResponseSchema, ConfigSetParamsSchema, ConnectParamsSchema, CronAddParamsSchema, CronJobSchema, CronListParamsSchema, CronRemoveParamsSchema, CronRunParamsSchema, CronRunsParamsSchema, CronStatusParamsSchema, CronUpdateParamsSchema, DevicePairApproveParamsSchema, DevicePairListParamsSchema, DevicePairRejectParamsSchema, DeviceTokenRevokeParamsSchema, DeviceTokenRotateParamsSchema, ExecApprovalsGetParamsSchema, ExecApprovalsNodeGetParamsSchema, ExecApprovalsNodeSetParamsSchema, ExecApprovalsSetParamsSchema, ExecApprovalRequestParamsSchema, ExecApprovalResolveParamsSchema, ErrorCodes, ErrorShapeSchema, EventFrameSchema, errorShape, GatewayFrameSchema, HelloOkSchema, LogsTailParamsSchema, LogsTailResultSchema, SessionsTranscriptParamsSchema, SessionsTranscriptResultSchema, ModelsListParamsSchema, NodeDescribeParamsSchema, NodeEventParamsSchema, NodeInvokeParamsSchema, NodeInvokeResultParamsSchema, NodeListParamsSchema, NodePairApproveParamsSchema, NodePairListParamsSchema, NodePairRejectParamsSchema, NodePairRequestParamsSchema, NodePairVerifyParamsSchema, NodeRenameParamsSchema, PollParamsSchema, PROTOCOL_VERSION, PresenceEntrySchema, ProtocolSchemas, RequestFrameSchema, ResponseFrameSchema, SendParamsSchema, SessionsCompactParamsSchema, SessionsDeleteParamsSchema, SessionsListParamsSchema, SessionsPatchParamsSchema, SessionsPreviewParamsSchema, SessionsResetParamsSchema, SessionsResolveParamsSchema, ShutdownEventSchema, SkillsBinsParamsSchema, SkillsCreateParamsSchema, SkillsDeleteParamsSchema, SkillsDeleteDraftParamsSchema, SkillsDraftsParamsSchema, SkillsInstallParamsSchema, SkillsReadParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, SnapshotSchema, StateVersionSchema, TalkModeParamsSchema, TickEventSchema, UpdateRunParamsSchema, WakeParamsSchema, WebLoginStartParamsSchema, WebLoginWaitParamsSchema, WizardCancelParamsSchema, WizardNextParamsSchema, WizardNextResultSchema, WizardStartParamsSchema, WizardStartResultSchema, WizardStatusParamsSchema, WizardStatusResultSchema, WizardStepSchema, } from "./schema.js";
|
|
3
3
|
const ajv = new AjvPkg({
|
|
4
4
|
allErrors: true,
|
|
5
5
|
strict: false,
|
|
@@ -51,6 +51,11 @@ export const validateSkillsStatusParams = ajv.compile(SkillsStatusParamsSchema);
|
|
|
51
51
|
export const validateSkillsBinsParams = ajv.compile(SkillsBinsParamsSchema);
|
|
52
52
|
export const validateSkillsInstallParams = ajv.compile(SkillsInstallParamsSchema);
|
|
53
53
|
export const validateSkillsUpdateParams = ajv.compile(SkillsUpdateParamsSchema);
|
|
54
|
+
export const validateSkillsReadParams = ajv.compile(SkillsReadParamsSchema);
|
|
55
|
+
export const validateSkillsCreateParams = ajv.compile(SkillsCreateParamsSchema);
|
|
56
|
+
export const validateSkillsDeleteParams = ajv.compile(SkillsDeleteParamsSchema);
|
|
57
|
+
export const validateSkillsDraftsParams = ajv.compile(SkillsDraftsParamsSchema);
|
|
58
|
+
export const validateSkillsDeleteDraftParams = ajv.compile(SkillsDeleteDraftParamsSchema);
|
|
54
59
|
export const validateCronListParams = ajv.compile(CronListParamsSchema);
|
|
55
60
|
export const validateCronStatusParams = ajv.compile(CronStatusParamsSchema);
|
|
56
61
|
export const validateCronAddParams = ajv.compile(CronAddParamsSchema);
|
|
@@ -45,3 +45,26 @@ export const SkillsUpdateParamsSchema = Type.Object({
|
|
|
45
45
|
apiKey: Type.Optional(Type.String()),
|
|
46
46
|
env: Type.Optional(Type.Record(NonEmptyString, Type.String())),
|
|
47
47
|
}, { additionalProperties: false });
|
|
48
|
+
export const SkillsReadParamsSchema = Type.Object({
|
|
49
|
+
name: NonEmptyString,
|
|
50
|
+
}, { additionalProperties: false });
|
|
51
|
+
const SkillReferenceFileSchema = Type.Object({
|
|
52
|
+
name: NonEmptyString,
|
|
53
|
+
content: Type.String(),
|
|
54
|
+
}, { additionalProperties: false });
|
|
55
|
+
export const SkillsCreateParamsSchema = Type.Object({
|
|
56
|
+
name: Type.String({
|
|
57
|
+
pattern: "^[a-z0-9][a-z0-9_-]*$",
|
|
58
|
+
minLength: 1,
|
|
59
|
+
description: "Skill directory name (lowercase, hyphens, underscores)",
|
|
60
|
+
}),
|
|
61
|
+
skillContent: NonEmptyString,
|
|
62
|
+
references: Type.Optional(Type.Array(SkillReferenceFileSchema)),
|
|
63
|
+
}, { additionalProperties: false });
|
|
64
|
+
export const SkillsDeleteParamsSchema = Type.Object({
|
|
65
|
+
name: NonEmptyString,
|
|
66
|
+
}, { additionalProperties: false });
|
|
67
|
+
export const SkillsDraftsParamsSchema = Type.Object({}, { additionalProperties: false });
|
|
68
|
+
export const SkillsDeleteDraftParamsSchema = Type.Object({
|
|
69
|
+
name: NonEmptyString,
|
|
70
|
+
}, { additionalProperties: false });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgentEventSchema, AgentIdentityParamsSchema, AgentIdentityResultSchema, AgentParamsSchema, AgentWaitParamsSchema, PollParamsSchema, SendParamsSchema, WakeParamsSchema, } from "./agent.js";
|
|
2
|
-
import { AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, ModelChoiceSchema, ModelsListParamsSchema, ModelsListResultSchema, SkillsBinsParamsSchema, SkillsBinsResultSchema, SkillsInstallParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, } from "./agents-models-skills.js";
|
|
2
|
+
import { AgentSummarySchema, AgentsListParamsSchema, AgentsListResultSchema, ModelChoiceSchema, ModelsListParamsSchema, ModelsListResultSchema, SkillsBinsParamsSchema, SkillsBinsResultSchema, SkillsCreateParamsSchema, SkillsDeleteParamsSchema, SkillsDeleteDraftParamsSchema, SkillsDraftsParamsSchema, SkillsInstallParamsSchema, SkillsReadParamsSchema, SkillsStatusParamsSchema, SkillsUpdateParamsSchema, } from "./agents-models-skills.js";
|
|
3
3
|
import { ChannelsLogoutParamsSchema, ChannelsStatusParamsSchema, ChannelsStatusResultSchema, TalkModeParamsSchema, WebLoginStartParamsSchema, WebLoginWaitParamsSchema, } from "./channels.js";
|
|
4
4
|
import { ConfigApplyParamsSchema, ConfigGetParamsSchema, ConfigPatchParamsSchema, ConfigSchemaParamsSchema, ConfigSchemaResponseSchema, ConfigSetParamsSchema, UpdateRunParamsSchema, } from "./config.js";
|
|
5
5
|
import { CronAddParamsSchema, CronJobSchema, CronListParamsSchema, CronRemoveParamsSchema, CronRunLogEntrySchema, CronRunParamsSchema, CronRunsParamsSchema, CronStatusParamsSchema, CronUpdateParamsSchema, } from "./cron.js";
|
|
@@ -80,6 +80,11 @@ export const ProtocolSchemas = {
|
|
|
80
80
|
SkillsBinsResult: SkillsBinsResultSchema,
|
|
81
81
|
SkillsInstallParams: SkillsInstallParamsSchema,
|
|
82
82
|
SkillsUpdateParams: SkillsUpdateParamsSchema,
|
|
83
|
+
SkillsReadParams: SkillsReadParamsSchema,
|
|
84
|
+
SkillsCreateParams: SkillsCreateParamsSchema,
|
|
85
|
+
SkillsDeleteParams: SkillsDeleteParamsSchema,
|
|
86
|
+
SkillsDraftsParams: SkillsDraftsParamsSchema,
|
|
87
|
+
SkillsDeleteDraftParams: SkillsDeleteDraftParamsSchema,
|
|
83
88
|
CronJob: CronJobSchema,
|
|
84
89
|
CronListParams: CronListParamsSchema,
|
|
85
90
|
CronStatusParams: CronStatusParamsSchema,
|
|
@@ -5,7 +5,7 @@ import { loadConfig } from "../config/config.js";
|
|
|
5
5
|
import { handleSlackHttpRequest } from "../slack/http/index.js";
|
|
6
6
|
import { createCloudApiWebhookHandler } from "../web/providers/cloud/webhook-http.js";
|
|
7
7
|
import { resolveAgentAvatar } from "../agents/identity-avatar.js";
|
|
8
|
-
import { handleBrandIconRequest, handleControlUiAvatarRequest, handleControlUiHttpRequest, handlePublicChatHttpRequest, handlePublicWidgetRequest, } from "./control-ui.js";
|
|
8
|
+
import { handleBrandIconRequest, handleBrandLogoRequest, handleControlUiAvatarRequest, handleControlUiHttpRequest, handlePublicChatHttpRequest, handlePublicWidgetRequest, } from "./control-ui.js";
|
|
9
9
|
import { handlePublicChatApiRequest } from "./public-chat-api.js";
|
|
10
10
|
import { isLicensed } from "../license/state.js";
|
|
11
11
|
import { getEffectiveTrustedProxies, isExternalRequest } from "./net.js";
|
|
@@ -162,6 +162,9 @@ export function createGatewayHttpServer(opts) {
|
|
|
162
162
|
return;
|
|
163
163
|
if (handlePublicWidgetRequest(req, res, { config: configSnapshot }))
|
|
164
164
|
return;
|
|
165
|
+
// Brand logos may be referenced from public chat pages
|
|
166
|
+
if (handleBrandLogoRequest(req, res, { config: configSnapshot }))
|
|
167
|
+
return;
|
|
165
168
|
// Funnel restriction: block non-local requests from accessing non-public paths.
|
|
166
169
|
// /public/* is already handled above, so any request reaching here is non-public.
|
|
167
170
|
const trustedProxies = getEffectiveTrustedProxies(configSnapshot);
|
|
@@ -238,6 +241,8 @@ export function createGatewayHttpServer(opts) {
|
|
|
238
241
|
if (controlUiEnabled) {
|
|
239
242
|
if (handleBrandIconRequest(req, res, { config: configSnapshot }))
|
|
240
243
|
return;
|
|
244
|
+
if (handleBrandLogoRequest(req, res, { config: configSnapshot }))
|
|
245
|
+
return;
|
|
241
246
|
if (handleControlUiAvatarRequest(req, res, {
|
|
242
247
|
basePath: controlUiBasePath,
|
|
243
248
|
resolveAvatar: (agentId) => resolveAgentAvatar(configSnapshot, agentId),
|
|
@@ -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
|
},
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { resolveAgentWorkspaceRoot } from "../../agents/agent-scope.js";
|
|
6
|
+
import { buildAgentSummaries } from "../../commands/agents.config.js";
|
|
7
|
+
import { ErrorCodes, errorShape } from "../protocol/index.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const BRAND_LOGO_FILENAME_PREFIX = "brand-logo";
|
|
12
|
+
const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp"]);
|
|
13
|
+
const MAX_LOGO_BYTES = 2 * 1024 * 1024; // 2 MB
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the workspace root directory for a given workspace name.
|
|
19
|
+
* Workspace name = basename of the workspace root (set during workspaces.create).
|
|
20
|
+
*/
|
|
21
|
+
function resolveWorkspaceRootByName(cfg, workspaceName) {
|
|
22
|
+
const summaries = buildAgentSummaries(cfg);
|
|
23
|
+
for (const summary of summaries) {
|
|
24
|
+
const root = resolveAgentWorkspaceRoot(cfg, summary.id);
|
|
25
|
+
if (path.basename(root) === workspaceName)
|
|
26
|
+
return root;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Find an existing brand logo file in a workspace directory.
|
|
32
|
+
* Returns the full path if found, null otherwise.
|
|
33
|
+
*/
|
|
34
|
+
export function findBrandLogo(workspaceDir) {
|
|
35
|
+
for (const ext of ALLOWED_EXTENSIONS) {
|
|
36
|
+
const candidate = path.join(workspaceDir, `${BRAND_LOGO_FILENAME_PREFIX}${ext}`);
|
|
37
|
+
if (fsSync.existsSync(candidate))
|
|
38
|
+
return candidate;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// RPC Handlers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
export const brandHandlers = {
|
|
46
|
+
/**
|
|
47
|
+
* Upload a brand logo for a workspace.
|
|
48
|
+
* Params: { workspace: string, data: string (base64), mimeType: string }
|
|
49
|
+
*/
|
|
50
|
+
"brand.uploadLogo": async ({ params, respond }) => {
|
|
51
|
+
const workspace = typeof params.workspace === "string" ? params.workspace.trim() : "";
|
|
52
|
+
const data = typeof params.data === "string" ? params.data : "";
|
|
53
|
+
const mimeType = typeof params.mimeType === "string" ? params.mimeType.trim() : "";
|
|
54
|
+
if (!workspace || !data) {
|
|
55
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "workspace and data are required"));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Derive extension from mimeType
|
|
59
|
+
const ext = mimeToExt(mimeType);
|
|
60
|
+
if (!ext || !ALLOWED_EXTENSIONS.has(ext)) {
|
|
61
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported image type "${mimeType}"; allowed: png, jpg, svg, webp`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const buffer = Buffer.from(data, "base64");
|
|
65
|
+
if (buffer.length > MAX_LOGO_BYTES) {
|
|
66
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `logo too large (${buffer.length} bytes, max ${MAX_LOGO_BYTES})`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const cfg = loadConfig();
|
|
70
|
+
const workspaceDir = resolveWorkspaceRootByName(cfg, workspace);
|
|
71
|
+
if (!workspaceDir) {
|
|
72
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `workspace "${workspace}" not found`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Remove any existing brand logo (different extension)
|
|
76
|
+
for (const existingExt of ALLOWED_EXTENSIONS) {
|
|
77
|
+
const existing = path.join(workspaceDir, `${BRAND_LOGO_FILENAME_PREFIX}${existingExt}`);
|
|
78
|
+
try {
|
|
79
|
+
await fs.unlink(existing);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// ignore — file may not exist
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const destPath = path.join(workspaceDir, `${BRAND_LOGO_FILENAME_PREFIX}${ext}`);
|
|
86
|
+
try {
|
|
87
|
+
await fs.writeFile(destPath, buffer);
|
|
88
|
+
respond(true, { path: destPath, size: buffer.length });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Remove the brand logo for a workspace.
|
|
96
|
+
* Params: { workspace: string }
|
|
97
|
+
*/
|
|
98
|
+
"brand.removeLogo": async ({ params, respond }) => {
|
|
99
|
+
const workspace = typeof params.workspace === "string" ? params.workspace.trim() : "";
|
|
100
|
+
if (!workspace) {
|
|
101
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "workspace is required"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const cfg = loadConfig();
|
|
105
|
+
const workspaceDir = resolveWorkspaceRootByName(cfg, workspace);
|
|
106
|
+
if (!workspaceDir) {
|
|
107
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `workspace "${workspace}" not found`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let removed = false;
|
|
111
|
+
for (const ext of ALLOWED_EXTENSIONS) {
|
|
112
|
+
const existing = path.join(workspaceDir, `${BRAND_LOGO_FILENAME_PREFIX}${ext}`);
|
|
113
|
+
try {
|
|
114
|
+
await fs.unlink(existing);
|
|
115
|
+
removed = true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
respond(true, { removed });
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Check whether a workspace has a brand logo.
|
|
125
|
+
* Params: { workspace: string }
|
|
126
|
+
*/
|
|
127
|
+
"brand.hasLogo": ({ params, respond }) => {
|
|
128
|
+
const workspace = typeof params.workspace === "string" ? params.workspace.trim() : "";
|
|
129
|
+
if (!workspace) {
|
|
130
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "workspace is required"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const cfg = loadConfig();
|
|
134
|
+
const workspaceDir = resolveWorkspaceRootByName(cfg, workspace);
|
|
135
|
+
if (!workspaceDir) {
|
|
136
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `workspace "${workspace}" not found`));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const logoPath = findBrandLogo(workspaceDir);
|
|
140
|
+
respond(true, { hasLogo: Boolean(logoPath) });
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Helpers (private)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
function mimeToExt(mime) {
|
|
147
|
+
switch (mime.toLowerCase()) {
|
|
148
|
+
case "image/png":
|
|
149
|
+
return ".png";
|
|
150
|
+
case "image/jpeg":
|
|
151
|
+
case "image/jpg":
|
|
152
|
+
return ".jpg";
|
|
153
|
+
case "image/svg+xml":
|
|
154
|
+
return ".svg";
|
|
155
|
+
case "image/webp":
|
|
156
|
+
return ".webp";
|
|
157
|
+
default:
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|