@rubytech/taskmaster 1.1.0 → 1.2.0
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/taskmaster-tools.js +6 -0
- package/dist/agents/tool-policy.js +2 -0
- package/dist/agents/tools/document-to-pdf-tool.js +136 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/provision-seed.js +1 -0
- package/dist/config/legacy.migrations.part-3.js +28 -0
- package/dist/control-ui/assets/index-DvB85yTz.css +1 -0
- package/dist/control-ui/assets/{index-D7ZHRWnP.js → index-DwMopZij.js} +387 -270
- package/dist/control-ui/assets/index-DwMopZij.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/chat-sanitize.js +105 -21
- package/dist/gateway/media-http.js +5 -4
- package/dist/gateway/public-chat-api.js +31 -2
- package/package.json +1 -1
- package/skills/business-assistant/SKILL.md +16 -406
- package/skills/business-assistant/references/crm.md +77 -0
- package/skills/business-assistant/references/document-management.md +84 -0
- package/skills/business-assistant/references/escalation.md +108 -0
- package/skills/business-assistant/references/invoicing.md +146 -0
- package/skills/business-assistant/references/quoting.md +56 -0
- package/skills/business-assistant/references/scheduling.md +127 -0
- package/dist/control-ui/assets/index-CfybK7_N.css +0 -1
- package/dist/control-ui/assets/index-D7ZHRWnP.js.map +0 -1
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<title>Taskmaster Control</title>
|
|
7
7
|
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-DwMopZij.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DvB85yTz.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<taskmaster-app></taskmaster-app>
|
|
@@ -175,10 +175,46 @@ function extractMediaRefs(text) {
|
|
|
175
175
|
}
|
|
176
176
|
// Pattern: MEDIA:/absolute/path (used by tool results like image_generate)
|
|
177
177
|
const MEDIA_PREFIX_PATTERN = /\bMEDIA:(\S+)/g;
|
|
178
|
+
/** Map file extension to MIME type for MEDIA: refs. */
|
|
179
|
+
export function mimeFromExt(ext) {
|
|
180
|
+
switch (ext) {
|
|
181
|
+
case "jpg":
|
|
182
|
+
case "jpeg":
|
|
183
|
+
return "image/jpeg";
|
|
184
|
+
case "png":
|
|
185
|
+
return "image/png";
|
|
186
|
+
case "gif":
|
|
187
|
+
return "image/gif";
|
|
188
|
+
case "webp":
|
|
189
|
+
return "image/webp";
|
|
190
|
+
case "heic":
|
|
191
|
+
return "image/heic";
|
|
192
|
+
case "heif":
|
|
193
|
+
return "image/heif";
|
|
194
|
+
case "bmp":
|
|
195
|
+
return "image/bmp";
|
|
196
|
+
case "tiff":
|
|
197
|
+
case "tif":
|
|
198
|
+
return "image/tiff";
|
|
199
|
+
case "pdf":
|
|
200
|
+
return "application/pdf";
|
|
201
|
+
case "mp3":
|
|
202
|
+
return "audio/mpeg";
|
|
203
|
+
case "ogg":
|
|
204
|
+
case "oga":
|
|
205
|
+
return "audio/ogg";
|
|
206
|
+
case "wav":
|
|
207
|
+
return "audio/wav";
|
|
208
|
+
case "m4a":
|
|
209
|
+
return "audio/mp4";
|
|
210
|
+
default:
|
|
211
|
+
return "application/octet-stream";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
178
214
|
/**
|
|
179
215
|
* Parse MEDIA:/path references from text to extract file paths.
|
|
180
|
-
* Tool results (e.g. image_generate) use this format
|
|
181
|
-
* [media attached: ...] annotations.
|
|
216
|
+
* Tool results (e.g. image_generate, document_to_pdf) use this format
|
|
217
|
+
* instead of [media attached: ...] annotations.
|
|
182
218
|
*/
|
|
183
219
|
function extractMediaPrefixRefs(text) {
|
|
184
220
|
if (!text.includes("MEDIA:"))
|
|
@@ -190,8 +226,7 @@ function extractMediaPrefixRefs(text) {
|
|
|
190
226
|
const absPath = match[1]?.trim();
|
|
191
227
|
if (absPath) {
|
|
192
228
|
const ext = absPath.split(".").pop()?.toLowerCase() ?? "";
|
|
193
|
-
|
|
194
|
-
refs.push({ absPath, mimeType });
|
|
229
|
+
refs.push({ absPath, mimeType: mimeFromExt(ext) });
|
|
195
230
|
}
|
|
196
231
|
}
|
|
197
232
|
return refs;
|
|
@@ -254,8 +289,11 @@ export function stripBase64ImagesFromMessages(messages) {
|
|
|
254
289
|
/**
|
|
255
290
|
* Sanitize media in chat messages for UI display.
|
|
256
291
|
* - Extracts file paths from [media attached: ...] text annotations
|
|
292
|
+
* - Extracts file paths from MEDIA:/path annotations (all messages)
|
|
257
293
|
* - Removes base64 image blocks
|
|
258
|
-
* - Creates URL-based image references for the /api/media endpoint
|
|
294
|
+
* - Creates URL-based image/file references for the /api/media endpoint
|
|
295
|
+
* - Deduplicates by path so assistant echoes of tool results don't produce
|
|
296
|
+
* duplicate blocks
|
|
259
297
|
*
|
|
260
298
|
* Must be called BEFORE stripEnvelopeFromMessages (which strips annotations).
|
|
261
299
|
*/
|
|
@@ -264,42 +302,56 @@ export function sanitizeMediaForChat(messages, workspaceRoot) {
|
|
|
264
302
|
// No workspace context — fall back to plain base64 stripping
|
|
265
303
|
return stripBase64ImagesFromMessages(messages);
|
|
266
304
|
}
|
|
305
|
+
// Track paths across all messages to prevent duplicate blocks when an
|
|
306
|
+
// assistant message echoes a MEDIA: ref that already appeared in a tool result.
|
|
307
|
+
const seenPaths = new Set();
|
|
267
308
|
let changed = false;
|
|
268
309
|
const next = messages.map((message) => {
|
|
269
|
-
const result = sanitizeMessageMedia(message, workspaceRoot);
|
|
310
|
+
const result = sanitizeMessageMedia(message, workspaceRoot, seenPaths);
|
|
270
311
|
if (result !== message)
|
|
271
312
|
changed = true;
|
|
272
313
|
return result;
|
|
273
314
|
});
|
|
274
315
|
return changed ? next : messages;
|
|
275
316
|
}
|
|
276
|
-
function sanitizeMessageMedia(message, workspaceRoot) {
|
|
317
|
+
function sanitizeMessageMedia(message, workspaceRoot, seenPaths) {
|
|
277
318
|
if (!message || typeof message !== "object")
|
|
278
319
|
return message;
|
|
279
320
|
const entry = message;
|
|
280
321
|
// Collect media refs from text content (works for both string and array content).
|
|
281
|
-
// MEDIA: prefix refs are
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
//
|
|
322
|
+
// MEDIA: prefix refs are extracted from ALL messages — the agent may reference
|
|
323
|
+
// existing workspace files directly via MEDIA: in its response text. The
|
|
324
|
+
// seenPaths set prevents duplicates when an assistant echoes a MEDIA: ref that
|
|
325
|
+
// already appeared in a tool result.
|
|
326
|
+
const mediaRefs = extractMediaRefsFromMessage(entry, true);
|
|
327
|
+
// Build URL-based media blocks from annotations.
|
|
328
|
+
// Images become { type: "image" }, everything else becomes { type: "file" }.
|
|
329
|
+
// Skip paths already converted in an earlier message (dedup across conversation).
|
|
288
330
|
const imageBlocks = [];
|
|
331
|
+
const fileBlocks = [];
|
|
289
332
|
for (const ref of mediaRefs) {
|
|
333
|
+
if (seenPaths.has(ref.absPath))
|
|
334
|
+
continue;
|
|
290
335
|
const url = mediaRefToUrl(ref, workspaceRoot);
|
|
291
|
-
if (url)
|
|
336
|
+
if (!url)
|
|
337
|
+
continue;
|
|
338
|
+
seenPaths.add(ref.absPath);
|
|
339
|
+
if (ref.mimeType.startsWith("image/")) {
|
|
292
340
|
imageBlocks.push({ type: "image", url });
|
|
293
341
|
}
|
|
342
|
+
else {
|
|
343
|
+
const name = nodePath.basename(ref.absPath);
|
|
344
|
+
fileBlocks.push({ type: "file", url, name, mimeType: ref.mimeType });
|
|
345
|
+
}
|
|
294
346
|
}
|
|
295
347
|
if (!Array.isArray(entry.content)) {
|
|
296
|
-
// String content — no base64 blocks to strip, just add
|
|
297
|
-
if (imageBlocks.length === 0)
|
|
348
|
+
// String content — no base64 blocks to strip, just add media blocks if found
|
|
349
|
+
if (imageBlocks.length === 0 && fileBlocks.length === 0)
|
|
298
350
|
return message;
|
|
299
351
|
const textContent = typeof entry.content === "string" ? entry.content : "";
|
|
300
352
|
return {
|
|
301
353
|
...entry,
|
|
302
|
-
content: [{ type: "text", text: textContent }, ...imageBlocks],
|
|
354
|
+
content: [{ type: "text", text: textContent }, ...imageBlocks, ...fileBlocks],
|
|
303
355
|
};
|
|
304
356
|
}
|
|
305
357
|
// Array content — remove base64 image blocks, add URL-based ones
|
|
@@ -332,15 +384,47 @@ function sanitizeMessageMedia(message, workspaceRoot) {
|
|
|
332
384
|
}
|
|
333
385
|
}
|
|
334
386
|
}
|
|
335
|
-
// Add URL-based
|
|
336
|
-
if (imageBlocks.length > 0) {
|
|
387
|
+
// Add URL-based media blocks from tool result annotations
|
|
388
|
+
if (imageBlocks.length > 0 || fileBlocks.length > 0) {
|
|
337
389
|
didChange = true;
|
|
338
|
-
filtered.push(...imageBlocks);
|
|
390
|
+
filtered.push(...imageBlocks, ...fileBlocks);
|
|
339
391
|
}
|
|
340
392
|
if (!didChange)
|
|
341
393
|
return message;
|
|
342
394
|
return { ...entry, content: filtered };
|
|
343
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Extract file attachment metadata from messages (for API responses).
|
|
398
|
+
* Scans all messages for MEDIA: refs that point to non-image files
|
|
399
|
+
* and returns structured attachment objects with download URLs.
|
|
400
|
+
* Deduplicates by path.
|
|
401
|
+
*/
|
|
402
|
+
export function extractFileAttachments(messages, workspaceRoot) {
|
|
403
|
+
const attachments = [];
|
|
404
|
+
const seen = new Set();
|
|
405
|
+
for (const message of messages) {
|
|
406
|
+
if (!message || typeof message !== "object")
|
|
407
|
+
continue;
|
|
408
|
+
const entry = message;
|
|
409
|
+
const refs = extractMediaRefsFromMessage(entry, true);
|
|
410
|
+
for (const ref of refs) {
|
|
411
|
+
if (ref.mimeType.startsWith("image/"))
|
|
412
|
+
continue;
|
|
413
|
+
if (seen.has(ref.absPath))
|
|
414
|
+
continue;
|
|
415
|
+
const url = mediaRefToUrl(ref, workspaceRoot);
|
|
416
|
+
if (!url)
|
|
417
|
+
continue;
|
|
418
|
+
seen.add(ref.absPath);
|
|
419
|
+
attachments.push({
|
|
420
|
+
url,
|
|
421
|
+
name: nodePath.basename(ref.absPath),
|
|
422
|
+
mimeType: ref.mimeType,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return attachments;
|
|
427
|
+
}
|
|
344
428
|
function extractMediaRefsFromMessage(entry, includeMediaPrefix) {
|
|
345
429
|
if (typeof entry.content === "string") {
|
|
346
430
|
const refs = extractMediaRefs(entry.content);
|
|
@@ -4,12 +4,13 @@ import { resolveAgentWorkspaceRoot } from "../agents/agent-scope.js";
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Workspace media file endpoint
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
|
-
// Serves
|
|
8
|
-
//
|
|
7
|
+
// Serves media files (images, PDFs, audio) from the workspace root so that
|
|
8
|
+
// chat history can display inline images and file download cards by URL
|
|
9
|
+
// instead of embedding base64 data in WebSocket messages.
|
|
9
10
|
//
|
|
10
11
|
// Route: GET /api/media?path=<workspace-relative-path>
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
|
-
const
|
|
13
|
+
const ALLOWED_MEDIA_EXTENSIONS = new Set([
|
|
13
14
|
".png",
|
|
14
15
|
".jpg",
|
|
15
16
|
".jpeg",
|
|
@@ -78,7 +79,7 @@ export function handleMediaRequest(req, res, opts) {
|
|
|
78
79
|
return true;
|
|
79
80
|
}
|
|
80
81
|
const ext = path.extname(relPath).toLowerCase();
|
|
81
|
-
if (!
|
|
82
|
+
if (!ALLOWED_MEDIA_EXTENSIONS.has(ext)) {
|
|
82
83
|
res.statusCode = 403;
|
|
83
84
|
res.end("Forbidden");
|
|
84
85
|
return true;
|
|
@@ -37,7 +37,7 @@ import { requestOtp, verifyOtp } from "./public-chat/otp.js";
|
|
|
37
37
|
import { deliverOtp } from "./public-chat/deliver-otp.js";
|
|
38
38
|
import { buildPublicSessionKey, resolvePublicAgentId } from "./public-chat/session.js";
|
|
39
39
|
import { loadSessionEntry, readSessionMessages } from "./session-utils.js";
|
|
40
|
-
import { sanitizeMediaForChat, stripEnvelopeFromMessages } from "./chat-sanitize.js";
|
|
40
|
+
import { extractFileAttachments, sanitizeMediaForChat, stripEnvelopeFromMessages } from "./chat-sanitize.js";
|
|
41
41
|
import { resolveWorkspaceRoot } from "./media-http.js";
|
|
42
42
|
import { readJsonBodyOrError, sendInvalidRequest, sendJson, sendMethodNotAllowed, setSseHeaders, writeDone, } from "./http-common.js";
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
@@ -442,11 +442,20 @@ async function handleChat(req, res, _accountId, cfg, maxBodyBytes) {
|
|
|
442
442
|
cfg,
|
|
443
443
|
}));
|
|
444
444
|
}
|
|
445
|
+
// Extract file attachments from tool results in this session
|
|
446
|
+
const workspaceRoot = resolveWorkspaceRoot(cfg);
|
|
447
|
+
const { entry: sessionEntry, storePath } = loadSessionEntry(sessionKey);
|
|
448
|
+
let attachments = [];
|
|
449
|
+
if (sessionEntry?.sessionId && storePath) {
|
|
450
|
+
const sessionMsgs = readSessionMessages(sessionEntry.sessionId, storePath, sessionEntry.sessionFile);
|
|
451
|
+
attachments = extractFileAttachments(sessionMsgs, workspaceRoot);
|
|
452
|
+
}
|
|
445
453
|
sendJson(res, 200, {
|
|
446
454
|
id: runId,
|
|
447
455
|
session_key: sessionKey,
|
|
448
456
|
message: combinedReply || null,
|
|
449
457
|
created: Math.floor(Date.now() / 1000),
|
|
458
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
450
459
|
});
|
|
451
460
|
}
|
|
452
461
|
catch (err) {
|
|
@@ -484,7 +493,12 @@ async function handleChat(req, res, _accountId, cfg, maxBodyBytes) {
|
|
|
484
493
|
}
|
|
485
494
|
if (evt.stream === "lifecycle") {
|
|
486
495
|
const phase = evt.data?.phase;
|
|
487
|
-
if (phase === "end"
|
|
496
|
+
if (phase === "end") {
|
|
497
|
+
// Don't close here — let the finally block emit file attachments
|
|
498
|
+
// before writing [DONE]. Just stop listening for further events.
|
|
499
|
+
unsubscribe();
|
|
500
|
+
}
|
|
501
|
+
else if (phase === "error") {
|
|
488
502
|
if (!closed) {
|
|
489
503
|
closed = true;
|
|
490
504
|
unsubscribe();
|
|
@@ -582,6 +596,21 @@ async function handleChat(req, res, _accountId, cfg, maxBodyBytes) {
|
|
|
582
596
|
}
|
|
583
597
|
finally {
|
|
584
598
|
if (!closed) {
|
|
599
|
+
// Emit file attachments (e.g. PDFs from document_to_pdf) before closing
|
|
600
|
+
try {
|
|
601
|
+
const workspaceRoot = resolveWorkspaceRoot(cfg);
|
|
602
|
+
const { entry: sessionEntry, storePath } = loadSessionEntry(sessionKey);
|
|
603
|
+
if (sessionEntry?.sessionId && storePath) {
|
|
604
|
+
const sessionMsgs = readSessionMessages(sessionEntry.sessionId, storePath, sessionEntry.sessionFile);
|
|
605
|
+
const attachments = extractFileAttachments(sessionMsgs, workspaceRoot);
|
|
606
|
+
for (const att of attachments) {
|
|
607
|
+
writeSse(res, { id: runId, type: "attachment", ...att });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Attachment extraction failure should not break the stream
|
|
613
|
+
}
|
|
585
614
|
closed = true;
|
|
586
615
|
unsubscribe();
|
|
587
616
|
writeDone(res);
|