heyhank 0.1.0 → 0.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/LICENSE +21 -0
- package/README.md +83 -10
- package/bin/cli.ts +7 -7
- package/bin/ctl.ts +42 -42
- package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-B-AAmsMK.js} +3 -3
- package/dist/assets/AssistantPage-BV1Mfwdt.js +2 -0
- package/dist/assets/BusinessPage-tLpNEz19.js +1 -0
- package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-B-K_n3Jg.js} +1 -1
- package/dist/assets/HelpPage-Bhf_j6Xr.js +1 -0
- package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-DAMjs9tM.js} +1 -1
- package/dist/assets/JarvisHUD-C_TGXCCn.js +120 -0
- package/dist/assets/MediaPage-C48HTTrt.js +1 -0
- package/dist/assets/MemoryPage-JkC-qtgp.js +1 -0
- package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-AUo7tNnE.js} +1 -1
- package/dist/assets/{Playground-Fc5cdc5p.js → Playground-AzNMsRBL.js} +1 -1
- package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-DpE_2sX3.js} +1 -1
- package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-C2RQOs6p.js} +2 -2
- package/dist/assets/RunsPage-B9UOyO79.js +1 -0
- package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-jHvYjwfh.js} +1 -1
- package/dist/assets/SettingsPage-BBJax6gt.js +51 -0
- package/dist/assets/SkillsMarketplace-IjmjfdjD.js +1 -0
- package/dist/assets/SocialMediaPage-DoPZHhr2.js +10 -0
- package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DDEY7ckO.js} +1 -1
- package/dist/assets/TelephonyPage-OPNBZYKt.js +9 -0
- package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-BjMbHHW3.js} +1 -1
- package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
- package/dist/assets/{index-CEqZnThB.js → index-BgYM4wXw.js} +94 -93
- package/dist/assets/index-BkjSoVgn.css +32 -0
- package/dist/assets/sw-register-C7NOHtIu.js +1 -0
- package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +6 -1
- package/server/agent-executor.ts +37 -2
- package/server/agent-store.ts +3 -3
- package/server/agent-types.ts +11 -0
- package/server/assistant-store.ts +232 -6
- package/server/auth-manager.ts +9 -0
- package/server/cache-headers.ts +1 -1
- package/server/calendar-service.ts +10 -0
- package/server/ceo/document-store.ts +129 -0
- package/server/ceo/finance-store.ts +343 -0
- package/server/ceo/kpi-store.ts +208 -0
- package/server/ceo/memory-import.ts +277 -0
- package/server/ceo/news-store.ts +208 -0
- package/server/ceo/template-store.ts +134 -0
- package/server/ceo/time-tracking-store.ts +227 -0
- package/server/claude-auth-monitor.ts +128 -0
- package/server/claude-code-worker.ts +86 -0
- package/server/claude-session-discovery.ts +74 -1
- package/server/cli-launcher.ts +32 -10
- package/server/codex-adapter.ts +2 -2
- package/server/codex-ws-proxy.cjs +1 -1
- package/server/container-manager.ts +4 -4
- package/server/content-intelligence/content-engine.ts +1112 -0
- package/server/content-intelligence/platform-knowledge.ts +870 -0
- package/server/cron-store.ts +3 -3
- package/server/embedding-service.ts +49 -0
- package/server/event-bus-types.ts +13 -0
- package/server/federation/node-store.ts +5 -4
- package/server/fs-utils.ts +28 -1
- package/server/hank-notifications-store.ts +91 -0
- package/server/hank-tool-executor.ts +1835 -0
- package/server/hank-tools.ts +2107 -0
- package/server/image-pull-manager.ts +2 -2
- package/server/index.ts +25 -2
- package/server/llm-providers-streaming.ts +541 -0
- package/server/llm-providers.ts +12 -0
- package/server/marketplace.ts +249 -0
- package/server/mcp-registry.ts +158 -0
- package/server/memory-service.ts +296 -0
- package/server/obsidian-sync.ts +184 -0
- package/server/provider-manager.ts +5 -2
- package/server/provider-registry.ts +12 -0
- package/server/reminder-scheduler.ts +37 -1
- package/server/routes/agent-routes.ts +2 -1
- package/server/routes/assistant-routes.ts +198 -5
- package/server/routes/ceo-finance-kpi-routes.ts +167 -0
- package/server/routes/ceo-news-time-routes.ts +137 -0
- package/server/routes/ceo-routes.ts +99 -0
- package/server/routes/content-routes.ts +116 -0
- package/server/routes/email-routes.ts +147 -0
- package/server/routes/env-routes.ts +3 -3
- package/server/routes/fs-routes.ts +12 -9
- package/server/routes/hank-chat-routes.ts +592 -0
- package/server/routes/llm-routes.ts +12 -0
- package/server/routes/marketplace-routes.ts +63 -0
- package/server/routes/media-routes.ts +1 -1
- package/server/routes/memory-routes.ts +127 -0
- package/server/routes/platform-routes.ts +14 -675
- package/server/routes/sandbox-routes.ts +1 -1
- package/server/routes/settings-routes.ts +51 -1
- package/server/routes/socialmedia-routes.ts +152 -2
- package/server/routes/system-routes.ts +2 -2
- package/server/routes/team-routes.ts +71 -0
- package/server/routes/telephony-routes.ts +98 -18
- package/server/routes.ts +36 -9
- package/server/session-creation-service.ts +2 -2
- package/server/session-orchestrator.ts +54 -2
- package/server/session-types.ts +2 -0
- package/server/settings-manager.ts +50 -2
- package/server/skill-discovery.ts +68 -0
- package/server/socialmedia/adapters/browser-adapter.ts +179 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
- package/server/socialmedia/manager.ts +234 -15
- package/server/socialmedia/store.ts +51 -1
- package/server/socialmedia/types.ts +35 -2
- package/server/socialview/browser-manager.ts +150 -0
- package/server/socialview/extractors.ts +1298 -0
- package/server/socialview/image-describe.ts +188 -0
- package/server/socialview/library.ts +119 -0
- package/server/socialview/poster.ts +276 -0
- package/server/socialview/routes.ts +371 -0
- package/server/socialview/style-analyzer.ts +187 -0
- package/server/socialview/style-profiles.ts +67 -0
- package/server/socialview/types.ts +166 -0
- package/server/socialview/vision.ts +127 -0
- package/server/socialview/vnc-manager.ts +110 -0
- package/server/style-injector.ts +135 -0
- package/server/team-service.ts +239 -0
- package/server/team-store.ts +75 -0
- package/server/team-types.ts +52 -0
- package/server/telephony/audio-bridge.ts +281 -35
- package/server/telephony/audio-recorder.ts +132 -0
- package/server/telephony/call-manager.ts +803 -104
- package/server/telephony/call-types.ts +67 -1
- package/server/telephony/esl-client.ts +319 -0
- package/server/telephony/freeswitch-sync.ts +155 -0
- package/server/telephony/phone-utils.ts +63 -0
- package/server/telephony/telephony-store.ts +9 -8
- package/server/url-validator.ts +82 -0
- package/server/vault-markdown.ts +317 -0
- package/server/vault-migration.ts +121 -0
- package/server/vault-store.ts +466 -0
- package/server/vault-watcher.ts +59 -0
- package/server/vector-store.ts +210 -0
- package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
- package/server/voice-pipeline/greeting-cache.ts +200 -0
- package/server/voice-pipeline/manager.ts +249 -0
- package/server/voice-pipeline/pipeline.ts +335 -0
- package/server/voice-pipeline/providers/index.ts +47 -0
- package/server/voice-pipeline/providers/llm-internal.ts +527 -0
- package/server/voice-pipeline/providers/stt-google.ts +157 -0
- package/server/voice-pipeline/providers/tts-google.ts +126 -0
- package/server/voice-pipeline/types.ts +247 -0
- package/server/ws-bridge-types.ts +6 -1
- package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
- package/dist/assets/HelpPage-DMfkzERp.js +0 -1
- package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
- package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
- package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
- package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
- package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
- package/dist/assets/index-C8M_PUmX.css +0 -32
- package/dist/assets/sw-register-LSSpj6RU.js +0 -1
- package/server/socialmedia/adapters/ayrshare-adapter.ts +0 -169
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
// ─── Hank Tool Executor ─────────────────────────────────────────────────────
|
|
2
|
+
// Executes tool calls for all Hank-UI providers.
|
|
3
|
+
// Extracted from platform-routes.ts POST /gemini/tool-call.
|
|
4
|
+
|
|
5
|
+
import * as assistantStore from "./assistant-store.js";
|
|
6
|
+
import { readSkillContent, listInstalledSkills } from "./skill-discovery.js";
|
|
7
|
+
import * as emailService from "./email-service.js";
|
|
8
|
+
import * as calendarService from "./calendar-service.js";
|
|
9
|
+
import { listAgents, createAgent } from "./agent-store.js";
|
|
10
|
+
import { buildStyleProfileBlockFromText } from "./style-injector.js";
|
|
11
|
+
|
|
12
|
+
const BASE_URL = `http://127.0.0.1:${process.env.PORT || 3100}/api`;
|
|
13
|
+
|
|
14
|
+
export async function executeHankTool(
|
|
15
|
+
name: string,
|
|
16
|
+
args: Record<string, unknown> | undefined,
|
|
17
|
+
authHeader: string,
|
|
18
|
+
): Promise<unknown> {
|
|
19
|
+
const headers: Record<string, string> = {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...(authHeader ? { Authorization: authHeader } : {}),
|
|
22
|
+
};
|
|
23
|
+
const base = BASE_URL;
|
|
24
|
+
|
|
25
|
+
console.log(`[hank-tool] ${name}`, JSON.stringify(args || {}).slice(0, 200));
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
switch (name) {
|
|
29
|
+
// ─── Skill Invocation ─────────────────────────────────────────
|
|
30
|
+
case "run_skill": {
|
|
31
|
+
const slug = (args?.slug as string) || "";
|
|
32
|
+
const input = (args?.input as string) || "";
|
|
33
|
+
if (!slug) return { error: "slug is required" };
|
|
34
|
+
const content = readSkillContent(slug);
|
|
35
|
+
if (!content) {
|
|
36
|
+
const available = listInstalledSkills().map((s) => s.slug).join(", ");
|
|
37
|
+
return { error: `Skill "${slug}" not found. Installed: ${available}` };
|
|
38
|
+
}
|
|
39
|
+
// Return the full SKILL.md so the LLM can follow the workflow in the
|
|
40
|
+
// current chat. Hank reads the skill's instructions and continues
|
|
41
|
+
// the multi-stage dialog with the user — no agent dispatch needed.
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
slug,
|
|
45
|
+
input: input || null,
|
|
46
|
+
skill: content,
|
|
47
|
+
instruction:
|
|
48
|
+
"You are now executing a multi-stage skill workflow. The SKILL.md content above defines the stages.\n\n"
|
|
49
|
+
+ "PROCESS:\n"
|
|
50
|
+
+ "1. If the skill needs inputs and you don't have them all yet, ask the user (in ONE message, all required fields).\n"
|
|
51
|
+
+ "2. Once inputs are clear, IMMEDIATELY produce Stage 1's output exactly as the skill specifies (tables, lists, structure).\n"
|
|
52
|
+
+ "3. End each stage by briefly asking: 'Weiter zum nächsten Stage oder diesen überarbeiten?'.\n"
|
|
53
|
+
+ "4. When the user replies with continuation (weiter / mache weiter / next / continue / fortfahren / ja / proceed / ok), IMMEDIATELY produce the NEXT stage's output. DO NOT ask clarifying questions. DO NOT call run_skill again. DO NOT echo the user back. Just produce the next stage.\n"
|
|
54
|
+
+ "5. Continue until all stages are complete or the user stops you.\n\n"
|
|
55
|
+
+ "MANDATORY STAGE HEADER FORMAT:\n"
|
|
56
|
+
+ "Every stage output (Stage 1 through final) MUST begin its first line with this exact marker so the system can track skill progress across turns:\n"
|
|
57
|
+
+ ` [skill:${slug} stage:N/TOTAL]\n`
|
|
58
|
+
+ "Replace N with the current stage number and TOTAL with the total number of stages defined by the skill. Then continue with a normal markdown header (e.g. `## Stage N — Title`) and the stage content.\n\n"
|
|
59
|
+
+ "Example first lines of a Stage 2 output:\n"
|
|
60
|
+
+ ` [skill:${slug} stage:2/7]\n`
|
|
61
|
+
+ " ## Stage 2 — Generate 30 Posts\n"
|
|
62
|
+
+ " …\n\n"
|
|
63
|
+
+ "The marker is parsed by the chat backend to re-inject this skill's instructions on subsequent turns. Without the marker, the skill loses state and the next stage cannot be produced. ALWAYS include it as the very first line of every stage's output.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Agent Orchestration ──────────────────────────────────────
|
|
68
|
+
case "run_agent": {
|
|
69
|
+
const agentQuery = (args?.agent as string) || "";
|
|
70
|
+
let task = (args?.task as string) || "";
|
|
71
|
+
if (!agentQuery || !task) {
|
|
72
|
+
return { error: "agent and task are required" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Inject uploaded file context if provided
|
|
76
|
+
const files = (args?.files as string[]) || (args?.attachments as string[]) || [];
|
|
77
|
+
if (files.length > 0) {
|
|
78
|
+
task = `The user has shared these files with you: ${files.join(", ")}. They are available on the local filesystem.\n\n${task}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-inject SocialView persona style profile if the task references
|
|
82
|
+
// a known role-model by name/handle. The Content Agent treats this
|
|
83
|
+
// block as binding (overrides platform defaults).
|
|
84
|
+
try {
|
|
85
|
+
const styleBlock = buildStyleProfileBlockFromText(task);
|
|
86
|
+
if (styleBlock) {
|
|
87
|
+
task = task + styleBlock;
|
|
88
|
+
console.log(`[hank-tool] run_agent: injected style profile block (${styleBlock.length} chars)`);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(`[hank-tool] style-injector failed:`, e);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fuzzy match agent by name or ID
|
|
95
|
+
const agents = listAgents();
|
|
96
|
+
let matched = agents.find((a) => a.id === agentQuery || a.name.toLowerCase() === agentQuery.toLowerCase());
|
|
97
|
+
if (!matched) {
|
|
98
|
+
// Fuzzy: find best match by name similarity
|
|
99
|
+
const q = agentQuery.toLowerCase();
|
|
100
|
+
let bestScore = 0;
|
|
101
|
+
for (const a of agents) {
|
|
102
|
+
const name = a.name.toLowerCase();
|
|
103
|
+
let score = 0;
|
|
104
|
+
if (name.includes(q) || q.includes(name)) score = 0.8;
|
|
105
|
+
else {
|
|
106
|
+
// Word overlap
|
|
107
|
+
const qWords = q.split(/\s+/);
|
|
108
|
+
const nWords = name.split(/\s+/);
|
|
109
|
+
const overlap = qWords.filter((w) => nWords.some((n) => n.includes(w) || w.includes(n))).length;
|
|
110
|
+
score = overlap / Math.max(qWords.length, nWords.length);
|
|
111
|
+
}
|
|
112
|
+
if (score > bestScore) { bestScore = score; matched = a; }
|
|
113
|
+
}
|
|
114
|
+
if (bestScore < 0.3) matched = undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!matched) {
|
|
118
|
+
const available = agents.map((a) => `"${a.name}" (${a.id})`).join(", ");
|
|
119
|
+
return { error: `Agent "${agentQuery}" not found. Available: ${available}` };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Run the agent via /api/agents/:id/run
|
|
123
|
+
const runRes = await fetch(`${base}/agents/${matched.id}/run`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers,
|
|
126
|
+
body: JSON.stringify({ input: task }),
|
|
127
|
+
});
|
|
128
|
+
const runData = await runRes.json() as Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
if (!runRes.ok) {
|
|
131
|
+
return { error: `Failed to run agent: ${(runData as { error?: string }).error || "Unknown error"}` };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sessionId = (runData as { sessionId?: string }).sessionId || null;
|
|
135
|
+
|
|
136
|
+
// Wait for agent to finish by watching message history growth.
|
|
137
|
+
// Agent sessions: CLI connects → user_message sent → agent streams response → result.
|
|
138
|
+
// We poll until we see a "result" message (= agent turn finished).
|
|
139
|
+
if (sessionId) {
|
|
140
|
+
const pollStart = Date.now();
|
|
141
|
+
const POLL_TIMEOUT = 300_000; // 5min max for agents that generate images/content
|
|
142
|
+
const POLL_INTERVAL = 2_000; // check every 2s
|
|
143
|
+
|
|
144
|
+
// Small initial delay for CLI to connect
|
|
145
|
+
await new Promise(r => setTimeout(r, 2_000));
|
|
146
|
+
|
|
147
|
+
while (Date.now() - pollStart < POLL_TIMEOUT) {
|
|
148
|
+
try {
|
|
149
|
+
const statusRes = await fetch(`${base}/sessions/${sessionId}/agent-status`, { headers });
|
|
150
|
+
if (statusRes.ok) {
|
|
151
|
+
const status = await statusRes.json() as Record<string, unknown>;
|
|
152
|
+
const activity = (status.recentActivity as Array<{ type: string; text?: string }>) || [];
|
|
153
|
+
const phase = status.phase as string;
|
|
154
|
+
const state = status.state as string;
|
|
155
|
+
|
|
156
|
+
// Check if agent has produced a final result message (= conversation ended)
|
|
157
|
+
const hasResult = activity.some(a => a.type === "result");
|
|
158
|
+
// Agent is truly finished only when:
|
|
159
|
+
// 1. Session terminated (CLI exited), OR
|
|
160
|
+
// 2. There's a "result" message (final SDK result) and session is idle
|
|
161
|
+
// NOTE: phase="ready" happens between EVERY tool call turn, not just at the end.
|
|
162
|
+
// We must NOT treat "ready" + "hasAssistantOutput" as finished — the agent
|
|
163
|
+
// may still have many more tool calls to make.
|
|
164
|
+
const isTerminated = phase === "terminated";
|
|
165
|
+
const isFinished = isTerminated || (phase === "ready" && hasResult);
|
|
166
|
+
|
|
167
|
+
console.log(`[hank-tool] polling ${sessionId.slice(0, 8)}: phase=${phase} hasResult=${hasResult} elapsed=${Math.round((Date.now() - pollStart) / 1000)}s`);
|
|
168
|
+
|
|
169
|
+
if (isFinished) {
|
|
170
|
+
const resultText = activity
|
|
171
|
+
.filter(a => a.text)
|
|
172
|
+
.map(a => a.text)
|
|
173
|
+
.join("\n");
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
agentName: matched.name,
|
|
178
|
+
agentId: matched.id,
|
|
179
|
+
sessionId,
|
|
180
|
+
status: "completed",
|
|
181
|
+
agentResult: resultText || "Agent finished the task.",
|
|
182
|
+
message: `Agent "${matched.name}" has finished. Check the session for full details.`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Agent needs permission
|
|
187
|
+
if (status.needsInput) {
|
|
188
|
+
const perms = (status.pendingPermissions as Array<{ toolName: string; description: string }>) || [];
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
agentName: matched.name,
|
|
192
|
+
agentId: matched.id,
|
|
193
|
+
sessionId,
|
|
194
|
+
status: "needs_permission",
|
|
195
|
+
message: `Agent "${matched.name}" needs permission: ${perms.map(p => p.toolName).join(", ")}. Check the session to approve.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Agent exited
|
|
200
|
+
if (state === "exited") {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
agentName: matched.name,
|
|
204
|
+
sessionId,
|
|
205
|
+
status: "exited",
|
|
206
|
+
message: `Agent "${matched.name}" exited unexpectedly. Check the session for details.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Polling error — continue trying
|
|
212
|
+
}
|
|
213
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Timeout — return what we have
|
|
217
|
+
return {
|
|
218
|
+
success: true,
|
|
219
|
+
agentName: matched.name,
|
|
220
|
+
agentId: matched.id,
|
|
221
|
+
sessionId,
|
|
222
|
+
status: "still_running",
|
|
223
|
+
message: `Agent "${matched.name}" is still working. Check the session for progress.`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
agentName: matched.name,
|
|
230
|
+
agentId: matched.id,
|
|
231
|
+
sessionId,
|
|
232
|
+
model: matched.model,
|
|
233
|
+
message: `Agent "${matched.name}" started.`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case "create_agent": {
|
|
238
|
+
const agentName = (args?.name as string) || "";
|
|
239
|
+
const agentPrompt = (args?.prompt as string) || "";
|
|
240
|
+
const agentDesc = (args?.description as string) || "";
|
|
241
|
+
const agentModel = (args?.model as string) || "claude-sonnet-4-20250514";
|
|
242
|
+
const agentCwd = (args?.cwd as string) || "";
|
|
243
|
+
const autoStart = args?.autoStart as boolean;
|
|
244
|
+
const autoTask = (args?.task as string) || "";
|
|
245
|
+
|
|
246
|
+
if (!agentName || !agentPrompt) {
|
|
247
|
+
return { error: "name and prompt are required" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const newAgent = createAgent({
|
|
252
|
+
name: agentName,
|
|
253
|
+
description: agentDesc,
|
|
254
|
+
prompt: agentPrompt,
|
|
255
|
+
backendType: "claude",
|
|
256
|
+
model: agentModel,
|
|
257
|
+
permissionMode: "auto-accept",
|
|
258
|
+
cwd: agentCwd,
|
|
259
|
+
version: 1,
|
|
260
|
+
enabled: true,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
let sessionId: string | null = null;
|
|
264
|
+
if (autoStart && autoTask) {
|
|
265
|
+
const runRes = await fetch(`${base}/agents/${newAgent.id}/run`, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers,
|
|
268
|
+
body: JSON.stringify({ input: autoTask }),
|
|
269
|
+
});
|
|
270
|
+
if (runRes.ok) {
|
|
271
|
+
const runData = await runRes.json() as { sessionId?: string };
|
|
272
|
+
sessionId = runData.sessionId || null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
agentName: newAgent.name,
|
|
279
|
+
agentId: newAgent.id,
|
|
280
|
+
...(sessionId ? { sessionId, message: `Agent "${newAgent.name}" created and started. Session ID: ${sessionId}. Use monitor_agent_session to check progress.` } : { message: `Agent "${newAgent.name}" created successfully. Use run_agent to start it.` }),
|
|
281
|
+
};
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return { error: `Failed to create agent: ${err instanceof Error ? err.message : "Unknown error"}` };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case "list_sessions": {
|
|
288
|
+
const res = await fetch(`${base}/sessions`, { headers });
|
|
289
|
+
const sessions = await res.json() as Array<Record<string, unknown>>;
|
|
290
|
+
// Return a compact summary
|
|
291
|
+
const summary = sessions
|
|
292
|
+
.filter((s) => s.state !== "exited" && !s.archived)
|
|
293
|
+
.map((s) => ({
|
|
294
|
+
id: s.sessionId,
|
|
295
|
+
state: s.state,
|
|
296
|
+
model: s.model || "unknown",
|
|
297
|
+
backend: s.backendType || "claude",
|
|
298
|
+
cwd: s.cwd,
|
|
299
|
+
name: s.name || null,
|
|
300
|
+
}));
|
|
301
|
+
return { sessions: summary, count: summary.length };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
case "create_session": {
|
|
305
|
+
const backend = (args?.backend as string) || "claude";
|
|
306
|
+
const cwd = (args?.cwd as string) || "/opt/agentplatform/web";
|
|
307
|
+
const message = args?.message as string | undefined;
|
|
308
|
+
|
|
309
|
+
const res = await fetch(`${base}/sessions/create`, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers,
|
|
312
|
+
body: JSON.stringify({ backend, cwd }),
|
|
313
|
+
});
|
|
314
|
+
const session = await res.json() as Record<string, unknown>;
|
|
315
|
+
|
|
316
|
+
if (!res.ok) {
|
|
317
|
+
const errMsg = typeof (session as { error?: string }).error === "string" ? (session as { error?: string }).error : "Unknown error";
|
|
318
|
+
return { error: `Failed to create session: ${errMsg}` };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result: Record<string, unknown> = {
|
|
322
|
+
sessionId: session.sessionId,
|
|
323
|
+
state: session.state,
|
|
324
|
+
cwd: session.cwd,
|
|
325
|
+
message: `Session created successfully with ${backend}.`,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// If there's an initial message, send it via WebSocket bridge endpoint
|
|
329
|
+
if (message && session.sessionId) {
|
|
330
|
+
result.initialMessage = message;
|
|
331
|
+
result.note = "Initial message will be sent once the session is connected. The user should see it in the chat.";
|
|
332
|
+
// Fire and forget — don't block the tool response to Gemini
|
|
333
|
+
fetch(`${base}/gemini/send-to-session`, {
|
|
334
|
+
method: "POST",
|
|
335
|
+
headers,
|
|
336
|
+
body: JSON.stringify({ sessionId: session.sessionId, message }),
|
|
337
|
+
}).catch(() => {});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case "send_message": {
|
|
344
|
+
const sessionId = args?.session_id as string;
|
|
345
|
+
const message = args?.message as string;
|
|
346
|
+
|
|
347
|
+
if (!sessionId || !message) {
|
|
348
|
+
return { error: "session_id and message are required" };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Use the internal send endpoint
|
|
352
|
+
const res = await fetch(`${base}/gemini/send-to-session`, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers,
|
|
355
|
+
body: JSON.stringify({ sessionId, message }),
|
|
356
|
+
});
|
|
357
|
+
const data = await res.json() as Record<string, unknown>;
|
|
358
|
+
|
|
359
|
+
if (!res.ok) {
|
|
360
|
+
const errMsg = typeof (data as { error?: string }).error === "string" ? (data as { error?: string }).error : "Unknown error";
|
|
361
|
+
return { error: `Failed to send message: ${errMsg}` };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { success: true, sessionId, message: "Message sent to session." };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case "get_session_status":
|
|
368
|
+
case "monitor_agent_session": {
|
|
369
|
+
const sessionId = args?.session_id as string;
|
|
370
|
+
if (!sessionId) {
|
|
371
|
+
return { error: "session_id is required" };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const res = await fetch(`${base}/sessions/${sessionId}/agent-status`, { headers });
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
return { error: "Session not found" };
|
|
377
|
+
}
|
|
378
|
+
const status = await res.json() as Record<string, unknown>;
|
|
379
|
+
|
|
380
|
+
// Build a human-readable summary for Gemini
|
|
381
|
+
let summary = "";
|
|
382
|
+
if (status.needsInput) {
|
|
383
|
+
const perms = status.pendingPermissions as Array<{ toolName: string; description: string }>;
|
|
384
|
+
summary = `ATTENTION: Agent needs permission! Pending: ${perms.map((p) => `${p.toolName}${p.description ? ` (${p.description})` : ""}`).join(", ")}. Tell the user immediately!`;
|
|
385
|
+
} else if (status.isCompleted) {
|
|
386
|
+
summary = "Agent has finished. Task is complete.";
|
|
387
|
+
} else if (status.isWorking) {
|
|
388
|
+
summary = "Agent is still working...";
|
|
389
|
+
} else {
|
|
390
|
+
summary = `Agent phase: ${status.phase}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
...status,
|
|
395
|
+
summary,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Todo Tools ───────────────────────────────────────────────
|
|
400
|
+
case "list_todos": {
|
|
401
|
+
const todos = assistantStore.listTodos({
|
|
402
|
+
done: args?.show_completed ? undefined : false,
|
|
403
|
+
priority: args?.priority as string | undefined,
|
|
404
|
+
category: args?.category as string | undefined,
|
|
405
|
+
});
|
|
406
|
+
return { todos, count: todos.length };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case "add_todo": {
|
|
410
|
+
const text = args?.text as string;
|
|
411
|
+
if (!text) return { error: "text is required" };
|
|
412
|
+
const todo = assistantStore.addTodo(text, args?.priority as string, args?.category as string | undefined);
|
|
413
|
+
return { todo, message: "Todo added." };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case "complete_todo": {
|
|
417
|
+
const id = args?.id as string;
|
|
418
|
+
if (!id) return { error: "id is required" };
|
|
419
|
+
const todo = assistantStore.completeTodo(id);
|
|
420
|
+
return todo ? { todo, message: "Todo completed." } : { error: "Todo not found" };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case "delete_todo": {
|
|
424
|
+
const id = args?.id as string;
|
|
425
|
+
if (!id) return { error: "id is required" };
|
|
426
|
+
const ok = assistantStore.deleteTodo(id);
|
|
427
|
+
return ok ? { message: "Todo deleted." } : { error: "Todo not found" };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
case "update_todo": {
|
|
431
|
+
const id = args?.id as string;
|
|
432
|
+
if (!id) return { error: "id is required" };
|
|
433
|
+
const todo = assistantStore.updateTodo(id, {
|
|
434
|
+
text: args?.text as string | undefined,
|
|
435
|
+
priority: args?.priority as string | undefined,
|
|
436
|
+
category: args?.category as string | undefined,
|
|
437
|
+
delegatedTo: args?.delegatedTo as string | undefined,
|
|
438
|
+
dueDate: args?.dueDate as string | undefined,
|
|
439
|
+
followUpDate: args?.followUpDate as string | undefined,
|
|
440
|
+
project: args?.project as string | undefined,
|
|
441
|
+
});
|
|
442
|
+
return todo ? { todo, message: "Todo updated." } : { error: "Todo not found" };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
case "delegate_task": {
|
|
446
|
+
const text = args?.text as string;
|
|
447
|
+
const delegatedTo = args?.delegatedTo as string;
|
|
448
|
+
if (!text || !delegatedTo) return { error: "text and delegatedTo are required" };
|
|
449
|
+
const todo = assistantStore.addTodo(text, (args?.priority as string) || "medium", args?.category as string | undefined, {
|
|
450
|
+
delegatedTo,
|
|
451
|
+
dueDate: args?.dueDate as string | undefined,
|
|
452
|
+
project: args?.project as string | undefined,
|
|
453
|
+
});
|
|
454
|
+
return { todo, message: `Task delegated to ${delegatedTo}.` };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case "list_delegations": {
|
|
458
|
+
const delegations = assistantStore.listDelegations(args?.person as string | undefined);
|
|
459
|
+
const grouped: Record<string, typeof delegations> = {};
|
|
460
|
+
for (const d of delegations) {
|
|
461
|
+
const key = d.delegatedTo || "unknown";
|
|
462
|
+
if (!grouped[key]) grouped[key] = [];
|
|
463
|
+
grouped[key].push(d);
|
|
464
|
+
}
|
|
465
|
+
return { delegations: grouped, count: delegations.length };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case "list_projects": {
|
|
469
|
+
const allTodos = assistantStore.listTodos();
|
|
470
|
+
const projects: Record<string, { project: string; total: number; done: number; open: number }> = {};
|
|
471
|
+
for (const t of allTodos) {
|
|
472
|
+
if (!t.project) continue;
|
|
473
|
+
if (!projects[t.project]) projects[t.project] = { project: t.project, total: 0, done: 0, open: 0 };
|
|
474
|
+
projects[t.project].total++;
|
|
475
|
+
if (t.done) projects[t.project].done++;
|
|
476
|
+
else projects[t.project].open++;
|
|
477
|
+
}
|
|
478
|
+
return { projects: Object.values(projects), count: Object.keys(projects).length };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
case "create_project": {
|
|
482
|
+
const projectName = args?.name as string;
|
|
483
|
+
if (!projectName) return { error: "name is required" };
|
|
484
|
+
const note = assistantStore.addNote(projectName, (args?.description as string) || "", ["project"]);
|
|
485
|
+
const todos: ReturnType<typeof assistantStore.addTodo>[] = [];
|
|
486
|
+
const todoTexts = (args?.todos as string[]) || [];
|
|
487
|
+
for (const text of todoTexts) {
|
|
488
|
+
todos.push(assistantStore.addTodo(text, "medium", undefined, { project: projectName }));
|
|
489
|
+
}
|
|
490
|
+
return { note, todos, message: `Project "${projectName}" created with ${todos.length} todo(s).` };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ─── Note Tools ──────────────────────────────────────────────
|
|
494
|
+
case "search_notes": {
|
|
495
|
+
const notes = assistantStore.listNotes(args?.query as string | undefined);
|
|
496
|
+
return { notes, count: notes.length };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
case "add_note": {
|
|
500
|
+
const title = args?.title as string;
|
|
501
|
+
const content = args?.content as string || "";
|
|
502
|
+
if (!title) return { error: "title is required" };
|
|
503
|
+
const tags = args?.tags ? (args.tags as string).split(",").map((t: string) => t.trim()) : [];
|
|
504
|
+
const note = assistantStore.addNote(title, content, tags);
|
|
505
|
+
return { note, message: "Note saved." };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
case "update_note": {
|
|
509
|
+
const id = args?.id as string;
|
|
510
|
+
if (!id) return { error: "id is required" };
|
|
511
|
+
const tags = args?.tags ? (args.tags as string).split(",").map((t: string) => t.trim()) : undefined;
|
|
512
|
+
const note = assistantStore.updateNote(id, {
|
|
513
|
+
title: args?.title as string | undefined,
|
|
514
|
+
content: args?.content as string | undefined,
|
|
515
|
+
tags,
|
|
516
|
+
});
|
|
517
|
+
return note ? { note, message: "Note updated." } : { error: "Note not found" };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
case "delete_note": {
|
|
521
|
+
const id = args?.id as string;
|
|
522
|
+
if (!id) return { error: "id is required" };
|
|
523
|
+
const ok = assistantStore.deleteNote(id);
|
|
524
|
+
return ok ? { message: "Note deleted." } : { error: "Note not found" };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ─── Reminder Tools ──────────────────────────────────────────
|
|
528
|
+
case "list_reminders": {
|
|
529
|
+
const reminders = assistantStore.listReminders(!!args?.include_fired);
|
|
530
|
+
return { reminders, count: reminders.length };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case "add_reminder": {
|
|
534
|
+
const text = args?.text as string;
|
|
535
|
+
const triggerAt = args?.trigger_at as string;
|
|
536
|
+
if (!text || !triggerAt) return { error: "text and trigger_at are required" };
|
|
537
|
+
const reminder = assistantStore.addReminder(text, triggerAt);
|
|
538
|
+
return { reminder, message: "Reminder set." };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
case "delete_reminder": {
|
|
542
|
+
const id = args?.id as string;
|
|
543
|
+
if (!id) return { error: "id is required" };
|
|
544
|
+
const ok = assistantStore.deleteReminder(id);
|
|
545
|
+
return ok ? { message: "Reminder deleted." } : { error: "Reminder not found" };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ─── Email Tools ─────────────────────────────────────────────
|
|
549
|
+
case "list_email_accounts": {
|
|
550
|
+
const accounts = emailService.loadAccounts();
|
|
551
|
+
return { accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email })), count: accounts.length };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
case "list_emails": {
|
|
555
|
+
const accountId = args?.account as string;
|
|
556
|
+
if (!accountId) return { error: "account (name, email or id) is required" };
|
|
557
|
+
const account = emailService.getAccount(accountId);
|
|
558
|
+
if (!account) return { error: `Account "${accountId}" not found. Use list_email_accounts to see available accounts.` };
|
|
559
|
+
const emails = await emailService.listEmails(account, {
|
|
560
|
+
limit: (args?.limit as number) || 10,
|
|
561
|
+
unseen: !!args?.unseen_only,
|
|
562
|
+
});
|
|
563
|
+
return { emails, count: emails.length, account: account.name };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case "read_email": {
|
|
567
|
+
const accountId = args?.account as string;
|
|
568
|
+
const uid = args?.uid as number;
|
|
569
|
+
if (!accountId || !uid) return { error: "account and uid are required" };
|
|
570
|
+
const account = emailService.getAccount(accountId);
|
|
571
|
+
if (!account) return { error: `Account "${accountId}" not found` };
|
|
572
|
+
const email = await emailService.readEmail(account, uid);
|
|
573
|
+
if (!email) return { error: "Email not found" };
|
|
574
|
+
// Clean up body for voice readability: strip encoding artifacts, excessive whitespace
|
|
575
|
+
let body = email.textBody || "(empty)";
|
|
576
|
+
// Remove common MIME/encoding artifacts
|
|
577
|
+
body = body.replace(/=\r?\n/g, "");
|
|
578
|
+
body = body.replace(/=([0-9A-Fa-f]{2})/g, (_m: string, hex: string) => String.fromCharCode(parseInt(hex, 16)));
|
|
579
|
+
body = body.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
580
|
+
body = body.replace(/\s+/g, " ").trim();
|
|
581
|
+
// Limit for Gemini tool response (keep it voice-friendly)
|
|
582
|
+
if (body.length > 1500) body = body.slice(0, 1500) + "...";
|
|
583
|
+
return { subject: email.subject, from: email.from, to: email.to, date: email.date, body };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
case "search_emails": {
|
|
587
|
+
const accountId = args?.account as string;
|
|
588
|
+
const query = args?.query as string;
|
|
589
|
+
if (!accountId || !query) return { error: "account and query are required" };
|
|
590
|
+
const account = emailService.getAccount(accountId);
|
|
591
|
+
if (!account) return { error: `Account "${accountId}" not found` };
|
|
592
|
+
const emails = await emailService.searchEmails(account, query, (args?.limit as number) || 10);
|
|
593
|
+
return { emails, count: emails.length };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
case "send_email": {
|
|
597
|
+
const accountId = args?.account as string;
|
|
598
|
+
const to = args?.to as string;
|
|
599
|
+
const subject = args?.subject as string;
|
|
600
|
+
const body = args?.body as string;
|
|
601
|
+
if (!accountId || !to || !subject || !body) return { error: "account, to, subject, and body are required" };
|
|
602
|
+
const account = emailService.getAccount(accountId);
|
|
603
|
+
if (!account) return { error: `Account "${accountId}" not found` };
|
|
604
|
+
const result = await emailService.sendEmail(account, to, subject, body);
|
|
605
|
+
return { ...result, message: `Email sent from ${account.email} to ${to}` };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
case "reply_email": {
|
|
609
|
+
const accountId = args?.account as string;
|
|
610
|
+
const uid = args?.uid as number;
|
|
611
|
+
const body = args?.body as string;
|
|
612
|
+
if (!accountId || !uid || !body) return { error: "account, uid, and body are required" };
|
|
613
|
+
const account = emailService.getAccount(accountId);
|
|
614
|
+
if (!account) return { error: `Account "${accountId}" not found` };
|
|
615
|
+
const result = await emailService.replyToEmail(account, uid, body);
|
|
616
|
+
return { ...result, message: "Reply sent." };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
case "email_summary": {
|
|
620
|
+
const summary = await emailService.getUnreadSummary();
|
|
621
|
+
return { accounts: summary, totalUnread: summary.reduce((s, a) => s + a.unread, 0) };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ─── Calendar Tools ────────────────────────────────────────────
|
|
625
|
+
case "list_calendar_accounts": {
|
|
626
|
+
const accounts = calendarService.loadAccounts();
|
|
627
|
+
return { accounts: accounts.map((a) => ({ id: a.id, name: a.name, provider: a.provider })), count: accounts.length };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
case "list_events": {
|
|
631
|
+
const accountId = args?.account as string;
|
|
632
|
+
if (!accountId) return { error: "account (name or id) is required" };
|
|
633
|
+
const calAccount = calendarService.getAccount(accountId);
|
|
634
|
+
if (!calAccount) return { error: `Calendar account "${accountId}" not found. Use list_calendar_accounts to see available accounts.` };
|
|
635
|
+
const events = await calendarService.listEvents(calAccount, {
|
|
636
|
+
from: args?.from as string | undefined,
|
|
637
|
+
to: args?.to as string | undefined,
|
|
638
|
+
});
|
|
639
|
+
return { events, count: events.length, account: calAccount.name };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
case "create_event": {
|
|
643
|
+
const accountId = args?.account as string;
|
|
644
|
+
const summary = args?.summary as string;
|
|
645
|
+
const start = args?.start as string;
|
|
646
|
+
const end = args?.end as string;
|
|
647
|
+
if (!accountId || !summary || !start || !end) {
|
|
648
|
+
return { error: "account, summary, start, and end are required" };
|
|
649
|
+
}
|
|
650
|
+
const calAccount = calendarService.getAccount(accountId);
|
|
651
|
+
if (!calAccount) return { error: `Calendar account "${accountId}" not found` };
|
|
652
|
+
const created = await calendarService.createEvent(calAccount, {
|
|
653
|
+
summary,
|
|
654
|
+
description: args?.description as string | undefined,
|
|
655
|
+
location: args?.location as string | undefined,
|
|
656
|
+
start,
|
|
657
|
+
end,
|
|
658
|
+
allDay: !!args?.all_day,
|
|
659
|
+
});
|
|
660
|
+
return { ...created, message: `Event "${summary}" created.` };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
case "search_events": {
|
|
664
|
+
const accountId = args?.account as string;
|
|
665
|
+
const query = args?.query as string;
|
|
666
|
+
if (!accountId || !query) return { error: "account and query are required" };
|
|
667
|
+
const calAccount = calendarService.getAccount(accountId);
|
|
668
|
+
if (!calAccount) return { error: `Calendar account "${accountId}" not found` };
|
|
669
|
+
const events = await calendarService.searchEvents(calAccount, query, {
|
|
670
|
+
from: args?.from as string | undefined,
|
|
671
|
+
to: args?.to as string | undefined,
|
|
672
|
+
});
|
|
673
|
+
return { events, count: events.length };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
case "delete_event": {
|
|
677
|
+
const accountId = args?.account as string;
|
|
678
|
+
const uid = args?.uid as string;
|
|
679
|
+
if (!accountId || !uid) return { error: "account and uid are required" };
|
|
680
|
+
const calAccount = calendarService.getAccount(accountId);
|
|
681
|
+
if (!calAccount) return { error: `Calendar account "${accountId}" not found` };
|
|
682
|
+
const deleted = await calendarService.deleteEvent(calAccount, uid);
|
|
683
|
+
return deleted ? { message: "Event deleted." } : { error: "Event not found" };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case "calendar_summary": {
|
|
687
|
+
const summary = await calendarService.getUpcomingSummary();
|
|
688
|
+
return { accounts: summary };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ─── Telephony ──────────────────────────────────────────────────
|
|
692
|
+
case "make_call": {
|
|
693
|
+
const contactNameOrPhone = (args?.phone as string) || "";
|
|
694
|
+
const task = (args?.task as string) || "";
|
|
695
|
+
const listen = args?.listen === true;
|
|
696
|
+
const useSavedScript = args?.useSavedScript === true;
|
|
697
|
+
if (!contactNameOrPhone || !task) {
|
|
698
|
+
return { error: "phone (contact name) and task are required" };
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
// Contacts-only: resolve by name. Raw phone numbers are not allowed.
|
|
702
|
+
const { resolveContactByName } = await import("./telephony/telephony-store.js");
|
|
703
|
+
const contact = resolveContactByName(contactNameOrPhone);
|
|
704
|
+
if (!contact) {
|
|
705
|
+
return { error: `Contact "${contactNameOrPhone}" not found. For safety, only saved contacts can be called. Add the contact in Settings → Telephony → Contacts first.` };
|
|
706
|
+
}
|
|
707
|
+
const { callManager } = await import("./telephony/call-manager.js");
|
|
708
|
+
const call = await callManager.startCall({ phone: contact.phone, prompt: task, voice: args?.voice as string, listen, useSavedScript });
|
|
709
|
+
return {
|
|
710
|
+
success: true,
|
|
711
|
+
callId: call.id,
|
|
712
|
+
contactName: contact.name,
|
|
713
|
+
phone: call.phone,
|
|
714
|
+
status: call.status,
|
|
715
|
+
listenMode: call.listenMode,
|
|
716
|
+
message: `Call to ${contact.name} (${call.phone}) initiated. Status: ${call.status}. Call ID: ${call.id}${listen ? " — Listen mode active." : ""}`,
|
|
717
|
+
};
|
|
718
|
+
} catch (err) {
|
|
719
|
+
return { error: `Failed to start call: ${err instanceof Error ? err.message : "Unknown error"}` };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
case "list_active_calls": {
|
|
724
|
+
try {
|
|
725
|
+
const { callManager } = await import("./telephony/call-manager.js");
|
|
726
|
+
const calls = callManager.getActiveCalls();
|
|
727
|
+
return {
|
|
728
|
+
calls: calls.map((c) => ({
|
|
729
|
+
callId: c.id,
|
|
730
|
+
phone: c.phone,
|
|
731
|
+
status: c.status,
|
|
732
|
+
durationSeconds: c.connectedAt ? Math.round((Date.now() - c.connectedAt) / 1000) : 0,
|
|
733
|
+
prompt: c.prompt,
|
|
734
|
+
})),
|
|
735
|
+
message: calls.length > 0
|
|
736
|
+
? `${calls.length} active call(s): ${calls.map((c) => `${c.phone} (${c.status})`).join(", ")}`
|
|
737
|
+
: "No active calls.",
|
|
738
|
+
};
|
|
739
|
+
} catch {
|
|
740
|
+
return { calls: [], message: "No active calls." };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
case "end_active_call": {
|
|
745
|
+
const callId = (args?.call_id as string) || "";
|
|
746
|
+
if (!callId) return { error: "call_id is required" };
|
|
747
|
+
try {
|
|
748
|
+
const { callManager } = await import("./telephony/call-manager.js");
|
|
749
|
+
const result = await callManager.endCall(callId);
|
|
750
|
+
if (!result) return { error: "Call not found or already ended" };
|
|
751
|
+
return {
|
|
752
|
+
success: true,
|
|
753
|
+
summary: result.summary,
|
|
754
|
+
durationSeconds: result.durationSeconds,
|
|
755
|
+
message: `Call ended. Duration: ${result.durationSeconds}s. ${result.summary || ""}`,
|
|
756
|
+
};
|
|
757
|
+
} catch (err) {
|
|
758
|
+
return { error: err instanceof Error ? err.message : "Failed to end call" };
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ─── Social Media ──────────────────────────────────────────────
|
|
763
|
+
case "prepare_social_post": {
|
|
764
|
+
const postText = (args?.text as string) || "";
|
|
765
|
+
const postPlatforms = (args?.platforms as string[]) || [];
|
|
766
|
+
if (!postText) return { error: "text is required" };
|
|
767
|
+
if (!postPlatforms.length) return { error: "platforms array is required" };
|
|
768
|
+
try {
|
|
769
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
770
|
+
const post = await smManager.createDraft({
|
|
771
|
+
text: postText,
|
|
772
|
+
platforms: postPlatforms as import("./socialmedia/types.js").SocialPlatform[],
|
|
773
|
+
scheduledAt: (args?.scheduledAt as string) || null,
|
|
774
|
+
mediaUrls: (args?.mediaUrls as string[]) || [],
|
|
775
|
+
title: (args?.title as string) || undefined,
|
|
776
|
+
firstComment: (args?.firstComment as string) || undefined,
|
|
777
|
+
videoUrl: (args?.videoUrl as string) || undefined,
|
|
778
|
+
thumbnailUrl: (args?.thumbnailUrl as string) || undefined,
|
|
779
|
+
createdBy: "gemini",
|
|
780
|
+
});
|
|
781
|
+
return {
|
|
782
|
+
success: true,
|
|
783
|
+
postId: post.id,
|
|
784
|
+
status: "draft",
|
|
785
|
+
platforms: post.platforms,
|
|
786
|
+
message: `Draft post prepared for ${postPlatforms.join(", ")}. The user can review and publish it from the Social Media page.`,
|
|
787
|
+
};
|
|
788
|
+
} catch (err) {
|
|
789
|
+
return { error: err instanceof Error ? err.message : "Failed to prepare post" };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
case "create_social_post": {
|
|
794
|
+
const postText = (args?.text as string) || "";
|
|
795
|
+
const postPlatforms = (args?.platforms as string[]) || [];
|
|
796
|
+
if (!postText) return { error: "text is required" };
|
|
797
|
+
if (!postPlatforms.length) return { error: "platforms array is required" };
|
|
798
|
+
try {
|
|
799
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
800
|
+
const post = await smManager.createPost({
|
|
801
|
+
text: postText,
|
|
802
|
+
platforms: postPlatforms as import("./socialmedia/types.js").SocialPlatform[],
|
|
803
|
+
scheduledAt: (args?.scheduledAt as string) || null,
|
|
804
|
+
mediaUrls: [],
|
|
805
|
+
});
|
|
806
|
+
return {
|
|
807
|
+
success: true,
|
|
808
|
+
postId: post.id,
|
|
809
|
+
status: post.status,
|
|
810
|
+
platforms: post.platforms,
|
|
811
|
+
message: `Post created on ${postPlatforms.join(", ")}. Status: ${post.status}.`,
|
|
812
|
+
};
|
|
813
|
+
} catch (err) {
|
|
814
|
+
return { error: err instanceof Error ? err.message : "Failed to create post" };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
case "list_social_posts": {
|
|
819
|
+
try {
|
|
820
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
821
|
+
const posts = await smManager.listPosts({
|
|
822
|
+
limit: (args?.limit as number) || 10,
|
|
823
|
+
status: (args?.status as string) || undefined,
|
|
824
|
+
});
|
|
825
|
+
return {
|
|
826
|
+
posts: posts.map((p) => ({
|
|
827
|
+
id: p.id,
|
|
828
|
+
text: p.text.slice(0, 100),
|
|
829
|
+
status: p.status,
|
|
830
|
+
platforms: p.platforms,
|
|
831
|
+
createdAt: p.createdAt,
|
|
832
|
+
})),
|
|
833
|
+
message: posts.length > 0
|
|
834
|
+
? `${posts.length} post(s) found.`
|
|
835
|
+
: "No posts found.",
|
|
836
|
+
};
|
|
837
|
+
} catch (err) {
|
|
838
|
+
return { error: err instanceof Error ? err.message : "Failed to list posts" };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
case "get_social_analytics": {
|
|
843
|
+
const profileId = (args?.profileId as string) || "";
|
|
844
|
+
if (!profileId) return { error: "profileId is required" };
|
|
845
|
+
try {
|
|
846
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
847
|
+
const analytics = await smManager.getAccountAnalytics(profileId);
|
|
848
|
+
return { ...analytics, message: `Followers: ${analytics.followers}, Following: ${analytics.following}, Posts: ${analytics.posts}` };
|
|
849
|
+
} catch (err) {
|
|
850
|
+
return { error: err instanceof Error ? err.message : "Failed to get analytics" };
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
case "reply_to_social_comment": {
|
|
855
|
+
const smPostId = (args?.postId as string) || "";
|
|
856
|
+
const smCommentId = (args?.commentId as string) || null;
|
|
857
|
+
const smText = (args?.text as string) || "";
|
|
858
|
+
if (!smPostId || !smText) return { error: "postId and text are required" };
|
|
859
|
+
try {
|
|
860
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
861
|
+
const result = await smManager.replyToComment(smPostId, smCommentId, smText);
|
|
862
|
+
return { ...result, message: result.ok ? "Reply sent." : `Failed: ${result.error}` };
|
|
863
|
+
} catch (err) {
|
|
864
|
+
return { error: err instanceof Error ? err.message : "Failed to reply" };
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
case "publish_draft": {
|
|
869
|
+
const postId = (args?.postId as string) || "";
|
|
870
|
+
if (!postId) return { error: "postId is required" };
|
|
871
|
+
try {
|
|
872
|
+
// Check if approval is required
|
|
873
|
+
const smStore = await import("./socialmedia/store.js");
|
|
874
|
+
const smSettings = smStore.getSettings();
|
|
875
|
+
if (smSettings.requireApproval) {
|
|
876
|
+
return {
|
|
877
|
+
success: false,
|
|
878
|
+
postId,
|
|
879
|
+
status: "awaiting_approval",
|
|
880
|
+
message: "Post requires manual approval. Please review and publish from the Social Media page.",
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
884
|
+
const post = await smManager.publishDraft(postId);
|
|
885
|
+
return { success: true, postId: post.id, status: post.status, message: `Draft published on ${post.platforms.join(", ")}.` };
|
|
886
|
+
} catch (err) {
|
|
887
|
+
return { error: err instanceof Error ? err.message : "Failed to publish draft" };
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
case "update_draft": {
|
|
892
|
+
const postId = (args?.postId as string) || "";
|
|
893
|
+
if (!postId) return { error: "postId is required" };
|
|
894
|
+
try {
|
|
895
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
896
|
+
const post = await smManager.updateDraft(postId, {
|
|
897
|
+
text: args?.text as string | undefined,
|
|
898
|
+
platforms: args?.platforms as string[] | undefined,
|
|
899
|
+
scheduledAt: args?.scheduledAt as string | undefined,
|
|
900
|
+
});
|
|
901
|
+
return { success: true, postId: post.id, message: "Draft updated." };
|
|
902
|
+
} catch (err) {
|
|
903
|
+
return { error: err instanceof Error ? err.message : "Failed to update draft" };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
case "delete_draft": {
|
|
908
|
+
const postId = (args?.postId as string) || "";
|
|
909
|
+
if (!postId) return { error: "postId is required" };
|
|
910
|
+
try {
|
|
911
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
912
|
+
const ok = await smManager.deleteDraft(postId);
|
|
913
|
+
return ok ? { message: "Draft deleted." } : { error: "Draft not found" };
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return { error: err instanceof Error ? err.message : "Failed to delete draft" };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
case "schedule_post": {
|
|
920
|
+
const postId = (args?.postId as string) || "";
|
|
921
|
+
const scheduledAt = (args?.scheduledAt as string) || "";
|
|
922
|
+
if (!postId || !scheduledAt) return { error: "postId and scheduledAt are required" };
|
|
923
|
+
try {
|
|
924
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
925
|
+
const post = await smManager.updateDraft(postId, { scheduledAt });
|
|
926
|
+
return { success: true, postId: post.id, scheduledAt, message: `Post scheduled for ${scheduledAt}.` };
|
|
927
|
+
} catch (err) {
|
|
928
|
+
return { error: err instanceof Error ? err.message : "Failed to schedule post" };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
case "fix_claude_auth": {
|
|
933
|
+
const { attemptRefresh, getAuthStatus } = await import("./claude-auth-monitor.js");
|
|
934
|
+
const success = await attemptRefresh();
|
|
935
|
+
const status = getAuthStatus();
|
|
936
|
+
return {
|
|
937
|
+
success,
|
|
938
|
+
status: status.status,
|
|
939
|
+
message: success
|
|
940
|
+
? "Authentication refreshed successfully. Sessions should work now."
|
|
941
|
+
: `Authentication refresh failed (status: ${status.status}). The user may need to run 'claude /login' in the terminal.`,
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ─── Memory Tools ────────────────────────────────────────────────
|
|
946
|
+
case "save_memory": {
|
|
947
|
+
const content = (args?.content as string) || "";
|
|
948
|
+
if (!content) return { error: "content is required" };
|
|
949
|
+
const memService = await import("./memory-service.js");
|
|
950
|
+
const memory = await memService.addMemory(content);
|
|
951
|
+
return { memory, message: `Remembered: "${content}"` };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
case "search_memory": {
|
|
955
|
+
const query = (args?.query as string) || "";
|
|
956
|
+
if (!query) return { error: "query is required" };
|
|
957
|
+
const memService = await import("./memory-service.js");
|
|
958
|
+
const memories = await memService.searchMemories(query);
|
|
959
|
+
return { memories, count: memories.length };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
case "list_memories": {
|
|
963
|
+
const memService = await import("./memory-service.js");
|
|
964
|
+
const memories = await memService.listMemories();
|
|
965
|
+
return { memories, count: memories.length };
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
case "delete_memory": {
|
|
969
|
+
const id = (args?.id as string) || "";
|
|
970
|
+
if (!id) return { error: "id is required" };
|
|
971
|
+
const memService = await import("./memory-service.js");
|
|
972
|
+
const ok = await memService.deleteMemory(id);
|
|
973
|
+
return ok ? { message: "Memory deleted." } : { error: "Memory not found" };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ─── Contact Tools ────────────────────────────────────────────────
|
|
977
|
+
case "list_contacts": {
|
|
978
|
+
const contacts = assistantStore.listContacts(args?.search as string | undefined);
|
|
979
|
+
return { contacts, count: contacts.length };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
case "add_contact": {
|
|
983
|
+
const name = (args?.name as string) || "";
|
|
984
|
+
if (!name) return { error: "name is required" };
|
|
985
|
+
const tags = args?.tags ? (args.tags as string).split(",").map((t: string) => t.trim()) : [];
|
|
986
|
+
const contact = assistantStore.addContact(
|
|
987
|
+
name,
|
|
988
|
+
args?.company as string | undefined,
|
|
989
|
+
args?.email as string | undefined,
|
|
990
|
+
args?.phone as string | undefined,
|
|
991
|
+
args?.notes as string | undefined,
|
|
992
|
+
tags,
|
|
993
|
+
);
|
|
994
|
+
return { contact, message: "Contact added." };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
case "update_contact": {
|
|
998
|
+
const id = (args?.id as string) || "";
|
|
999
|
+
if (!id) return { error: "id is required" };
|
|
1000
|
+
const tags = args?.tags ? (args.tags as string).split(",").map((t: string) => t.trim()) : undefined;
|
|
1001
|
+
const contact = assistantStore.updateContact(id, {
|
|
1002
|
+
name: args?.name as string | undefined,
|
|
1003
|
+
company: args?.company as string | undefined,
|
|
1004
|
+
email: args?.email as string | undefined,
|
|
1005
|
+
phone: args?.phone as string | undefined,
|
|
1006
|
+
notes: args?.notes as string | undefined,
|
|
1007
|
+
tags,
|
|
1008
|
+
});
|
|
1009
|
+
return contact ? { contact, message: "Contact updated." } : { error: "Contact not found" };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
case "search_contacts": {
|
|
1013
|
+
const query = (args?.query as string) || "";
|
|
1014
|
+
if (!query) return { error: "query is required" };
|
|
1015
|
+
const contacts = assistantStore.listContacts(query);
|
|
1016
|
+
return { contacts, count: contacts.length };
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
case "log_interaction": {
|
|
1020
|
+
const contactId = (args?.contactId as string) || "";
|
|
1021
|
+
const type = (args?.type as string) || "";
|
|
1022
|
+
const summary = (args?.summary as string) || "";
|
|
1023
|
+
if (!contactId || !type || !summary) return { error: "contactId, type and summary are required" };
|
|
1024
|
+
const contact = assistantStore.logInteraction(contactId, {
|
|
1025
|
+
type: type as "call" | "email" | "meeting" | "note",
|
|
1026
|
+
summary,
|
|
1027
|
+
});
|
|
1028
|
+
return contact ? { contact, message: "Interaction logged." } : { error: "Contact not found" };
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ─── Decision Tools ───────────────────────────────────────────────
|
|
1032
|
+
case "log_decision": {
|
|
1033
|
+
const title = (args?.title as string) || "";
|
|
1034
|
+
const context = (args?.context as string) || "";
|
|
1035
|
+
const decision = (args?.decision as string) || "";
|
|
1036
|
+
if (!title || !context || !decision) return { error: "title, context and decision are required" };
|
|
1037
|
+
const alternatives = args?.alternatives ? (args.alternatives as string).split(",").map((a: string) => a.trim()) : [];
|
|
1038
|
+
const reasoning = (args?.reasoning as string) || "";
|
|
1039
|
+
const entry = assistantStore.addDecision(title, context, decision, alternatives, reasoning);
|
|
1040
|
+
return { decision: entry, message: "Decision logged." };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
case "search_decisions": {
|
|
1044
|
+
const query = (args?.query as string) || "";
|
|
1045
|
+
if (!query) return { error: "query is required" };
|
|
1046
|
+
const decisions = assistantStore.listDecisions(query);
|
|
1047
|
+
return { decisions, count: decisions.length };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ─── Daily Briefing ──────────────────────────────────────────────
|
|
1051
|
+
case "get_daily_briefing": {
|
|
1052
|
+
const dateStr = (args?.date as string) || new Date().toISOString().slice(0, 10);
|
|
1053
|
+
const today = dateStr;
|
|
1054
|
+
const tomorrowDate = new Date(dateStr);
|
|
1055
|
+
tomorrowDate.setDate(tomorrowDate.getDate() + 1);
|
|
1056
|
+
const tomorrow = tomorrowDate.toISOString().slice(0, 10);
|
|
1057
|
+
|
|
1058
|
+
const emailSummary = await emailService.getUnreadSummary();
|
|
1059
|
+
const totalUnread = emailSummary.reduce((s, a) => s + a.unread, 0);
|
|
1060
|
+
|
|
1061
|
+
let todayEvents: Array<Record<string, unknown>> = [];
|
|
1062
|
+
try {
|
|
1063
|
+
const calAccounts = calendarService.loadAccounts();
|
|
1064
|
+
for (const acc of calAccounts) {
|
|
1065
|
+
const events = await calendarService.listEvents(acc, { from: today, to: tomorrow });
|
|
1066
|
+
todayEvents.push(...events.map((e) => ({ ...e, account: acc.name })));
|
|
1067
|
+
}
|
|
1068
|
+
} catch {}
|
|
1069
|
+
|
|
1070
|
+
const allTodos = assistantStore.listTodos({ done: false });
|
|
1071
|
+
const overdueTodos = allTodos.filter((t) => t.dueDate && t.dueDate < today);
|
|
1072
|
+
const dueTodayTodos = allTodos.filter((t) => t.dueDate === today);
|
|
1073
|
+
|
|
1074
|
+
const delegations = assistantStore.listDelegations();
|
|
1075
|
+
|
|
1076
|
+
let activeSessions: Array<Record<string, unknown>> = [];
|
|
1077
|
+
try {
|
|
1078
|
+
const sessRes = await fetch(`${base}/sessions`, { headers });
|
|
1079
|
+
const sessData = await sessRes.json() as Array<Record<string, unknown>>;
|
|
1080
|
+
activeSessions = sessData
|
|
1081
|
+
.filter((s) => s.state !== "exited" && !s.archived)
|
|
1082
|
+
.map((s) => ({ id: s.sessionId, state: s.state, model: s.model, name: s.name }));
|
|
1083
|
+
} catch {}
|
|
1084
|
+
|
|
1085
|
+
const projectTodos = assistantStore.listTodos();
|
|
1086
|
+
const projects: Record<string, { total: number; done: number; open: number }> = {};
|
|
1087
|
+
for (const t of projectTodos) {
|
|
1088
|
+
if (!t.project) continue;
|
|
1089
|
+
if (!projects[t.project]) projects[t.project] = { total: 0, done: 0, open: 0 };
|
|
1090
|
+
projects[t.project].total++;
|
|
1091
|
+
if (t.done) projects[t.project].done++;
|
|
1092
|
+
else projects[t.project].open++;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
date: today,
|
|
1097
|
+
email: { accounts: emailSummary, totalUnread },
|
|
1098
|
+
calendar: { events: todayEvents, count: todayEvents.length },
|
|
1099
|
+
todos: { open: allTodos.length, overdue: overdueTodos.length, dueToday: dueTodayTodos.length, overdueItems: overdueTodos, dueTodayItems: dueTodayTodos },
|
|
1100
|
+
delegations: { items: delegations, count: delegations.length },
|
|
1101
|
+
sessions: { active: activeSessions, count: activeSessions.length },
|
|
1102
|
+
projects: Object.entries(projects).map(([name, p]) => ({ name, ...p })),
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// ─── Meeting Notes ──────────────────────────────────────────────
|
|
1107
|
+
case "create_meeting_notes": {
|
|
1108
|
+
const title = (args?.title as string) || "";
|
|
1109
|
+
const summary = (args?.summary as string) || "";
|
|
1110
|
+
if (!title || !summary) return { error: "title and summary are required" };
|
|
1111
|
+
|
|
1112
|
+
const participants = args?.participants ? (args.participants as string).split(",").map((p: string) => p.trim()) : [];
|
|
1113
|
+
const actionItemTexts = args?.actionItems ? (args.actionItems as string).split(",").map((a: string) => a.trim()) : [];
|
|
1114
|
+
const callId = (args?.callId as string) || "";
|
|
1115
|
+
|
|
1116
|
+
let content = summary;
|
|
1117
|
+
if (participants.length > 0) {
|
|
1118
|
+
content += `\n\nParticipants: ${participants.join(", ")}`;
|
|
1119
|
+
}
|
|
1120
|
+
if (callId) {
|
|
1121
|
+
content += `\n\nRelated call: ${callId}`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const note = assistantStore.addNote(title, content, ["meeting"]);
|
|
1125
|
+
|
|
1126
|
+
const todoIds: string[] = [];
|
|
1127
|
+
for (const item of actionItemTexts) {
|
|
1128
|
+
if (!item) continue;
|
|
1129
|
+
const todo = assistantStore.addTodo(item, "medium", "meeting-action");
|
|
1130
|
+
todoIds.push(todo.id);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
noteId: note.id,
|
|
1135
|
+
todoIds,
|
|
1136
|
+
message: `Meeting notes "${title}" created${todoIds.length > 0 ? ` with ${todoIds.length} action item(s)` : ""}.`,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// ─── Team Coordination ──────────────────────────────────────────
|
|
1141
|
+
case "run_team": {
|
|
1142
|
+
const goal = (args?.goal as string) || "";
|
|
1143
|
+
const cwd = (args?.cwd as string) || "";
|
|
1144
|
+
if (!goal || !cwd) {
|
|
1145
|
+
return { error: "goal and cwd are required" };
|
|
1146
|
+
}
|
|
1147
|
+
const suggestedAgents = args?.agents
|
|
1148
|
+
? (args.agents as string).split(",").map((a: string) => a.trim()).filter(Boolean)
|
|
1149
|
+
: [];
|
|
1150
|
+
|
|
1151
|
+
const { createTeam, ensureCoordinatorAgent, buildCoordinatorPrompt, updateTeamState } = await import("./team-service.js");
|
|
1152
|
+
const team = createTeam({ goal, repoRoot: cwd, suggestedAgents });
|
|
1153
|
+
|
|
1154
|
+
const coordinatorId = ensureCoordinatorAgent();
|
|
1155
|
+
const apiBase = `http://127.0.0.1:${process.env.PORT || 3100}`;
|
|
1156
|
+
const authToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
|
|
1157
|
+
const systemPrompt = buildCoordinatorPrompt(team, apiBase, authToken);
|
|
1158
|
+
|
|
1159
|
+
const runRes = await fetch(`${base}/agents/${coordinatorId}/run`, {
|
|
1160
|
+
method: "POST",
|
|
1161
|
+
headers,
|
|
1162
|
+
body: JSON.stringify({ input: goal, cwd }),
|
|
1163
|
+
});
|
|
1164
|
+
const runData = await runRes.json() as Record<string, unknown>;
|
|
1165
|
+
|
|
1166
|
+
if (!runRes.ok) {
|
|
1167
|
+
return { error: `Failed to start Team Coordinator: ${(runData as { error?: string }).error || "Unknown error"}` };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const sessionId = (runData as { sessionId?: string }).sessionId || null;
|
|
1171
|
+
if (sessionId) {
|
|
1172
|
+
updateTeamState(team.id, "planning", { coordinatorSessionId: sessionId });
|
|
1173
|
+
|
|
1174
|
+
// Inject system prompt via WebSocket bridge
|
|
1175
|
+
try {
|
|
1176
|
+
const wsBridgeModule = await import("./ws-bridge.js");
|
|
1177
|
+
// The system prompt needs to be injected; use the send-to-session endpoint
|
|
1178
|
+
await fetch(`${base}/gemini/send-to-session`, {
|
|
1179
|
+
method: "POST",
|
|
1180
|
+
headers,
|
|
1181
|
+
body: JSON.stringify({ sessionId, message: systemPrompt }),
|
|
1182
|
+
});
|
|
1183
|
+
} catch {
|
|
1184
|
+
// Best effort — the agent prompt will still work
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return {
|
|
1189
|
+
teamId: team.id,
|
|
1190
|
+
sessionId,
|
|
1191
|
+
message: "Team Coordinator started. Use monitor_team to track progress.",
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
case "monitor_team": {
|
|
1196
|
+
const teamId = (args?.team_id as string) || "";
|
|
1197
|
+
if (!teamId) return { error: "team_id is required" };
|
|
1198
|
+
const { getTeamStatus } = await import("./team-service.js");
|
|
1199
|
+
const status = getTeamStatus(teamId);
|
|
1200
|
+
if (!status) return { error: "Team not found" };
|
|
1201
|
+
|
|
1202
|
+
let summary = "";
|
|
1203
|
+
if (status.state === "completed") {
|
|
1204
|
+
summary = `Team completed! ${status.result || "All tasks finished."}`;
|
|
1205
|
+
} else if (status.state === "failed") {
|
|
1206
|
+
summary = `Team failed: ${status.error || "Unknown error"}`;
|
|
1207
|
+
} else {
|
|
1208
|
+
summary = `Team ${status.state}: ${status.tasksCompleted}/${status.tasksTotal} tasks completed, ${status.tasksRunning} running, ${status.tasksFailed} failed.`;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return { ...status, summary };
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// ─── Content Engine ──────────────────────────────────────────────
|
|
1215
|
+
case "analyze_website": {
|
|
1216
|
+
const url = (args?.url as string) || "";
|
|
1217
|
+
if (!url) return { error: "url is required" };
|
|
1218
|
+
try {
|
|
1219
|
+
const { analyzeWebsite } = await import("./content-intelligence/content-engine.js");
|
|
1220
|
+
const intelligence = await analyzeWebsite(url);
|
|
1221
|
+
return {
|
|
1222
|
+
success: true,
|
|
1223
|
+
companyName: intelligence.companyName,
|
|
1224
|
+
businessType: intelligence.businessType,
|
|
1225
|
+
industry: intelligence.industry,
|
|
1226
|
+
language: intelligence.language,
|
|
1227
|
+
targetAudience: intelligence.targetAudience,
|
|
1228
|
+
tone: intelligence.tone,
|
|
1229
|
+
usp: intelligence.usp,
|
|
1230
|
+
products: intelligence.products.slice(0, 5),
|
|
1231
|
+
services: intelligence.services.slice(0, 5),
|
|
1232
|
+
colors: intelligence.colors,
|
|
1233
|
+
heroImages: intelligence.heroImages.length,
|
|
1234
|
+
crawledPages: intelligence.crawledPages.length,
|
|
1235
|
+
message: `Website analyzed: ${intelligence.companyName} (${intelligence.businessType}, ${intelligence.industry}). Found ${intelligence.products.length} products, ${intelligence.services.length} services. Tone: ${intelligence.tone}. Target: ${intelligence.targetAudience}.`,
|
|
1236
|
+
};
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
return { error: err instanceof Error ? err.message : "Failed to analyze website" };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
case "create_content_strategy": {
|
|
1243
|
+
const url = (args?.url as string) || "";
|
|
1244
|
+
if (!url) return { error: "url is required" };
|
|
1245
|
+
const platformsStr = (args?.platforms as string) || "instagram,linkedin,facebook";
|
|
1246
|
+
const platforms = platformsStr.split(",").map((p: string) => p.trim()).filter(Boolean);
|
|
1247
|
+
try {
|
|
1248
|
+
const { analyzeWebsite, createContentStrategy } = await import("./content-intelligence/content-engine.js");
|
|
1249
|
+
const intelligence = await analyzeWebsite(url);
|
|
1250
|
+
const strategy = createContentStrategy(intelligence, platforms);
|
|
1251
|
+
return {
|
|
1252
|
+
success: true,
|
|
1253
|
+
businessType: strategy.businessType,
|
|
1254
|
+
pillars: strategy.pillars.map((p) => ({ name: p.name, painPoints: p.painPoints.length, ideas: p.contentIdeas.length })),
|
|
1255
|
+
schedules: strategy.schedules,
|
|
1256
|
+
tone: strategy.tone,
|
|
1257
|
+
ctas: strategy.ctas,
|
|
1258
|
+
journeyMapping: {
|
|
1259
|
+
attract: strategy.journeyMapping.attract.length,
|
|
1260
|
+
convert: strategy.journeyMapping.convert.length,
|
|
1261
|
+
close: strategy.journeyMapping.close.length,
|
|
1262
|
+
},
|
|
1263
|
+
message: `Content strategy created for ${intelligence.companyName}: ${strategy.pillars.length} content pillars, ${strategy.schedules.length} platform schedules. Tone: ${strategy.tone}.`,
|
|
1264
|
+
};
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
return { error: err instanceof Error ? err.message : "Failed to create content strategy" };
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
case "generate_content": {
|
|
1271
|
+
const url = (args?.url as string) || "";
|
|
1272
|
+
const platform = (args?.platform as string) || "";
|
|
1273
|
+
if (!url || !platform) return { error: "url and platform are required" };
|
|
1274
|
+
const count = (args?.count as number) || 5;
|
|
1275
|
+
const journeyStage = (args?.journeyStage as string) || undefined;
|
|
1276
|
+
const styleProfileHandle =
|
|
1277
|
+
typeof args?.styleProfileHandle === "string" && args.styleProfileHandle.trim()
|
|
1278
|
+
? args.styleProfileHandle.trim()
|
|
1279
|
+
: undefined;
|
|
1280
|
+
try {
|
|
1281
|
+
const { analyzeWebsite, createContentStrategy, generateSmartContent } = await import("./content-intelligence/content-engine.js");
|
|
1282
|
+
const intelligence = await analyzeWebsite(url);
|
|
1283
|
+
const strategy = createContentStrategy(intelligence, [platform]);
|
|
1284
|
+
const pieces = await generateSmartContent({
|
|
1285
|
+
intelligence,
|
|
1286
|
+
strategy,
|
|
1287
|
+
platform,
|
|
1288
|
+
journeyStage: journeyStage as "attract" | "convert" | "close" | undefined,
|
|
1289
|
+
count,
|
|
1290
|
+
styleProfileHandle,
|
|
1291
|
+
});
|
|
1292
|
+
return {
|
|
1293
|
+
success: true,
|
|
1294
|
+
platform,
|
|
1295
|
+
count: pieces.length,
|
|
1296
|
+
pieces: pieces.map((p) => ({
|
|
1297
|
+
id: p.id,
|
|
1298
|
+
framework: p.framework,
|
|
1299
|
+
journeyStage: p.journeyStage,
|
|
1300
|
+
pillar: p.pillar,
|
|
1301
|
+
hook: p.hook,
|
|
1302
|
+
headline: p.headline,
|
|
1303
|
+
body: p.body.slice(0, 300) + (p.body.length > 300 ? "..." : ""),
|
|
1304
|
+
cta: p.cta,
|
|
1305
|
+
hashtags: p.hashtags,
|
|
1306
|
+
imagePrompt: p.imagePrompt,
|
|
1307
|
+
})),
|
|
1308
|
+
message: `Generated ${pieces.length} content pieces for ${platform}. Each includes hook, body, CTA, hashtags, and image prompt.`,
|
|
1309
|
+
};
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
return { error: err instanceof Error ? err.message : "Failed to generate content" };
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
case "generate_ad_creatives": {
|
|
1316
|
+
const url = (args?.url as string) || "";
|
|
1317
|
+
const platform = (args?.platform as string) || "";
|
|
1318
|
+
if (!url || !platform) return { error: "url and platform are required" };
|
|
1319
|
+
const count = (args?.count as number) || 3;
|
|
1320
|
+
try {
|
|
1321
|
+
const { analyzeWebsite, generateAdCreatives: genAds } = await import("./content-intelligence/content-engine.js");
|
|
1322
|
+
const intelligence = await analyzeWebsite(url);
|
|
1323
|
+
const ads = await genAds({ intelligence, platform, count });
|
|
1324
|
+
return {
|
|
1325
|
+
success: true,
|
|
1326
|
+
platform,
|
|
1327
|
+
count: ads.length,
|
|
1328
|
+
ads: ads.map((a) => ({
|
|
1329
|
+
id: a.id,
|
|
1330
|
+
format: a.format,
|
|
1331
|
+
aspectRatio: a.aspectRatio,
|
|
1332
|
+
resolution: a.resolution,
|
|
1333
|
+
headline: a.headline,
|
|
1334
|
+
body: a.body,
|
|
1335
|
+
cta: a.cta,
|
|
1336
|
+
imagePrompt: a.imagePrompt,
|
|
1337
|
+
brandColors: a.brandColors,
|
|
1338
|
+
})),
|
|
1339
|
+
message: `Generated ${ads.length} ad creatives for ${platform}. Each includes headline, body, CTA, and image prompt.`,
|
|
1340
|
+
};
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
return { error: err instanceof Error ? err.message : "Failed to generate ad creatives" };
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
case "generate_ads": {
|
|
1347
|
+
const url = (args?.url as string) || "";
|
|
1348
|
+
if (!url) return { error: "url is required" };
|
|
1349
|
+
const platformsStr = (args?.platforms as string) || "";
|
|
1350
|
+
const count = (args?.count as number) || 2;
|
|
1351
|
+
const style = (args?.style as string) || "professional";
|
|
1352
|
+
|
|
1353
|
+
try {
|
|
1354
|
+
const { analyzeWebsite, generateAdCreatives: genAds } = await import("./content-intelligence/content-engine.js");
|
|
1355
|
+
const { generateImage } = await import("./google-media.js");
|
|
1356
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
1357
|
+
|
|
1358
|
+
// 1. Analyze website
|
|
1359
|
+
const intelligence = await analyzeWebsite(url);
|
|
1360
|
+
|
|
1361
|
+
// 2. Determine platforms
|
|
1362
|
+
let platforms: string[];
|
|
1363
|
+
if (platformsStr) {
|
|
1364
|
+
platforms = platformsStr.split(",").map((p: string) => p.trim()).filter(Boolean);
|
|
1365
|
+
} else {
|
|
1366
|
+
try {
|
|
1367
|
+
const profiles = await smManager.getProfiles();
|
|
1368
|
+
platforms = [...new Set(profiles.map((p: { platform: string }) => p.platform))];
|
|
1369
|
+
} catch {
|
|
1370
|
+
platforms = ["instagram", "facebook", "linkedin"];
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// 3. Platform aspect ratio mapping
|
|
1375
|
+
const platformAspectRatios: Record<string, string> = {
|
|
1376
|
+
instagram: "3:4",
|
|
1377
|
+
facebook: "3:4",
|
|
1378
|
+
linkedin: "1:1",
|
|
1379
|
+
twitter: "16:9",
|
|
1380
|
+
tiktok: "9:16",
|
|
1381
|
+
youtube: "16:9",
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const styleHints: Record<string, string> = {
|
|
1385
|
+
professional: "Clean, corporate design with subtle gradients, professional typography",
|
|
1386
|
+
bold: "High contrast, vibrant colors, large bold text, eye-catching",
|
|
1387
|
+
minimal: "Lots of whitespace, elegant, simple, one focal point",
|
|
1388
|
+
playful: "Colorful, casual, friendly, rounded shapes, fun elements",
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
const savedDrafts: Array<{ id: string; platform: string; headline: string; aspectRatio: string; imageUrl: string }> = [];
|
|
1392
|
+
|
|
1393
|
+
// 4. For each platform: generate ads, images, save drafts
|
|
1394
|
+
for (const platform of platforms) {
|
|
1395
|
+
try {
|
|
1396
|
+
const ads = await genAds({ intelligence, platform, count });
|
|
1397
|
+
const aspectRatio = platformAspectRatios[platform] || "1:1";
|
|
1398
|
+
|
|
1399
|
+
for (const ad of ads) {
|
|
1400
|
+
try {
|
|
1401
|
+
// Generate image with brand context
|
|
1402
|
+
const imagePrompt = `${styleHints[style] || styleHints.professional}. ${ad.imagePrompt}. Brand colors: ${intelligence.colors.slice(0, 3).join(", ") || "blue, white"}. Bold text overlay: "${ad.headline}". Aspect ratio: ${aspectRatio}`;
|
|
1403
|
+
|
|
1404
|
+
const images = await generateImage(imagePrompt, {
|
|
1405
|
+
model: "gemini-3.1-flash-image-preview",
|
|
1406
|
+
aspectRatio,
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
const mediaUrl = images.length > 0 ? `/api/media/file/${images[0].filename}` : undefined;
|
|
1410
|
+
|
|
1411
|
+
// Build ad text
|
|
1412
|
+
const adText = `${ad.headline}\n\n${ad.body}\n\n${ad.cta}`;
|
|
1413
|
+
|
|
1414
|
+
// Save as draft
|
|
1415
|
+
const draft = await smManager.createDraft({
|
|
1416
|
+
text: adText,
|
|
1417
|
+
platforms: [platform as any],
|
|
1418
|
+
isDraft: true,
|
|
1419
|
+
mediaUrls: mediaUrl ? [mediaUrl] : [],
|
|
1420
|
+
createdBy: "agent",
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
savedDrafts.push({
|
|
1424
|
+
id: draft.id,
|
|
1425
|
+
platform,
|
|
1426
|
+
headline: ad.headline,
|
|
1427
|
+
aspectRatio,
|
|
1428
|
+
imageUrl: mediaUrl || "",
|
|
1429
|
+
});
|
|
1430
|
+
} catch (imgErr) {
|
|
1431
|
+
// If image fails, still save draft without image
|
|
1432
|
+
const adText = `${ad.headline}\n\n${ad.body}\n\n${ad.cta}`;
|
|
1433
|
+
const draft = await smManager.createDraft({
|
|
1434
|
+
text: adText,
|
|
1435
|
+
platforms: [platform as any],
|
|
1436
|
+
isDraft: true,
|
|
1437
|
+
createdBy: "agent",
|
|
1438
|
+
});
|
|
1439
|
+
savedDrafts.push({
|
|
1440
|
+
id: draft.id,
|
|
1441
|
+
platform,
|
|
1442
|
+
headline: ad.headline,
|
|
1443
|
+
aspectRatio,
|
|
1444
|
+
imageUrl: "",
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
} catch (platErr) {
|
|
1449
|
+
console.error(`[generate_ads] Failed for platform ${platform}:`, platErr);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return {
|
|
1454
|
+
success: true,
|
|
1455
|
+
company: intelligence.companyName,
|
|
1456
|
+
industry: intelligence.industry,
|
|
1457
|
+
brandColors: intelligence.colors.slice(0, 5),
|
|
1458
|
+
totalDrafts: savedDrafts.length,
|
|
1459
|
+
drafts: savedDrafts,
|
|
1460
|
+
message: `${savedDrafts.length} Ad-Drafts erstellt fuer ${intelligence.companyName}. Alle mit Bildern als Drafts gespeichert.`,
|
|
1461
|
+
link: "#/socialmedia/drafts",
|
|
1462
|
+
};
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
return { error: err instanceof Error ? err.message : "Failed to generate ads" };
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
case "generate_content_plan": {
|
|
1469
|
+
const url = (args?.url as string) || "";
|
|
1470
|
+
if (!url) return { error: "url is required" };
|
|
1471
|
+
const platformsStr = (args?.platforms as string) || "instagram,linkedin,facebook";
|
|
1472
|
+
const platforms = platformsStr.split(",").map((p: string) => p.trim()).filter(Boolean);
|
|
1473
|
+
const weeks = (args?.weeks as number) || 4;
|
|
1474
|
+
try {
|
|
1475
|
+
const { generateContentPlan } = await import("./content-intelligence/content-engine.js");
|
|
1476
|
+
const plan = await generateContentPlan({ url, platforms, weeks });
|
|
1477
|
+
|
|
1478
|
+
// Save drafts via social media manager if possible
|
|
1479
|
+
let savedDrafts = 0;
|
|
1480
|
+
try {
|
|
1481
|
+
const smManager = await import("./socialmedia/manager.js");
|
|
1482
|
+
for (const [platformKey, pieces] of Object.entries(plan.content)) {
|
|
1483
|
+
for (const piece of pieces.slice(0, 10)) { // Cap at 10 drafts per platform
|
|
1484
|
+
await smManager.createDraft({
|
|
1485
|
+
text: `${piece.headline}\n\n${piece.body}\n\n${piece.cta}\n\n${piece.hashtags.map((h: string) => `#${h.replace(/^#/, "")}`).join(" ")}`,
|
|
1486
|
+
platforms: [platformKey] as import("./socialmedia/types.js").SocialPlatform[],
|
|
1487
|
+
scheduledAt: null,
|
|
1488
|
+
mediaUrls: [],
|
|
1489
|
+
createdBy: "agent",
|
|
1490
|
+
});
|
|
1491
|
+
savedDrafts++;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
} catch {
|
|
1495
|
+
// Social media manager might not be configured — that's OK
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const contentSummary: Record<string, number> = {};
|
|
1499
|
+
for (const [p, pieces] of Object.entries(plan.content)) {
|
|
1500
|
+
contentSummary[p] = pieces.length;
|
|
1501
|
+
}
|
|
1502
|
+
const adSummary: Record<string, number> = {};
|
|
1503
|
+
for (const [p, ads] of Object.entries(plan.ads)) {
|
|
1504
|
+
adSummary[p] = ads.length;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
return {
|
|
1508
|
+
success: true,
|
|
1509
|
+
companyName: plan.intelligence.companyName,
|
|
1510
|
+
businessType: plan.intelligence.businessType,
|
|
1511
|
+
weeks,
|
|
1512
|
+
platforms,
|
|
1513
|
+
contentPieces: contentSummary,
|
|
1514
|
+
adCreatives: adSummary,
|
|
1515
|
+
pillars: plan.strategy.pillars.map((p) => p.name),
|
|
1516
|
+
savedDrafts,
|
|
1517
|
+
message: `Content plan generated for ${plan.intelligence.companyName}: ${Object.values(contentSummary).reduce((a, b) => a + b, 0)} content pieces and ${Object.values(adSummary).reduce((a, b) => a + b, 0)} ad creatives across ${platforms.length} platforms for ${weeks} weeks.${savedDrafts > 0 ? ` ${savedDrafts} drafts saved to Social Media.` : ""}`,
|
|
1518
|
+
};
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
return { error: err instanceof Error ? err.message : "Failed to generate content plan" };
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ─── Documents ────────────────────────────────────────────────
|
|
1525
|
+
case "list_documents": {
|
|
1526
|
+
const docStore = await import("./ceo/document-store.js");
|
|
1527
|
+
const folder = (args?.folder as string) || undefined;
|
|
1528
|
+
const tag = (args?.tag as string) || undefined;
|
|
1529
|
+
const docs = docStore.listDocuments(folder, tag);
|
|
1530
|
+
return { documents: docs, count: docs.length, message: docs.length > 0 ? `Found ${docs.length} document(s).` : "No documents found." };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
case "upload_document": {
|
|
1534
|
+
const title = (args?.title as string) || "";
|
|
1535
|
+
const content = (args?.content as string) || "";
|
|
1536
|
+
const fileType = (args?.fileType as string) || "txt";
|
|
1537
|
+
if (!title || !content) return { error: "title and content are required" };
|
|
1538
|
+
const docStore = await import("./ceo/document-store.js");
|
|
1539
|
+
const tagsStr = (args?.tags as string) || "";
|
|
1540
|
+
const tags = tagsStr ? tagsStr.split(",").map((t: string) => t.trim()).filter(Boolean) : [];
|
|
1541
|
+
const doc = docStore.addDocument(title, content, fileType, (args?.folder as string) || undefined, tags, (args?.summary as string) || undefined);
|
|
1542
|
+
return { document: doc, message: `Document "${title}" saved.` };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
case "get_document": {
|
|
1546
|
+
const id = (args?.id as string) || "";
|
|
1547
|
+
if (!id) return { error: "id is required" };
|
|
1548
|
+
const docStore = await import("./ceo/document-store.js");
|
|
1549
|
+
const result = docStore.getDocument(id);
|
|
1550
|
+
if (!result) return { error: "Document not found" };
|
|
1551
|
+
return result;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
case "search_documents": {
|
|
1555
|
+
const query = (args?.query as string) || "";
|
|
1556
|
+
if (!query) return { error: "query is required" };
|
|
1557
|
+
const docStore = await import("./ceo/document-store.js");
|
|
1558
|
+
const docs = docStore.searchDocuments(query);
|
|
1559
|
+
return { documents: docs, count: docs.length, message: docs.length > 0 ? `Found ${docs.length} document(s) matching "${query}".` : `No documents found for "${query}".` };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
case "delete_document": {
|
|
1563
|
+
const id = (args?.id as string) || "";
|
|
1564
|
+
if (!id) return { error: "id is required" };
|
|
1565
|
+
const docStore = await import("./ceo/document-store.js");
|
|
1566
|
+
const ok = docStore.deleteDocument(id);
|
|
1567
|
+
return ok ? { message: "Document deleted." } : { error: "Document not found" };
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// ─── Templates ────────────────────────────────────────────────
|
|
1571
|
+
case "list_templates": {
|
|
1572
|
+
const tplStore = await import("./ceo/template-store.js");
|
|
1573
|
+
const category = (args?.category as string) || undefined;
|
|
1574
|
+
const templates = tplStore.listTemplates(category);
|
|
1575
|
+
return { templates, count: templates.length, message: templates.length > 0 ? `Found ${templates.length} template(s).` : "No templates found." };
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
case "create_template": {
|
|
1579
|
+
const tplName = (args?.name as string) || "";
|
|
1580
|
+
const tplContent = (args?.content as string) || "";
|
|
1581
|
+
const tplCategory = (args?.category as string) || "custom";
|
|
1582
|
+
if (!tplName || !tplContent) return { error: "name and content are required" };
|
|
1583
|
+
const tplStore = await import("./ceo/template-store.js");
|
|
1584
|
+
const tagsStr = (args?.tags as string) || "";
|
|
1585
|
+
const tags = tagsStr ? tagsStr.split(",").map((t: string) => t.trim()).filter(Boolean) : [];
|
|
1586
|
+
const tpl = tplStore.createTemplate(tplName, tplContent, tplCategory, undefined, tags);
|
|
1587
|
+
return { template: tpl, message: `Template "${tplName}" created with ${tpl.variables.length} variable(s).` };
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
case "use_template": {
|
|
1591
|
+
const tplId = (args?.id as string) || "";
|
|
1592
|
+
if (!tplId) return { error: "id is required" };
|
|
1593
|
+
const tplStore = await import("./ceo/template-store.js");
|
|
1594
|
+
let variables: Record<string, string> = {};
|
|
1595
|
+
try {
|
|
1596
|
+
const varsStr = (args?.variables as string) || "{}";
|
|
1597
|
+
variables = JSON.parse(varsStr);
|
|
1598
|
+
} catch { return { error: "variables must be a valid JSON object" }; }
|
|
1599
|
+
const result = tplStore.useTemplate(tplId, variables);
|
|
1600
|
+
if (!result) return { error: "Template not found" };
|
|
1601
|
+
return { result: result.result, templateName: result.template.name, message: `Template "${result.template.name}" filled.` };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
case "search_templates": {
|
|
1605
|
+
const query = (args?.query as string) || "";
|
|
1606
|
+
if (!query) return { error: "query is required" };
|
|
1607
|
+
const tplStore = await import("./ceo/template-store.js");
|
|
1608
|
+
const templates = tplStore.searchTemplates(query);
|
|
1609
|
+
return { templates, count: templates.length };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
case "delete_template": {
|
|
1613
|
+
const id = (args?.id as string) || "";
|
|
1614
|
+
if (!id) return { error: "id is required" };
|
|
1615
|
+
const tplStore = await import("./ceo/template-store.js");
|
|
1616
|
+
const ok = tplStore.deleteTemplate(id);
|
|
1617
|
+
return ok ? { message: "Template deleted." } : { error: "Template not found" };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// ─── News & Monitoring ────────────────────────────────────────
|
|
1621
|
+
case "add_news_source": {
|
|
1622
|
+
const nsName = (args?.name as string) || "";
|
|
1623
|
+
const nsType = (args?.type as string) || "";
|
|
1624
|
+
const nsCat = (args?.category as string) || "";
|
|
1625
|
+
if (!nsName || !nsType || !nsCat) return { error: "name, type, and category are required" };
|
|
1626
|
+
const newsStore = await import("./ceo/news-store.js");
|
|
1627
|
+
const kwStr = (args?.keywords as string) || "";
|
|
1628
|
+
const keywords = kwStr ? kwStr.split(",").map((k: string) => k.trim()).filter(Boolean) : undefined;
|
|
1629
|
+
const source = newsStore.addSource(nsName, nsType as "rss" | "website" | "keyword", nsCat, (args?.url as string) || undefined, keywords, (args?.checkInterval as number) || undefined);
|
|
1630
|
+
return { source, message: `News source "${nsName}" added.` };
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
case "list_news_sources": {
|
|
1634
|
+
const newsStore = await import("./ceo/news-store.js");
|
|
1635
|
+
const sources = newsStore.listSources();
|
|
1636
|
+
return { sources, count: sources.length };
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
case "list_news": {
|
|
1640
|
+
const newsStore = await import("./ceo/news-store.js");
|
|
1641
|
+
const items = newsStore.listNews(
|
|
1642
|
+
(args?.category as string) || undefined,
|
|
1643
|
+
(args?.unreadOnly as boolean) || false,
|
|
1644
|
+
false,
|
|
1645
|
+
(args?.limit as number) || 20
|
|
1646
|
+
);
|
|
1647
|
+
return { items, count: items.length, message: items.length > 0 ? `${items.length} news item(s).` : "No news." };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
case "search_news": {
|
|
1651
|
+
const query = (args?.query as string) || "";
|
|
1652
|
+
if (!query) return { error: "query is required" };
|
|
1653
|
+
const newsStore = await import("./ceo/news-store.js");
|
|
1654
|
+
const items = newsStore.searchNews(query);
|
|
1655
|
+
return { items, count: items.length };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
case "mark_news_read": {
|
|
1659
|
+
const id = (args?.id as string) || "";
|
|
1660
|
+
if (!id) return { error: "id is required" };
|
|
1661
|
+
const newsStore = await import("./ceo/news-store.js");
|
|
1662
|
+
const ok = newsStore.markRead(id);
|
|
1663
|
+
return ok ? { message: "Marked as read." } : { error: "News item not found" };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
case "get_news_stats": {
|
|
1667
|
+
const newsStore = await import("./ceo/news-store.js");
|
|
1668
|
+
return newsStore.getNewsStats();
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// ─── Time Tracking ────────────────────────────────────────────
|
|
1672
|
+
case "start_timer": {
|
|
1673
|
+
const task = (args?.task as string) || "";
|
|
1674
|
+
if (!task) return { error: "task is required" };
|
|
1675
|
+
const timeStore = await import("./ceo/time-tracking-store.js");
|
|
1676
|
+
const timer = timeStore.startTimer(task, (args?.project as string) || undefined, (args?.category as string) || undefined);
|
|
1677
|
+
return { timer, message: `Timer started for "${task}".` };
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
case "stop_timer": {
|
|
1681
|
+
const timeStore = await import("./ceo/time-tracking-store.js");
|
|
1682
|
+
const entry = timeStore.stopTimer((args?.notes as string) || undefined);
|
|
1683
|
+
if (!entry) return { error: "No active timer" };
|
|
1684
|
+
return { entry, message: `Timer stopped. ${entry.duration} minutes logged for "${entry.task}".` };
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
case "get_active_timer": {
|
|
1688
|
+
const timeStore = await import("./ceo/time-tracking-store.js");
|
|
1689
|
+
const timer = timeStore.getActiveTimer();
|
|
1690
|
+
if (!timer) return { message: "No active timer." };
|
|
1691
|
+
const elapsed = Math.round((Date.now() - new Date(timer.startTime).getTime()) / 60000);
|
|
1692
|
+
return { timer, elapsed, message: `Timer running for "${timer.task}" — ${elapsed} minutes.` };
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
case "log_time": {
|
|
1696
|
+
const task = (args?.task as string) || "";
|
|
1697
|
+
const duration = (args?.duration as number) || 0;
|
|
1698
|
+
if (!task || !duration) return { error: "task and duration are required" };
|
|
1699
|
+
const timeStore = await import("./ceo/time-tracking-store.js");
|
|
1700
|
+
const entry = timeStore.logTime(task, duration, (args?.project as string) || undefined, (args?.category as string) || undefined, (args?.notes as string) || undefined, (args?.date as string) || undefined);
|
|
1701
|
+
return { entry, message: `${duration} minutes logged for "${task}".` };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
case "get_time_report": {
|
|
1705
|
+
const timeStore = await import("./ceo/time-tracking-store.js");
|
|
1706
|
+
const period = (args?.period as string) || "week";
|
|
1707
|
+
const report = timeStore.getReport(period as "today" | "week" | "month");
|
|
1708
|
+
const hours = Math.round(report.totalMinutes / 60 * 10) / 10;
|
|
1709
|
+
return { ...report, message: `${period} report: ${hours} hours total across ${Object.keys(report.byProject).length} project(s).` };
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// ─── Finance & Invoices ───────────────────────────────────────
|
|
1713
|
+
case "create_invoice": {
|
|
1714
|
+
const clientName = (args?.clientName as string) || "";
|
|
1715
|
+
if (!clientName) return { error: "clientName is required" };
|
|
1716
|
+
const finStore = await import("./ceo/finance-store.js");
|
|
1717
|
+
let items;
|
|
1718
|
+
try {
|
|
1719
|
+
const itemsStr = (args?.items as string) || "[]";
|
|
1720
|
+
items = JSON.parse(itemsStr);
|
|
1721
|
+
} catch { return { error: "items must be a valid JSON array" }; }
|
|
1722
|
+
const invoice = finStore.createInvoice(clientName, items, {
|
|
1723
|
+
clientEmail: (args?.clientEmail as string) || undefined,
|
|
1724
|
+
taxRate: (args?.taxRate as number) || undefined,
|
|
1725
|
+
currency: (args?.currency as string) || undefined,
|
|
1726
|
+
dueDate: (args?.dueDate as string) || undefined,
|
|
1727
|
+
notes: (args?.notes as string) || undefined,
|
|
1728
|
+
});
|
|
1729
|
+
return { invoice, message: `Invoice ${invoice.invoiceNumber} created for ${clientName}: ${invoice.total} ${invoice.currency}.` };
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
case "list_invoices": {
|
|
1733
|
+
const finStore = await import("./ceo/finance-store.js");
|
|
1734
|
+
const invoices = finStore.listInvoices((args?.status as string) || undefined);
|
|
1735
|
+
const total = invoices.reduce((s, i) => s + i.total, 0);
|
|
1736
|
+
return { invoices, count: invoices.length, total, message: `${invoices.length} invoice(s), total: ${total.toFixed(2)}.` };
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
case "mark_invoice_paid": {
|
|
1740
|
+
const id = (args?.id as string) || "";
|
|
1741
|
+
if (!id) return { error: "id is required" };
|
|
1742
|
+
const finStore = await import("./ceo/finance-store.js");
|
|
1743
|
+
const invoice = finStore.markPaid(id);
|
|
1744
|
+
if (!invoice) return { error: "Invoice not found" };
|
|
1745
|
+
return { invoice, message: `Invoice ${invoice.invoiceNumber} marked as paid.` };
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
case "log_expense": {
|
|
1749
|
+
const desc = (args?.description as string) || "";
|
|
1750
|
+
const amount = (args?.amount as number) || 0;
|
|
1751
|
+
const cat = (args?.category as string) || "";
|
|
1752
|
+
if (!desc || !amount || !cat) return { error: "description, amount, and category are required" };
|
|
1753
|
+
const finStore = await import("./ceo/finance-store.js");
|
|
1754
|
+
const expense = finStore.logExpense(desc, amount, cat, {
|
|
1755
|
+
vendor: (args?.vendor as string) || undefined,
|
|
1756
|
+
project: (args?.project as string) || undefined,
|
|
1757
|
+
date: (args?.date as string) || undefined,
|
|
1758
|
+
notes: (args?.notes as string) || undefined,
|
|
1759
|
+
});
|
|
1760
|
+
return { expense, message: `Expense logged: ${desc} — ${amount} ${expense.currency}.` };
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
case "list_expenses": {
|
|
1764
|
+
const finStore = await import("./ceo/finance-store.js");
|
|
1765
|
+
const expenses = finStore.listExpenses(
|
|
1766
|
+
(args?.category as string) || undefined,
|
|
1767
|
+
(args?.startDate as string) || undefined,
|
|
1768
|
+
(args?.endDate as string) || undefined
|
|
1769
|
+
);
|
|
1770
|
+
const total = expenses.reduce((s, e) => s + e.amount, 0);
|
|
1771
|
+
return { expenses, count: expenses.length, total, message: `${expenses.length} expense(s), total: ${total.toFixed(2)}.` };
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
case "get_financial_summary": {
|
|
1775
|
+
const finStore = await import("./ceo/finance-store.js");
|
|
1776
|
+
const period = (args?.period as string) || "month";
|
|
1777
|
+
const summary = finStore.getFinancialSummary(period as "month" | "quarter" | "year");
|
|
1778
|
+
return { ...summary, message: `${period} summary: Revenue ${summary.totalRevenue.toFixed(2)}, Expenses ${summary.totalExpenses.toFixed(2)}, Profit ${summary.netProfit.toFixed(2)} ${summary.currency}.` };
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// ─── KPI Dashboard ────────────────────────────────────────────
|
|
1782
|
+
case "define_kpi": {
|
|
1783
|
+
const kpiName = (args?.name as string) || "";
|
|
1784
|
+
const unit = (args?.unit as string) || "";
|
|
1785
|
+
const category = (args?.category as string) || "";
|
|
1786
|
+
if (!kpiName || !unit || !category) return { error: "name, unit, and category are required" };
|
|
1787
|
+
const kpiStore = await import("./ceo/kpi-store.js");
|
|
1788
|
+
const kpi = kpiStore.defineKPI(kpiName, unit, category, {
|
|
1789
|
+
description: (args?.description as string) || undefined,
|
|
1790
|
+
target: (args?.target as number) || undefined,
|
|
1791
|
+
direction: (args?.direction as "up" | "down") || undefined,
|
|
1792
|
+
});
|
|
1793
|
+
return { kpi, message: `KPI "${kpiName}" defined${kpi.target ? ` with target ${kpi.target} ${unit}` : ""}.` };
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
case "record_kpi_value": {
|
|
1797
|
+
const kpiId = (args?.kpiId as string) || "";
|
|
1798
|
+
const value = args?.value as number;
|
|
1799
|
+
if (!kpiId || value === undefined) return { error: "kpiId and value are required" };
|
|
1800
|
+
const kpiStore = await import("./ceo/kpi-store.js");
|
|
1801
|
+
const kpi = kpiStore.recordValue(kpiId, value, (args?.date as string) || undefined, (args?.note as string) || undefined);
|
|
1802
|
+
if (!kpi) return { error: "KPI not found" };
|
|
1803
|
+
return { kpi, message: `KPI "${kpi.name}" updated: ${value} ${kpi.unit}${kpi.trend ? ` (${kpi.trend} ${kpi.trendPercent}%)` : ""}.` };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
case "get_kpi_dashboard": {
|
|
1807
|
+
const kpiStore = await import("./ceo/kpi-store.js");
|
|
1808
|
+
const dashboard = kpiStore.getDashboard();
|
|
1809
|
+
return { ...dashboard, message: `${dashboard.summary.total} KPI(s): ${dashboard.summary.onTarget} on target, ${dashboard.summary.warning} warning, ${dashboard.summary.critical} critical.` };
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
case "get_kpi_history": {
|
|
1813
|
+
const kpiId = (args?.kpiId as string) || "";
|
|
1814
|
+
if (!kpiId) return { error: "kpiId is required" };
|
|
1815
|
+
const kpiStore = await import("./ceo/kpi-store.js");
|
|
1816
|
+
const period = (args?.period as string) || undefined;
|
|
1817
|
+
const history = kpiStore.getKPIHistory(kpiId, period as "week" | "month" | "quarter" | "year" | undefined);
|
|
1818
|
+
return { history, count: history.length };
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
case "delete_kpi": {
|
|
1822
|
+
const id = (args?.id as string) || "";
|
|
1823
|
+
if (!id) return { error: "id is required" };
|
|
1824
|
+
const kpiStore = await import("./ceo/kpi-store.js");
|
|
1825
|
+
const ok = kpiStore.deleteKPI(id);
|
|
1826
|
+
return ok ? { message: "KPI deleted." } : { error: "KPI not found" };
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
default:
|
|
1830
|
+
return { error: `Unknown tool: ${name}` };
|
|
1831
|
+
}
|
|
1832
|
+
} catch (err) {
|
|
1833
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
1834
|
+
}
|
|
1835
|
+
}
|