plazbot-cli 0.2.26 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +34 -5
- package/README.md +21 -0
- package/dist/cli.js +32 -20
- package/dist/commands/agent/ai-config.js +98 -50
- package/dist/commands/agent/chat.js +80 -74
- package/dist/commands/agent/copy.js +23 -21
- package/dist/commands/agent/create.js +42 -72
- package/dist/commands/agent/delete.js +29 -30
- package/dist/commands/agent/enable-widget.js +30 -26
- package/dist/commands/agent/export.js +90 -77
- package/dist/commands/agent/files.js +68 -60
- package/dist/commands/agent/get.js +101 -87
- package/dist/commands/agent/index.js +53 -39
- package/dist/commands/agent/list.js +26 -24
- package/dist/commands/agent/monitor.js +91 -86
- package/dist/commands/agent/on-message.js +40 -37
- package/dist/commands/agent/set.js +62 -59
- package/dist/commands/agent/templates.js +109 -108
- package/dist/commands/agent/tools.js +64 -65
- package/dist/commands/agent/update.js +28 -27
- package/dist/commands/agent/validate.js +127 -0
- package/dist/commands/agent/wizard.js +152 -159
- package/dist/commands/auth/index.js +7 -10
- package/dist/commands/auth/login.js +50 -37
- package/dist/commands/auth/logout.js +16 -14
- package/dist/commands/auth/status.js +19 -16
- package/dist/commands/portal/add-agent.js +26 -24
- package/dist/commands/portal/add-link.js +21 -17
- package/dist/commands/portal/clear-links.js +17 -15
- package/dist/commands/portal/create.js +25 -21
- package/dist/commands/portal/delete.js +31 -30
- package/dist/commands/portal/get.js +33 -31
- package/dist/commands/portal/index.js +30 -22
- package/dist/commands/portal/list.js +34 -30
- package/dist/commands/portal/update.js +41 -33
- package/dist/commands/whatsapp/broadcast.js +40 -37
- package/dist/commands/whatsapp/channels.js +40 -34
- package/dist/commands/whatsapp/chat.js +33 -32
- package/dist/commands/whatsapp/connect.js +53 -52
- package/dist/commands/whatsapp/delete-webhook.js +19 -17
- package/dist/commands/whatsapp/index.js +35 -25
- package/dist/commands/whatsapp/register-webhook.js +21 -19
- package/dist/commands/whatsapp/send-template.js +39 -31
- package/dist/commands/whatsapp/send.js +27 -23
- package/dist/commands/whatsapp/widget.js +35 -31
- package/dist/commands/workers/deploy.js +49 -44
- package/dist/commands/workers/index.js +28 -18
- package/dist/commands/workers/list.js +43 -35
- package/dist/commands/workers/logs.js +38 -32
- package/dist/commands/workers/remove.js +38 -37
- package/dist/commands/workers/secret.js +63 -58
- package/dist/commands/workers/test.js +44 -36
- package/dist/schemas/agent.config.schema.json +569 -0
- package/dist/studio/api/sseClient.js +97 -0
- package/dist/studio/api/studioApi.js +25 -0
- package/dist/studio/api/types.js +16 -0
- package/dist/studio/components/AgentPanel.js +35 -0
- package/dist/studio/components/App.js +214 -0
- package/dist/studio/components/ChatLog.js +59 -0
- package/dist/studio/components/Footer.js +11 -0
- package/dist/studio/components/Header.js +8 -0
- package/dist/studio/components/Input.js +15 -0
- package/dist/studio/components/Message.js +56 -0
- package/dist/studio/components/Suggestions.js +11 -0
- package/dist/studio/components/ToolCall.js +33 -0
- package/dist/studio/components/WhatsappConnectCard.js +57 -0
- package/dist/studio/index.js +42 -0
- package/dist/studio/render/json.js +16 -0
- package/dist/studio/render/markdown.js +32 -0
- package/dist/studio/render/steps.js +58 -0
- package/dist/studio/runOneShot.js +96 -0
- package/dist/studio/runRepl.js +52 -0
- package/dist/studio/slash/handlers.js +199 -0
- package/dist/studio/slash/parser.js +46 -0
- package/dist/studio/slash/registry.js +16 -0
- package/dist/studio/state/store.js +181 -0
- package/dist/studio/whatsapp/api.js +63 -0
- package/dist/studio/whatsapp/polling.js +77 -0
- package/dist/studio/whatsapp/types.js +31 -0
- package/dist/types/agent.js +1 -2
- package/dist/types/auth.js +1 -2
- package/dist/types/common.js +1 -2
- package/dist/types/message.js +1 -2
- package/dist/types/portal.js +1 -2
- package/dist/types/workers.js +1 -2
- package/dist/utils/agent-errors.js +46 -0
- package/dist/utils/api.js +8 -9
- package/dist/utils/banner.js +33 -34
- package/dist/utils/credentials.js +12 -20
- package/dist/utils/help.js +44 -0
- package/dist/utils/logger.js +13 -19
- package/dist/utils/ui.js +35 -49
- package/package.json +21 -10
- package/src/cli.ts +24 -8
- package/src/commands/agent/ai-config.ts +89 -34
- package/src/commands/agent/chat.ts +49 -37
- package/src/commands/agent/copy.ts +19 -13
- package/src/commands/agent/create.ts +32 -22
- package/src/commands/agent/delete.ts +24 -18
- package/src/commands/agent/enable-widget.ts +31 -23
- package/src/commands/agent/export.ts +72 -51
- package/src/commands/agent/files.ts +51 -39
- package/src/commands/agent/get.ts +86 -66
- package/src/commands/agent/index.ts +36 -18
- package/src/commands/agent/list.ts +22 -16
- package/src/commands/agent/monitor.ts +67 -56
- package/src/commands/agent/on-message.ts +36 -27
- package/src/commands/agent/set.ts +47 -37
- package/src/commands/agent/templates.ts +90 -82
- package/src/commands/agent/tools.ts +53 -47
- package/src/commands/agent/update.ts +28 -20
- package/src/commands/agent/validate.ts +135 -0
- package/src/commands/agent/wizard.ts +114 -114
- package/src/commands/auth/index.ts +3 -3
- package/src/commands/auth/login.ts +44 -29
- package/src/commands/auth/logout.ts +16 -10
- package/src/commands/auth/status.ts +14 -8
- package/src/commands/portal/add-agent.ts +23 -17
- package/src/commands/portal/add-link.ts +17 -9
- package/src/commands/portal/clear-links.ts +13 -7
- package/src/commands/portal/create.ts +20 -12
- package/src/commands/portal/delete.ts +28 -20
- package/src/commands/portal/get.ts +25 -19
- package/src/commands/portal/index.ts +22 -10
- package/src/commands/portal/list.ts +27 -19
- package/src/commands/portal/update.ts +38 -26
- package/src/commands/whatsapp/broadcast.ts +28 -18
- package/src/commands/whatsapp/channels.ts +31 -20
- package/src/commands/whatsapp/chat.ts +20 -12
- package/src/commands/whatsapp/connect.ts +39 -31
- package/src/commands/whatsapp/delete-webhook.ts +15 -9
- package/src/commands/whatsapp/index.ts +24 -10
- package/src/commands/whatsapp/register-webhook.ts +16 -10
- package/src/commands/whatsapp/send-template.ts +33 -21
- package/src/commands/whatsapp/send.ts +23 -15
- package/src/commands/whatsapp/widget.ts +25 -17
- package/src/commands/workers/deploy.ts +34 -22
- package/src/commands/workers/index.ts +21 -7
- package/src/commands/workers/list.ts +31 -19
- package/src/commands/workers/logs.ts +30 -20
- package/src/commands/workers/remove.ts +30 -22
- package/src/commands/workers/secret.ts +46 -34
- package/src/commands/workers/test.ts +34 -22
- package/src/schemas/agent.config.schema.json +569 -0
- package/src/studio/api/sseClient.ts +91 -0
- package/src/studio/api/studioApi.ts +27 -0
- package/src/studio/api/types.ts +96 -0
- package/src/studio/components/App.tsx +266 -0
- package/src/studio/components/ChatLog.tsx +95 -0
- package/src/studio/components/Footer.tsx +38 -0
- package/src/studio/components/Header.tsx +39 -0
- package/src/studio/components/Input.tsx +32 -0
- package/src/studio/components/Message.tsx +87 -0
- package/src/studio/components/Suggestions.tsx +26 -0
- package/src/studio/components/ToolCall.tsx +58 -0
- package/src/studio/components/WhatsappConnectCard.tsx +139 -0
- package/src/studio/index.ts +58 -0
- package/src/studio/render/markdown.ts +32 -0
- package/src/studio/render/steps.ts +57 -0
- package/src/studio/runOneShot.ts +114 -0
- package/src/studio/runRepl.tsx +76 -0
- package/src/studio/slash/handlers.ts +226 -0
- package/src/studio/slash/parser.ts +41 -0
- package/src/studio/slash/registry.ts +54 -0
- package/src/studio/state/store.ts +273 -0
- package/src/studio/whatsapp/api.ts +96 -0
- package/src/studio/whatsapp/polling.ts +93 -0
- package/src/studio/whatsapp/types.ts +80 -0
- package/src/types/agent.ts +1 -1
- package/src/types/auth.ts +4 -3
- package/src/types/portal.ts +1 -1
- package/src/types/workers.ts +1 -1
- package/src/utils/agent-errors.ts +67 -0
- package/src/utils/api.ts +6 -0
- package/src/utils/banner.ts +14 -9
- package/src/utils/credentials.ts +6 -5
- package/src/utils/help.ts +51 -0
- package/tsconfig.json +9 -6
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { slashRegistry } from './registry.js';
|
|
5
|
+
import type { SlashResult } from './registry.js';
|
|
6
|
+
import { useStudioStore } from '../state/store.js';
|
|
7
|
+
|
|
8
|
+
function expandTilde(p: string): string {
|
|
9
|
+
if (p === '~') return homedir();
|
|
10
|
+
if (p.startsWith('~/')) return resolve(homedir(), p.slice(2));
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function slugify(s: string): string {
|
|
15
|
+
return s
|
|
16
|
+
.normalize('NFKD')
|
|
17
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
18
|
+
.replace(/[^a-zA-Z0-9-_]+/g, '-')
|
|
19
|
+
.replace(/^-+|-+$/g, '')
|
|
20
|
+
.toLowerCase() || 'agent';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CATEGORY_LABELS: Record<NonNullable<import('./registry.js').SlashCategory>, string> = {
|
|
24
|
+
general: 'General',
|
|
25
|
+
agent: 'Agent',
|
|
26
|
+
session: 'Session',
|
|
27
|
+
};
|
|
28
|
+
const CATEGORY_ORDER: Array<NonNullable<import('./registry.js').SlashCategory>> = [
|
|
29
|
+
'general',
|
|
30
|
+
'agent',
|
|
31
|
+
'session',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
slashRegistry.register({
|
|
35
|
+
name: 'help',
|
|
36
|
+
aliases: ['?'],
|
|
37
|
+
description: 'Show this help with every available command',
|
|
38
|
+
category: 'general',
|
|
39
|
+
handler(): SlashResult {
|
|
40
|
+
const all = slashRegistry.list();
|
|
41
|
+
const grouped = new Map<string, typeof all>();
|
|
42
|
+
for (const cmd of all) {
|
|
43
|
+
const cat = cmd.category ?? 'general';
|
|
44
|
+
const arr = grouped.get(cat) ?? [];
|
|
45
|
+
arr.push(cmd);
|
|
46
|
+
grouped.set(cat, arr);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines: string[] = ['**Plazbot Studio — slash commands**', ''];
|
|
50
|
+
lines.push('Type plain text to chat with the assistant.');
|
|
51
|
+
lines.push('Slash commands run locally or send a templated prompt to the model.');
|
|
52
|
+
lines.push('');
|
|
53
|
+
|
|
54
|
+
for (const cat of CATEGORY_ORDER) {
|
|
55
|
+
const items = grouped.get(cat);
|
|
56
|
+
if (!items || items.length === 0) continue;
|
|
57
|
+
lines.push(`**${CATEGORY_LABELS[cat]}**`);
|
|
58
|
+
for (const cmd of items) {
|
|
59
|
+
const usage = cmd.usage ?? `/${cmd.name}`;
|
|
60
|
+
const aliasPart = cmd.aliases?.length
|
|
61
|
+
? ` _(alias: ${cmd.aliases.map((a) => '/' + a).join(', ')})_`
|
|
62
|
+
: '';
|
|
63
|
+
lines.push(`- \`${usage}\`${aliasPart} — ${cmd.description}`);
|
|
64
|
+
}
|
|
65
|
+
lines.push('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push('**Keyboard shortcuts**');
|
|
69
|
+
lines.push('- `Esc` — cancel the current stream');
|
|
70
|
+
lines.push('- `Ctrl+C` — exit Studio');
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push('**Tip:** you can also speak naturally — e.g. _"list my agents"_, _"create a sales agent for WhatsApp"_ or _"connect this agent to WhatsApp Business"_.');
|
|
73
|
+
|
|
74
|
+
useStudioStore.getState().pushSyntheticAssistant(lines.join('\n'));
|
|
75
|
+
return { kind: 'noop' };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
slashRegistry.register({
|
|
80
|
+
name: 'clear',
|
|
81
|
+
aliases: ['cls'],
|
|
82
|
+
description: 'Clear the conversation (keeps the loaded agent and token usage)',
|
|
83
|
+
category: 'session',
|
|
84
|
+
handler(): SlashResult {
|
|
85
|
+
useStudioStore.getState().clearMessages();
|
|
86
|
+
return { kind: 'noop' };
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
slashRegistry.register({
|
|
91
|
+
name: 'reset',
|
|
92
|
+
description: 'Full reset: clears messages, loaded agent and token usage',
|
|
93
|
+
category: 'session',
|
|
94
|
+
handler(): SlashResult {
|
|
95
|
+
useStudioStore.getState().reset();
|
|
96
|
+
return { kind: 'noop' };
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const COMPACT_PROMPT = [
|
|
101
|
+
'You are about to compact this conversation.',
|
|
102
|
+
'',
|
|
103
|
+
'Produce a concise but complete summary of everything we have discussed so far, in Markdown:',
|
|
104
|
+
'- Goal / task the user is working on.',
|
|
105
|
+
'- Key decisions and constraints.',
|
|
106
|
+
'- Current agent configuration state (if any) and its `id`.',
|
|
107
|
+
'- Pending items or next steps.',
|
|
108
|
+
'',
|
|
109
|
+
'Reply with ONLY the summary content (no preamble, no farewell).',
|
|
110
|
+
'After this message, the previous history will be replaced by your summary so we can keep working with less context.',
|
|
111
|
+
].join('\n');
|
|
112
|
+
|
|
113
|
+
slashRegistry.register({
|
|
114
|
+
name: 'compact',
|
|
115
|
+
description: 'Summarize the conversation and replace history with that summary',
|
|
116
|
+
category: 'session',
|
|
117
|
+
handler(): SlashResult {
|
|
118
|
+
const store = useStudioStore.getState();
|
|
119
|
+
if (store.messages.length === 0) {
|
|
120
|
+
store.pushSyntheticAssistant('Nothing to compact yet.', { error: true });
|
|
121
|
+
return { kind: 'noop' };
|
|
122
|
+
}
|
|
123
|
+
store.setPendingCompact(true);
|
|
124
|
+
return { kind: 'message', content: COMPACT_PROMPT };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
slashRegistry.register({
|
|
129
|
+
name: 'quit',
|
|
130
|
+
aliases: ['exit', 'q'],
|
|
131
|
+
description: 'Exit Studio',
|
|
132
|
+
category: 'general',
|
|
133
|
+
handler(_parsed, ctx): SlashResult {
|
|
134
|
+
ctx.exit();
|
|
135
|
+
return { kind: 'exit' };
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
slashRegistry.register({
|
|
140
|
+
name: 'agents',
|
|
141
|
+
description: 'List the agents in the current workspace',
|
|
142
|
+
category: 'agent',
|
|
143
|
+
handler(): SlashResult {
|
|
144
|
+
return { kind: 'message', content: 'List all agents in my workspace.' };
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
slashRegistry.register({
|
|
149
|
+
name: 'load',
|
|
150
|
+
description: 'Load an agent by id and show its current configuration',
|
|
151
|
+
category: 'agent',
|
|
152
|
+
usage: '/load <agentId>',
|
|
153
|
+
handler(parsed): SlashResult {
|
|
154
|
+
const id = parsed.args[0];
|
|
155
|
+
if (!id) {
|
|
156
|
+
useStudioStore.getState().pushSyntheticAssistant('Usage: `/load <agentId>`', { error: true });
|
|
157
|
+
return { kind: 'noop' };
|
|
158
|
+
}
|
|
159
|
+
useStudioStore.getState().setAgentId(id);
|
|
160
|
+
return { kind: 'message', content: `Load the agent with id "${id}" and show me its current configuration.` };
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
slashRegistry.register({
|
|
165
|
+
name: 'save',
|
|
166
|
+
description: 'Save the currently loaded agent configuration to the backend',
|
|
167
|
+
category: 'agent',
|
|
168
|
+
handler(): SlashResult {
|
|
169
|
+
const { agentConfig } = useStudioStore.getState();
|
|
170
|
+
if (!agentConfig) {
|
|
171
|
+
useStudioStore.getState().pushSyntheticAssistant(
|
|
172
|
+
'No agent configuration loaded. Create one first or use `/load <id>`.',
|
|
173
|
+
{ error: true },
|
|
174
|
+
);
|
|
175
|
+
return { kind: 'noop' };
|
|
176
|
+
}
|
|
177
|
+
return { kind: 'message', content: 'Save this agent with its current configuration.' };
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
slashRegistry.register({
|
|
182
|
+
name: 'diagnose',
|
|
183
|
+
description: 'Run diagnostics on the current agent and report any issues found',
|
|
184
|
+
category: 'agent',
|
|
185
|
+
handler(): SlashResult {
|
|
186
|
+
return { kind: 'message', content: 'Diagnose this agent and report any issues you find.' };
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
slashRegistry.register({
|
|
191
|
+
name: 'export',
|
|
192
|
+
aliases: ['download'],
|
|
193
|
+
description: 'Export the current agent configuration to a JSON file on disk',
|
|
194
|
+
category: 'agent',
|
|
195
|
+
usage: '/export [path]',
|
|
196
|
+
async handler(parsed): Promise<SlashResult> {
|
|
197
|
+
const store = useStudioStore.getState();
|
|
198
|
+
const { agentConfig } = store;
|
|
199
|
+
|
|
200
|
+
if (!agentConfig) {
|
|
201
|
+
store.pushSyntheticAssistant(
|
|
202
|
+
'No hay agente cargado. Crea o carga uno antes con `/load <id>`.',
|
|
203
|
+
{ error: true },
|
|
204
|
+
);
|
|
205
|
+
return { kind: 'noop' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const cfg = agentConfig as any;
|
|
209
|
+
const baseName = slugify(cfg?.name ?? store.agentId ?? 'agent');
|
|
210
|
+
const defaultPath = `./${baseName}.json`;
|
|
211
|
+
const targetArg = parsed.args[0] ?? defaultPath;
|
|
212
|
+
const targetPath = resolve(process.cwd(), expandTilde(targetArg));
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await writeFile(targetPath, JSON.stringify(agentConfig, null, 2), 'utf-8');
|
|
216
|
+
store.pushSyntheticAssistant(
|
|
217
|
+
`Agente exportado a \`${targetPath}\`.\n\nValídalo con: \`plazbot agent validate ${targetPath}\``,
|
|
218
|
+
);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
+
store.pushSyntheticAssistant(`No pude escribir el archivo: ${message}`, { error: true });
|
|
222
|
+
}
|
|
223
|
+
return { kind: 'noop' };
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ParsedSlash {
|
|
2
|
+
name: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
raw: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parsea un input crudo del usuario.
|
|
9
|
+
* - Si empieza con `/`, retorna { name, args, raw } para ejecutar como slash command.
|
|
10
|
+
* - Si no, retorna null (mensaje normal al backend).
|
|
11
|
+
*/
|
|
12
|
+
export function parseSlash(input: string): ParsedSlash | null {
|
|
13
|
+
const trimmed = input.trim();
|
|
14
|
+
if (!trimmed.startsWith('/')) return null;
|
|
15
|
+
const without = trimmed.slice(1);
|
|
16
|
+
if (!without) return { name: '', args: [], raw: trimmed };
|
|
17
|
+
// Split por espacios respetando comillas básicas.
|
|
18
|
+
const tokens = tokenize(without);
|
|
19
|
+
const [name, ...args] = tokens;
|
|
20
|
+
return { name: (name ?? '').toLowerCase(), args, raw: trimmed };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tokenize(s: string): string[] {
|
|
24
|
+
const out: string[] = [];
|
|
25
|
+
let cur = '';
|
|
26
|
+
let quote: '"' | "'" | null = null;
|
|
27
|
+
for (const ch of s) {
|
|
28
|
+
if (quote) {
|
|
29
|
+
if (ch === quote) { quote = null; continue; }
|
|
30
|
+
cur += ch;
|
|
31
|
+
} else if (ch === '"' || ch === "'") {
|
|
32
|
+
quote = ch;
|
|
33
|
+
} else if (/\s/.test(ch)) {
|
|
34
|
+
if (cur) { out.push(cur); cur = ''; }
|
|
35
|
+
} else {
|
|
36
|
+
cur += ch;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (cur) out.push(cur);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ParsedSlash } from './parser.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resultado de un slash handler:
|
|
5
|
+
* - { kind: 'message', content } -> enviar `content` como mensaje normal al backend.
|
|
6
|
+
* - { kind: 'noop' } -> ya consumido por el handler (sin tráfico).
|
|
7
|
+
* - { kind: 'exit' } -> el REPL debe cerrarse.
|
|
8
|
+
* - { kind: 'error', content } -> mostrar mensaje de error sintético.
|
|
9
|
+
*/
|
|
10
|
+
export type SlashResult =
|
|
11
|
+
| { kind: 'message'; content: string }
|
|
12
|
+
| { kind: 'noop' }
|
|
13
|
+
| { kind: 'exit' }
|
|
14
|
+
| { kind: 'error'; content: string };
|
|
15
|
+
|
|
16
|
+
export interface SlashHandlerContext {
|
|
17
|
+
exit: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SlashHandler = (parsed: ParsedSlash, ctx: SlashHandlerContext) => SlashResult | Promise<SlashResult>;
|
|
21
|
+
|
|
22
|
+
export type SlashCategory = 'general' | 'agent' | 'session';
|
|
23
|
+
|
|
24
|
+
export interface SlashSpec {
|
|
25
|
+
name: string;
|
|
26
|
+
aliases?: string[];
|
|
27
|
+
description: string;
|
|
28
|
+
/** Group used by `/help` to render commands by section. Defaults to 'general'. */
|
|
29
|
+
category?: SlashCategory;
|
|
30
|
+
/** Optional usage line (e.g. `/load <agentId>`). Falls back to `/<name>`. */
|
|
31
|
+
usage?: string;
|
|
32
|
+
handler: SlashHandler;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class SlashRegistry {
|
|
36
|
+
private map = new Map<string, SlashSpec>();
|
|
37
|
+
private order: SlashSpec[] = [];
|
|
38
|
+
|
|
39
|
+
register(spec: SlashSpec): void {
|
|
40
|
+
this.order.push(spec);
|
|
41
|
+
this.map.set(spec.name, spec);
|
|
42
|
+
spec.aliases?.forEach((a) => this.map.set(a, spec));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get(name: string): SlashSpec | undefined {
|
|
46
|
+
return this.map.get(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
list(): SlashSpec[] {
|
|
50
|
+
return this.order.slice();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const slashRegistry = new SlashRegistry();
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import type { WaConnectState, WaConnectStatus, WhatsappLinkData } from '../whatsapp/types.js';
|
|
4
|
+
|
|
5
|
+
export type Role = 'user' | 'assistant' | 'system';
|
|
6
|
+
|
|
7
|
+
export interface ChatMessage {
|
|
8
|
+
id: string;
|
|
9
|
+
role: Role;
|
|
10
|
+
content: string;
|
|
11
|
+
/** Mensaje aún siendo recibido por streaming. */
|
|
12
|
+
streaming?: boolean;
|
|
13
|
+
/** Mensaje sintético generado por slash commands (no enviado al backend). */
|
|
14
|
+
synthetic?: boolean;
|
|
15
|
+
/** Marca de error visual. */
|
|
16
|
+
error?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type StepStatus = 'running' | 'success' | 'error';
|
|
20
|
+
|
|
21
|
+
export interface ToolStep {
|
|
22
|
+
id: string;
|
|
23
|
+
toolName: string;
|
|
24
|
+
status: StepStatus;
|
|
25
|
+
/** Anclamos el step debajo del mensaje del assistant que lo originó. */
|
|
26
|
+
afterMessageId: string;
|
|
27
|
+
result?: unknown;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StudioState {
|
|
32
|
+
messages: ChatMessage[];
|
|
33
|
+
steps: ToolStep[];
|
|
34
|
+
agentConfig: unknown | null;
|
|
35
|
+
agentId: string | null;
|
|
36
|
+
usage: { inputTokens: number; outputTokens: number };
|
|
37
|
+
streaming: boolean;
|
|
38
|
+
abortController: AbortController | null;
|
|
39
|
+
/** True mientras se está corriendo el flujo de /compact (el próximo assistant
|
|
40
|
+
* reemplazará todo el historial cuando termine). */
|
|
41
|
+
pendingCompact: boolean;
|
|
42
|
+
|
|
43
|
+
pushUserMessage(content: string): string;
|
|
44
|
+
pushSyntheticAssistant(content: string, opts?: { error?: boolean }): string;
|
|
45
|
+
startAssistantMessage(): string;
|
|
46
|
+
appendAssistantText(id: string, delta: string): void;
|
|
47
|
+
finishAssistantMessage(id: string): void;
|
|
48
|
+
|
|
49
|
+
startStep(toolName: string, afterMessageId: string): string;
|
|
50
|
+
resolveLastStep(toolName: string, status: StepStatus, result?: unknown, errorMessage?: string): void;
|
|
51
|
+
failAllRunningSteps(message?: string): void;
|
|
52
|
+
|
|
53
|
+
setAgentConfig(cfg: unknown): void;
|
|
54
|
+
setAgentId(id: string | null): void;
|
|
55
|
+
addUsage(inT: number, outT: number): void;
|
|
56
|
+
|
|
57
|
+
setStreaming(s: boolean): void;
|
|
58
|
+
setAbortController(ctrl: AbortController | null): void;
|
|
59
|
+
abortStream(): void;
|
|
60
|
+
|
|
61
|
+
/** WhatsApp connection card state (only one active at a time). */
|
|
62
|
+
waConnect: WaConnectState | null;
|
|
63
|
+
startWaConnect(linkData: WhatsappLinkData, anchorMessageId: string): string;
|
|
64
|
+
updateWaConnect(patch: Partial<WaConnectState>): void;
|
|
65
|
+
setWaStatus(status: WaConnectStatus, extra?: Partial<WaConnectState>): void;
|
|
66
|
+
clearWaConnect(): void;
|
|
67
|
+
|
|
68
|
+
/** Borra mensajes/steps/waConnect pero MANTIENE agente cargado, usage y config. */
|
|
69
|
+
clearMessages(): void;
|
|
70
|
+
/** Marca que el próximo stream completo debe reemplazar el historial. */
|
|
71
|
+
setPendingCompact(v: boolean): void;
|
|
72
|
+
/** Reemplaza todo el historial por el contenido del mensaje `id` (el resumen). */
|
|
73
|
+
compactInto(id: string): void;
|
|
74
|
+
/** Reset total (mensajes + agente + usage). */
|
|
75
|
+
reset(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function uid(): string {
|
|
79
|
+
// randomUUID está disponible en Node >=14.17 con crypto. Fallback simple.
|
|
80
|
+
try { return randomUUID(); } catch { return Math.random().toString(36).slice(2); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const useStudioStore = create<StudioState>((set, get) => ({
|
|
84
|
+
messages: [],
|
|
85
|
+
steps: [],
|
|
86
|
+
agentConfig: null,
|
|
87
|
+
agentId: null,
|
|
88
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
89
|
+
streaming: false,
|
|
90
|
+
abortController: null,
|
|
91
|
+
waConnect: null,
|
|
92
|
+
pendingCompact: false,
|
|
93
|
+
|
|
94
|
+
pushUserMessage(content) {
|
|
95
|
+
const id = uid();
|
|
96
|
+
set((s) => ({ messages: [...s.messages, { id, role: 'user', content }] }));
|
|
97
|
+
return id;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
pushSyntheticAssistant(content, opts) {
|
|
101
|
+
const id = uid();
|
|
102
|
+
set((s) => ({
|
|
103
|
+
messages: [...s.messages, { id, role: 'assistant', content, synthetic: true, error: opts?.error }],
|
|
104
|
+
}));
|
|
105
|
+
return id;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
startAssistantMessage() {
|
|
109
|
+
const id = uid();
|
|
110
|
+
set((s) => ({ messages: [...s.messages, { id, role: 'assistant', content: '', streaming: true }] }));
|
|
111
|
+
return id;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
appendAssistantText(id, delta) {
|
|
115
|
+
set((s) => ({
|
|
116
|
+
messages: s.messages.map((m) => (m.id === id ? { ...m, content: m.content + delta } : m)),
|
|
117
|
+
}));
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
finishAssistantMessage(id) {
|
|
121
|
+
set((s) => ({
|
|
122
|
+
messages: s.messages.map((m) => (m.id === id ? { ...m, streaming: false } : m)),
|
|
123
|
+
}));
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
startStep(toolName, afterMessageId) {
|
|
127
|
+
const id = uid();
|
|
128
|
+
set((s) => ({ steps: [...s.steps, { id, toolName, status: 'running', afterMessageId }] }));
|
|
129
|
+
return id;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
resolveLastStep(toolName, status, result, errorMessage) {
|
|
133
|
+
set((s) => {
|
|
134
|
+
const idx = [...s.steps].reverse().findIndex((x) => x.toolName === toolName && x.status === 'running');
|
|
135
|
+
if (idx === -1) return {};
|
|
136
|
+
const real = s.steps.length - 1 - idx;
|
|
137
|
+
const updated = [...s.steps];
|
|
138
|
+
updated[real] = { ...updated[real], status, result, errorMessage };
|
|
139
|
+
return { steps: updated };
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
failAllRunningSteps(message) {
|
|
144
|
+
set((s) => ({
|
|
145
|
+
steps: s.steps.map((x) =>
|
|
146
|
+
x.status === 'running' ? { ...x, status: 'error', errorMessage: message ?? 'Cancelado' } : x,
|
|
147
|
+
),
|
|
148
|
+
}));
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
setAgentConfig(cfg) {
|
|
152
|
+
set({ agentConfig: cfg });
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
setAgentId(id) {
|
|
156
|
+
set({ agentId: id });
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
addUsage(inT, outT) {
|
|
160
|
+
set((s) => ({
|
|
161
|
+
usage: {
|
|
162
|
+
inputTokens: s.usage.inputTokens + (inT || 0),
|
|
163
|
+
outputTokens: s.usage.outputTokens + (outT || 0),
|
|
164
|
+
},
|
|
165
|
+
}));
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
setStreaming(streaming) {
|
|
169
|
+
set({ streaming });
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
setAbortController(abortController) {
|
|
173
|
+
set({ abortController });
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
abortStream() {
|
|
177
|
+
const ctrl = get().abortController;
|
|
178
|
+
if (ctrl && !ctrl.signal.aborted) ctrl.abort();
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
startWaConnect(linkData, anchorMessageId) {
|
|
182
|
+
const id = uid();
|
|
183
|
+
set({
|
|
184
|
+
waConnect: {
|
|
185
|
+
id,
|
|
186
|
+
linkData,
|
|
187
|
+
status: 'waiting',
|
|
188
|
+
anchorMessageId,
|
|
189
|
+
attempt: 0,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return id;
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
updateWaConnect(patch) {
|
|
196
|
+
set((s) => (s.waConnect ? { waConnect: { ...s.waConnect, ...patch } } : {}));
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
setWaStatus(status, extra) {
|
|
200
|
+
set((s) => (s.waConnect ? { waConnect: { ...s.waConnect, status, ...(extra ?? {}) } } : {}));
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
clearWaConnect() {
|
|
204
|
+
set({ waConnect: null });
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
clearMessages() {
|
|
208
|
+
const ctrl = get().abortController;
|
|
209
|
+
if (ctrl && !ctrl.signal.aborted) ctrl.abort();
|
|
210
|
+
set({
|
|
211
|
+
messages: [],
|
|
212
|
+
steps: [],
|
|
213
|
+
streaming: false,
|
|
214
|
+
abortController: null,
|
|
215
|
+
waConnect: null,
|
|
216
|
+
pendingCompact: false,
|
|
217
|
+
// agentConfig, agentId, usage se mantienen para seguir trabajando.
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
setPendingCompact(v) {
|
|
222
|
+
set({ pendingCompact: v });
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
compactInto(id) {
|
|
226
|
+
set((s) => {
|
|
227
|
+
const summary = s.messages.find((m) => m.id === id);
|
|
228
|
+
if (!summary || !summary.content.trim()) {
|
|
229
|
+
return { pendingCompact: false };
|
|
230
|
+
}
|
|
231
|
+
const newId = uid();
|
|
232
|
+
return {
|
|
233
|
+
messages: [
|
|
234
|
+
{
|
|
235
|
+
id: newId,
|
|
236
|
+
role: 'assistant',
|
|
237
|
+
content: summary.content,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
steps: [],
|
|
241
|
+
waConnect: null,
|
|
242
|
+
pendingCompact: false,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
reset() {
|
|
248
|
+
const ctrl = get().abortController;
|
|
249
|
+
if (ctrl && !ctrl.signal.aborted) ctrl.abort();
|
|
250
|
+
set({
|
|
251
|
+
messages: [],
|
|
252
|
+
steps: [],
|
|
253
|
+
agentConfig: null,
|
|
254
|
+
agentId: null,
|
|
255
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
256
|
+
streaming: false,
|
|
257
|
+
abortController: null,
|
|
258
|
+
waConnect: null,
|
|
259
|
+
pendingCompact: false,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Construye el array `messages` que se envía al backend a partir del estado.
|
|
266
|
+
* Filtra mensajes sintéticos (synthetic === true) y los aún en streaming vacíos.
|
|
267
|
+
*/
|
|
268
|
+
export function selectBackendMessages(): { role: 'user' | 'assistant'; content: string }[] {
|
|
269
|
+
const { messages } = useStudioStore.getState();
|
|
270
|
+
return messages
|
|
271
|
+
.filter((m) => !m.synthetic && (m.role === 'user' || m.role === 'assistant') && m.content.trim().length > 0)
|
|
272
|
+
.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
|
273
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { getBaseUrl } from '../api/studioApi.js';
|
|
2
|
+
import type { StudioStreamOptions } from '../api/types.js';
|
|
3
|
+
import type { WorkspaceIntegrationItem } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* REST helpers used during the WhatsApp connection polling loop.
|
|
7
|
+
* Reuses the same zone/dev base URL + Bearer headers as the SSE client.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface WorkspaceFetchOpts {
|
|
11
|
+
apiKey: string;
|
|
12
|
+
workspaceId: string;
|
|
13
|
+
zone: 'LA' | 'EU';
|
|
14
|
+
dev?: boolean;
|
|
15
|
+
userId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface WorkspaceShape {
|
|
19
|
+
id?: string;
|
|
20
|
+
integrations?: WorkspaceIntegrationItem[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function headers(opts: WorkspaceFetchOpts): Record<string, string> {
|
|
24
|
+
const h: Record<string, string> = {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
27
|
+
'x-workspace-id': opts.workspaceId,
|
|
28
|
+
};
|
|
29
|
+
if (opts.userId) h['x-user-id'] = opts.userId;
|
|
30
|
+
return h;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* GET /api/workspace/{workspaceId} — the API returns an array of workspaces
|
|
35
|
+
* (the front uses `[0]`). Returns the first element or null.
|
|
36
|
+
*/
|
|
37
|
+
export async function getWorkspaceById(
|
|
38
|
+
opts: WorkspaceFetchOpts,
|
|
39
|
+
signal?: AbortSignal,
|
|
40
|
+
): Promise<WorkspaceShape | null> {
|
|
41
|
+
const url = `${getBaseUrl(opts.zone, opts.dev)}/api/workspace/${encodeURIComponent(opts.workspaceId)}`;
|
|
42
|
+
const res = await fetch(url, { method: 'GET', headers: headers(opts), signal });
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`GET workspace ${res.status} ${res.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
const json = (await res.json()) as WorkspaceShape | WorkspaceShape[] | null;
|
|
47
|
+
if (!json) return null;
|
|
48
|
+
if (Array.isArray(json)) return json[0] ?? null;
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ActivateResult {
|
|
53
|
+
success?: boolean;
|
|
54
|
+
message?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* POST /api/workspace/{workspaceId}/integrations/{integrationId}/activate
|
|
59
|
+
* Body: { isActive: true, platformCode: "whatsapp" }
|
|
60
|
+
*/
|
|
61
|
+
export async function activateIntegration(
|
|
62
|
+
opts: WorkspaceFetchOpts,
|
|
63
|
+
integrationId: string,
|
|
64
|
+
signal?: AbortSignal,
|
|
65
|
+
): Promise<ActivateResult> {
|
|
66
|
+
const url = `${getBaseUrl(opts.zone, opts.dev)}/api/workspace/${encodeURIComponent(opts.workspaceId)}/integrations/${encodeURIComponent(integrationId)}/activate`;
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: headers(opts),
|
|
70
|
+
body: JSON.stringify({ isActive: true, platformCode: 'whatsapp' }),
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
let parsed: ActivateResult = {};
|
|
75
|
+
if (text) {
|
|
76
|
+
try { parsed = JSON.parse(text) as ActivateResult; } catch { /* not json */ }
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
return { success: false, message: parsed.message || `HTTP ${res.status} ${res.statusText}` };
|
|
80
|
+
}
|
|
81
|
+
return parsed.success === false ? parsed : { success: true, ...parsed };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Re-export for callers that need just the auth shape. */
|
|
85
|
+
export type WorkspaceAuthOpts = WorkspaceFetchOpts;
|
|
86
|
+
|
|
87
|
+
/** Adapter: derives the workspace fetch options from a StudioStreamOptions. */
|
|
88
|
+
export function workspaceOptsFromStream(stream: StudioStreamOptions): WorkspaceAuthOpts {
|
|
89
|
+
return {
|
|
90
|
+
apiKey: stream.apiKey,
|
|
91
|
+
workspaceId: stream.workspaceId,
|
|
92
|
+
zone: stream.zone,
|
|
93
|
+
dev: stream.dev,
|
|
94
|
+
userId: stream.userId,
|
|
95
|
+
};
|
|
96
|
+
}
|