plazbot-cli 0.2.25 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +34 -5
- package/README.md +21 -0
- package/dist/cli.js +32 -20
- package/dist/commands/agent/ai-config.js +98 -50
- package/dist/commands/agent/chat.js +80 -74
- package/dist/commands/agent/copy.js +23 -21
- package/dist/commands/agent/create.js +42 -72
- package/dist/commands/agent/delete.js +29 -30
- package/dist/commands/agent/enable-widget.js +30 -26
- package/dist/commands/agent/export.js +90 -77
- package/dist/commands/agent/files.js +68 -60
- package/dist/commands/agent/get.js +101 -87
- package/dist/commands/agent/index.js +53 -39
- package/dist/commands/agent/list.js +26 -24
- package/dist/commands/agent/monitor.js +91 -86
- package/dist/commands/agent/on-message.js +40 -37
- package/dist/commands/agent/set.js +62 -59
- package/dist/commands/agent/templates.js +109 -108
- package/dist/commands/agent/tools.js +64 -65
- package/dist/commands/agent/update.js +28 -27
- package/dist/commands/agent/validate.js +127 -0
- package/dist/commands/agent/wizard.js +152 -159
- package/dist/commands/auth/index.js +7 -10
- package/dist/commands/auth/login.js +50 -37
- package/dist/commands/auth/logout.js +16 -14
- package/dist/commands/auth/status.js +19 -16
- package/dist/commands/portal/add-agent.js +26 -24
- package/dist/commands/portal/add-link.js +21 -17
- package/dist/commands/portal/clear-links.js +17 -15
- package/dist/commands/portal/create.js +25 -21
- package/dist/commands/portal/delete.js +31 -30
- package/dist/commands/portal/get.js +33 -31
- package/dist/commands/portal/index.js +30 -22
- package/dist/commands/portal/list.js +34 -30
- package/dist/commands/portal/update.js +41 -33
- package/dist/commands/whatsapp/broadcast.js +40 -37
- package/dist/commands/whatsapp/channels.js +40 -34
- package/dist/commands/whatsapp/chat.js +33 -32
- package/dist/commands/whatsapp/connect.js +59 -55
- package/dist/commands/whatsapp/delete-webhook.js +19 -17
- package/dist/commands/whatsapp/index.js +35 -25
- package/dist/commands/whatsapp/register-webhook.js +21 -19
- package/dist/commands/whatsapp/send-template.js +39 -31
- package/dist/commands/whatsapp/send.js +27 -23
- package/dist/commands/whatsapp/widget.js +35 -31
- package/dist/commands/workers/deploy.js +49 -44
- package/dist/commands/workers/index.js +28 -18
- package/dist/commands/workers/list.js +43 -35
- package/dist/commands/workers/logs.js +38 -32
- package/dist/commands/workers/remove.js +38 -37
- package/dist/commands/workers/secret.js +63 -58
- package/dist/commands/workers/test.js +44 -36
- package/dist/schemas/agent.config.schema.json +569 -0
- package/dist/studio/api/sseClient.js +97 -0
- package/dist/studio/api/studioApi.js +25 -0
- package/dist/studio/api/types.js +16 -0
- package/dist/studio/components/AgentPanel.js +35 -0
- package/dist/studio/components/App.js +214 -0
- package/dist/studio/components/ChatLog.js +59 -0
- package/dist/studio/components/Footer.js +11 -0
- package/dist/studio/components/Header.js +8 -0
- package/dist/studio/components/Input.js +15 -0
- package/dist/studio/components/Message.js +56 -0
- package/dist/studio/components/Suggestions.js +11 -0
- package/dist/studio/components/ToolCall.js +33 -0
- package/dist/studio/components/WhatsappConnectCard.js +57 -0
- package/dist/studio/index.js +42 -0
- package/dist/studio/render/json.js +16 -0
- package/dist/studio/render/markdown.js +32 -0
- package/dist/studio/render/steps.js +58 -0
- package/dist/studio/runOneShot.js +96 -0
- package/dist/studio/runRepl.js +52 -0
- package/dist/studio/slash/handlers.js +199 -0
- package/dist/studio/slash/parser.js +46 -0
- package/dist/studio/slash/registry.js +16 -0
- package/dist/studio/state/store.js +181 -0
- package/dist/studio/whatsapp/api.js +63 -0
- package/dist/studio/whatsapp/polling.js +77 -0
- package/dist/studio/whatsapp/types.js +31 -0
- package/dist/types/agent.js +1 -2
- package/dist/types/auth.js +1 -2
- package/dist/types/common.js +1 -2
- package/dist/types/message.js +1 -2
- package/dist/types/portal.js +1 -2
- package/dist/types/workers.js +1 -2
- package/dist/utils/agent-errors.js +46 -0
- package/dist/utils/api.js +8 -9
- package/dist/utils/banner.js +33 -34
- package/dist/utils/credentials.js +12 -20
- package/dist/utils/help.js +44 -0
- package/dist/utils/logger.js +13 -19
- package/dist/utils/ui.js +35 -49
- package/package.json +21 -10
- package/src/cli.ts +24 -8
- package/src/commands/agent/ai-config.ts +89 -34
- package/src/commands/agent/chat.ts +49 -37
- package/src/commands/agent/copy.ts +19 -13
- package/src/commands/agent/create.ts +32 -22
- package/src/commands/agent/delete.ts +24 -18
- package/src/commands/agent/enable-widget.ts +31 -23
- package/src/commands/agent/export.ts +72 -51
- package/src/commands/agent/files.ts +51 -39
- package/src/commands/agent/get.ts +86 -66
- package/src/commands/agent/index.ts +36 -18
- package/src/commands/agent/list.ts +22 -16
- package/src/commands/agent/monitor.ts +67 -56
- package/src/commands/agent/on-message.ts +36 -27
- package/src/commands/agent/set.ts +47 -37
- package/src/commands/agent/templates.ts +90 -82
- package/src/commands/agent/tools.ts +53 -47
- package/src/commands/agent/update.ts +28 -20
- package/src/commands/agent/validate.ts +135 -0
- package/src/commands/agent/wizard.ts +114 -114
- package/src/commands/auth/index.ts +3 -3
- package/src/commands/auth/login.ts +44 -29
- package/src/commands/auth/logout.ts +16 -10
- package/src/commands/auth/status.ts +14 -8
- package/src/commands/portal/add-agent.ts +23 -17
- package/src/commands/portal/add-link.ts +17 -9
- package/src/commands/portal/clear-links.ts +13 -7
- package/src/commands/portal/create.ts +20 -12
- package/src/commands/portal/delete.ts +28 -20
- package/src/commands/portal/get.ts +25 -19
- package/src/commands/portal/index.ts +22 -10
- package/src/commands/portal/list.ts +27 -19
- package/src/commands/portal/update.ts +38 -26
- package/src/commands/whatsapp/broadcast.ts +28 -18
- package/src/commands/whatsapp/channels.ts +31 -20
- package/src/commands/whatsapp/chat.ts +20 -12
- package/src/commands/whatsapp/connect.ts +48 -36
- package/src/commands/whatsapp/delete-webhook.ts +15 -9
- package/src/commands/whatsapp/index.ts +24 -10
- package/src/commands/whatsapp/register-webhook.ts +16 -10
- package/src/commands/whatsapp/send-template.ts +33 -21
- package/src/commands/whatsapp/send.ts +23 -15
- package/src/commands/whatsapp/widget.ts +25 -17
- package/src/commands/workers/deploy.ts +34 -22
- package/src/commands/workers/index.ts +21 -7
- package/src/commands/workers/list.ts +31 -19
- package/src/commands/workers/logs.ts +30 -20
- package/src/commands/workers/remove.ts +30 -22
- package/src/commands/workers/secret.ts +46 -34
- package/src/commands/workers/test.ts +34 -22
- package/src/schemas/agent.config.schema.json +569 -0
- package/src/studio/api/sseClient.ts +91 -0
- package/src/studio/api/studioApi.ts +27 -0
- package/src/studio/api/types.ts +96 -0
- package/src/studio/components/App.tsx +266 -0
- package/src/studio/components/ChatLog.tsx +95 -0
- package/src/studio/components/Footer.tsx +38 -0
- package/src/studio/components/Header.tsx +39 -0
- package/src/studio/components/Input.tsx +32 -0
- package/src/studio/components/Message.tsx +87 -0
- package/src/studio/components/Suggestions.tsx +26 -0
- package/src/studio/components/ToolCall.tsx +58 -0
- package/src/studio/components/WhatsappConnectCard.tsx +139 -0
- package/src/studio/index.ts +58 -0
- package/src/studio/render/markdown.ts +32 -0
- package/src/studio/render/steps.ts +57 -0
- package/src/studio/runOneShot.ts +114 -0
- package/src/studio/runRepl.tsx +76 -0
- package/src/studio/slash/handlers.ts +226 -0
- package/src/studio/slash/parser.ts +41 -0
- package/src/studio/slash/registry.ts +54 -0
- package/src/studio/state/store.ts +273 -0
- package/src/studio/whatsapp/api.ts +96 -0
- package/src/studio/whatsapp/polling.ts +93 -0
- package/src/studio/whatsapp/types.ts +80 -0
- package/src/types/agent.ts +1 -1
- package/src/types/auth.ts +4 -3
- package/src/types/portal.ts +1 -1
- package/src/types/workers.ts +1 -1
- package/src/utils/agent-errors.ts +67 -0
- package/src/utils/api.ts +6 -0
- package/src/utils/banner.ts +14 -9
- package/src/utils/credentials.ts +6 -5
- package/src/utils/help.ts +51 -0
- package/tsconfig.json +9 -6
|
@@ -0,0 +1,58 @@
|
|
|
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 = {
|
|
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
|
+
export function stepLabel(toolName, status) {
|
|
48
|
+
const entry = LABELS[toolName] ?? {
|
|
49
|
+
running: `Running ${toolName}`,
|
|
50
|
+
done: `Ran ${toolName}`,
|
|
51
|
+
failed: `Failed: ${toolName}`,
|
|
52
|
+
};
|
|
53
|
+
if (status === 'running')
|
|
54
|
+
return entry.running;
|
|
55
|
+
if (status === 'success')
|
|
56
|
+
return entry.done;
|
|
57
|
+
return entry.failed;
|
|
58
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { stepLabel } from './render/steps.js';
|
|
6
|
+
export async function runOneShot(opts) {
|
|
7
|
+
let creds;
|
|
8
|
+
try {
|
|
9
|
+
creds = await getStoredCredentials();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
logger.error('No active session. Run:');
|
|
13
|
+
console.log(' plazbot init -e <email> -k <jwt> -w <workspace> -z <LA|EU>');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const effectiveWorkspace = opts.workspaceOverride ?? creds.workspace;
|
|
18
|
+
const supportMode = !!opts.workspaceOverride && opts.workspaceOverride !== creds.workspace;
|
|
19
|
+
if (supportMode && !opts.json) {
|
|
20
|
+
process.stderr.write(`! Support mode: acting on workspace "${effectiveWorkspace}" (your account belongs to "${creds.workspace}")\n`);
|
|
21
|
+
}
|
|
22
|
+
const stream = {
|
|
23
|
+
apiKey: creds.apiKey,
|
|
24
|
+
workspaceId: effectiveWorkspace,
|
|
25
|
+
userId: creds.userId ?? creds.email ?? undefined,
|
|
26
|
+
zone: creds.zone,
|
|
27
|
+
dev: opts.dev,
|
|
28
|
+
};
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
process.on('SIGINT', () => controller.abort());
|
|
31
|
+
let assistantText = '';
|
|
32
|
+
const onChunk = (chunk) => {
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
process.stdout.write(JSON.stringify(chunk) + '\n');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
switch (chunk.type) {
|
|
38
|
+
case 'text':
|
|
39
|
+
assistantText += chunk.content ?? '';
|
|
40
|
+
process.stdout.write(chunk.content ?? '');
|
|
41
|
+
break;
|
|
42
|
+
case 'tool_call':
|
|
43
|
+
process.stderr.write(`\n ▸ ${stepLabel(chunk.tool_name, 'running')}…\n`);
|
|
44
|
+
break;
|
|
45
|
+
case 'tool_result': {
|
|
46
|
+
const tr = chunk.tool_result;
|
|
47
|
+
const ok = tr?.success !== false && !tr?.error;
|
|
48
|
+
const label = stepLabel(chunk.tool_name, ok ? 'success' : 'error');
|
|
49
|
+
process.stderr.write(` ${ok ? '✓' : '✗'} ${label}${tr?.error ? ' — ' + tr.error : ''}\n`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'usage':
|
|
53
|
+
process.stderr.write(` tokens: ${chunk.input_tokens} in / ${chunk.output_tokens} out\n`);
|
|
54
|
+
break;
|
|
55
|
+
case 'error':
|
|
56
|
+
process.stderr.write(`\n ✖ ${chunk.error}\n`);
|
|
57
|
+
break;
|
|
58
|
+
case 'done':
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await streamStudio(stream, {
|
|
64
|
+
message: opts.message,
|
|
65
|
+
messages: [{ role: 'user', content: opts.message }],
|
|
66
|
+
agentId: opts.agentId ?? null,
|
|
67
|
+
}, onChunk, controller.signal);
|
|
68
|
+
if (!opts.json)
|
|
69
|
+
process.stdout.write('\n');
|
|
70
|
+
if (!assistantText && !opts.json)
|
|
71
|
+
process.stderr.write(' (no text response)\n');
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
process.stderr.write('\n' + formatError(err, opts.dev) + '\n');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function formatError(err, dev) {
|
|
79
|
+
if (err instanceof StudioHttpError) {
|
|
80
|
+
if (err.status === 401)
|
|
81
|
+
return '✖ Token expired or invalid. Run `plazbot init`.';
|
|
82
|
+
if (err.status === 403)
|
|
83
|
+
return '✖ No permission for this workspace.';
|
|
84
|
+
if (err.status === 429)
|
|
85
|
+
return '✖ Rate limit reached.';
|
|
86
|
+
if (err.status >= 500) {
|
|
87
|
+
return dev
|
|
88
|
+
? `✖ Backend ${err.status}: ${err.body ?? err.statusText}`
|
|
89
|
+
: `✖ Backend returned ${err.status}. Retry.`;
|
|
90
|
+
}
|
|
91
|
+
return `✖ HTTP ${err.status} ${err.statusText}`;
|
|
92
|
+
}
|
|
93
|
+
if (err instanceof Error)
|
|
94
|
+
return `✖ ${err.message}`;
|
|
95
|
+
return '✖ Unknown error';
|
|
96
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
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
|
+
export async function runRepl(opts) {
|
|
10
|
+
let creds;
|
|
11
|
+
try {
|
|
12
|
+
creds = await getStoredCredentials();
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
logger.error('No active session. Run:');
|
|
16
|
+
console.log(' plazbot init -e <email> -k <jwt> -w <workspace> -z <LA|EU>');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const effectiveWorkspace = opts.workspaceOverride ?? creds.workspace;
|
|
20
|
+
const supportMode = !!opts.workspaceOverride && opts.workspaceOverride !== creds.workspace;
|
|
21
|
+
if (supportMode) {
|
|
22
|
+
logger.warning(`Support mode: acting on workspace "${effectiveWorkspace}" (your account belongs to "${creds.workspace}")`);
|
|
23
|
+
logger.warning('All actions are tied to your user and will be visible in audit trails.');
|
|
24
|
+
}
|
|
25
|
+
const stream = {
|
|
26
|
+
apiKey: creds.apiKey,
|
|
27
|
+
workspaceId: effectiveWorkspace,
|
|
28
|
+
userId: creds.userId ?? creds.email ?? undefined,
|
|
29
|
+
zone: creds.zone,
|
|
30
|
+
dev: opts.dev,
|
|
31
|
+
};
|
|
32
|
+
const version = readVersion();
|
|
33
|
+
const { waitUntilExit } = render(_jsx(App, { version: version, stream: stream, initialAgentId: opts.agentId ?? null, dev: opts.dev, supportMode: supportMode }), {
|
|
34
|
+
exitOnCtrlC: false,
|
|
35
|
+
});
|
|
36
|
+
// Cierre limpio con Ctrl+C: la App lo gestiona vía useInput.
|
|
37
|
+
process.on('SIGINT', () => {
|
|
38
|
+
// no-op aquí; App.tsx llama exit() al detectar Ctrl+C.
|
|
39
|
+
});
|
|
40
|
+
await waitUntilExit();
|
|
41
|
+
}
|
|
42
|
+
function readVersion() {
|
|
43
|
+
try {
|
|
44
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const pkgPath = join(__dirname, '..', '..', 'package.json');
|
|
46
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
47
|
+
return pkg.version ?? '0.0.0';
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return '0.0.0';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { slashRegistry } from './registry.js';
|
|
5
|
+
import { useStudioStore } from '../state/store.js';
|
|
6
|
+
function expandTilde(p) {
|
|
7
|
+
if (p === '~')
|
|
8
|
+
return homedir();
|
|
9
|
+
if (p.startsWith('~/'))
|
|
10
|
+
return resolve(homedir(), p.slice(2));
|
|
11
|
+
return p;
|
|
12
|
+
}
|
|
13
|
+
function slugify(s) {
|
|
14
|
+
return s
|
|
15
|
+
.normalize('NFKD')
|
|
16
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
17
|
+
.replace(/[^a-zA-Z0-9-_]+/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '')
|
|
19
|
+
.toLowerCase() || 'agent';
|
|
20
|
+
}
|
|
21
|
+
const CATEGORY_LABELS = {
|
|
22
|
+
general: 'General',
|
|
23
|
+
agent: 'Agent',
|
|
24
|
+
session: 'Session',
|
|
25
|
+
};
|
|
26
|
+
const CATEGORY_ORDER = [
|
|
27
|
+
'general',
|
|
28
|
+
'agent',
|
|
29
|
+
'session',
|
|
30
|
+
];
|
|
31
|
+
slashRegistry.register({
|
|
32
|
+
name: 'help',
|
|
33
|
+
aliases: ['?'],
|
|
34
|
+
description: 'Show this help with every available command',
|
|
35
|
+
category: 'general',
|
|
36
|
+
handler() {
|
|
37
|
+
const all = slashRegistry.list();
|
|
38
|
+
const grouped = new Map();
|
|
39
|
+
for (const cmd of all) {
|
|
40
|
+
const cat = cmd.category ?? 'general';
|
|
41
|
+
const arr = grouped.get(cat) ?? [];
|
|
42
|
+
arr.push(cmd);
|
|
43
|
+
grouped.set(cat, arr);
|
|
44
|
+
}
|
|
45
|
+
const lines = ['**Plazbot Studio — slash commands**', ''];
|
|
46
|
+
lines.push('Type plain text to chat with the assistant.');
|
|
47
|
+
lines.push('Slash commands run locally or send a templated prompt to the model.');
|
|
48
|
+
lines.push('');
|
|
49
|
+
for (const cat of CATEGORY_ORDER) {
|
|
50
|
+
const items = grouped.get(cat);
|
|
51
|
+
if (!items || items.length === 0)
|
|
52
|
+
continue;
|
|
53
|
+
lines.push(`**${CATEGORY_LABELS[cat]}**`);
|
|
54
|
+
for (const cmd of items) {
|
|
55
|
+
const usage = cmd.usage ?? `/${cmd.name}`;
|
|
56
|
+
const aliasPart = cmd.aliases?.length
|
|
57
|
+
? ` _(alias: ${cmd.aliases.map((a) => '/' + a).join(', ')})_`
|
|
58
|
+
: '';
|
|
59
|
+
lines.push(`- \`${usage}\`${aliasPart} — ${cmd.description}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
lines.push('**Keyboard shortcuts**');
|
|
64
|
+
lines.push('- `Esc` — cancel the current stream');
|
|
65
|
+
lines.push('- `Ctrl+C` — exit Studio');
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push('**Tip:** you can also speak naturally — e.g. _"list my agents"_, _"create a sales agent for WhatsApp"_ or _"connect this agent to WhatsApp Business"_.');
|
|
68
|
+
useStudioStore.getState().pushSyntheticAssistant(lines.join('\n'));
|
|
69
|
+
return { kind: 'noop' };
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
slashRegistry.register({
|
|
73
|
+
name: 'clear',
|
|
74
|
+
aliases: ['cls'],
|
|
75
|
+
description: 'Clear the conversation (keeps the loaded agent and token usage)',
|
|
76
|
+
category: 'session',
|
|
77
|
+
handler() {
|
|
78
|
+
useStudioStore.getState().clearMessages();
|
|
79
|
+
return { kind: 'noop' };
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
slashRegistry.register({
|
|
83
|
+
name: 'reset',
|
|
84
|
+
description: 'Full reset: clears messages, loaded agent and token usage',
|
|
85
|
+
category: 'session',
|
|
86
|
+
handler() {
|
|
87
|
+
useStudioStore.getState().reset();
|
|
88
|
+
return { kind: 'noop' };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const COMPACT_PROMPT = [
|
|
92
|
+
'You are about to compact this conversation.',
|
|
93
|
+
'',
|
|
94
|
+
'Produce a concise but complete summary of everything we have discussed so far, in Markdown:',
|
|
95
|
+
'- Goal / task the user is working on.',
|
|
96
|
+
'- Key decisions and constraints.',
|
|
97
|
+
'- Current agent configuration state (if any) and its `id`.',
|
|
98
|
+
'- Pending items or next steps.',
|
|
99
|
+
'',
|
|
100
|
+
'Reply with ONLY the summary content (no preamble, no farewell).',
|
|
101
|
+
'After this message, the previous history will be replaced by your summary so we can keep working with less context.',
|
|
102
|
+
].join('\n');
|
|
103
|
+
slashRegistry.register({
|
|
104
|
+
name: 'compact',
|
|
105
|
+
description: 'Summarize the conversation and replace history with that summary',
|
|
106
|
+
category: 'session',
|
|
107
|
+
handler() {
|
|
108
|
+
const store = useStudioStore.getState();
|
|
109
|
+
if (store.messages.length === 0) {
|
|
110
|
+
store.pushSyntheticAssistant('Nothing to compact yet.', { error: true });
|
|
111
|
+
return { kind: 'noop' };
|
|
112
|
+
}
|
|
113
|
+
store.setPendingCompact(true);
|
|
114
|
+
return { kind: 'message', content: COMPACT_PROMPT };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
slashRegistry.register({
|
|
118
|
+
name: 'quit',
|
|
119
|
+
aliases: ['exit', 'q'],
|
|
120
|
+
description: 'Exit Studio',
|
|
121
|
+
category: 'general',
|
|
122
|
+
handler(_parsed, ctx) {
|
|
123
|
+
ctx.exit();
|
|
124
|
+
return { kind: 'exit' };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
slashRegistry.register({
|
|
128
|
+
name: 'agents',
|
|
129
|
+
description: 'List the agents in the current workspace',
|
|
130
|
+
category: 'agent',
|
|
131
|
+
handler() {
|
|
132
|
+
return { kind: 'message', content: 'List all agents in my workspace.' };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
slashRegistry.register({
|
|
136
|
+
name: 'load',
|
|
137
|
+
description: 'Load an agent by id and show its current configuration',
|
|
138
|
+
category: 'agent',
|
|
139
|
+
usage: '/load <agentId>',
|
|
140
|
+
handler(parsed) {
|
|
141
|
+
const id = parsed.args[0];
|
|
142
|
+
if (!id) {
|
|
143
|
+
useStudioStore.getState().pushSyntheticAssistant('Usage: `/load <agentId>`', { error: true });
|
|
144
|
+
return { kind: 'noop' };
|
|
145
|
+
}
|
|
146
|
+
useStudioStore.getState().setAgentId(id);
|
|
147
|
+
return { kind: 'message', content: `Load the agent with id "${id}" and show me its current configuration.` };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
slashRegistry.register({
|
|
151
|
+
name: 'save',
|
|
152
|
+
description: 'Save the currently loaded agent configuration to the backend',
|
|
153
|
+
category: 'agent',
|
|
154
|
+
handler() {
|
|
155
|
+
const { agentConfig } = useStudioStore.getState();
|
|
156
|
+
if (!agentConfig) {
|
|
157
|
+
useStudioStore.getState().pushSyntheticAssistant('No agent configuration loaded. Create one first or use `/load <id>`.', { error: true });
|
|
158
|
+
return { kind: 'noop' };
|
|
159
|
+
}
|
|
160
|
+
return { kind: 'message', content: 'Save this agent with its current configuration.' };
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
slashRegistry.register({
|
|
164
|
+
name: 'diagnose',
|
|
165
|
+
description: 'Run diagnostics on the current agent and report any issues found',
|
|
166
|
+
category: 'agent',
|
|
167
|
+
handler() {
|
|
168
|
+
return { kind: 'message', content: 'Diagnose this agent and report any issues you find.' };
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
slashRegistry.register({
|
|
172
|
+
name: 'export',
|
|
173
|
+
aliases: ['download'],
|
|
174
|
+
description: 'Export the current agent configuration to a JSON file on disk',
|
|
175
|
+
category: 'agent',
|
|
176
|
+
usage: '/export [path]',
|
|
177
|
+
async handler(parsed) {
|
|
178
|
+
const store = useStudioStore.getState();
|
|
179
|
+
const { agentConfig } = store;
|
|
180
|
+
if (!agentConfig) {
|
|
181
|
+
store.pushSyntheticAssistant('No hay agente cargado. Crea o carga uno antes con `/load <id>`.', { error: true });
|
|
182
|
+
return { kind: 'noop' };
|
|
183
|
+
}
|
|
184
|
+
const cfg = agentConfig;
|
|
185
|
+
const baseName = slugify(cfg?.name ?? store.agentId ?? 'agent');
|
|
186
|
+
const defaultPath = `./${baseName}.json`;
|
|
187
|
+
const targetArg = parsed.args[0] ?? defaultPath;
|
|
188
|
+
const targetPath = resolve(process.cwd(), expandTilde(targetArg));
|
|
189
|
+
try {
|
|
190
|
+
await writeFile(targetPath, JSON.stringify(agentConfig, null, 2), 'utf-8');
|
|
191
|
+
store.pushSyntheticAssistant(`Agente exportado a \`${targetPath}\`.\n\nValídalo con: \`plazbot agent validate ${targetPath}\``);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
195
|
+
store.pushSyntheticAssistant(`No pude escribir el archivo: ${message}`, { error: true });
|
|
196
|
+
}
|
|
197
|
+
return { kind: 'noop' };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsea un input crudo del usuario.
|
|
3
|
+
* - Si empieza con `/`, retorna { name, args, raw } para ejecutar como slash command.
|
|
4
|
+
* - Si no, retorna null (mensaje normal al backend).
|
|
5
|
+
*/
|
|
6
|
+
export function parseSlash(input) {
|
|
7
|
+
const trimmed = input.trim();
|
|
8
|
+
if (!trimmed.startsWith('/'))
|
|
9
|
+
return null;
|
|
10
|
+
const without = trimmed.slice(1);
|
|
11
|
+
if (!without)
|
|
12
|
+
return { name: '', args: [], raw: trimmed };
|
|
13
|
+
// Split por espacios respetando comillas básicas.
|
|
14
|
+
const tokens = tokenize(without);
|
|
15
|
+
const [name, ...args] = tokens;
|
|
16
|
+
return { name: (name ?? '').toLowerCase(), args, raw: trimmed };
|
|
17
|
+
}
|
|
18
|
+
function tokenize(s) {
|
|
19
|
+
const out = [];
|
|
20
|
+
let cur = '';
|
|
21
|
+
let quote = null;
|
|
22
|
+
for (const ch of s) {
|
|
23
|
+
if (quote) {
|
|
24
|
+
if (ch === quote) {
|
|
25
|
+
quote = null;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
cur += ch;
|
|
29
|
+
}
|
|
30
|
+
else if (ch === '"' || ch === "'") {
|
|
31
|
+
quote = ch;
|
|
32
|
+
}
|
|
33
|
+
else if (/\s/.test(ch)) {
|
|
34
|
+
if (cur) {
|
|
35
|
+
out.push(cur);
|
|
36
|
+
cur = '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
cur += ch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (cur)
|
|
44
|
+
out.push(cur);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class SlashRegistry {
|
|
2
|
+
map = new Map();
|
|
3
|
+
order = [];
|
|
4
|
+
register(spec) {
|
|
5
|
+
this.order.push(spec);
|
|
6
|
+
this.map.set(spec.name, spec);
|
|
7
|
+
spec.aliases?.forEach((a) => this.map.set(a, spec));
|
|
8
|
+
}
|
|
9
|
+
get(name) {
|
|
10
|
+
return this.map.get(name);
|
|
11
|
+
}
|
|
12
|
+
list() {
|
|
13
|
+
return this.order.slice();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const slashRegistry = new SlashRegistry();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
function uid() {
|
|
4
|
+
// randomUUID está disponible en Node >=14.17 con crypto. Fallback simple.
|
|
5
|
+
try {
|
|
6
|
+
return randomUUID();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return Math.random().toString(36).slice(2);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const useStudioStore = create((set, get) => ({
|
|
13
|
+
messages: [],
|
|
14
|
+
steps: [],
|
|
15
|
+
agentConfig: null,
|
|
16
|
+
agentId: null,
|
|
17
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
18
|
+
streaming: false,
|
|
19
|
+
abortController: null,
|
|
20
|
+
waConnect: null,
|
|
21
|
+
pendingCompact: false,
|
|
22
|
+
pushUserMessage(content) {
|
|
23
|
+
const id = uid();
|
|
24
|
+
set((s) => ({ messages: [...s.messages, { id, role: 'user', content }] }));
|
|
25
|
+
return id;
|
|
26
|
+
},
|
|
27
|
+
pushSyntheticAssistant(content, opts) {
|
|
28
|
+
const id = uid();
|
|
29
|
+
set((s) => ({
|
|
30
|
+
messages: [...s.messages, { id, role: 'assistant', content, synthetic: true, error: opts?.error }],
|
|
31
|
+
}));
|
|
32
|
+
return id;
|
|
33
|
+
},
|
|
34
|
+
startAssistantMessage() {
|
|
35
|
+
const id = uid();
|
|
36
|
+
set((s) => ({ messages: [...s.messages, { id, role: 'assistant', content: '', streaming: true }] }));
|
|
37
|
+
return id;
|
|
38
|
+
},
|
|
39
|
+
appendAssistantText(id, delta) {
|
|
40
|
+
set((s) => ({
|
|
41
|
+
messages: s.messages.map((m) => (m.id === id ? { ...m, content: m.content + delta } : m)),
|
|
42
|
+
}));
|
|
43
|
+
},
|
|
44
|
+
finishAssistantMessage(id) {
|
|
45
|
+
set((s) => ({
|
|
46
|
+
messages: s.messages.map((m) => (m.id === id ? { ...m, streaming: false } : m)),
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
startStep(toolName, afterMessageId) {
|
|
50
|
+
const id = uid();
|
|
51
|
+
set((s) => ({ steps: [...s.steps, { id, toolName, status: 'running', afterMessageId }] }));
|
|
52
|
+
return id;
|
|
53
|
+
},
|
|
54
|
+
resolveLastStep(toolName, status, result, errorMessage) {
|
|
55
|
+
set((s) => {
|
|
56
|
+
const idx = [...s.steps].reverse().findIndex((x) => x.toolName === toolName && x.status === 'running');
|
|
57
|
+
if (idx === -1)
|
|
58
|
+
return {};
|
|
59
|
+
const real = s.steps.length - 1 - idx;
|
|
60
|
+
const updated = [...s.steps];
|
|
61
|
+
updated[real] = { ...updated[real], status, result, errorMessage };
|
|
62
|
+
return { steps: updated };
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
failAllRunningSteps(message) {
|
|
66
|
+
set((s) => ({
|
|
67
|
+
steps: s.steps.map((x) => x.status === 'running' ? { ...x, status: 'error', errorMessage: message ?? 'Cancelado' } : x),
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
setAgentConfig(cfg) {
|
|
71
|
+
set({ agentConfig: cfg });
|
|
72
|
+
},
|
|
73
|
+
setAgentId(id) {
|
|
74
|
+
set({ agentId: id });
|
|
75
|
+
},
|
|
76
|
+
addUsage(inT, outT) {
|
|
77
|
+
set((s) => ({
|
|
78
|
+
usage: {
|
|
79
|
+
inputTokens: s.usage.inputTokens + (inT || 0),
|
|
80
|
+
outputTokens: s.usage.outputTokens + (outT || 0),
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
setStreaming(streaming) {
|
|
85
|
+
set({ streaming });
|
|
86
|
+
},
|
|
87
|
+
setAbortController(abortController) {
|
|
88
|
+
set({ abortController });
|
|
89
|
+
},
|
|
90
|
+
abortStream() {
|
|
91
|
+
const ctrl = get().abortController;
|
|
92
|
+
if (ctrl && !ctrl.signal.aborted)
|
|
93
|
+
ctrl.abort();
|
|
94
|
+
},
|
|
95
|
+
startWaConnect(linkData, anchorMessageId) {
|
|
96
|
+
const id = uid();
|
|
97
|
+
set({
|
|
98
|
+
waConnect: {
|
|
99
|
+
id,
|
|
100
|
+
linkData,
|
|
101
|
+
status: 'waiting',
|
|
102
|
+
anchorMessageId,
|
|
103
|
+
attempt: 0,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
return id;
|
|
107
|
+
},
|
|
108
|
+
updateWaConnect(patch) {
|
|
109
|
+
set((s) => (s.waConnect ? { waConnect: { ...s.waConnect, ...patch } } : {}));
|
|
110
|
+
},
|
|
111
|
+
setWaStatus(status, extra) {
|
|
112
|
+
set((s) => (s.waConnect ? { waConnect: { ...s.waConnect, status, ...(extra ?? {}) } } : {}));
|
|
113
|
+
},
|
|
114
|
+
clearWaConnect() {
|
|
115
|
+
set({ waConnect: null });
|
|
116
|
+
},
|
|
117
|
+
clearMessages() {
|
|
118
|
+
const ctrl = get().abortController;
|
|
119
|
+
if (ctrl && !ctrl.signal.aborted)
|
|
120
|
+
ctrl.abort();
|
|
121
|
+
set({
|
|
122
|
+
messages: [],
|
|
123
|
+
steps: [],
|
|
124
|
+
streaming: false,
|
|
125
|
+
abortController: null,
|
|
126
|
+
waConnect: null,
|
|
127
|
+
pendingCompact: false,
|
|
128
|
+
// agentConfig, agentId, usage se mantienen para seguir trabajando.
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
setPendingCompact(v) {
|
|
132
|
+
set({ pendingCompact: v });
|
|
133
|
+
},
|
|
134
|
+
compactInto(id) {
|
|
135
|
+
set((s) => {
|
|
136
|
+
const summary = s.messages.find((m) => m.id === id);
|
|
137
|
+
if (!summary || !summary.content.trim()) {
|
|
138
|
+
return { pendingCompact: false };
|
|
139
|
+
}
|
|
140
|
+
const newId = uid();
|
|
141
|
+
return {
|
|
142
|
+
messages: [
|
|
143
|
+
{
|
|
144
|
+
id: newId,
|
|
145
|
+
role: 'assistant',
|
|
146
|
+
content: summary.content,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
steps: [],
|
|
150
|
+
waConnect: null,
|
|
151
|
+
pendingCompact: false,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
reset() {
|
|
156
|
+
const ctrl = get().abortController;
|
|
157
|
+
if (ctrl && !ctrl.signal.aborted)
|
|
158
|
+
ctrl.abort();
|
|
159
|
+
set({
|
|
160
|
+
messages: [],
|
|
161
|
+
steps: [],
|
|
162
|
+
agentConfig: null,
|
|
163
|
+
agentId: null,
|
|
164
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
165
|
+
streaming: false,
|
|
166
|
+
abortController: null,
|
|
167
|
+
waConnect: null,
|
|
168
|
+
pendingCompact: false,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
}));
|
|
172
|
+
/**
|
|
173
|
+
* Construye el array `messages` que se envía al backend a partir del estado.
|
|
174
|
+
* Filtra mensajes sintéticos (synthetic === true) y los aún en streaming vacíos.
|
|
175
|
+
*/
|
|
176
|
+
export function selectBackendMessages() {
|
|
177
|
+
const { messages } = useStudioStore.getState();
|
|
178
|
+
return messages
|
|
179
|
+
.filter((m) => !m.synthetic && (m.role === 'user' || m.role === 'assistant') && m.content.trim().length > 0)
|
|
180
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
181
|
+
}
|