@rubytech/taskmaster 1.9.6 → 1.9.8
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/build-info.json
CHANGED
|
@@ -122,15 +122,35 @@ export const tailscaleHandlers = {
|
|
|
122
122
|
message: typeof errObj.message === "string" ? errObj.message : "",
|
|
123
123
|
};
|
|
124
124
|
};
|
|
125
|
-
const
|
|
125
|
+
const isPermissionDenied = (output) => {
|
|
126
126
|
const lower = output.toLowerCase();
|
|
127
127
|
return (lower.includes("access denied") ||
|
|
128
128
|
lower.includes("permission denied") ||
|
|
129
129
|
lower.includes("use 'sudo tailscale") ||
|
|
130
130
|
lower.includes("requires root"));
|
|
131
131
|
};
|
|
132
|
+
// Ensure the gateway's OS user is set as the Tailscale operator.
|
|
133
|
+
// This is a persistent setting — once applied, `tailscale up` works
|
|
134
|
+
// without root for the lifetime of the Tailscale install.
|
|
135
|
+
const ensureOperator = async (binary) => {
|
|
136
|
+
const whoami = (await runExec("whoami", [], { timeoutMs: 3_000 }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
137
|
+
if (!whoami)
|
|
138
|
+
return false;
|
|
139
|
+
context.logGateway.info(`tailscale.enable: setting Tailscale operator to "${whoami}"`);
|
|
140
|
+
try {
|
|
141
|
+
await runExec("sudo", ["-n", binary, "set", `--operator=${whoami}`], {
|
|
142
|
+
timeoutMs: 10_000,
|
|
143
|
+
});
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
context.logGateway.warn("tailscale.enable: failed to set operator (sudo -n not available)");
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
132
151
|
// Run `tailscale up` (with optional extra args) and capture output.
|
|
133
|
-
// If the command fails with a permission error,
|
|
152
|
+
// If the command fails with a permission error, sets the current user
|
|
153
|
+
// as Tailscale operator (permanent fix) and retries without sudo.
|
|
134
154
|
// `tailscale up` blocks until auth completes, so timeouts are expected —
|
|
135
155
|
// the auth URL appears in stdout/stderr before the timeout kills it.
|
|
136
156
|
const runTailscaleUp = async (binary, extraArgs = []) => {
|
|
@@ -138,10 +158,17 @@ export const tailscaleHandlers = {
|
|
|
138
158
|
const execOpts = { timeoutMs: 60_000, maxBuffer: 100_000 };
|
|
139
159
|
const result = await runExec(binary, args, execOpts).catch((err) => captureExecOutput(err));
|
|
140
160
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
141
|
-
if (!
|
|
161
|
+
if (!isPermissionDenied(output))
|
|
142
162
|
return output;
|
|
143
|
-
// Permission denied —
|
|
144
|
-
|
|
163
|
+
// Permission denied — fix permanently by setting operator, then retry
|
|
164
|
+
const fixed = await ensureOperator(binary);
|
|
165
|
+
if (fixed) {
|
|
166
|
+
// Operator set — retry without sudo (clean output capture)
|
|
167
|
+
const retryResult = await runExec(binary, args, execOpts).catch((err) => captureExecOutput(err));
|
|
168
|
+
return `${retryResult.stdout}\n${retryResult.stderr}`;
|
|
169
|
+
}
|
|
170
|
+
// Fallback: direct sudo attempt
|
|
171
|
+
context.logGateway.info(`tailscale.enable: falling back to sudo: ${args.join(" ")}`);
|
|
145
172
|
const sudoResult = await runExec("sudo", ["-n", binary, ...args], execOpts).catch((err) => captureExecOutput(err));
|
|
146
173
|
return `${sudoResult.stdout}\n${sudoResult.stderr}`;
|
|
147
174
|
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget suggestion generation + broadcast.
|
|
3
|
+
*
|
|
4
|
+
* Called after broadcastChatFinal (follow-up mode) and after
|
|
5
|
+
* chat.history returns empty messages (session-start mode).
|
|
6
|
+
*/
|
|
7
|
+
import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../agents/agent-scope.js";
|
|
8
|
+
import { resolveIdentityName } from "../agents/identity.js";
|
|
9
|
+
import { loadConfig } from "../config/config.js";
|
|
10
|
+
import { loadSessionEntry, readSessionMessages } from "../gateway/session-utils.js";
|
|
11
|
+
import { generateSuggestions } from "./generator.js";
|
|
12
|
+
/** Default model for suggestion generation. */
|
|
13
|
+
const SUGGESTION_MODEL = "claude-haiku-4-5-20251001";
|
|
14
|
+
/** Number of recent messages to include as context for suggestion generation. */
|
|
15
|
+
const RECENT_MESSAGES_LIMIT = 10;
|
|
16
|
+
/** Extract text from a session message (handles both string and structured content). */
|
|
17
|
+
function extractMessageText(msg) {
|
|
18
|
+
if (!msg || typeof msg !== "object")
|
|
19
|
+
return "";
|
|
20
|
+
const m = msg;
|
|
21
|
+
if (typeof m.content === "string")
|
|
22
|
+
return m.content;
|
|
23
|
+
if (Array.isArray(m.content)) {
|
|
24
|
+
return m.content
|
|
25
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
26
|
+
.map((c) => c.text)
|
|
27
|
+
.join(" ");
|
|
28
|
+
}
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load recent conversation messages for context. Returns a formatted string
|
|
33
|
+
* with the last N messages (user + assistant turns).
|
|
34
|
+
*/
|
|
35
|
+
function loadRecentMessages(sessionKey) {
|
|
36
|
+
try {
|
|
37
|
+
const { storePath, entry } = loadSessionEntry(sessionKey);
|
|
38
|
+
if (!entry?.sessionId || !storePath)
|
|
39
|
+
return "";
|
|
40
|
+
const messages = readSessionMessages(entry.sessionId, storePath, entry.sessionFile);
|
|
41
|
+
if (messages.length === 0)
|
|
42
|
+
return "";
|
|
43
|
+
// Take the last N messages
|
|
44
|
+
const recent = messages.slice(-RECENT_MESSAGES_LIMIT);
|
|
45
|
+
return recent
|
|
46
|
+
.map((msg) => {
|
|
47
|
+
const m = msg;
|
|
48
|
+
const role = m.role === "assistant" ? "Assistant" : "User";
|
|
49
|
+
const text = extractMessageText(msg);
|
|
50
|
+
if (!text)
|
|
51
|
+
return null;
|
|
52
|
+
// Truncate long messages
|
|
53
|
+
const truncated = text.length > 200 ? `${text.slice(0, 200)}...` : text;
|
|
54
|
+
return `${role}: ${truncated}`;
|
|
55
|
+
})
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join("\n");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generate suggestions and broadcast them. Fire-and-forget — errors are logged, never thrown.
|
|
65
|
+
*/
|
|
66
|
+
export function fireSuggestion(params) {
|
|
67
|
+
const { sessionKey, broadcast, cfg, lastUserMessage, lastAssistantReply } = params;
|
|
68
|
+
const config = cfg ?? loadConfig();
|
|
69
|
+
const agentId = resolveSessionAgentId({ sessionKey, config });
|
|
70
|
+
const agentDir = resolveAgentWorkspaceDir(config, agentId);
|
|
71
|
+
const assistantName = resolveIdentityName(config, agentId);
|
|
72
|
+
// Load recent conversation for better context
|
|
73
|
+
const recentMessages = loadRecentMessages(sessionKey);
|
|
74
|
+
if (!recentMessages) {
|
|
75
|
+
console.warn(`[suggestions] no recent messages found for sessionKey=${sessionKey}`);
|
|
76
|
+
}
|
|
77
|
+
void generateSuggestions({
|
|
78
|
+
model: SUGGESTION_MODEL,
|
|
79
|
+
lastUserMessage,
|
|
80
|
+
lastAssistantReply,
|
|
81
|
+
recentMessages,
|
|
82
|
+
assistantName,
|
|
83
|
+
cfg: config,
|
|
84
|
+
agentDir,
|
|
85
|
+
})
|
|
86
|
+
.then((result) => {
|
|
87
|
+
if (result.ok) {
|
|
88
|
+
broadcast("suggestions", {
|
|
89
|
+
sessionKey,
|
|
90
|
+
suggestions: result.texts,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.warn(`[suggestions] generation failed: ${result.error.message}`);
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
console.warn(`[suggestions] unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggestion chip generation.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - Session start: picks from curated opener pairs (no AI call)
|
|
6
|
+
* - Follow-up: uses Haiku to generate two contextual quick-replies (affirmative + negative)
|
|
7
|
+
*/
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { complete } from "@mariozechner/pi-ai";
|
|
10
|
+
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js";
|
|
12
|
+
import { ensureTaskmasterModelsJson } from "../agents/models-config.js";
|
|
13
|
+
const FOLLOW_UP_SYSTEM_PROMPT = `Output ONLY a JSON array with exactly two strings. No explanation, no markdown, no labels.`;
|
|
14
|
+
/**
|
|
15
|
+
* Curated session-start opener pairs. Picked randomly.
|
|
16
|
+
* Each pair has an affirmative and an alternative option.
|
|
17
|
+
*/
|
|
18
|
+
const SESSION_START_OPENER_PAIRS = [
|
|
19
|
+
["What's on the schedule today?", "Give me a quick briefing"],
|
|
20
|
+
["Any messages I should catch up on?", "What needs my attention?"],
|
|
21
|
+
["Help me plan my day", "Anything urgent right now?"],
|
|
22
|
+
["What's new since yesterday?", "Show me my to-do list"],
|
|
23
|
+
["Any updates I should know about?", "Let's get started"],
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Pick a random session-start opener pair. No AI call needed.
|
|
27
|
+
*/
|
|
28
|
+
export function pickSessionStartSuggestions() {
|
|
29
|
+
const pair = SESSION_START_OPENER_PAIRS[Math.floor(Math.random() * SESSION_START_OPENER_PAIRS.length)];
|
|
30
|
+
return { ok: true, texts: [pair[0], pair[1]] };
|
|
31
|
+
}
|
|
32
|
+
/** Strip surrounding quotes from a string. */
|
|
33
|
+
function stripQuotes(s) {
|
|
34
|
+
let text = s.trim();
|
|
35
|
+
if ((text.startsWith('"') && text.endsWith('"')) ||
|
|
36
|
+
(text.startsWith("'") && text.endsWith("'"))) {
|
|
37
|
+
text = text.slice(1, -1).trim();
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generate two follow-up suggestions using Haiku. Requires conversation context.
|
|
43
|
+
*/
|
|
44
|
+
export async function generateFollowUpSuggestions(params) {
|
|
45
|
+
const { model: modelId, lastUserMessage, lastAssistantReply, recentMessages, assistantName, timeoutMs = 4000, cfg, agentDir, } = params;
|
|
46
|
+
if (!lastAssistantReply) {
|
|
47
|
+
return { ok: false, error: new Error("Follow-up requires lastAssistantReply") };
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await ensureTaskmasterModelsJson(cfg, agentDir);
|
|
51
|
+
const authStorage = agentDir
|
|
52
|
+
? new AuthStorage(path.join(agentDir, "auth.json"))
|
|
53
|
+
: new AuthStorage();
|
|
54
|
+
const modelRegistry = agentDir
|
|
55
|
+
? new ModelRegistry(authStorage, path.join(agentDir, "models.json"))
|
|
56
|
+
: new ModelRegistry(authStorage);
|
|
57
|
+
const model = modelRegistry.find("anthropic", modelId);
|
|
58
|
+
if (!model) {
|
|
59
|
+
return { ok: false, error: new Error(`Suggestion model not found: anthropic/${modelId}`) };
|
|
60
|
+
}
|
|
61
|
+
const apiKeyInfo = await getApiKeyForModel({ model, cfg, agentDir });
|
|
62
|
+
const apiKey = requireApiKey(apiKeyInfo, model.provider);
|
|
63
|
+
authStorage.setRuntimeApiKey(model.provider, apiKey);
|
|
64
|
+
// Build prompt from conversation history when available, fall back to last exchange
|
|
65
|
+
let prompt;
|
|
66
|
+
// Build the conversation context — prefer full history, fall back to last exchange
|
|
67
|
+
const name = assistantName || "Assistant";
|
|
68
|
+
let conversationContext;
|
|
69
|
+
if (recentMessages) {
|
|
70
|
+
conversationContext = recentMessages;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const truncatedReply = lastAssistantReply.length > 300
|
|
74
|
+
? `${lastAssistantReply.slice(0, 300)}...`
|
|
75
|
+
: lastAssistantReply;
|
|
76
|
+
const truncatedUser = lastUserMessage && lastUserMessage.length > 200
|
|
77
|
+
? `${lastUserMessage.slice(0, 200)}...`
|
|
78
|
+
: (lastUserMessage ?? "");
|
|
79
|
+
conversationContext = `User: ${truncatedUser}\n${name}: ${truncatedReply}`;
|
|
80
|
+
}
|
|
81
|
+
prompt = `${conversationContext}\n\nPredict the next message from the user. Give me two options that oppose each other if possible.`;
|
|
82
|
+
const context = {
|
|
83
|
+
systemPrompt: FOLLOW_UP_SYSTEM_PROMPT,
|
|
84
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
85
|
+
};
|
|
86
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
87
|
+
setTimeout(() => reject(new Error("Suggestion generation timeout")), timeoutMs);
|
|
88
|
+
});
|
|
89
|
+
const message = (await Promise.race([
|
|
90
|
+
complete(model, context, {
|
|
91
|
+
apiKey,
|
|
92
|
+
maxTokens: 100,
|
|
93
|
+
temperature: 0.8,
|
|
94
|
+
}),
|
|
95
|
+
timeoutPromise,
|
|
96
|
+
]));
|
|
97
|
+
const content = message.content?.[0];
|
|
98
|
+
if (!content || content.type !== "text" || !content.text) {
|
|
99
|
+
return { ok: false, error: new Error("Empty suggestion response") };
|
|
100
|
+
}
|
|
101
|
+
// Strip markdown code fences (```json ... ```) before parsing
|
|
102
|
+
let raw = content.text.trim();
|
|
103
|
+
raw = raw
|
|
104
|
+
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
105
|
+
.replace(/\n?```\s*$/, "")
|
|
106
|
+
.trim();
|
|
107
|
+
// Parse JSON array response
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
if (Array.isArray(parsed) && parsed.length >= 2) {
|
|
111
|
+
const texts = parsed.slice(0, 2).map((s) => stripQuotes(String(s)));
|
|
112
|
+
if (texts.every((t) => t.length > 0)) {
|
|
113
|
+
return { ok: true, texts };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// If JSON parsing fails, try to extract two lines
|
|
119
|
+
const lines = raw
|
|
120
|
+
.split("\n")
|
|
121
|
+
.map((l) => stripQuotes(l))
|
|
122
|
+
.filter((l) => l.length > 0);
|
|
123
|
+
if (lines.length >= 2) {
|
|
124
|
+
return { ok: true, texts: [lines[0], lines[1]] };
|
|
125
|
+
}
|
|
126
|
+
// Fall back to single suggestion
|
|
127
|
+
if (lines.length === 1) {
|
|
128
|
+
return { ok: true, texts: [lines[0]] };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { ok: false, error: new Error("Could not parse suggestion pair from response") };
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
135
|
+
return { ok: false, error };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generate suggestions. Delegates to the appropriate strategy based on context.
|
|
140
|
+
*/
|
|
141
|
+
export async function generateSuggestions(params) {
|
|
142
|
+
if (params.lastAssistantReply) {
|
|
143
|
+
return generateFollowUpSuggestions(params);
|
|
144
|
+
}
|
|
145
|
+
return pickSessionStartSuggestions();
|
|
146
|
+
}
|
|
147
|
+
// --- Legacy single-suggestion API (preserved for backwards compatibility) ---
|
|
148
|
+
/**
|
|
149
|
+
* Pick a random session-start opener. No AI call needed.
|
|
150
|
+
*/
|
|
151
|
+
export function pickSessionStartSuggestion() {
|
|
152
|
+
const result = pickSessionStartSuggestions();
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
return result;
|
|
155
|
+
return { ok: true, text: result.texts[0] };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Generate a suggestion. Delegates to the appropriate strategy based on context.
|
|
159
|
+
*/
|
|
160
|
+
export async function generateSuggestion(params) {
|
|
161
|
+
const result = await generateSuggestions(params);
|
|
162
|
+
if (!result.ok)
|
|
163
|
+
return result;
|
|
164
|
+
return { ok: true, text: result.texts[0] };
|
|
165
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a proactive greeting for the public chat widget.
|
|
3
|
+
*
|
|
4
|
+
* Uses Haiku to create a short, friendly first message based on the agent's
|
|
5
|
+
* identity (name + SOUL.md persona). If no model/key is available, falls back
|
|
6
|
+
* to a generic greeting.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { complete } from "@mariozechner/pi-ai";
|
|
11
|
+
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
|
13
|
+
import { resolveIdentityName } from "../agents/identity.js";
|
|
14
|
+
import { DEFAULT_SOUL_FILENAME } from "../agents/workspace.js";
|
|
15
|
+
import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js";
|
|
16
|
+
import { ensureTaskmasterModelsJson } from "../agents/models-config.js";
|
|
17
|
+
const GREETING_MODEL = "claude-haiku-4-5-20251001";
|
|
18
|
+
const SYSTEM_PROMPT = `You write a single short, friendly greeting that an AI assistant would use to start a conversation with a website visitor. Output ONLY the greeting text — no quotes, no prefix, no explanation.
|
|
19
|
+
|
|
20
|
+
Rules:
|
|
21
|
+
1. Keep it warm, approachable, and natural (10-25 words)
|
|
22
|
+
2. Reflect the assistant's personality and role if context is provided
|
|
23
|
+
3. Invite the visitor to ask a question or start a conversation
|
|
24
|
+
4. Never mention being an AI or chatbot — just be helpful
|
|
25
|
+
5. Do not use emojis`;
|
|
26
|
+
export async function generateGreeting(params) {
|
|
27
|
+
const { cfg, agentId } = params;
|
|
28
|
+
const agentDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
29
|
+
const name = resolveIdentityName(cfg, agentId) ?? "Assistant";
|
|
30
|
+
// Read SOUL.md for persona context (if present)
|
|
31
|
+
let soulContext = "";
|
|
32
|
+
try {
|
|
33
|
+
const soulPath = path.join(agentDir, DEFAULT_SOUL_FILENAME);
|
|
34
|
+
if (fs.existsSync(soulPath)) {
|
|
35
|
+
const raw = fs.readFileSync(soulPath, "utf8").trim();
|
|
36
|
+
if (raw.length > 0) {
|
|
37
|
+
soulContext = raw.length > 500 ? raw.slice(0, 500) + "..." : raw;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore — SOUL.md is optional
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await ensureTaskmasterModelsJson(cfg, agentDir);
|
|
46
|
+
const authStorage = new AuthStorage(path.join(agentDir, "auth.json"));
|
|
47
|
+
const modelRegistry = new ModelRegistry(authStorage, path.join(agentDir, "models.json"));
|
|
48
|
+
const model = modelRegistry.find("anthropic", GREETING_MODEL);
|
|
49
|
+
if (!model) {
|
|
50
|
+
return fallbackGreeting(name);
|
|
51
|
+
}
|
|
52
|
+
const apiKeyInfo = await getApiKeyForModel({ model, cfg, agentDir });
|
|
53
|
+
const apiKey = requireApiKey(apiKeyInfo, model.provider);
|
|
54
|
+
authStorage.setRuntimeApiKey(model.provider, apiKey);
|
|
55
|
+
const prompt = soulContext
|
|
56
|
+
? `Assistant name: "${name}"\n\nPersona/role:\n${soulContext}\n\nGenerate a greeting:`
|
|
57
|
+
: `Assistant name: "${name}"\n\nGenerate a greeting:`;
|
|
58
|
+
const context = {
|
|
59
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
60
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
61
|
+
};
|
|
62
|
+
const message = (await Promise.race([
|
|
63
|
+
complete(model, context, {
|
|
64
|
+
apiKey,
|
|
65
|
+
maxTokens: 80,
|
|
66
|
+
temperature: 0.7,
|
|
67
|
+
}),
|
|
68
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Greeting generation timeout")), 8000)),
|
|
69
|
+
]));
|
|
70
|
+
const content = message.content?.[0];
|
|
71
|
+
if (!content || content.type !== "text" || !content.text) {
|
|
72
|
+
return fallbackGreeting(name);
|
|
73
|
+
}
|
|
74
|
+
let text = content.text.trim();
|
|
75
|
+
// Strip quotes if wrapped
|
|
76
|
+
if ((text.startsWith('"') && text.endsWith('"')) ||
|
|
77
|
+
(text.startsWith("'") && text.endsWith("'"))) {
|
|
78
|
+
text = text.slice(1, -1).trim();
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, greeting: text };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return fallbackGreeting(name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function fallbackGreeting(name) {
|
|
87
|
+
return { ok: true, greeting: `Hi, I'm ${name}. How can I help you today?` };
|
|
88
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/taskmaster",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.8",
|
|
4
4
|
"description": "AI-powered business assistant for small businesses",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -75,7 +75,8 @@
|
|
|
75
75
|
"dist/whatsapp/**",
|
|
76
76
|
"dist/records/**",
|
|
77
77
|
"dist/filler/**",
|
|
78
|
-
"dist/license/**"
|
|
78
|
+
"dist/license/**",
|
|
79
|
+
"dist/suggestions/**"
|
|
79
80
|
],
|
|
80
81
|
"scripts": {
|
|
81
82
|
"dev": "node scripts/run-node.mjs",
|