@rubytech/taskmaster 1.0.105 → 1.0.107
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 +1 -0
- package/dist/agents/tools/memory-tool.js +2 -1
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.js +12 -1
- package/dist/control-ui/assets/index-2XyxmiR6.css +1 -0
- package/dist/control-ui/assets/{index-DtuDNTAC.js → index-B_zHmTQU.js} +823 -645
- package/dist/control-ui/assets/index-B_zHmTQU.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/control-ui/maxy-icon.png +0 -0
- 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-close.js +2 -0
- package/dist/gateway/server-http.js +6 -1
- package/dist/gateway/server-methods/brand.js +160 -0
- package/dist/gateway/server-methods/skills.js +159 -3
- package/dist/gateway/server-methods/wifi.js +0 -10
- package/dist/gateway/server-methods-list.js +5 -0
- package/dist/gateway/server-methods.js +2 -0
- package/dist/gateway/server-wifi-watchdog.js +105 -0
- package/dist/gateway/server.impl.js +3 -0
- package/dist/memory/manager.js +12 -3
- 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 +58 -2
- 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.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-B_zHmTQU.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-2XyxmiR6.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<taskmaster-app></taskmaster-app>
|
|
Binary file
|
|
@@ -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,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listChannelPlugins } from "../channels/plugins/index.js";
|
|
2
2
|
import { stopGmailWatcher } from "../hooks/gmail-watcher.js";
|
|
3
|
+
import { stopWifiWatchdog } from "./server-wifi-watchdog.js";
|
|
3
4
|
export function createGatewayCloseHandler(params) {
|
|
4
5
|
return async (opts) => {
|
|
5
6
|
const reasonRaw = typeof opts?.reason === "string" ? opts.reason.trim() : "";
|
|
@@ -54,6 +55,7 @@ export function createGatewayCloseHandler(params) {
|
|
|
54
55
|
clearInterval(params.tickInterval);
|
|
55
56
|
clearInterval(params.healthInterval);
|
|
56
57
|
clearInterval(params.dedupeCleanup);
|
|
58
|
+
stopWifiWatchdog();
|
|
57
59
|
if (params.agentUnsub) {
|
|
58
60
|
try {
|
|
59
61
|
params.agentUnsub();
|
|
@@ -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),
|
|
@@ -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
|
+
}
|