plazbot-cli 0.2.26 → 0.3.2
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 +86 -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 +22 -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 +93 -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,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construye la URL base según zona / modo dev.
|
|
3
|
+
* LA -> https://api.plazbot.com
|
|
4
|
+
* EU -> https://apieu.plazbot.com
|
|
5
|
+
* dev -> http://localhost:5090
|
|
6
|
+
*/
|
|
7
|
+
export function getBaseUrl(zone, dev) {
|
|
8
|
+
if (dev)
|
|
9
|
+
return 'http://localhost:5090';
|
|
10
|
+
return zone === 'EU' ? 'https://apieu.plazbot.com' : 'https://api.plazbot.com';
|
|
11
|
+
}
|
|
12
|
+
export function buildStudioUrl(zone, dev) {
|
|
13
|
+
return `${getBaseUrl(zone, dev)}/api/agent/studio`;
|
|
14
|
+
}
|
|
15
|
+
export function buildStudioHeaders(opts) {
|
|
16
|
+
const headers = {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
Accept: 'text/event-stream',
|
|
19
|
+
Authorization: `Bearer ${opts.apiKey}`,
|
|
20
|
+
'x-workspace-id': opts.workspaceId,
|
|
21
|
+
};
|
|
22
|
+
if (opts.userId)
|
|
23
|
+
headers['x-user-id'] = opts.userId;
|
|
24
|
+
return headers;
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de los chunks que emite el endpoint POST /api/agent/studio (SSE).
|
|
3
|
+
* Espejo del modelo del backend AgentStudioModels.cs.
|
|
4
|
+
*/
|
|
5
|
+
export class StudioHttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
statusText;
|
|
8
|
+
body;
|
|
9
|
+
constructor(status, statusText, body) {
|
|
10
|
+
super(`HTTP ${status} ${statusText}`);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.statusText = statusText;
|
|
13
|
+
this.body = body;
|
|
14
|
+
this.name = 'StudioHttpError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useStudioStore } from '../state/store.js';
|
|
4
|
+
import { JsonView, safeStringify } from '../render/json.js';
|
|
5
|
+
export function AgentPanel() {
|
|
6
|
+
const open = useStudioStore((s) => s.panelOpen);
|
|
7
|
+
const tab = useStudioStore((s) => s.panelTab);
|
|
8
|
+
const cfg = useStudioStore((s) => s.agentConfig);
|
|
9
|
+
if (!open)
|
|
10
|
+
return null;
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "green", children: "Agent panel" }) }), _jsxs(Box, { children: [_jsx(Tab, { active: tab === 'preview', children: "preview" }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Tab, { active: tab === 'json', children: "json" }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Tab, { active: tab === 'diagnose', children: "diagnose" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: renderTab(tab, cfg) })] }));
|
|
12
|
+
}
|
|
13
|
+
function Tab({ active, children }) {
|
|
14
|
+
return active ? _jsx(Text, { color: "cyan", bold: true, children: children }) : _jsx(Text, { dimColor: true, children: children });
|
|
15
|
+
}
|
|
16
|
+
function renderTab(tab, cfg) {
|
|
17
|
+
if (!cfg) {
|
|
18
|
+
return _jsx(Text, { dimColor: true, children: "No agent loaded yet." });
|
|
19
|
+
}
|
|
20
|
+
if (tab === 'json') {
|
|
21
|
+
return _jsx(JsonView, { value: cfg });
|
|
22
|
+
}
|
|
23
|
+
if (tab === 'diagnose') {
|
|
24
|
+
return _jsx(Text, { dimColor: true, children: "Run `/diagnose` to analyze the current agent." });
|
|
25
|
+
}
|
|
26
|
+
// preview: key agent fields
|
|
27
|
+
const c = cfg;
|
|
28
|
+
const name = c?.name ?? '(unnamed)';
|
|
29
|
+
const description = c?.description ?? '';
|
|
30
|
+
const model = c?.model ?? c?.modelId ?? '';
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Name: " }), _jsx(Text, { children: name })] }), model ? _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Model: " }), _jsx(Text, { children: model })] }) : null, description ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Description:" }), _jsx(Text, { children: description })] })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: truncate(safeStringify(c), 400) }) })] }));
|
|
32
|
+
}
|
|
33
|
+
function truncate(s, n) {
|
|
34
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
35
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { Box, useApp, useInput } from 'ink';
|
|
4
|
+
import { Header } from './Header.js';
|
|
5
|
+
import { Footer } from './Footer.js';
|
|
6
|
+
import { ChatLog } from './ChatLog.js';
|
|
7
|
+
import { Input } from './Input.js';
|
|
8
|
+
import { Suggestions } from './Suggestions.js';
|
|
9
|
+
import { useStudioStore, selectBackendMessages } from '../state/store.js';
|
|
10
|
+
import { parseSlash } from '../slash/parser.js';
|
|
11
|
+
import '../slash/handlers.js'; // side effect: registra slashes
|
|
12
|
+
import { slashRegistry } from '../slash/registry.js';
|
|
13
|
+
import { streamStudio } from '../api/sseClient.js';
|
|
14
|
+
import { StudioHttpError } from '../api/types.js';
|
|
15
|
+
import { startWhatsappPolling } from '../whatsapp/polling.js';
|
|
16
|
+
import { workspaceOptsFromStream } from '../whatsapp/api.js';
|
|
17
|
+
export function App({ version, stream, initialAgentId, dev, supportMode }) {
|
|
18
|
+
const { exit } = useApp();
|
|
19
|
+
const messages = useStudioStore((s) => s.messages);
|
|
20
|
+
const usage = useStudioStore((s) => s.usage);
|
|
21
|
+
const streaming = useStudioStore((s) => s.streaming);
|
|
22
|
+
const agentId = useStudioStore((s) => s.agentId);
|
|
23
|
+
// Active WhatsApp polling handle (cancelled on Esc / unmount / new stream).
|
|
24
|
+
const waPollingRef = useRef(null);
|
|
25
|
+
const streamingRef = useRef(streaming);
|
|
26
|
+
useEffect(() => { streamingRef.current = streaming; }, [streaming]);
|
|
27
|
+
// Ref so the polling onConnected callback can trigger sendToBackend from the
|
|
28
|
+
// most recent closure without resubscribing every render.
|
|
29
|
+
const sendBackendRef = useRef(async () => { });
|
|
30
|
+
const stopWaPolling = useCallback(() => {
|
|
31
|
+
if (waPollingRef.current) {
|
|
32
|
+
waPollingRef.current.cancel();
|
|
33
|
+
waPollingRef.current = null;
|
|
34
|
+
}
|
|
35
|
+
}, []);
|
|
36
|
+
// Cargar agente inicial si se pasó por flag.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (initialAgentId) {
|
|
39
|
+
useStudioStore.getState().setAgentId(initialAgentId);
|
|
40
|
+
}
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, []);
|
|
43
|
+
// Cleanup on unmount.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
return () => { stopWaPolling(); };
|
|
46
|
+
}, [stopWaPolling]);
|
|
47
|
+
// Atajos de teclado globales.
|
|
48
|
+
useInput((input, key) => {
|
|
49
|
+
if (key.escape) {
|
|
50
|
+
const wa = useStudioStore.getState().waConnect;
|
|
51
|
+
if (wa && (wa.status === 'waiting' || wa.status === 'activating')) {
|
|
52
|
+
stopWaPolling();
|
|
53
|
+
useStudioStore.getState().setWaStatus('cancelled');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
useStudioStore.getState().abortStream();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (key.ctrl && input === 'c') {
|
|
60
|
+
stopWaPolling();
|
|
61
|
+
useStudioStore.getState().abortStream();
|
|
62
|
+
exit();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const sendToBackend = useCallback(async (userText) => {
|
|
67
|
+
const store = useStudioStore.getState();
|
|
68
|
+
// Snapshot del historial ANTES de agregar el nuevo user message, igual que
|
|
69
|
+
// el front (AgentStudioModal.tsx:504). Si se calcula después, el último
|
|
70
|
+
// user input queda duplicado (en `message` y en `messages`).
|
|
71
|
+
const historyForApi = selectBackendMessages();
|
|
72
|
+
store.pushUserMessage(userText);
|
|
73
|
+
const assistantId = store.startAssistantMessage();
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
store.setAbortController(controller);
|
|
76
|
+
store.setStreaming(true);
|
|
77
|
+
const onChunk = (chunk) => {
|
|
78
|
+
const s = useStudioStore.getState();
|
|
79
|
+
switch (chunk.type) {
|
|
80
|
+
case 'text':
|
|
81
|
+
s.appendAssistantText(assistantId, chunk.content ?? '');
|
|
82
|
+
break;
|
|
83
|
+
case 'tool_call':
|
|
84
|
+
s.startStep(chunk.tool_name, assistantId);
|
|
85
|
+
break;
|
|
86
|
+
case 'tool_result': {
|
|
87
|
+
const tr = chunk.tool_result;
|
|
88
|
+
const success = tr?.success !== false && !tr?.error;
|
|
89
|
+
s.resolveLastStep(chunk.tool_name, success ? 'success' : 'error', tr?.data, tr?.error);
|
|
90
|
+
// Stash the agent config in state (used as `agentConfig` context on
|
|
91
|
+
// the next request) and remember its id. We no longer render it.
|
|
92
|
+
const data = tr?.data;
|
|
93
|
+
if (data) {
|
|
94
|
+
const cfg = data.config ?? data.agentConfig ?? data.agent ?? null;
|
|
95
|
+
if (cfg && typeof cfg === 'object') {
|
|
96
|
+
s.setAgentConfig(cfg);
|
|
97
|
+
const idCandidate = cfg.id;
|
|
98
|
+
if (typeof idCandidate === 'string')
|
|
99
|
+
s.setAgentId(idCandidate);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// WhatsApp connect tool: spin up the card + polling.
|
|
103
|
+
if (tr?.type === 'whatsapp_link' && data) {
|
|
104
|
+
const linkData = data;
|
|
105
|
+
// No link to show (e.g. alreadyConnected without a new url) → skip card.
|
|
106
|
+
if (linkData.shortUrl) {
|
|
107
|
+
// Cancel any previous active polling before starting a new one.
|
|
108
|
+
stopWaPolling();
|
|
109
|
+
s.startWaConnect(linkData, assistantId);
|
|
110
|
+
waPollingRef.current = startWhatsappPolling(workspaceOptsFromStream(stream), linkData, {
|
|
111
|
+
onAttempt(attempt) {
|
|
112
|
+
useStudioStore.getState().updateWaConnect({ attempt });
|
|
113
|
+
},
|
|
114
|
+
onActivating() {
|
|
115
|
+
useStudioStore.getState().setWaStatus('activating');
|
|
116
|
+
},
|
|
117
|
+
onConnected(number) {
|
|
118
|
+
useStudioStore.getState().setWaStatus('connected', { connectedNumber: number });
|
|
119
|
+
waPollingRef.current = null;
|
|
120
|
+
if (!streamingRef.current) {
|
|
121
|
+
const followUp = `My WhatsApp is now connected. Number: ${number || '(no number)'}. ` +
|
|
122
|
+
`Continue with the agent setup.`;
|
|
123
|
+
void sendBackendRef.current(followUp);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
onTimeout() {
|
|
127
|
+
useStudioStore.getState().setWaStatus('timeout');
|
|
128
|
+
waPollingRef.current = null;
|
|
129
|
+
},
|
|
130
|
+
onError(message) {
|
|
131
|
+
useStudioStore.getState().setWaStatus('error', { errorMessage: message });
|
|
132
|
+
waPollingRef.current = null;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'usage':
|
|
140
|
+
s.addUsage(chunk.input_tokens ?? 0, chunk.output_tokens ?? 0);
|
|
141
|
+
break;
|
|
142
|
+
case 'error':
|
|
143
|
+
s.failAllRunningSteps(chunk.error);
|
|
144
|
+
s.pushSyntheticAssistant(`✖ ${chunk.error}`, { error: true });
|
|
145
|
+
break;
|
|
146
|
+
case 'done':
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
await streamStudio(stream, {
|
|
152
|
+
message: userText,
|
|
153
|
+
messages: historyForApi,
|
|
154
|
+
agentId: useStudioStore.getState().agentId,
|
|
155
|
+
agentConfig: useStudioStore.getState().agentConfig,
|
|
156
|
+
}, onChunk, controller.signal);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const msg = formatError(err, dev);
|
|
160
|
+
useStudioStore.getState().failAllRunningSteps('Network error');
|
|
161
|
+
useStudioStore.getState().pushSyntheticAssistant(msg, { error: true });
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
useStudioStore.getState().finishAssistantMessage(assistantId);
|
|
165
|
+
useStudioStore.getState().setStreaming(false);
|
|
166
|
+
useStudioStore.getState().setAbortController(null);
|
|
167
|
+
// /compact flow: replace history with the assistant summary just produced.
|
|
168
|
+
if (useStudioStore.getState().pendingCompact) {
|
|
169
|
+
useStudioStore.getState().compactInto(assistantId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}, [stream, dev, stopWaPolling]);
|
|
173
|
+
useEffect(() => { sendBackendRef.current = sendToBackend; }, [sendToBackend]);
|
|
174
|
+
const handleSubmit = useCallback(async (raw) => {
|
|
175
|
+
const parsed = parseSlash(raw);
|
|
176
|
+
if (parsed) {
|
|
177
|
+
const spec = slashRegistry.get(parsed.name);
|
|
178
|
+
if (!spec) {
|
|
179
|
+
useStudioStore.getState().pushSyntheticAssistant(`Unknown command: \`/${parsed.name}\`. Run \`/help\` to see the list.`, { error: true });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const res = await spec.handler(parsed, { exit });
|
|
183
|
+
if (res.kind === 'message') {
|
|
184
|
+
await sendToBackend(res.content);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await sendToBackend(raw);
|
|
189
|
+
}, [sendToBackend, exit]);
|
|
190
|
+
const empty = messages.length === 0;
|
|
191
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { version: version, workspace: stream.workspaceId, zone: stream.zone, agentId: agentId, dev: dev, supportMode: !!supportMode }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: empty ? _jsx(Suggestions, {}) : _jsx(ChatLog, {}) }), _jsx(Input, { disabled: streaming, onSubmit: handleSubmit }), _jsx(Footer, { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, streaming: streaming })] }));
|
|
192
|
+
}
|
|
193
|
+
function formatError(err, dev) {
|
|
194
|
+
if (err instanceof StudioHttpError) {
|
|
195
|
+
if (err.status === 401)
|
|
196
|
+
return '✖ Token expired or invalid. Run `plazbot init` with a valid JWT.';
|
|
197
|
+
if (err.status === 403)
|
|
198
|
+
return '✖ No permission for this workspace.';
|
|
199
|
+
if (err.status === 429)
|
|
200
|
+
return '✖ Rate limit reached. Wait a few seconds and try again.';
|
|
201
|
+
if (err.status >= 500) {
|
|
202
|
+
return dev
|
|
203
|
+
? `✖ Backend error ${err.status}.\n\`\`\`\n${err.body ?? err.statusText}\n\`\`\``
|
|
204
|
+
: `✖ The backend returned error ${err.status}. Retry in a few seconds.`;
|
|
205
|
+
}
|
|
206
|
+
return `✖ HTTP ${err.status} ${err.statusText}`;
|
|
207
|
+
}
|
|
208
|
+
if (err instanceof Error) {
|
|
209
|
+
if (err.name === 'AbortError')
|
|
210
|
+
return '✖ Stream cancelled.';
|
|
211
|
+
return `✖ ${err.message}`;
|
|
212
|
+
}
|
|
213
|
+
return '✖ Unknown error';
|
|
214
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Static } from 'ink';
|
|
4
|
+
import { useStudioStore } from '../state/store.js';
|
|
5
|
+
import { Message } from './Message.js';
|
|
6
|
+
import { ToolCall } from './ToolCall.js';
|
|
7
|
+
import { WhatsappConnectCard } from './WhatsappConnectCard.js';
|
|
8
|
+
/**
|
|
9
|
+
* Renders the conversation log.
|
|
10
|
+
*
|
|
11
|
+
* Stable items (finished assistant/user messages + resolved tool steps that
|
|
12
|
+
* are not anchored to an active WhatsApp card) are rendered inside <Static>
|
|
13
|
+
* so Ink writes them to scrollback once and never re-renders them on resize.
|
|
14
|
+
* This is what fixes the "Header echoed multiple times" effect you see when
|
|
15
|
+
* dragging the terminal window while the dynamic frame is taller than the
|
|
16
|
+
* viewport.
|
|
17
|
+
*
|
|
18
|
+
* The dynamic block at the bottom contains the streaming message, any steps
|
|
19
|
+
* still in `running`, and the WhatsApp connect card while it is active.
|
|
20
|
+
*/
|
|
21
|
+
export function ChatLog() {
|
|
22
|
+
const messages = useStudioStore((s) => s.messages);
|
|
23
|
+
const steps = useStudioStore((s) => s.steps);
|
|
24
|
+
const waConnect = useStudioStore((s) => s.waConnect);
|
|
25
|
+
// Group steps by their anchor message for quick lookup.
|
|
26
|
+
const stepsByMsg = new Map();
|
|
27
|
+
for (const step of steps) {
|
|
28
|
+
const arr = stepsByMsg.get(step.afterMessageId) ?? [];
|
|
29
|
+
arr.push(step);
|
|
30
|
+
stepsByMsg.set(step.afterMessageId, arr);
|
|
31
|
+
}
|
|
32
|
+
// Determine which message ids must stay in the dynamic frame:
|
|
33
|
+
// - the last message if it's still streaming
|
|
34
|
+
// - any message whose tool steps are still running
|
|
35
|
+
// - the message anchored to an active WhatsApp connect card
|
|
36
|
+
const dynamicMsgIds = new Set();
|
|
37
|
+
const last = messages[messages.length - 1];
|
|
38
|
+
if (last?.streaming)
|
|
39
|
+
dynamicMsgIds.add(last.id);
|
|
40
|
+
for (const step of steps) {
|
|
41
|
+
if (step.status === 'running')
|
|
42
|
+
dynamicMsgIds.add(step.afterMessageId);
|
|
43
|
+
}
|
|
44
|
+
if (waConnect)
|
|
45
|
+
dynamicMsgIds.add(waConnect.anchorMessageId);
|
|
46
|
+
// Build the static (scrollback) list. Each message and step gets a unique
|
|
47
|
+
// key so React/Ink only writes it once.
|
|
48
|
+
const staticItems = [];
|
|
49
|
+
for (const m of messages) {
|
|
50
|
+
if (dynamicMsgIds.has(m.id))
|
|
51
|
+
continue;
|
|
52
|
+
staticItems.push({ kind: 'message', key: `m-${m.id}`, message: m });
|
|
53
|
+
for (const s of stepsByMsg.get(m.id) ?? []) {
|
|
54
|
+
staticItems.push({ kind: 'step', key: `s-${s.id}`, step: s });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const dynamicMessages = messages.filter((m) => dynamicMsgIds.has(m.id));
|
|
58
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: staticItems, children: (item) => item.kind === 'message' ? (_jsx(Message, { message: item.message }, item.key)) : (_jsx(ToolCall, { step: item.step }, item.key)) }), _jsxs(Box, { flexDirection: "column", children: [dynamicMessages.map((m) => (_jsxs(React.Fragment, { children: [_jsx(Message, { message: m }), (stepsByMsg.get(m.id) ?? []).map((s) => (_jsx(ToolCall, { step: s }, s.id))), waConnect && waConnect.anchorMessageId === m.id ? (_jsx(WhatsappConnectCard, { state: waConnect })) : null] }, m.id))), waConnect && !messages.some((m) => m.id === waConnect.anchorMessageId) ? (_jsx(WhatsappConnectCard, { state: waConnect })) : null] })] }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
function fmt(n) {
|
|
5
|
+
if (n >= 1000)
|
|
6
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
7
|
+
return String(n);
|
|
8
|
+
}
|
|
9
|
+
export function Footer({ inputTokens, outputTokens, streaming }) {
|
|
10
|
+
return (_jsxs(Box, { paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "tokens: " }), _jsx(Text, { children: fmt(inputTokens) }), _jsx(Text, { dimColor: true, children: " in / " }), _jsx(Text, { children: fmt(outputTokens) }), _jsx(Text, { dimColor: true, children: " out" }), streaming ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "yellow", children: " streaming\u2026" })] })) : null] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "/help \u00B7 Esc cancel \u00B7 Ctrl+C quit" }) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function Header({ version, workspace, zone, agentId, dev, supportMode }) {
|
|
4
|
+
// Sin borde: cualquier `borderStyle` se acumula visualmente cuando el usuario
|
|
5
|
+
// redimensiona la terminal (Ink no limpia el scrollback). Estilo Claude Code:
|
|
6
|
+
// una sola línea con título a la izquierda y metadata a la derecha.
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: supportMode ? 'yellow' : 'green', children: "\u25CF Plazbot Studio" }), _jsxs(Text, { dimColor: true, children: [" v", version] }), dev ? _jsx(Text, { color: "yellow", children: " dev" }) : null, supportMode ? _jsx(Text, { color: "yellow", bold: true, children: " \u00B7 SUPPORT" }) : null] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "workspace " }), _jsx(Text, { color: supportMode ? 'yellow' : undefined, children: workspace }), _jsx(Text, { dimColor: true, children: " zone " }), _jsx(Text, { children: zone }), agentId ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " agent " }), _jsx(Text, { color: "cyan", children: agentId.slice(0, 12) })] })) : null] })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
export function Input({ disabled, onSubmit }) {
|
|
6
|
+
const [value, setValue] = useState('');
|
|
7
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: disabled ? 'gray' : 'green', paddingX: 1, children: [_jsxs(Text, { color: disabled ? 'gray' : 'green', bold: true, children: [disabled ? '·' : '›', ' '] }), _jsx(TextInput, { value: value, onChange: disabled ? () => { } : setValue, onSubmit: (v) => {
|
|
8
|
+
if (disabled)
|
|
9
|
+
return;
|
|
10
|
+
if (!v.trim())
|
|
11
|
+
return;
|
|
12
|
+
setValue('');
|
|
13
|
+
onSubmit(v);
|
|
14
|
+
}, placeholder: disabled ? 'Waiting for response… (press Esc to cancel)' : 'Type a message or /help', showCursor: !disabled })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { renderMarkdown } from '../render/markdown.js';
|
|
6
|
+
/**
|
|
7
|
+
* Phrases shown next to the spinner while waiting for the first text token.
|
|
8
|
+
* Rotated every ~1.8s for a Claude Code–style feel.
|
|
9
|
+
*/
|
|
10
|
+
const THINKING_PHRASES = [
|
|
11
|
+
'Thinking',
|
|
12
|
+
'Pondering',
|
|
13
|
+
'Cooking',
|
|
14
|
+
'Reasoning',
|
|
15
|
+
'Crafting',
|
|
16
|
+
'Working',
|
|
17
|
+
'Brewing',
|
|
18
|
+
];
|
|
19
|
+
function useRotatingPhrase(active) {
|
|
20
|
+
const [idx, setIdx] = useState(0);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!active)
|
|
23
|
+
return;
|
|
24
|
+
const t = setInterval(() => setIdx((i) => (i + 1) % THINKING_PHRASES.length), 1800);
|
|
25
|
+
return () => clearInterval(t);
|
|
26
|
+
}, [active]);
|
|
27
|
+
return THINKING_PHRASES[idx];
|
|
28
|
+
}
|
|
29
|
+
function useBlinkingCursor(active) {
|
|
30
|
+
const [on, setOn] = useState(true);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!active)
|
|
33
|
+
return;
|
|
34
|
+
const t = setInterval(() => setOn((v) => !v), 450);
|
|
35
|
+
return () => clearInterval(t);
|
|
36
|
+
}, [active]);
|
|
37
|
+
return on;
|
|
38
|
+
}
|
|
39
|
+
export function Message({ message }) {
|
|
40
|
+
const isUser = message.role === 'user';
|
|
41
|
+
const dotColor = isUser ? 'cyan' : message.error ? 'red' : 'green';
|
|
42
|
+
const labelColor = message.error ? 'red' : undefined;
|
|
43
|
+
const label = isUser ? 'You' : 'Plazbot';
|
|
44
|
+
const streamingNoContent = !!message.streaming && !message.content && !isUser;
|
|
45
|
+
const streamingWithContent = !!message.streaming && !!message.content && !isUser;
|
|
46
|
+
const phrase = useRotatingPhrase(streamingNoContent);
|
|
47
|
+
const cursorOn = useBlinkingCursor(streamingWithContent);
|
|
48
|
+
const body = useMemo(() => {
|
|
49
|
+
if (!message.content)
|
|
50
|
+
return '';
|
|
51
|
+
if (isUser)
|
|
52
|
+
return message.content;
|
|
53
|
+
return renderMarkdown(message.content);
|
|
54
|
+
}, [message.content, isUser]);
|
|
55
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, bold: true, children: "\u25CF " }), _jsx(Text, { bold: true, color: labelColor, children: label }), streamingNoContent ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { dimColor: true, children: [" ", phrase, "\u2026"] })] })) : null] }), body || streamingWithContent ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [body, streamingWithContent ? (_jsx(Text, { color: "green", dimColor: true, children: cursorOn ? '▍' : ' ' })) : null] }) })) : null] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const HINTS = [
|
|
4
|
+
{ title: 'List my agents', cmd: '/agents' },
|
|
5
|
+
{ title: 'Load an agent', cmd: '/load <id>' },
|
|
6
|
+
{ title: 'Diagnose the current agent', cmd: '/diagnose' },
|
|
7
|
+
{ title: 'Create a new agent', cmd: 'Create a sales agent for WhatsApp' },
|
|
8
|
+
];
|
|
9
|
+
export function Suggestions() {
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, children: [_jsx(Text, { dimColor: true, children: "Try one of these to get started:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: HINTS.map((h) => (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: " \u25B8 " }), _jsx(Text, { bold: true, children: h.title }), _jsxs(Text, { dimColor: true, children: [" ", h.cmd] })] }, h.title))) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { stepLabel } from '../render/steps.js';
|
|
5
|
+
/**
|
|
6
|
+
* Some tool errors are expected control flow (e.g. the model asks to load
|
|
7
|
+
* an agent that doesn't exist; the backend returns "not found"). The model
|
|
8
|
+
* always explains these in plain text afterwards, so showing a loud red ✗
|
|
9
|
+
* with a duplicated error string is just visual noise. We detect those
|
|
10
|
+
* messages and render them as a soft dim line instead.
|
|
11
|
+
*/
|
|
12
|
+
function isSoftFailure(msg) {
|
|
13
|
+
if (!msg)
|
|
14
|
+
return false;
|
|
15
|
+
const m = msg.toLowerCase();
|
|
16
|
+
return (m.includes('no encontrado') ||
|
|
17
|
+
m.includes('no existe') ||
|
|
18
|
+
m.includes('not found') ||
|
|
19
|
+
m.includes("doesn't exist") ||
|
|
20
|
+
m.includes('does not exist'));
|
|
21
|
+
}
|
|
22
|
+
export function ToolCall({ step }) {
|
|
23
|
+
const label = stepLabel(step.toolName, step.status);
|
|
24
|
+
if (step.status === 'running') {
|
|
25
|
+
return (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [label, "\u2026"] })] }));
|
|
26
|
+
}
|
|
27
|
+
if (step.status === 'success') {
|
|
28
|
+
return (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: "green", children: "\u2713" }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
|
|
29
|
+
}
|
|
30
|
+
// error
|
|
31
|
+
const soft = isSoftFailure(step.errorMessage);
|
|
32
|
+
return (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: soft ? 'gray' : 'red', children: soft ? '○' : '✗' }), _jsxs(Text, { color: soft ? undefined : 'red', dimColor: soft, children: [" ", label] }), step.errorMessage ? _jsxs(Text, { dimColor: true, children: [" \u2014 ", step.errorMessage] }) : null] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { WA_POLL_MAX_ATTEMPTS } from '../whatsapp/types.js';
|
|
5
|
+
function formatExpiry(iso) {
|
|
6
|
+
if (!iso)
|
|
7
|
+
return '';
|
|
8
|
+
const d = new Date(iso);
|
|
9
|
+
if (Number.isNaN(d.getTime()))
|
|
10
|
+
return '';
|
|
11
|
+
return d.toLocaleString('en-US', {
|
|
12
|
+
day: 'numeric',
|
|
13
|
+
month: 'short',
|
|
14
|
+
hour: '2-digit',
|
|
15
|
+
minute: '2-digit',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wraps a URL in an OSC 8 hyperlink escape sequence. Terminals that don't
|
|
20
|
+
* support it just print the visible text — but iTerm2, kitty, Wezterm,
|
|
21
|
+
* Alacritty (recent), and modern VS Code/Hyper terminals do.
|
|
22
|
+
*/
|
|
23
|
+
function osc8(url, label) {
|
|
24
|
+
return `\u001b]8;;${url}\u001b\\${label}\u001b]8;;\u001b\\`;
|
|
25
|
+
}
|
|
26
|
+
export function WhatsappConnectCard({ state }) {
|
|
27
|
+
const { linkData, status, connectedNumber, errorMessage, attempt } = state;
|
|
28
|
+
const shortUrl = linkData.shortUrl || '';
|
|
29
|
+
const expiry = formatExpiry(linkData.expiresAt);
|
|
30
|
+
const typeLabel = linkData.linkType === 'whatsapp_business' ? 'WhatsApp Business' : 'WhatsApp Cloud API';
|
|
31
|
+
// Header glyph + color per status.
|
|
32
|
+
let headerIcon = '●';
|
|
33
|
+
let headerColor = 'green';
|
|
34
|
+
let headerText = 'Connect your WhatsApp';
|
|
35
|
+
if (status === 'connected') {
|
|
36
|
+
headerIcon = '✓';
|
|
37
|
+
headerColor = 'green';
|
|
38
|
+
headerText = 'WhatsApp connected';
|
|
39
|
+
}
|
|
40
|
+
else if (status === 'error') {
|
|
41
|
+
headerIcon = '✖';
|
|
42
|
+
headerColor = 'red';
|
|
43
|
+
headerText = 'Could not activate the number';
|
|
44
|
+
}
|
|
45
|
+
else if (status === 'timeout') {
|
|
46
|
+
headerIcon = '○';
|
|
47
|
+
headerColor = 'yellow';
|
|
48
|
+
headerText = 'No connection detected';
|
|
49
|
+
}
|
|
50
|
+
else if (status === 'cancelled') {
|
|
51
|
+
headerIcon = '○';
|
|
52
|
+
headerColor = 'yellow';
|
|
53
|
+
headerText = 'Connection cancelled';
|
|
54
|
+
}
|
|
55
|
+
const showLink = status !== 'connected' && status !== 'cancelled' && !!shortUrl;
|
|
56
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === 'error' ? 'red' : status === 'connected' ? 'green' : 'gray', paddingX: 1, marginY: 1, marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: headerColor, children: [headerIcon, " "] }), _jsx(Text, { bold: true, children: headerText }), status !== 'connected' && status !== 'error' && status !== 'cancelled' ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { dimColor: true, children: ["\u00B7 ", typeLabel] })] })) : null] }), showLink ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "link: " }), _jsx(Text, { color: "cyan", underline: true, children: osc8(shortUrl, shortUrl) })] }), expiry ? (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: ["expires: ", expiry] }) })) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Open the link from a phone with WhatsApp installed. We'll keep watching for the new number." }) })] })) : null, _jsxs(Box, { marginTop: 1, children: [status === 'waiting' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " Waiting for connection\u2026 " }), _jsxs(Text, { dimColor: true, children: ["(", attempt, "/", WA_POLL_MAX_ATTEMPTS, ")"] }), _jsx(Text, { dimColor: true, children: " \u00B7 press Esc to cancel" })] })) : null, status === 'activating' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { dimColor: true, children: " Number detected, activating\u2026" })] })) : null, status === 'connected' ? (_jsxs(Text, { color: "green", children: ["Number ", connectedNumber || '(connected)', " activated successfully."] })) : null, status === 'timeout' ? (_jsx(Text, { dimColor: true, children: "We didn't detect the connection within 5 minutes. Ask the agent to try again." })) : null, status === 'cancelled' ? (_jsx(Text, { dimColor: true, children: "Polling cancelled by user." })) : null, status === 'error' ? (_jsx(Text, { color: "red", children: errorMessage || 'An error occurred while activating the integration.' })) : null] })] }));
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
export const studioCommand = new Command('studio')
|
|
3
|
+
.description('Interactive Plazbot Studio REPL (create, diagnose and manage AI agents)')
|
|
4
|
+
.option('--dev', 'Target the local backend (http://localhost:5090)', false)
|
|
5
|
+
.option('-a, --agent-id <id>', 'Preload an agent on startup')
|
|
6
|
+
.option('-w, --workspace-id <id>', 'Override the workspace (support / white-label partner mode)')
|
|
7
|
+
.option('-m, --message <text>', 'Run a one-shot prompt and exit')
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
if (opts.message) {
|
|
10
|
+
const { runOneShot } = await import('./runOneShot.js');
|
|
11
|
+
await runOneShot({
|
|
12
|
+
dev: opts.dev,
|
|
13
|
+
agentId: opts.agentId,
|
|
14
|
+
message: opts.message,
|
|
15
|
+
workspaceOverride: opts.workspaceId,
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const { runRepl } = await import('./runRepl.js');
|
|
20
|
+
await runRepl({
|
|
21
|
+
dev: opts.dev,
|
|
22
|
+
agentId: opts.agentId,
|
|
23
|
+
workspaceOverride: opts.workspaceId,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
studioCommand
|
|
27
|
+
.command('ask <message>')
|
|
28
|
+
.description('Run a one-shot studio query without opening the TUI')
|
|
29
|
+
.option('--dev', 'Target the local backend (http://localhost:5090)', false)
|
|
30
|
+
.option('-a, --agent-id <id>', 'Agent to use as context')
|
|
31
|
+
.option('-w, --workspace-id <id>', 'Override the workspace (support / white-label partner mode)')
|
|
32
|
+
.option('--json', 'Print SSE chunks as raw NDJSON', false)
|
|
33
|
+
.action(async (message, opts) => {
|
|
34
|
+
const { runOneShot } = await import('./runOneShot.js');
|
|
35
|
+
await runOneShot({
|
|
36
|
+
dev: opts.dev,
|
|
37
|
+
agentId: opts.agentId,
|
|
38
|
+
message,
|
|
39
|
+
json: opts.json,
|
|
40
|
+
workspaceOverride: opts.workspaceId,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import SyntaxHighlight from 'ink-syntax-highlight';
|
|
4
|
+
export function JsonView({ value, maxHeight }) {
|
|
5
|
+
const text = safeStringify(value);
|
|
6
|
+
return (_jsx(Box, { flexDirection: "column", height: maxHeight, children: _jsx(SyntaxHighlight, { code: text, language: "json" }) }));
|
|
7
|
+
}
|
|
8
|
+
function safeStringify(v) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(v, null, 2);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return String(v);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export { safeStringify };
|