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.
Files changed (178) hide show
  1. package/CLAUDE.md +34 -5
  2. package/README.md +21 -0
  3. package/dist/cli.js +32 -20
  4. package/dist/commands/agent/ai-config.js +98 -50
  5. package/dist/commands/agent/chat.js +80 -74
  6. package/dist/commands/agent/copy.js +23 -21
  7. package/dist/commands/agent/create.js +42 -72
  8. package/dist/commands/agent/delete.js +29 -30
  9. package/dist/commands/agent/enable-widget.js +30 -26
  10. package/dist/commands/agent/export.js +90 -77
  11. package/dist/commands/agent/files.js +68 -60
  12. package/dist/commands/agent/get.js +101 -87
  13. package/dist/commands/agent/index.js +53 -39
  14. package/dist/commands/agent/list.js +26 -24
  15. package/dist/commands/agent/monitor.js +91 -86
  16. package/dist/commands/agent/on-message.js +40 -37
  17. package/dist/commands/agent/set.js +62 -59
  18. package/dist/commands/agent/templates.js +109 -108
  19. package/dist/commands/agent/tools.js +64 -65
  20. package/dist/commands/agent/update.js +28 -27
  21. package/dist/commands/agent/validate.js +127 -0
  22. package/dist/commands/agent/wizard.js +152 -159
  23. package/dist/commands/auth/index.js +7 -10
  24. package/dist/commands/auth/login.js +50 -37
  25. package/dist/commands/auth/logout.js +16 -14
  26. package/dist/commands/auth/status.js +19 -16
  27. package/dist/commands/portal/add-agent.js +26 -24
  28. package/dist/commands/portal/add-link.js +21 -17
  29. package/dist/commands/portal/clear-links.js +17 -15
  30. package/dist/commands/portal/create.js +25 -21
  31. package/dist/commands/portal/delete.js +31 -30
  32. package/dist/commands/portal/get.js +33 -31
  33. package/dist/commands/portal/index.js +30 -22
  34. package/dist/commands/portal/list.js +34 -30
  35. package/dist/commands/portal/update.js +41 -33
  36. package/dist/commands/whatsapp/broadcast.js +40 -37
  37. package/dist/commands/whatsapp/channels.js +40 -34
  38. package/dist/commands/whatsapp/chat.js +33 -32
  39. package/dist/commands/whatsapp/connect.js +53 -52
  40. package/dist/commands/whatsapp/delete-webhook.js +19 -17
  41. package/dist/commands/whatsapp/index.js +35 -25
  42. package/dist/commands/whatsapp/register-webhook.js +21 -19
  43. package/dist/commands/whatsapp/send-template.js +39 -31
  44. package/dist/commands/whatsapp/send.js +27 -23
  45. package/dist/commands/whatsapp/widget.js +35 -31
  46. package/dist/commands/workers/deploy.js +49 -44
  47. package/dist/commands/workers/index.js +28 -18
  48. package/dist/commands/workers/list.js +43 -35
  49. package/dist/commands/workers/logs.js +38 -32
  50. package/dist/commands/workers/remove.js +38 -37
  51. package/dist/commands/workers/secret.js +63 -58
  52. package/dist/commands/workers/test.js +44 -36
  53. package/dist/schemas/agent.config.schema.json +569 -0
  54. package/dist/studio/api/sseClient.js +97 -0
  55. package/dist/studio/api/studioApi.js +25 -0
  56. package/dist/studio/api/types.js +16 -0
  57. package/dist/studio/components/AgentPanel.js +35 -0
  58. package/dist/studio/components/App.js +214 -0
  59. package/dist/studio/components/ChatLog.js +59 -0
  60. package/dist/studio/components/Footer.js +11 -0
  61. package/dist/studio/components/Header.js +8 -0
  62. package/dist/studio/components/Input.js +15 -0
  63. package/dist/studio/components/Message.js +56 -0
  64. package/dist/studio/components/Suggestions.js +11 -0
  65. package/dist/studio/components/ToolCall.js +33 -0
  66. package/dist/studio/components/WhatsappConnectCard.js +57 -0
  67. package/dist/studio/index.js +42 -0
  68. package/dist/studio/render/json.js +16 -0
  69. package/dist/studio/render/markdown.js +86 -0
  70. package/dist/studio/render/steps.js +58 -0
  71. package/dist/studio/runOneShot.js +96 -0
  72. package/dist/studio/runRepl.js +52 -0
  73. package/dist/studio/slash/handlers.js +199 -0
  74. package/dist/studio/slash/parser.js +46 -0
  75. package/dist/studio/slash/registry.js +16 -0
  76. package/dist/studio/state/store.js +181 -0
  77. package/dist/studio/whatsapp/api.js +63 -0
  78. package/dist/studio/whatsapp/polling.js +77 -0
  79. package/dist/studio/whatsapp/types.js +31 -0
  80. package/dist/types/agent.js +1 -2
  81. package/dist/types/auth.js +1 -2
  82. package/dist/types/common.js +1 -2
  83. package/dist/types/message.js +1 -2
  84. package/dist/types/portal.js +1 -2
  85. package/dist/types/workers.js +1 -2
  86. package/dist/utils/agent-errors.js +46 -0
  87. package/dist/utils/api.js +8 -9
  88. package/dist/utils/banner.js +33 -34
  89. package/dist/utils/credentials.js +12 -20
  90. package/dist/utils/help.js +44 -0
  91. package/dist/utils/logger.js +13 -19
  92. package/dist/utils/ui.js +35 -49
  93. package/package.json +22 -10
  94. package/src/cli.ts +24 -8
  95. package/src/commands/agent/ai-config.ts +89 -34
  96. package/src/commands/agent/chat.ts +49 -37
  97. package/src/commands/agent/copy.ts +19 -13
  98. package/src/commands/agent/create.ts +32 -22
  99. package/src/commands/agent/delete.ts +24 -18
  100. package/src/commands/agent/enable-widget.ts +31 -23
  101. package/src/commands/agent/export.ts +72 -51
  102. package/src/commands/agent/files.ts +51 -39
  103. package/src/commands/agent/get.ts +86 -66
  104. package/src/commands/agent/index.ts +36 -18
  105. package/src/commands/agent/list.ts +22 -16
  106. package/src/commands/agent/monitor.ts +67 -56
  107. package/src/commands/agent/on-message.ts +36 -27
  108. package/src/commands/agent/set.ts +47 -37
  109. package/src/commands/agent/templates.ts +90 -82
  110. package/src/commands/agent/tools.ts +53 -47
  111. package/src/commands/agent/update.ts +28 -20
  112. package/src/commands/agent/validate.ts +135 -0
  113. package/src/commands/agent/wizard.ts +114 -114
  114. package/src/commands/auth/index.ts +3 -3
  115. package/src/commands/auth/login.ts +44 -29
  116. package/src/commands/auth/logout.ts +16 -10
  117. package/src/commands/auth/status.ts +14 -8
  118. package/src/commands/portal/add-agent.ts +23 -17
  119. package/src/commands/portal/add-link.ts +17 -9
  120. package/src/commands/portal/clear-links.ts +13 -7
  121. package/src/commands/portal/create.ts +20 -12
  122. package/src/commands/portal/delete.ts +28 -20
  123. package/src/commands/portal/get.ts +25 -19
  124. package/src/commands/portal/index.ts +22 -10
  125. package/src/commands/portal/list.ts +27 -19
  126. package/src/commands/portal/update.ts +38 -26
  127. package/src/commands/whatsapp/broadcast.ts +28 -18
  128. package/src/commands/whatsapp/channels.ts +31 -20
  129. package/src/commands/whatsapp/chat.ts +20 -12
  130. package/src/commands/whatsapp/connect.ts +39 -31
  131. package/src/commands/whatsapp/delete-webhook.ts +15 -9
  132. package/src/commands/whatsapp/index.ts +24 -10
  133. package/src/commands/whatsapp/register-webhook.ts +16 -10
  134. package/src/commands/whatsapp/send-template.ts +33 -21
  135. package/src/commands/whatsapp/send.ts +23 -15
  136. package/src/commands/whatsapp/widget.ts +25 -17
  137. package/src/commands/workers/deploy.ts +34 -22
  138. package/src/commands/workers/index.ts +21 -7
  139. package/src/commands/workers/list.ts +31 -19
  140. package/src/commands/workers/logs.ts +30 -20
  141. package/src/commands/workers/remove.ts +30 -22
  142. package/src/commands/workers/secret.ts +46 -34
  143. package/src/commands/workers/test.ts +34 -22
  144. package/src/schemas/agent.config.schema.json +569 -0
  145. package/src/studio/api/sseClient.ts +91 -0
  146. package/src/studio/api/studioApi.ts +27 -0
  147. package/src/studio/api/types.ts +96 -0
  148. package/src/studio/components/App.tsx +266 -0
  149. package/src/studio/components/ChatLog.tsx +95 -0
  150. package/src/studio/components/Footer.tsx +38 -0
  151. package/src/studio/components/Header.tsx +39 -0
  152. package/src/studio/components/Input.tsx +32 -0
  153. package/src/studio/components/Message.tsx +87 -0
  154. package/src/studio/components/Suggestions.tsx +26 -0
  155. package/src/studio/components/ToolCall.tsx +58 -0
  156. package/src/studio/components/WhatsappConnectCard.tsx +139 -0
  157. package/src/studio/index.ts +58 -0
  158. package/src/studio/render/markdown.ts +93 -0
  159. package/src/studio/render/steps.ts +57 -0
  160. package/src/studio/runOneShot.ts +114 -0
  161. package/src/studio/runRepl.tsx +76 -0
  162. package/src/studio/slash/handlers.ts +226 -0
  163. package/src/studio/slash/parser.ts +41 -0
  164. package/src/studio/slash/registry.ts +54 -0
  165. package/src/studio/state/store.ts +273 -0
  166. package/src/studio/whatsapp/api.ts +96 -0
  167. package/src/studio/whatsapp/polling.ts +93 -0
  168. package/src/studio/whatsapp/types.ts +80 -0
  169. package/src/types/agent.ts +1 -1
  170. package/src/types/auth.ts +4 -3
  171. package/src/types/portal.ts +1 -1
  172. package/src/types/workers.ts +1 -1
  173. package/src/utils/agent-errors.ts +67 -0
  174. package/src/utils/api.ts +6 -0
  175. package/src/utils/banner.ts +14 -9
  176. package/src/utils/credentials.ts +6 -5
  177. package/src/utils/help.ts +51 -0
  178. package/tsconfig.json +9 -6
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,58 @@
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
+ }
@@ -0,0 +1,139 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import type { WaConnectState } from '../whatsapp/types.js';
5
+ import { WA_POLL_MAX_ATTEMPTS } from '../whatsapp/types.js';
6
+
7
+ interface Props {
8
+ state: WaConnectState;
9
+ }
10
+
11
+ function formatExpiry(iso?: string): string {
12
+ if (!iso) return '';
13
+ const d = new Date(iso);
14
+ if (Number.isNaN(d.getTime())) return '';
15
+ return d.toLocaleString('en-US', {
16
+ day: 'numeric',
17
+ month: 'short',
18
+ hour: '2-digit',
19
+ minute: '2-digit',
20
+ });
21
+ }
22
+
23
+ /**
24
+ * Wraps a URL in an OSC 8 hyperlink escape sequence. Terminals that don't
25
+ * support it just print the visible text — but iTerm2, kitty, Wezterm,
26
+ * Alacritty (recent), and modern VS Code/Hyper terminals do.
27
+ */
28
+ function osc8(url: string, label: string): string {
29
+ return `\u001b]8;;${url}\u001b\\${label}\u001b]8;;\u001b\\`;
30
+ }
31
+
32
+ export function WhatsappConnectCard({ state }: Props): React.ReactElement {
33
+ const { linkData, status, connectedNumber, errorMessage, attempt } = state;
34
+ const shortUrl = linkData.shortUrl || '';
35
+ const expiry = formatExpiry(linkData.expiresAt);
36
+ const typeLabel =
37
+ linkData.linkType === 'whatsapp_business' ? 'WhatsApp Business' : 'WhatsApp Cloud API';
38
+
39
+ // Header glyph + color per status.
40
+ let headerIcon = '●';
41
+ let headerColor: 'green' | 'red' | 'yellow' = 'green';
42
+ let headerText = 'Connect your WhatsApp';
43
+ if (status === 'connected') {
44
+ headerIcon = '✓';
45
+ headerColor = 'green';
46
+ headerText = 'WhatsApp connected';
47
+ } else if (status === 'error') {
48
+ headerIcon = '✖';
49
+ headerColor = 'red';
50
+ headerText = 'Could not activate the number';
51
+ } else if (status === 'timeout') {
52
+ headerIcon = '○';
53
+ headerColor = 'yellow';
54
+ headerText = 'No connection detected';
55
+ } else if (status === 'cancelled') {
56
+ headerIcon = '○';
57
+ headerColor = 'yellow';
58
+ headerText = 'Connection cancelled';
59
+ }
60
+
61
+ const showLink = status !== 'connected' && status !== 'cancelled' && !!shortUrl;
62
+
63
+ return (
64
+ <Box
65
+ flexDirection="column"
66
+ borderStyle="round"
67
+ borderColor={status === 'error' ? 'red' : status === 'connected' ? 'green' : 'gray'}
68
+ paddingX={1}
69
+ marginY={1}
70
+ marginLeft={2}
71
+ >
72
+ {/* Header */}
73
+ <Box>
74
+ <Text bold color={headerColor}>{headerIcon} </Text>
75
+ <Text bold>{headerText}</Text>
76
+ {status !== 'connected' && status !== 'error' && status !== 'cancelled' ? (
77
+ <>
78
+ <Text dimColor> </Text>
79
+ <Text dimColor>· {typeLabel}</Text>
80
+ </>
81
+ ) : null}
82
+ </Box>
83
+
84
+ {/* Link block */}
85
+ {showLink ? (
86
+ <Box marginTop={1} flexDirection="column">
87
+ <Box>
88
+ <Text dimColor>link: </Text>
89
+ <Text color="cyan" underline>{osc8(shortUrl, shortUrl)}</Text>
90
+ </Box>
91
+ {expiry ? (
92
+ <Box>
93
+ <Text dimColor>expires: {expiry}</Text>
94
+ </Box>
95
+ ) : null}
96
+ <Box marginTop={1}>
97
+ <Text dimColor>
98
+ Open the link from a phone with WhatsApp installed. We'll keep watching for the new number.
99
+ </Text>
100
+ </Box>
101
+ </Box>
102
+ ) : null}
103
+
104
+ {/* Status line */}
105
+ <Box marginTop={1}>
106
+ {status === 'waiting' ? (
107
+ <>
108
+ <Text color="yellow"><Spinner type="dots" /></Text>
109
+ <Text dimColor> Waiting for connection… </Text>
110
+ <Text dimColor>({attempt}/{WA_POLL_MAX_ATTEMPTS})</Text>
111
+ <Text dimColor> · press Esc to cancel</Text>
112
+ </>
113
+ ) : null}
114
+ {status === 'activating' ? (
115
+ <>
116
+ <Text color="yellow"><Spinner type="dots" /></Text>
117
+ <Text dimColor> Number detected, activating…</Text>
118
+ </>
119
+ ) : null}
120
+ {status === 'connected' ? (
121
+ <Text color="green">
122
+ Number {connectedNumber || '(connected)'} activated successfully.
123
+ </Text>
124
+ ) : null}
125
+ {status === 'timeout' ? (
126
+ <Text dimColor>
127
+ We didn't detect the connection within 5 minutes. Ask the agent to try again.
128
+ </Text>
129
+ ) : null}
130
+ {status === 'cancelled' ? (
131
+ <Text dimColor>Polling cancelled by user.</Text>
132
+ ) : null}
133
+ {status === 'error' ? (
134
+ <Text color="red">{errorMessage || 'An error occurred while activating the integration.'}</Text>
135
+ ) : null}
136
+ </Box>
137
+ </Box>
138
+ );
139
+ }
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander';
2
+
3
+ interface StudioCmdOpts {
4
+ dev: boolean;
5
+ agentId?: string;
6
+ message?: string;
7
+ workspaceId?: string;
8
+ }
9
+
10
+ interface AskCmdOpts {
11
+ dev: boolean;
12
+ agentId?: string;
13
+ json: boolean;
14
+ workspaceId?: string;
15
+ }
16
+
17
+ export const studioCommand = new Command('studio')
18
+ .description('Interactive Plazbot Studio REPL (create, diagnose and manage AI agents)')
19
+ .option('--dev', 'Target the local backend (http://localhost:5090)', false)
20
+ .option('-a, --agent-id <id>', 'Preload an agent on startup')
21
+ .option('-w, --workspace-id <id>', 'Override the workspace (support / white-label partner mode)')
22
+ .option('-m, --message <text>', 'Run a one-shot prompt and exit')
23
+ .action(async (opts: StudioCmdOpts) => {
24
+ if (opts.message) {
25
+ const { runOneShot } = await import('./runOneShot.js');
26
+ await runOneShot({
27
+ dev: opts.dev,
28
+ agentId: opts.agentId,
29
+ message: opts.message,
30
+ workspaceOverride: opts.workspaceId,
31
+ });
32
+ return;
33
+ }
34
+ const { runRepl } = await import('./runRepl.js');
35
+ await runRepl({
36
+ dev: opts.dev,
37
+ agentId: opts.agentId,
38
+ workspaceOverride: opts.workspaceId,
39
+ });
40
+ });
41
+
42
+ studioCommand
43
+ .command('ask <message>')
44
+ .description('Run a one-shot studio query without opening the TUI')
45
+ .option('--dev', 'Target the local backend (http://localhost:5090)', false)
46
+ .option('-a, --agent-id <id>', 'Agent to use as context')
47
+ .option('-w, --workspace-id <id>', 'Override the workspace (support / white-label partner mode)')
48
+ .option('--json', 'Print SSE chunks as raw NDJSON', false)
49
+ .action(async (message: string, opts: AskCmdOpts) => {
50
+ const { runOneShot } = await import('./runOneShot.js');
51
+ await runOneShot({
52
+ dev: opts.dev,
53
+ agentId: opts.agentId,
54
+ message,
55
+ json: opts.json,
56
+ workspaceOverride: opts.workspaceId,
57
+ });
58
+ });
@@ -0,0 +1,93 @@
1
+ // @ts-ignore - marked-terminal no provee tipos
2
+ import TerminalRenderer from 'marked-terminal';
3
+ import { marked } from 'marked';
4
+ import chalk from 'chalk';
5
+ // @ts-ignore - cli-highlight no provee tipos completos
6
+ import { highlight, supportsLanguage } from 'cli-highlight';
7
+
8
+ let configured = false;
9
+
10
+ const LANG_ALIASES: Record<string, string> = {
11
+ sh: 'bash',
12
+ shell: 'bash',
13
+ zsh: 'bash',
14
+ curl: 'bash',
15
+ jsx: 'javascript',
16
+ ts: 'typescript',
17
+ tsx: 'typescript',
18
+ yml: 'yaml',
19
+ html: 'xml',
20
+ text: 'plaintext',
21
+ txt: 'plaintext',
22
+ };
23
+
24
+ function resolveLang(raw: string | undefined): string {
25
+ const k = (raw ?? '').toLowerCase().trim();
26
+ const aliased = LANG_ALIASES[k] ?? k;
27
+ if (!aliased) return 'plaintext';
28
+ try {
29
+ return supportsLanguage(aliased) ? aliased : 'plaintext';
30
+ } catch {
31
+ return 'plaintext';
32
+ }
33
+ }
34
+
35
+ function renderCodeBlock(code: string, rawLang: string | undefined): string {
36
+ const lang = resolveLang(rawLang);
37
+ let body: string;
38
+ try {
39
+ body = highlight(code, { language: lang, ignoreIllegals: true });
40
+ } catch {
41
+ body = code;
42
+ }
43
+
44
+ const label = (rawLang ?? '').trim() || lang;
45
+ const width = Math.max(20, Math.min(80, (process.stdout.columns || 80) - 8));
46
+ const headerLabel = ` ${label} `;
47
+ const headerRule = '─'.repeat(Math.max(0, width - headerLabel.length - 4));
48
+ const header = chalk.gray.dim(`──${headerLabel}${headerRule}`);
49
+ const footer = chalk.gray.dim('─'.repeat(width - 2));
50
+
51
+ // Indent del cuerpo para diferenciar del prosa
52
+ const indented = body
53
+ .split('\n')
54
+ .map((l) => ' ' + l)
55
+ .join('\n');
56
+
57
+ return `\n${header}\n${indented}\n${footer}\n`;
58
+ }
59
+
60
+ function configure(): void {
61
+ if (configured) return;
62
+
63
+ const renderer = new TerminalRenderer({
64
+ width: Math.max(40, (process.stdout.columns || 80) - 8),
65
+ reflowText: true,
66
+ tab: 2,
67
+ // Inline code mejor diferenciado del texto normal
68
+ codespan: chalk.cyan,
69
+ }) as any;
70
+
71
+ // Override del code block para syntax highlight + box delimitado.
72
+ // marked-terminal pasa (code, lang, escaped) al método `code`.
73
+ renderer.code = (code: string, lang: string | undefined): string =>
74
+ renderCodeBlock(code, lang);
75
+
76
+ marked.setOptions({ renderer });
77
+ configured = true;
78
+ }
79
+
80
+ /**
81
+ * Renderiza markdown a una cadena con secuencias ANSI lista para imprimir
82
+ * dentro de un <Text/> de Ink. Limpia el salto de línea trailing que añade marked.
83
+ */
84
+ export function renderMarkdown(md: string): string {
85
+ if (!md) return '';
86
+ configure();
87
+ try {
88
+ const out = marked.parse(md) as string;
89
+ return typeof out === 'string' ? out.replace(/\n+$/, '') : md;
90
+ } catch {
91
+ return md;
92
+ }
93
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Maps backend tool_name values to human-readable step labels for the REPL.
3
+ * Keeps both forms (gerund for "running" and past tense for "success").
4
+ */
5
+ const LABELS: Record<string, { running: string; done: string; failed: string }> = {
6
+ list_agents: {
7
+ running: 'Listing agents',
8
+ done: 'Agents listed',
9
+ failed: 'Could not list agents',
10
+ },
11
+ load_agent: {
12
+ running: 'Loading agent',
13
+ done: 'Agent loaded',
14
+ failed: 'Could not load agent',
15
+ },
16
+ diagnose_agent: {
17
+ running: 'Diagnosing agent',
18
+ done: 'Diagnosis complete',
19
+ failed: 'Could not diagnose agent',
20
+ },
21
+ create_agent_config: {
22
+ running: 'Generating agent configuration',
23
+ done: 'Configuration generated',
24
+ failed: 'Could not generate configuration',
25
+ },
26
+ update_agent_config: {
27
+ running: 'Updating agent configuration',
28
+ done: 'Configuration updated',
29
+ failed: 'Could not update configuration',
30
+ },
31
+ save_agent: {
32
+ running: 'Saving agent',
33
+ done: 'Agent saved',
34
+ failed: 'Could not save agent',
35
+ },
36
+ delete_agent: {
37
+ running: 'Deleting agent',
38
+ done: 'Agent deleted',
39
+ failed: 'Could not delete agent',
40
+ },
41
+ connect_whatsapp: {
42
+ running: 'Connecting WhatsApp',
43
+ done: 'WhatsApp connected',
44
+ failed: 'Could not connect WhatsApp',
45
+ },
46
+ };
47
+
48
+ export function stepLabel(toolName: string, status: 'running' | 'success' | 'error'): string {
49
+ const entry = LABELS[toolName] ?? {
50
+ running: `Running ${toolName}`,
51
+ done: `Ran ${toolName}`,
52
+ failed: `Failed: ${toolName}`,
53
+ };
54
+ if (status === 'running') return entry.running;
55
+ if (status === 'success') return entry.done;
56
+ return entry.failed;
57
+ }
@@ -0,0 +1,114 @@
1
+ import { getStoredCredentials } from '../utils/credentials.js';
2
+ import { logger } from '../utils/logger.js';
3
+ import { streamStudio } from './api/sseClient.js';
4
+ import { StudioHttpError } from './api/types.js';
5
+ import type { StudioChunk, StudioStreamOptions } from './api/types.js';
6
+ import { stepLabel } from './render/steps.js';
7
+
8
+ interface RunOneShotOptions {
9
+ dev: boolean;
10
+ agentId?: string;
11
+ message: string;
12
+ /** Si true, imprime cada chunk como una línea JSON (NDJSON). */
13
+ json?: boolean;
14
+ /** Overrides the workspace from local config (support / white-label partner mode). */
15
+ workspaceOverride?: string;
16
+ }
17
+
18
+ export async function runOneShot(opts: RunOneShotOptions): Promise<void> {
19
+ let creds;
20
+ try {
21
+ creds = await getStoredCredentials();
22
+ } catch {
23
+ logger.error('No active session. Run:');
24
+ console.log(' plazbot init -e <email> -k <jwt> -w <workspace> -z <LA|EU>');
25
+ process.exit(1);
26
+ return;
27
+ }
28
+
29
+ const effectiveWorkspace = opts.workspaceOverride ?? creds.workspace;
30
+ const supportMode = !!opts.workspaceOverride && opts.workspaceOverride !== creds.workspace;
31
+
32
+ if (supportMode && !opts.json) {
33
+ process.stderr.write(`! Support mode: acting on workspace "${effectiveWorkspace}" (your account belongs to "${creds.workspace}")\n`);
34
+ }
35
+
36
+ const stream: StudioStreamOptions = {
37
+ apiKey: creds.apiKey,
38
+ workspaceId: effectiveWorkspace,
39
+ userId: creds.userId ?? creds.email ?? undefined,
40
+ zone: creds.zone,
41
+ dev: opts.dev,
42
+ };
43
+
44
+ const controller = new AbortController();
45
+ process.on('SIGINT', () => controller.abort());
46
+
47
+ let assistantText = '';
48
+ const onChunk = (chunk: StudioChunk) => {
49
+ if (opts.json) {
50
+ process.stdout.write(JSON.stringify(chunk) + '\n');
51
+ return;
52
+ }
53
+ switch (chunk.type) {
54
+ case 'text':
55
+ assistantText += chunk.content ?? '';
56
+ process.stdout.write(chunk.content ?? '');
57
+ break;
58
+ case 'tool_call':
59
+ process.stderr.write(`\n ▸ ${stepLabel(chunk.tool_name, 'running')}…\n`);
60
+ break;
61
+ case 'tool_result': {
62
+ const tr = chunk.tool_result;
63
+ const ok = tr?.success !== false && !tr?.error;
64
+ const label = stepLabel(chunk.tool_name, ok ? 'success' : 'error');
65
+ process.stderr.write(` ${ok ? '✓' : '✗'} ${label}${tr?.error ? ' — ' + tr.error : ''}\n`);
66
+ break;
67
+ }
68
+ case 'usage':
69
+ process.stderr.write(
70
+ ` tokens: ${chunk.input_tokens} in / ${chunk.output_tokens} out\n`,
71
+ );
72
+ break;
73
+ case 'error':
74
+ process.stderr.write(`\n ✖ ${chunk.error}\n`);
75
+ break;
76
+ case 'done':
77
+ break;
78
+ }
79
+ };
80
+
81
+ try {
82
+ await streamStudio(
83
+ stream,
84
+ {
85
+ message: opts.message,
86
+ messages: [{ role: 'user', content: opts.message }],
87
+ agentId: opts.agentId ?? null,
88
+ },
89
+ onChunk,
90
+ controller.signal,
91
+ );
92
+ if (!opts.json) process.stdout.write('\n');
93
+ if (!assistantText && !opts.json) process.stderr.write(' (no text response)\n');
94
+ } catch (err) {
95
+ process.stderr.write('\n' + formatError(err, opts.dev) + '\n');
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ function formatError(err: unknown, dev: boolean): string {
101
+ if (err instanceof StudioHttpError) {
102
+ if (err.status === 401) return '✖ Token expired or invalid. Run `plazbot init`.';
103
+ if (err.status === 403) return '✖ No permission for this workspace.';
104
+ if (err.status === 429) return '✖ Rate limit reached.';
105
+ if (err.status >= 500) {
106
+ return dev
107
+ ? `✖ Backend ${err.status}: ${err.body ?? err.statusText}`
108
+ : `✖ Backend returned ${err.status}. Retry.`;
109
+ }
110
+ return `✖ HTTP ${err.status} ${err.statusText}`;
111
+ }
112
+ if (err instanceof Error) return `✖ ${err.message}`;
113
+ return '✖ Unknown error';
114
+ }
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+ import { render } from 'ink';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ import { App } from './components/App.js';
7
+ import { getStoredCredentials } from '../utils/credentials.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import type { StudioStreamOptions } from './api/types.js';
10
+
11
+ interface RunReplOptions {
12
+ dev: boolean;
13
+ agentId?: string;
14
+ /** Overrides the workspace from local config (support / white-label partner mode). */
15
+ workspaceOverride?: string;
16
+ }
17
+
18
+ export async function runRepl(opts: RunReplOptions): Promise<void> {
19
+ let creds;
20
+ try {
21
+ creds = await getStoredCredentials();
22
+ } catch (err) {
23
+ logger.error('No active session. Run:');
24
+ console.log(' plazbot init -e <email> -k <jwt> -w <workspace> -z <LA|EU>');
25
+ process.exit(1);
26
+ }
27
+
28
+ const effectiveWorkspace = opts.workspaceOverride ?? creds.workspace;
29
+ const supportMode = !!opts.workspaceOverride && opts.workspaceOverride !== creds.workspace;
30
+
31
+ if (supportMode) {
32
+ logger.warning(`Support mode: acting on workspace "${effectiveWorkspace}" (your account belongs to "${creds.workspace}")`);
33
+ logger.warning('All actions are tied to your user and will be visible in audit trails.');
34
+ }
35
+
36
+ const stream: StudioStreamOptions = {
37
+ apiKey: creds.apiKey,
38
+ workspaceId: effectiveWorkspace,
39
+ userId: creds.userId ?? creds.email ?? undefined,
40
+ zone: creds.zone,
41
+ dev: opts.dev,
42
+ };
43
+
44
+ const version = readVersion();
45
+
46
+ const { waitUntilExit } = render(
47
+ <App
48
+ version={version}
49
+ stream={stream}
50
+ initialAgentId={opts.agentId ?? null}
51
+ dev={opts.dev}
52
+ supportMode={supportMode}
53
+ />,
54
+ {
55
+ exitOnCtrlC: false,
56
+ },
57
+ );
58
+
59
+ // Cierre limpio con Ctrl+C: la App lo gestiona vía useInput.
60
+ process.on('SIGINT', () => {
61
+ // no-op aquí; App.tsx llama exit() al detectar Ctrl+C.
62
+ });
63
+
64
+ await waitUntilExit();
65
+ }
66
+
67
+ function readVersion(): string {
68
+ try {
69
+ const __dirname = dirname(fileURLToPath(import.meta.url));
70
+ const pkgPath = join(__dirname, '..', '..', 'package.json');
71
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
72
+ return pkg.version ?? '0.0.0';
73
+ } catch {
74
+ return '0.0.0';
75
+ }
76
+ }