@rubytech/taskmaster 1.0.106 → 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.
Files changed (36) hide show
  1. package/dist/agents/skills-status.js +23 -3
  2. package/dist/agents/skills.js +1 -0
  3. package/dist/agents/system-prompt.js +1 -0
  4. package/dist/agents/tools/memory-tool.js +2 -1
  5. package/dist/build-info.json +3 -3
  6. package/dist/config/zod-schema.js +12 -1
  7. package/dist/control-ui/assets/index-2XyxmiR6.css +1 -0
  8. package/dist/control-ui/assets/{index-DtuDNTAC.js → index-B_zHmTQU.js} +823 -645
  9. package/dist/control-ui/assets/index-B_zHmTQU.js.map +1 -0
  10. package/dist/control-ui/index.html +2 -2
  11. package/dist/control-ui/maxy-icon.png +0 -0
  12. package/dist/gateway/config-reload.js +1 -0
  13. package/dist/gateway/control-ui.js +111 -5
  14. package/dist/gateway/protocol/index.js +6 -1
  15. package/dist/gateway/protocol/schema/agents-models-skills.js +23 -0
  16. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -1
  17. package/dist/gateway/server-http.js +6 -1
  18. package/dist/gateway/server-methods/brand.js +160 -0
  19. package/dist/gateway/server-methods/skills.js +159 -3
  20. package/dist/gateway/server-methods-list.js +5 -0
  21. package/dist/gateway/server-methods.js +2 -0
  22. package/dist/memory/manager.js +12 -3
  23. package/package.json +1 -1
  24. package/skills/skill-builder/SKILL.md +97 -0
  25. package/skills/skill-builder/references/lean-pattern.md +118 -0
  26. package/skills/zero-to-prototype/SKILL.md +35 -0
  27. package/skills/zero-to-prototype/references/discovery.md +64 -0
  28. package/skills/zero-to-prototype/references/prd.md +83 -0
  29. package/skills/zero-to-prototype/references/validation.md +67 -0
  30. package/taskmaster-docs/USER-GUIDE.md +58 -2
  31. package/templates/customer/agents/public/AGENTS.md +3 -10
  32. package/templates/taskmaster/agents/public/SOUL.md +0 -4
  33. package/templates/tradesupport/agents/public/AGENTS.md +3 -10
  34. package/dist/control-ui/assets/index-DjhCZlZd.css +0 -1
  35. package/dist/control-ui/assets/index-DtuDNTAC.js.map +0 -1
  36. 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-DtuDNTAC.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-DjhCZlZd.css">
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
- const accentColor = config.ui?.seamColor;
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:#1a1a2e}",
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),
@@ -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
+ }