@rubytech/taskmaster 1.14.2 → 1.16.1
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/frontmatter.js +1 -0
- package/dist/agents/skills/workspace.js +64 -22
- package/dist/agents/system-prompt.js +1 -1
- package/dist/agents/taskmaster-tools.js +6 -4
- package/dist/agents/tool-policy.js +2 -1
- package/dist/agents/tools/contact-create-tool.js +4 -3
- package/dist/agents/tools/contact-delete-tool.js +3 -2
- package/dist/agents/tools/contact-lookup-tool.js +5 -4
- package/dist/agents/tools/contact-update-tool.js +6 -3
- package/dist/agents/tools/memory-tool.js +3 -1
- package/dist/agents/tools/qr-generate-tool.js +45 -0
- package/dist/agents/workspace-migrations.js +351 -0
- package/dist/build-info.json +3 -3
- package/dist/config/agent-tools-reconcile.js +79 -0
- package/dist/control-ui/assets/{index-B3nkSwMP.js → index-Bd75cI7J.js} +547 -573
- package/dist/control-ui/assets/index-Bd75cI7J.js.map +1 -0
- package/dist/control-ui/assets/index-BkymP95Y.css +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/server-http.js +5 -0
- package/dist/gateway/server-methods/web.js +13 -0
- package/dist/gateway/server.impl.js +29 -1
- package/dist/hooks/bundled/ride-dispatch/HOOK.md +57 -0
- package/dist/hooks/bundled/ride-dispatch/handler.js +450 -0
- package/dist/hooks/bundled/ride-dispatch/stripe-webhook.js +191 -0
- package/dist/memory/internal.js +24 -1
- package/dist/memory/manager.js +3 -3
- package/dist/records/records-manager.js +7 -2
- package/package.json +1 -1
- package/skills/business-assistant/SKILL.md +1 -1
- package/skills/qr-code/SKILL.md +63 -0
- package/skills/sales-closer/SKILL.md +1 -1
- package/templates/beagle-zanzibar/agents/admin/AGENTS.md +67 -1
- package/templates/beagle-zanzibar/agents/public/AGENTS.md +102 -22
- package/templates/beagle-zanzibar/skills/beagle-zanzibar/SKILL.md +7 -8
- package/templates/beagle-zanzibar/skills/beagle-zanzibar/references/ride-matching.md +46 -55
- package/templates/customer/agents/admin/BOOTSTRAP.md +5 -1
- package/templates/customer/agents/public/AGENTS.md +1 -2
- package/templates/real-agent/skills/buyer-feedback/SKILL.md +111 -0
- package/templates/real-agent/skills/property-enquiry/SKILL.md +126 -0
- package/templates/real-agent/skills/valuation-booking/SKILL.md +182 -0
- package/templates/real-agent/skills/vendor-updates/SKILL.md +153 -0
- package/templates/real-agent/skills/viewing-management/SKILL.md +111 -0
- package/templates/taskmaster/agents/public/AGENTS.md +1 -1
- package/templates/taskmaster/agents/public/IDENTITY.md +1 -1
- package/templates/taskmaster/agents/public/SOUL.md +2 -2
- package/dist/control-ui/assets/index-B3nkSwMP.js.map +0 -1
- package/dist/control-ui/assets/index-l54GcTyj.css +0 -1
|
@@ -90,6 +90,7 @@ export function resolveTaskmasterMetadata(frontmatter) {
|
|
|
90
90
|
const osRaw = normalizeStringList(taskmasterObj.os);
|
|
91
91
|
return {
|
|
92
92
|
always: typeof taskmasterObj.always === "boolean" ? taskmasterObj.always : undefined,
|
|
93
|
+
embed: typeof taskmasterObj.embed === "boolean" ? taskmasterObj.embed : undefined,
|
|
93
94
|
emoji: typeof taskmasterObj.emoji === "string" ? taskmasterObj.emoji : undefined,
|
|
94
95
|
homepage: typeof taskmasterObj.homepage === "string" ? taskmasterObj.homepage : undefined,
|
|
95
96
|
skillKey: typeof taskmasterObj.skillKey === "string" ? taskmasterObj.skillKey : undefined,
|
|
@@ -18,32 +18,73 @@ function escapeXml(str) {
|
|
|
18
18
|
.replace(/"/g, """)
|
|
19
19
|
.replace(/'/g, "'");
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Strip YAML frontmatter (between opening and closing `---`) from a skill
|
|
23
|
+
* file, returning only the body content.
|
|
24
|
+
*/
|
|
25
|
+
function stripFrontmatter(content) {
|
|
26
|
+
const match = content.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
|
|
27
|
+
return match ? content.slice(match[0].length).trim() : content.trim();
|
|
28
|
+
}
|
|
21
29
|
/**
|
|
22
30
|
* Format skills for inclusion in a system prompt.
|
|
23
31
|
* Identical to the library's formatSkillsForPrompt except it references
|
|
24
32
|
* `skill_read` instead of `read` so agents denied group:fs can still
|
|
25
33
|
* load skill content.
|
|
34
|
+
*
|
|
35
|
+
* Skills with `embed: true` metadata have their body content directly
|
|
36
|
+
* included in the system prompt instead of requiring `skill_read`.
|
|
26
37
|
*/
|
|
27
|
-
function formatSkillsForPromptTaskmaster(
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
38
|
+
function formatSkillsForPromptTaskmaster(entries) {
|
|
39
|
+
const visibleEntries = entries.filter((e) => !e.skill.disableModelInvocation);
|
|
40
|
+
if (visibleEntries.length === 0)
|
|
30
41
|
return "";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Partition into embedded and on-demand skills.
|
|
43
|
+
const embedded = [];
|
|
44
|
+
const onDemand = [];
|
|
45
|
+
for (const entry of visibleEntries) {
|
|
46
|
+
if (entry.taskmaster?.embed === true) {
|
|
47
|
+
embedded.push(entry);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
onDemand.push(entry);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const sections = [];
|
|
54
|
+
// Embedded skills: content included directly.
|
|
55
|
+
for (const entry of embedded) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(entry.skill.filePath, "utf-8");
|
|
58
|
+
const body = stripFrontmatter(raw);
|
|
59
|
+
if (body) {
|
|
60
|
+
sections.push(`<embedded_skill name="${escapeXml(entry.skill.name)}" location="${escapeXml(entry.skill.filePath)}">\n${body}\n</embedded_skill>`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// If the file can't be read, fall through to on-demand.
|
|
65
|
+
onDemand.push(entry);
|
|
66
|
+
}
|
|
44
67
|
}
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
// On-demand skills: listed for skill_read loading.
|
|
69
|
+
if (onDemand.length > 0) {
|
|
70
|
+
const lines = [
|
|
71
|
+
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
72
|
+
"Use the skill_read tool to load a skill's file when the task matches its description.",
|
|
73
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
74
|
+
"",
|
|
75
|
+
"<available_skills>",
|
|
76
|
+
];
|
|
77
|
+
for (const entry of onDemand) {
|
|
78
|
+
lines.push(" <skill>");
|
|
79
|
+
lines.push(` <name>${escapeXml(entry.skill.name)}</name>`);
|
|
80
|
+
lines.push(` <description>${escapeXml(entry.skill.description)}</description>`);
|
|
81
|
+
lines.push(` <location>${escapeXml(entry.skill.filePath)}</location>`);
|
|
82
|
+
lines.push(" </skill>");
|
|
83
|
+
}
|
|
84
|
+
lines.push("</available_skills>");
|
|
85
|
+
sections.push(lines.join("\n"));
|
|
86
|
+
}
|
|
87
|
+
return sections.join("\n\n");
|
|
47
88
|
}
|
|
48
89
|
const skillCommandDebugOnce = new Set();
|
|
49
90
|
function debugSkillCommandOnce(messageKey, message, meta) {
|
|
@@ -176,7 +217,7 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
|
|
|
176
217
|
const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
|
|
177
218
|
const resolvedSkills = promptEntries.map((entry) => entry.skill);
|
|
178
219
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
|
179
|
-
const prompt = [remoteNote, formatSkillsForPromptTaskmaster(
|
|
220
|
+
const prompt = [remoteNote, formatSkillsForPromptTaskmaster(promptEntries)]
|
|
180
221
|
.filter(Boolean)
|
|
181
222
|
.join("\n");
|
|
182
223
|
return {
|
|
@@ -194,9 +235,7 @@ export function buildWorkspaceSkillsPrompt(workspaceDir, opts) {
|
|
|
194
235
|
const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter, opts?.eligibility);
|
|
195
236
|
const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
|
|
196
237
|
const remoteNote = opts?.eligibility?.remote?.note?.trim();
|
|
197
|
-
return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries.
|
|
198
|
-
.filter(Boolean)
|
|
199
|
-
.join("\n");
|
|
238
|
+
return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries)].filter(Boolean).join("\n");
|
|
200
239
|
}
|
|
201
240
|
export function resolveSkillsPromptForRun(params) {
|
|
202
241
|
const snapshotPrompt = params.skillsSnapshot?.prompt?.trim();
|
|
@@ -264,6 +303,9 @@ const FORCE_RESYNC_SKILLS = new Set([
|
|
|
264
303
|
// v1.11.2: brevo skill updated with SMS credits reference; twilio scoped to voice-call only
|
|
265
304
|
"brevo",
|
|
266
305
|
"twilio",
|
|
306
|
+
// v1.14: sales-closer and business-assistant updated with embed:true
|
|
307
|
+
"sales-closer",
|
|
308
|
+
"business-assistant",
|
|
267
309
|
]);
|
|
268
310
|
/**
|
|
269
311
|
* Copy bundled skills into a workspace's `skills/` directory.
|
|
@@ -27,7 +27,7 @@ function buildMemorySection(params) {
|
|
|
27
27
|
return [
|
|
28
28
|
"## Memory Recall",
|
|
29
29
|
"Memory is your knowledge base — customer profiles, business data, preferences, prior interactions, lessons, and instructions all live there. Proactively use memory_search whenever a topic might have stored context: who a person is, what was discussed before, business rules, pricing, product details, or anything that could have been recorded. Do not rely on conversation history alone — it only covers the current session. Use memory_get to pull specific lines when you know the file. If a search returns no results, say you checked.",
|
|
30
|
-
"
|
|
30
|
+
"Paths for memory_get and memory_write accept either form: `memory/public/data.md` or `public/data.md` — the `memory/` prefix is added automatically when missing. Never use read or skill_read for memory content.",
|
|
31
31
|
"",
|
|
32
32
|
];
|
|
33
33
|
}
|
|
@@ -6,6 +6,7 @@ import { createCanvasTool } from "./tools/canvas-tool.js";
|
|
|
6
6
|
import { createCronTool } from "./tools/cron-tool.js";
|
|
7
7
|
import { createDocumentTool } from "./tools/document-tool.js";
|
|
8
8
|
import { createDocumentToPdfTool } from "./tools/document-to-pdf-tool.js";
|
|
9
|
+
import { createQrGenerateTool } from "./tools/qr-generate-tool.js";
|
|
9
10
|
import { createGatewayTool } from "./tools/gateway-tool.js";
|
|
10
11
|
import { createImageTool } from "./tools/image-tool.js";
|
|
11
12
|
import { createMemoryGetTool, createMemorySaveMediaTool, createMemorySearchTool, createMemoryWriteTool, } from "./tools/memory-tool.js";
|
|
@@ -148,16 +149,17 @@ export function createTaskmasterTools(options) {
|
|
|
148
149
|
agentSessionKey: options?.agentSessionKey,
|
|
149
150
|
sandboxRoot: options?.sandboxRoot,
|
|
150
151
|
}),
|
|
152
|
+
createQrGenerateTool(),
|
|
151
153
|
createCurrentTimeTool({ config: options?.config }),
|
|
152
154
|
createAuthorizeAdminTool({ agentAccountId: options?.agentAccountId }),
|
|
153
155
|
createRevokeAdminTool({ agentAccountId: options?.agentAccountId }),
|
|
154
156
|
createListAdminsTool({ agentAccountId: options?.agentAccountId }),
|
|
155
157
|
createBootstrapCompleteTool({ agentSessionKey: options?.agentSessionKey }),
|
|
156
158
|
createLicenseGenerateTool(),
|
|
157
|
-
createContactCreateTool(),
|
|
158
|
-
createContactDeleteTool(),
|
|
159
|
-
createContactLookupTool(),
|
|
160
|
-
createContactUpdateTool(),
|
|
159
|
+
createContactCreateTool({ agentAccountId: options?.agentAccountId }),
|
|
160
|
+
createContactDeleteTool({ agentAccountId: options?.agentAccountId }),
|
|
161
|
+
createContactLookupTool({ agentAccountId: options?.agentAccountId }),
|
|
162
|
+
createContactUpdateTool({ agentAccountId: options?.agentAccountId }),
|
|
161
163
|
createVerifyContactTool({ agentSessionKey: options?.agentSessionKey }),
|
|
162
164
|
createVerifyContactCodeTool({ agentSessionKey: options?.agentSessionKey }),
|
|
163
165
|
createRelayMessageTool(),
|
|
@@ -15,7 +15,7 @@ export const TOOL_GROUPS = {
|
|
|
15
15
|
"message_history",
|
|
16
16
|
"document_read",
|
|
17
17
|
],
|
|
18
|
-
"group:documents": ["document_read", "document_to_pdf"],
|
|
18
|
+
"group:documents": ["document_read", "document_to_pdf", "qr_generate"],
|
|
19
19
|
"group:web": ["web_search", "web_fetch"],
|
|
20
20
|
// Time/date utilities
|
|
21
21
|
"group:time": ["current_time"],
|
|
@@ -76,6 +76,7 @@ export const TOOL_GROUPS = {
|
|
|
76
76
|
"message_history",
|
|
77
77
|
"document_read",
|
|
78
78
|
"document_to_pdf",
|
|
79
|
+
"qr_generate",
|
|
79
80
|
"web_search",
|
|
80
81
|
"web_fetch",
|
|
81
82
|
"image",
|
|
@@ -20,7 +20,8 @@ const ContactCreateSchema = Type.Object({
|
|
|
20
20
|
* are the canonical identifiers that link to user-scoped memory at
|
|
21
21
|
* `memory/users/{phone}/`.
|
|
22
22
|
*/
|
|
23
|
-
export function createContactCreateTool() {
|
|
23
|
+
export function createContactCreateTool(opts) {
|
|
24
|
+
const workspace = opts?.agentAccountId;
|
|
24
25
|
return {
|
|
25
26
|
label: "Contact Create",
|
|
26
27
|
name: "contact_create",
|
|
@@ -42,13 +43,13 @@ export function createContactCreateTool() {
|
|
|
42
43
|
if (!name) {
|
|
43
44
|
return jsonResult({ error: "Name is required." });
|
|
44
45
|
}
|
|
45
|
-
const existing = getRecord(phone);
|
|
46
|
+
const existing = getRecord(phone, workspace);
|
|
46
47
|
if (existing) {
|
|
47
48
|
return jsonResult({
|
|
48
49
|
error: `A contact record already exists for ${phone} (${existing.name}). Use contact_update to modify it.`,
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
|
-
const record = setRecord(phone, { name, fields });
|
|
52
|
+
const record = setRecord(phone, { name, fields, workspace });
|
|
52
53
|
return jsonResult({
|
|
53
54
|
ok: true,
|
|
54
55
|
action: "created",
|
|
@@ -13,7 +13,8 @@ const ContactDeleteSchema = Type.Object({
|
|
|
13
13
|
* Does NOT delete associated user-scoped memory at `memory/users/{phone}/` —
|
|
14
14
|
* that is a separate decision for the business owner.
|
|
15
15
|
*/
|
|
16
|
-
export function createContactDeleteTool() {
|
|
16
|
+
export function createContactDeleteTool(opts) {
|
|
17
|
+
const workspace = opts?.agentAccountId;
|
|
17
18
|
return {
|
|
18
19
|
label: "Contact Delete",
|
|
19
20
|
name: "contact_delete",
|
|
@@ -27,7 +28,7 @@ export function createContactDeleteTool() {
|
|
|
27
28
|
if (!phone) {
|
|
28
29
|
return jsonResult({ error: "Phone number is required." });
|
|
29
30
|
}
|
|
30
|
-
const existing = getRecord(phone);
|
|
31
|
+
const existing = getRecord(phone, workspace);
|
|
31
32
|
if (!existing) {
|
|
32
33
|
return jsonResult({
|
|
33
34
|
error: `No contact record found for ${phone}.`,
|
|
@@ -9,7 +9,8 @@ const ContactLookupSchema = Type.Object({
|
|
|
9
9
|
description: "Name to search for (partial match).",
|
|
10
10
|
})),
|
|
11
11
|
});
|
|
12
|
-
export function createContactLookupTool() {
|
|
12
|
+
export function createContactLookupTool(opts) {
|
|
13
|
+
const workspace = opts?.agentAccountId;
|
|
13
14
|
return {
|
|
14
15
|
label: "Contact Lookup",
|
|
15
16
|
name: "contact_lookup",
|
|
@@ -22,20 +23,20 @@ export function createContactLookupTool() {
|
|
|
22
23
|
const phone = typeof params.phone === "string" ? params.phone.trim() : undefined;
|
|
23
24
|
const name = typeof params.name === "string" ? params.name.trim() : undefined;
|
|
24
25
|
if (!phone && !name) {
|
|
25
|
-
const all = listRecords();
|
|
26
|
+
const all = listRecords(workspace);
|
|
26
27
|
if (all.length === 0) {
|
|
27
28
|
return jsonResult({ found: false, message: "No contact records exist yet." });
|
|
28
29
|
}
|
|
29
30
|
return jsonResult({ found: true, count: all.length, records: all });
|
|
30
31
|
}
|
|
31
32
|
if (phone) {
|
|
32
|
-
const record = getRecord(phone);
|
|
33
|
+
const record = getRecord(phone, workspace);
|
|
33
34
|
if (!record) {
|
|
34
35
|
return jsonResult({ found: false, message: `No record found for ${phone}.` });
|
|
35
36
|
}
|
|
36
37
|
return jsonResult({ found: true, record });
|
|
37
38
|
}
|
|
38
|
-
const results = searchRecords(name);
|
|
39
|
+
const results = searchRecords(name, workspace);
|
|
39
40
|
if (results.length === 0) {
|
|
40
41
|
return jsonResult({ found: false, message: `No records matching "${name}".` });
|
|
41
42
|
}
|
|
@@ -20,7 +20,8 @@ const ContactUpdateSchema = Type.Object({
|
|
|
20
20
|
* The public agent does NOT have access to this tool (same gating as
|
|
21
21
|
* license_generate — not in any profile allow list).
|
|
22
22
|
*/
|
|
23
|
-
export function createContactUpdateTool() {
|
|
23
|
+
export function createContactUpdateTool(opts) {
|
|
24
|
+
const workspace = opts?.agentAccountId;
|
|
24
25
|
return {
|
|
25
26
|
label: "Contact Update",
|
|
26
27
|
name: "contact_update",
|
|
@@ -39,7 +40,7 @@ export function createContactUpdateTool() {
|
|
|
39
40
|
if (!field) {
|
|
40
41
|
return jsonResult({ error: "Field name is required." });
|
|
41
42
|
}
|
|
42
|
-
const existing = getRecord(phone);
|
|
43
|
+
const existing = getRecord(phone, workspace);
|
|
43
44
|
if (!existing) {
|
|
44
45
|
return jsonResult({
|
|
45
46
|
error: `No contact record found for ${phone}. Create one via the Contacts admin page first.`,
|
|
@@ -48,7 +49,9 @@ export function createContactUpdateTool() {
|
|
|
48
49
|
// "name" updates the contact's canonical display name, not a custom field.
|
|
49
50
|
if (field === "name") {
|
|
50
51
|
if (value === undefined) {
|
|
51
|
-
return jsonResult({
|
|
52
|
+
return jsonResult({
|
|
53
|
+
error: "Cannot delete the contact name. Provide a new value instead.",
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
const updated = setRecordName(phone, value);
|
|
54
57
|
return jsonResult({
|
|
@@ -9,7 +9,9 @@ const MemorySearchSchema = Type.Object({
|
|
|
9
9
|
minScore: Type.Optional(Type.Number()),
|
|
10
10
|
});
|
|
11
11
|
const MemoryGetSchema = Type.Object({
|
|
12
|
-
path: Type.String(
|
|
12
|
+
path: Type.String({
|
|
13
|
+
description: 'Path within memory, e.g. "memory/public/data.md" or "public/data.md"',
|
|
14
|
+
}),
|
|
13
15
|
from: Type.Optional(Type.Number()),
|
|
14
16
|
lines: Type.Optional(Type.Number()),
|
|
15
17
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { renderQrPngBase64 } from "../../web/qr-image.js";
|
|
6
|
+
import { jsonResult, readStringParam } from "./common.js";
|
|
7
|
+
const QrGenerateSchema = Type.Object({
|
|
8
|
+
data: Type.String({
|
|
9
|
+
description: "The content to encode as a QR code. Pass the final string directly: " +
|
|
10
|
+
"a URL, plain text, a vCard block, or a wa.me deep link.",
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
export function createQrGenerateTool() {
|
|
14
|
+
return {
|
|
15
|
+
label: "QR Code Generator",
|
|
16
|
+
name: "qr_generate",
|
|
17
|
+
description: "Generate a QR code PNG from any string (URL, text, vCard, WhatsApp link). " +
|
|
18
|
+
"Returns a MEDIA: path — copy it into the message tool's media parameter to send the image.",
|
|
19
|
+
parameters: QrGenerateSchema,
|
|
20
|
+
execute: async (_toolCallId, params) => {
|
|
21
|
+
const data = readStringParam(params, "data");
|
|
22
|
+
if (!data) {
|
|
23
|
+
return jsonResult({ ok: false, error: "data is empty — provide the content to encode" });
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const base64 = await renderQrPngBase64(data);
|
|
27
|
+
const buf = Buffer.from(base64, "base64");
|
|
28
|
+
const filename = `qr-${Date.now()}.png`;
|
|
29
|
+
const pngPath = path.join(os.tmpdir(), filename);
|
|
30
|
+
await fs.writeFile(pngPath, buf);
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{ type: "text", text: `MEDIA:${pngPath}` },
|
|
34
|
+
{ type: "text", text: JSON.stringify({ ok: true, path: pngPath }) },
|
|
35
|
+
],
|
|
36
|
+
details: { ok: true, path: pngPath },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
return jsonResult({ ok: false, error: `QR generation failed: ${message}` });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|