plazbot-cli 0.3.7 → 0.4.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/README.md +38 -2
- package/dist/commands/auth/browser-login.js +121 -0
- package/dist/commands/auth/index.js +5 -1
- package/dist/commands/auth/login.js +1 -3
- package/dist/commands/auth/logout.js +4 -3
- package/dist/commands/auth/status.js +8 -5
- package/dist/commands/auth/workspace.js +73 -0
- package/dist/commands/whatsapp/connect-flow.js +137 -0
- package/dist/commands/whatsapp/connect.js +26 -160
- package/dist/commands/whatsapp/index.js +4 -0
- package/dist/commands/whatsapp/setup.js +242 -0
- package/dist/studio/components/App.js +1 -1
- package/dist/studio/runOneShot.js +2 -2
- package/dist/studio/runRepl.js +1 -1
- package/dist/utils/agent-errors.js +2 -2
- package/dist/utils/banner.js +10 -9
- package/dist/utils/credentials.js +31 -3
- package/dist/utils/open-browser.js +17 -0
- package/package.json +14 -6
- package/.claude/settings.local.json +0 -19
- package/CLAUDE.md +0 -121
- package/agent.config.json +0 -21
- package/dist/studio/components/AgentPanel.js +0 -35
- package/dist/studio/render/json.js +0 -16
- package/dist/studio/whatsapp/api.js +0 -63
- package/dist/studio/whatsapp/polling.js +0 -77
- package/dist/studio/whatsapp/types.js +0 -31
- package/src/cli.ts +0 -57
- package/src/commands/agent/ai-config.ts +0 -164
- package/src/commands/agent/chat.ts +0 -225
- package/src/commands/agent/copy.ts +0 -46
- package/src/commands/agent/create.ts +0 -103
- package/src/commands/agent/delete.ts +0 -71
- package/src/commands/agent/enable-widget.ts +0 -67
- package/src/commands/agent/export.ts +0 -430
- package/src/commands/agent/files.ts +0 -171
- package/src/commands/agent/get.ts +0 -233
- package/src/commands/agent/index.ts +0 -55
- package/src/commands/agent/list.ts +0 -58
- package/src/commands/agent/monitor.ts +0 -375
- package/src/commands/agent/on-message.ts +0 -82
- package/src/commands/agent/set.ts +0 -149
- package/src/commands/agent/templates.ts +0 -892
- package/src/commands/agent/tools.ts +0 -168
- package/src/commands/agent/update.ts +0 -61
- package/src/commands/agent/validate.ts +0 -135
- package/src/commands/agent/wizard.ts +0 -756
- package/src/commands/auth/index.ts +0 -10
- package/src/commands/auth/login.ts +0 -72
- package/src/commands/auth/logout.ts +0 -28
- package/src/commands/auth/status.ts +0 -28
- package/src/commands/portal/add-agent.ts +0 -66
- package/src/commands/portal/add-link.ts +0 -47
- package/src/commands/portal/clear-links.ts +0 -33
- package/src/commands/portal/create.ts +0 -58
- package/src/commands/portal/delete.ts +0 -70
- package/src/commands/portal/get.ts +0 -77
- package/src/commands/portal/index.ts +0 -32
- package/src/commands/portal/list.ts +0 -79
- package/src/commands/portal/update.ts +0 -93
- package/src/commands/whatsapp/broadcast.ts +0 -110
- package/src/commands/whatsapp/channels.ts +0 -109
- package/src/commands/whatsapp/chat.ts +0 -85
- package/src/commands/whatsapp/connect.ts +0 -220
- package/src/commands/whatsapp/delete-webhook.ts +0 -43
- package/src/commands/whatsapp/index.ts +0 -37
- package/src/commands/whatsapp/register-webhook.ts +0 -47
- package/src/commands/whatsapp/send-template.ts +0 -94
- package/src/commands/whatsapp/send.ts +0 -55
- package/src/commands/whatsapp/widget.ts +0 -75
- package/src/commands/workers/deploy.ts +0 -156
- package/src/commands/workers/index.ts +0 -30
- package/src/commands/workers/list.ts +0 -105
- package/src/commands/workers/logs.ts +0 -99
- package/src/commands/workers/remove.ts +0 -103
- package/src/commands/workers/secret.ts +0 -191
- package/src/commands/workers/test.ts +0 -145
- package/src/schemas/agent.config.schema.json +0 -569
- package/src/studio/api/sseClient.ts +0 -91
- package/src/studio/api/studioApi.ts +0 -27
- package/src/studio/api/types.ts +0 -96
- package/src/studio/components/App.tsx +0 -274
- package/src/studio/components/ChatLog.tsx +0 -121
- package/src/studio/components/ConnectCard.tsx +0 -171
- package/src/studio/components/Footer.tsx +0 -38
- package/src/studio/components/Header.tsx +0 -39
- package/src/studio/components/Input.tsx +0 -176
- package/src/studio/components/Message.tsx +0 -87
- package/src/studio/components/Suggestions.tsx +0 -26
- package/src/studio/components/ToolCall.tsx +0 -58
- package/src/studio/connect/api.ts +0 -98
- package/src/studio/connect/polling.ts +0 -94
- package/src/studio/connect/types.ts +0 -141
- package/src/studio/index.ts +0 -58
- package/src/studio/render/markdown.ts +0 -93
- package/src/studio/render/steps.ts +0 -57
- package/src/studio/runOneShot.ts +0 -114
- package/src/studio/runRepl.tsx +0 -82
- package/src/studio/slash/handlers.ts +0 -226
- package/src/studio/slash/parser.ts +0 -41
- package/src/studio/slash/registry.ts +0 -54
- package/src/studio/state/store.ts +0 -279
- package/src/types/agent.ts +0 -63
- package/src/types/auth.ts +0 -9
- package/src/types/common.ts +0 -10
- package/src/types/message.ts +0 -34
- package/src/types/portal.ts +0 -56
- package/src/types/workers.ts +0 -86
- package/src/utils/agent-errors.ts +0 -67
- package/src/utils/api.ts +0 -35
- package/src/utils/banner.ts +0 -99
- package/src/utils/credentials.ts +0 -38
- package/src/utils/help.ts +0 -51
- package/src/utils/logger.ts +0 -40
- package/src/utils/ui.ts +0 -109
- package/tsconfig.json +0 -18
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import Spinner from 'ink-spinner';
|
|
4
|
-
|
|
5
|
-
interface FooterProps {
|
|
6
|
-
inputTokens: number;
|
|
7
|
-
outputTokens: number;
|
|
8
|
-
streaming: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function fmt(n: number): string {
|
|
12
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
13
|
-
return String(n);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Footer({ inputTokens, outputTokens, streaming }: FooterProps): React.ReactElement {
|
|
17
|
-
return (
|
|
18
|
-
<Box paddingX={1} flexDirection="row" justifyContent="space-between">
|
|
19
|
-
<Box>
|
|
20
|
-
<Text dimColor>tokens: </Text>
|
|
21
|
-
<Text>{fmt(inputTokens)}</Text>
|
|
22
|
-
<Text dimColor> in / </Text>
|
|
23
|
-
<Text>{fmt(outputTokens)}</Text>
|
|
24
|
-
<Text dimColor> out</Text>
|
|
25
|
-
{streaming ? (
|
|
26
|
-
<>
|
|
27
|
-
<Text> </Text>
|
|
28
|
-
<Text color="yellow"><Spinner type="dots" /></Text>
|
|
29
|
-
<Text color="yellow"> streaming…</Text>
|
|
30
|
-
</>
|
|
31
|
-
) : null}
|
|
32
|
-
</Box>
|
|
33
|
-
<Box>
|
|
34
|
-
<Text dimColor>/help · ↑↓ history · Tab accept · Esc cancel · Ctrl+C quit</Text>
|
|
35
|
-
</Box>
|
|
36
|
-
</Box>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
interface HeaderProps {
|
|
5
|
-
version: string;
|
|
6
|
-
workspace: string;
|
|
7
|
-
zone: 'LA' | 'EU';
|
|
8
|
-
agentId: string | null;
|
|
9
|
-
dev: boolean;
|
|
10
|
-
supportMode?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function Header({ version, workspace, zone, agentId, dev, supportMode }: HeaderProps): React.ReactElement {
|
|
14
|
-
// Sin borde: cualquier `borderStyle` se acumula visualmente cuando el usuario
|
|
15
|
-
// redimensiona la terminal (Ink no limpia el scrollback). Estilo Claude Code:
|
|
16
|
-
// una sola línea con título a la izquierda y metadata a la derecha.
|
|
17
|
-
return (
|
|
18
|
-
<Box flexDirection="row" justifyContent="space-between" paddingX={1}>
|
|
19
|
-
<Box>
|
|
20
|
-
<Text bold color={supportMode ? 'yellow' : 'green'}>● Plazbot Studio</Text>
|
|
21
|
-
<Text dimColor> v{version}</Text>
|
|
22
|
-
{dev ? <Text color="yellow"> dev</Text> : null}
|
|
23
|
-
{supportMode ? <Text color="yellow" bold> · SUPPORT</Text> : null}
|
|
24
|
-
</Box>
|
|
25
|
-
<Box>
|
|
26
|
-
<Text dimColor>workspace </Text>
|
|
27
|
-
<Text color={supportMode ? 'yellow' : undefined}>{workspace}</Text>
|
|
28
|
-
<Text dimColor> zone </Text>
|
|
29
|
-
<Text>{zone}</Text>
|
|
30
|
-
{agentId ? (
|
|
31
|
-
<>
|
|
32
|
-
<Text dimColor> agent </Text>
|
|
33
|
-
<Text color="cyan">{agentId.slice(0, 12)}</Text>
|
|
34
|
-
</>
|
|
35
|
-
) : null}
|
|
36
|
-
</Box>
|
|
37
|
-
</Box>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
2
|
-
import { Box, Text, useInput } from 'ink';
|
|
3
|
-
import { useStudioStore } from '../state/store.js';
|
|
4
|
-
|
|
5
|
-
interface InputProps {
|
|
6
|
-
disabled: boolean;
|
|
7
|
-
onSubmit: (value: string) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Caja de texto del REPL. Implementación custom (sin ink-text-input) para poder
|
|
12
|
-
* soportar:
|
|
13
|
-
*
|
|
14
|
-
* - Historial estilo bash: ↑/↓ navega por mensajes previos del usuario.
|
|
15
|
-
* - Ghost text estilo zsh autosuggestions: muestra en gris la coincidencia
|
|
16
|
-
* más reciente del historial cuyo prefijo es lo que el usuario ya tipeó.
|
|
17
|
-
* Se acepta con Tab o → (solo cuando el cursor está al final del texto).
|
|
18
|
-
*
|
|
19
|
-
* Esc y Ctrl+C no se manejan aquí — burbujean al useInput global de App para
|
|
20
|
-
* que cancele stream / haga exit, sin cambiar el comportamiento existente.
|
|
21
|
-
*/
|
|
22
|
-
export function Input({ disabled, onSubmit }: InputProps): React.ReactElement {
|
|
23
|
-
const [value, setValue] = useState('');
|
|
24
|
-
const [cursor, setCursor] = useState(0);
|
|
25
|
-
// -1 = no estamos navegando historial (ghost activo, texto editable libre).
|
|
26
|
-
// 0 = mensaje más reciente; aumenta hacia mensajes más viejos.
|
|
27
|
-
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
28
|
-
|
|
29
|
-
// Historial derivado del store: solo mensajes del usuario, deduplicados
|
|
30
|
-
// consecutivamente, más recientes primero.
|
|
31
|
-
const history = useStudioStore((s) => {
|
|
32
|
-
const out: string[] = [];
|
|
33
|
-
for (let i = s.messages.length - 1; i >= 0; i--) {
|
|
34
|
-
const m = s.messages[i];
|
|
35
|
-
if (m.role !== 'user') continue;
|
|
36
|
-
if (out[out.length - 1] === m.content) continue;
|
|
37
|
-
out.push(m.content);
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Ghost: primer match del historial cuyo prefijo es `value`. Solo se muestra
|
|
43
|
-
// mientras NO estemos navegando con flechas (para no confundir al usuario).
|
|
44
|
-
const ghost = useMemo(() => {
|
|
45
|
-
if (!value || historyIdx >= 0) return '';
|
|
46
|
-
for (const h of history) {
|
|
47
|
-
if (h.length > value.length && h.startsWith(value)) {
|
|
48
|
-
return h.slice(value.length);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return '';
|
|
52
|
-
}, [value, history, historyIdx]);
|
|
53
|
-
|
|
54
|
-
const resetAfterSubmit = () => {
|
|
55
|
-
setValue('');
|
|
56
|
-
setCursor(0);
|
|
57
|
-
setHistoryIdx(-1);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
useInput(
|
|
61
|
-
(input, key) => {
|
|
62
|
-
if (disabled) return;
|
|
63
|
-
|
|
64
|
-
// Enter → submit
|
|
65
|
-
if (key.return) {
|
|
66
|
-
if (!value.trim()) return;
|
|
67
|
-
const toSend = value;
|
|
68
|
-
resetAfterSubmit();
|
|
69
|
-
onSubmit(toSend);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ↑ → mensaje más viejo
|
|
74
|
-
if (key.upArrow) {
|
|
75
|
-
if (history.length === 0) return;
|
|
76
|
-
const next = Math.min(historyIdx + 1, history.length - 1);
|
|
77
|
-
const text = history[next];
|
|
78
|
-
setHistoryIdx(next);
|
|
79
|
-
setValue(text);
|
|
80
|
-
setCursor(text.length);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ↓ → mensaje más reciente / volver al input vacío
|
|
85
|
-
if (key.downArrow) {
|
|
86
|
-
if (historyIdx <= 0) {
|
|
87
|
-
setHistoryIdx(-1);
|
|
88
|
-
setValue('');
|
|
89
|
-
setCursor(0);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const next = historyIdx - 1;
|
|
93
|
-
const text = history[next];
|
|
94
|
-
setHistoryIdx(next);
|
|
95
|
-
setValue(text);
|
|
96
|
-
setCursor(text.length);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Tab → aceptar ghost completo
|
|
101
|
-
if (key.tab) {
|
|
102
|
-
if (ghost) {
|
|
103
|
-
const full = value + ghost;
|
|
104
|
-
setValue(full);
|
|
105
|
-
setCursor(full.length);
|
|
106
|
-
}
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// → → mover cursor; si está al final y hay ghost, acepta el ghost
|
|
111
|
-
if (key.rightArrow) {
|
|
112
|
-
if (cursor < value.length) {
|
|
113
|
-
setCursor(cursor + 1);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (ghost) {
|
|
117
|
-
const full = value + ghost;
|
|
118
|
-
setValue(full);
|
|
119
|
-
setCursor(full.length);
|
|
120
|
-
}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ← → mover cursor
|
|
125
|
-
if (key.leftArrow) {
|
|
126
|
-
if (cursor > 0) setCursor(cursor - 1);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Backspace / Delete → borrar antes del cursor
|
|
131
|
-
if (key.backspace || key.delete) {
|
|
132
|
-
if (cursor === 0) return;
|
|
133
|
-
const next = value.slice(0, cursor - 1) + value.slice(cursor);
|
|
134
|
-
setValue(next);
|
|
135
|
-
setCursor(cursor - 1);
|
|
136
|
-
if (historyIdx >= 0) setHistoryIdx(-1);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Esc, Ctrl+*, Meta → no consumir, los maneja App.tsx
|
|
141
|
-
if (key.escape || key.ctrl || key.meta) return;
|
|
142
|
-
|
|
143
|
-
// Char inputs (incluye pegado multibyte). Filtramos secuencias raras
|
|
144
|
-
// que Ink a veces entrega como `input` vacío.
|
|
145
|
-
if (input && input.length > 0) {
|
|
146
|
-
// Si veníamos de navegar historial y el usuario escribe algo, salimos
|
|
147
|
-
// del modo navegación y empezamos a editar normalmente.
|
|
148
|
-
if (historyIdx >= 0) setHistoryIdx(-1);
|
|
149
|
-
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
150
|
-
setValue(next);
|
|
151
|
-
setCursor(cursor + input.length);
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
{ isActive: !disabled },
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
return (
|
|
158
|
-
<Box borderStyle="round" borderColor={disabled ? 'gray' : 'green'} paddingX={1}>
|
|
159
|
-
<Text color={disabled ? 'gray' : 'green'} bold>
|
|
160
|
-
{disabled ? '·' : '›'}{' '}
|
|
161
|
-
</Text>
|
|
162
|
-
{disabled ? (
|
|
163
|
-
<Text dimColor>Waiting for response… (press Esc to cancel)</Text>
|
|
164
|
-
) : value.length === 0 ? (
|
|
165
|
-
<Text dimColor>Type a message or /help</Text>
|
|
166
|
-
) : (
|
|
167
|
-
<Text>
|
|
168
|
-
<Text>{value.slice(0, cursor)}</Text>
|
|
169
|
-
<Text inverse>{value.slice(cursor, cursor + 1) || ' '}</Text>
|
|
170
|
-
<Text>{value.slice(cursor + 1)}</Text>
|
|
171
|
-
{ghost ? <Text dimColor>{ghost}</Text> : null}
|
|
172
|
-
</Text>
|
|
173
|
-
)}
|
|
174
|
-
</Box>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import Spinner from 'ink-spinner';
|
|
4
|
-
import type { ChatMessage } from '../state/store.js';
|
|
5
|
-
import { renderMarkdown } from '../render/markdown.js';
|
|
6
|
-
|
|
7
|
-
interface MessageProps {
|
|
8
|
-
message: ChatMessage;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Phrases shown next to the spinner while waiting for the first text token.
|
|
13
|
-
* Rotated every ~1.8s for a Claude Code–style feel.
|
|
14
|
-
*/
|
|
15
|
-
const THINKING_PHRASES = [
|
|
16
|
-
'Thinking',
|
|
17
|
-
'Pondering',
|
|
18
|
-
'Cooking',
|
|
19
|
-
'Reasoning',
|
|
20
|
-
'Crafting',
|
|
21
|
-
'Working',
|
|
22
|
-
'Brewing',
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
function useRotatingPhrase(active: boolean): string {
|
|
26
|
-
const [idx, setIdx] = useState(0);
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (!active) return;
|
|
29
|
-
const t = setInterval(() => setIdx((i) => (i + 1) % THINKING_PHRASES.length), 1800);
|
|
30
|
-
return () => clearInterval(t);
|
|
31
|
-
}, [active]);
|
|
32
|
-
return THINKING_PHRASES[idx];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function useBlinkingCursor(active: boolean): boolean {
|
|
36
|
-
const [on, setOn] = useState(true);
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (!active) return;
|
|
39
|
-
const t = setInterval(() => setOn((v) => !v), 450);
|
|
40
|
-
return () => clearInterval(t);
|
|
41
|
-
}, [active]);
|
|
42
|
-
return on;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function Message({ message }: MessageProps): React.ReactElement {
|
|
46
|
-
const isUser = message.role === 'user';
|
|
47
|
-
const dotColor = isUser ? 'cyan' : message.error ? 'red' : 'green';
|
|
48
|
-
const labelColor = message.error ? 'red' : undefined;
|
|
49
|
-
const label = isUser ? 'You' : 'Plazbot';
|
|
50
|
-
const streamingNoContent = !!message.streaming && !message.content && !isUser;
|
|
51
|
-
const streamingWithContent = !!message.streaming && !!message.content && !isUser;
|
|
52
|
-
|
|
53
|
-
const phrase = useRotatingPhrase(streamingNoContent);
|
|
54
|
-
const cursorOn = useBlinkingCursor(streamingWithContent);
|
|
55
|
-
|
|
56
|
-
const body = useMemo(() => {
|
|
57
|
-
if (!message.content) return '';
|
|
58
|
-
if (isUser) return message.content;
|
|
59
|
-
return renderMarkdown(message.content);
|
|
60
|
-
}, [message.content, isUser]);
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
64
|
-
<Box>
|
|
65
|
-
<Text color={dotColor} bold>● </Text>
|
|
66
|
-
<Text bold color={labelColor}>{label}</Text>
|
|
67
|
-
{streamingNoContent ? (
|
|
68
|
-
<>
|
|
69
|
-
<Text dimColor> </Text>
|
|
70
|
-
<Text color="yellow"><Spinner type="dots" /></Text>
|
|
71
|
-
<Text dimColor> {phrase}…</Text>
|
|
72
|
-
</>
|
|
73
|
-
) : null}
|
|
74
|
-
</Box>
|
|
75
|
-
{body || streamingWithContent ? (
|
|
76
|
-
<Box marginLeft={2}>
|
|
77
|
-
<Text>
|
|
78
|
-
{body}
|
|
79
|
-
{streamingWithContent ? (
|
|
80
|
-
<Text color="green" dimColor>{cursorOn ? '▍' : ' '}</Text>
|
|
81
|
-
) : null}
|
|
82
|
-
</Text>
|
|
83
|
-
</Box>
|
|
84
|
-
) : null}
|
|
85
|
-
</Box>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
const HINTS = [
|
|
5
|
-
{ title: 'List my agents', cmd: '/agents' },
|
|
6
|
-
{ title: 'Load an agent', cmd: '/load <id>' },
|
|
7
|
-
{ title: 'Diagnose the current agent', cmd: '/diagnose' },
|
|
8
|
-
{ title: 'Create a new agent', cmd: 'Create a sales agent for WhatsApp' },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
export function Suggestions(): React.ReactElement {
|
|
12
|
-
return (
|
|
13
|
-
<Box flexDirection="column" marginY={1} paddingX={1}>
|
|
14
|
-
<Text dimColor>Try one of these to get started:</Text>
|
|
15
|
-
<Box flexDirection="column" marginTop={1}>
|
|
16
|
-
{HINTS.map((h) => (
|
|
17
|
-
<Box key={h.title}>
|
|
18
|
-
<Text color="green"> ▸ </Text>
|
|
19
|
-
<Text bold>{h.title}</Text>
|
|
20
|
-
<Text dimColor> {h.cmd}</Text>
|
|
21
|
-
</Box>
|
|
22
|
-
))}
|
|
23
|
-
</Box>
|
|
24
|
-
</Box>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import Spinner from 'ink-spinner';
|
|
4
|
-
import type { ToolStep } from '../state/store.js';
|
|
5
|
-
import { stepLabel } from '../render/steps.js';
|
|
6
|
-
|
|
7
|
-
interface ToolCallProps {
|
|
8
|
-
step: ToolStep;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Some tool errors are expected control flow (e.g. the model asks to load
|
|
13
|
-
* an agent that doesn't exist; the backend returns "not found"). The model
|
|
14
|
-
* always explains these in plain text afterwards, so showing a loud red ✗
|
|
15
|
-
* with a duplicated error string is just visual noise. We detect those
|
|
16
|
-
* messages and render them as a soft dim line instead.
|
|
17
|
-
*/
|
|
18
|
-
function isSoftFailure(msg?: string): boolean {
|
|
19
|
-
if (!msg) return false;
|
|
20
|
-
const m = msg.toLowerCase();
|
|
21
|
-
return (
|
|
22
|
-
m.includes('no encontrado') ||
|
|
23
|
-
m.includes('no existe') ||
|
|
24
|
-
m.includes('not found') ||
|
|
25
|
-
m.includes("doesn't exist") ||
|
|
26
|
-
m.includes('does not exist')
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function ToolCall({ step }: ToolCallProps): React.ReactElement {
|
|
31
|
-
const label = stepLabel(step.toolName, step.status);
|
|
32
|
-
if (step.status === 'running') {
|
|
33
|
-
return (
|
|
34
|
-
<Box marginLeft={2}>
|
|
35
|
-
<Text color="yellow"><Spinner type="dots" /></Text>
|
|
36
|
-
<Text> </Text>
|
|
37
|
-
<Text dimColor>{label}…</Text>
|
|
38
|
-
</Box>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
if (step.status === 'success') {
|
|
42
|
-
return (
|
|
43
|
-
<Box marginLeft={2}>
|
|
44
|
-
<Text color="green">✓</Text>
|
|
45
|
-
<Text dimColor> {label}</Text>
|
|
46
|
-
</Box>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
// error
|
|
50
|
-
const soft = isSoftFailure(step.errorMessage);
|
|
51
|
-
return (
|
|
52
|
-
<Box marginLeft={2}>
|
|
53
|
-
<Text color={soft ? 'gray' : 'red'}>{soft ? '○' : '✗'}</Text>
|
|
54
|
-
<Text color={soft ? undefined : 'red'} dimColor={soft}> {label}</Text>
|
|
55
|
-
{step.errorMessage ? <Text dimColor> — {step.errorMessage}</Text> : null}
|
|
56
|
-
</Box>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { getBaseUrl } from '../api/studioApi.js';
|
|
2
|
-
import type { StudioStreamOptions } from '../api/types.js';
|
|
3
|
-
import { platformCodeForChannel, type ConnectChannel, type WorkspaceIntegrationItem } from './types.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Helpers REST para el polling/activacion durante la conexion de un canal.
|
|
7
|
-
* Reusa el mismo base URL + Bearer del 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} — el backend a veces devuelve un array
|
|
35
|
-
* (el front usa `[0]`). Devuelve el primer elemento o 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 }
|
|
60
|
-
*
|
|
61
|
-
* `platformCode` se deriva del canal logico (whatsapp/instagram/facebook).
|
|
62
|
-
*/
|
|
63
|
-
export async function activateIntegration(
|
|
64
|
-
opts: WorkspaceFetchOpts,
|
|
65
|
-
channel: ConnectChannel,
|
|
66
|
-
integrationId: string,
|
|
67
|
-
signal?: AbortSignal,
|
|
68
|
-
): Promise<ActivateResult> {
|
|
69
|
-
const url = `${getBaseUrl(opts.zone, opts.dev)}/api/workspace/${encodeURIComponent(opts.workspaceId)}/integrations/${encodeURIComponent(integrationId)}/activate`;
|
|
70
|
-
const res = await fetch(url, {
|
|
71
|
-
method: 'POST',
|
|
72
|
-
headers: headers(opts),
|
|
73
|
-
body: JSON.stringify({ isActive: true, platformCode: platformCodeForChannel(channel) }),
|
|
74
|
-
signal,
|
|
75
|
-
});
|
|
76
|
-
const text = await res.text();
|
|
77
|
-
let parsed: ActivateResult = {};
|
|
78
|
-
if (text) {
|
|
79
|
-
try { parsed = JSON.parse(text) as ActivateResult; } catch { /* not json */ }
|
|
80
|
-
}
|
|
81
|
-
if (!res.ok) {
|
|
82
|
-
return { success: false, message: parsed.message || `HTTP ${res.status} ${res.statusText}` };
|
|
83
|
-
}
|
|
84
|
-
return parsed.success === false ? parsed : { success: true, ...parsed };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export type WorkspaceAuthOpts = WorkspaceFetchOpts;
|
|
88
|
-
|
|
89
|
-
/** Adapter: deriva las opciones de fetch desde un StudioStreamOptions. */
|
|
90
|
-
export function workspaceOptsFromStream(stream: StudioStreamOptions): WorkspaceAuthOpts {
|
|
91
|
-
return {
|
|
92
|
-
apiKey: stream.apiKey,
|
|
93
|
-
workspaceId: stream.workspaceId,
|
|
94
|
-
zone: stream.zone,
|
|
95
|
-
dev: stream.dev,
|
|
96
|
-
userId: stream.userId,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { activateIntegration, getWorkspaceById, type WorkspaceAuthOpts } from './api.js';
|
|
2
|
-
import {
|
|
3
|
-
CONNECT_POLL_INTERVAL_MS,
|
|
4
|
-
CONNECT_POLL_MAX_ATTEMPTS,
|
|
5
|
-
describeIntegration,
|
|
6
|
-
findNewIntegrationForChannel,
|
|
7
|
-
type ChannelLinkData,
|
|
8
|
-
type ConnectChannel,
|
|
9
|
-
} from './types.js';
|
|
10
|
-
|
|
11
|
-
export interface PollingCallbacks {
|
|
12
|
-
onAttempt(attempt: number): void;
|
|
13
|
-
onActivating(integrationId: string, label: string): void;
|
|
14
|
-
onConnected(label: string): void;
|
|
15
|
-
onTimeout(): void;
|
|
16
|
-
onError(message: string): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface PollingHandle {
|
|
20
|
-
cancel(): void;
|
|
21
|
-
/** True una vez que el loop se detuvo (success, timeout, error o cancel). */
|
|
22
|
-
readonly stopped: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Inicia el polling de conexion de canal. Devuelve un handle para cancelarlo.
|
|
27
|
-
* El loop:
|
|
28
|
-
* 1. GET workspace cada CONNECT_POLL_INTERVAL_MS hasta CONNECT_POLL_MAX_ATTEMPTS.
|
|
29
|
-
* 2. Busca una integracion del `channel` cuyo id NO este en `existingIntegrationIds`.
|
|
30
|
-
* 3. Cuando la encuentra, POST .../activate y reporta el resultado.
|
|
31
|
-
*/
|
|
32
|
-
export function startConnectPolling(
|
|
33
|
-
opts: WorkspaceAuthOpts,
|
|
34
|
-
channel: ConnectChannel,
|
|
35
|
-
linkData: ChannelLinkData,
|
|
36
|
-
cb: PollingCallbacks,
|
|
37
|
-
): PollingHandle {
|
|
38
|
-
const knownIds = new Set(linkData.existingIntegrationIds ?? []);
|
|
39
|
-
const controller = new AbortController();
|
|
40
|
-
let attempts = 0;
|
|
41
|
-
let busy = false;
|
|
42
|
-
let stopped = false;
|
|
43
|
-
let timer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
-
|
|
45
|
-
const stop = () => {
|
|
46
|
-
stopped = true;
|
|
47
|
-
if (timer) { clearInterval(timer); timer = null; }
|
|
48
|
-
controller.abort();
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const tick = async () => {
|
|
52
|
-
if (busy || stopped) return;
|
|
53
|
-
busy = true;
|
|
54
|
-
attempts++;
|
|
55
|
-
cb.onAttempt(attempts);
|
|
56
|
-
try {
|
|
57
|
-
const ws = await getWorkspaceById(opts, controller.signal);
|
|
58
|
-
const newIntegration = findNewIntegrationForChannel(channel, ws?.integrations, knownIds);
|
|
59
|
-
if (newIntegration?.id) {
|
|
60
|
-
const label = describeIntegration(channel, newIntegration);
|
|
61
|
-
cb.onActivating(newIntegration.id, label);
|
|
62
|
-
if (timer) { clearInterval(timer); timer = null; }
|
|
63
|
-
const res = await activateIntegration(opts, channel, newIntegration.id, controller.signal);
|
|
64
|
-
if (res.success !== false) {
|
|
65
|
-
cb.onConnected(label);
|
|
66
|
-
} else {
|
|
67
|
-
cb.onError(res.message || 'Could not activate the integration.');
|
|
68
|
-
}
|
|
69
|
-
stopped = true;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
} catch (err) {
|
|
73
|
-
if ((err as Error).name === 'AbortError') return;
|
|
74
|
-
// Network transient: swallow and keep polling.
|
|
75
|
-
} finally {
|
|
76
|
-
busy = false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (attempts >= CONNECT_POLL_MAX_ATTEMPTS) {
|
|
80
|
-
if (timer) { clearInterval(timer); timer = null; }
|
|
81
|
-
stopped = true;
|
|
82
|
-
cb.onTimeout();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
timer = setInterval(() => { void tick(); }, CONNECT_POLL_INTERVAL_MS);
|
|
87
|
-
// Primer attempt inmediato para feedback visual.
|
|
88
|
-
void tick();
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
cancel() { stop(); },
|
|
92
|
-
get stopped() { return stopped; },
|
|
93
|
-
};
|
|
94
|
-
}
|