@protoboxai/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/bin/protobox.js +8 -0
- package/dist/cli.js +60 -0
- package/dist/commands/auth.js +294 -0
- package/dist/commands/chat.js +203 -0
- package/dist/commands/config.js +237 -0
- package/dist/commands/health.js +59 -0
- package/dist/commands/index.js +25 -0
- package/dist/commands/knowledge.js +539 -0
- package/dist/commands/mcp.js +589 -0
- package/dist/commands/prompts.js +295 -0
- package/dist/commands/tools.js +632 -0
- package/dist/commands/toolsets.js +464 -0
- package/dist/commands/workspaces.js +170 -0
- package/dist/index.js +6 -0
- package/dist/utils/config-store.js +209 -0
- package/dist/utils/output.js +221 -0
- package/dist/utils/sdk-factory.js +46 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @chanl/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [Chanl AI](https://chanl.ai) platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @chanl/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @chanl/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Authenticate
|
|
21
|
+
chanl login
|
|
22
|
+
|
|
23
|
+
# Check health
|
|
24
|
+
chanl health
|
|
25
|
+
|
|
26
|
+
# List agents
|
|
27
|
+
chanl agents list
|
|
28
|
+
|
|
29
|
+
# Chat with an agent
|
|
30
|
+
chanl chat <agent-id>
|
|
31
|
+
|
|
32
|
+
# Run test scenarios
|
|
33
|
+
chanl scenarios list
|
|
34
|
+
chanl scenarios run <id>
|
|
35
|
+
|
|
36
|
+
# Manage tools
|
|
37
|
+
chanl tools list
|
|
38
|
+
chanl toolsets list
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Command | Description |
|
|
44
|
+
|---------|-------------|
|
|
45
|
+
| `chanl login` | Authenticate with API key or browser |
|
|
46
|
+
| `chanl health` | Check API health |
|
|
47
|
+
| `chanl agents` | Manage AI agents |
|
|
48
|
+
| `chanl tools` | Manage MCP tools |
|
|
49
|
+
| `chanl toolsets` | Manage tool collections |
|
|
50
|
+
| `chanl scenarios` | Run test scenarios |
|
|
51
|
+
| `chanl calls` | Import and analyze calls |
|
|
52
|
+
| `chanl call <phone>` | Make an outbound AI call |
|
|
53
|
+
| `chanl chat <agent>` | Interactive chat session |
|
|
54
|
+
| `chanl kb` | Manage knowledge base |
|
|
55
|
+
| `chanl memory` | Manage agent memory |
|
|
56
|
+
| `chanl scorecards` | Manage evaluation scorecards |
|
|
57
|
+
| `chanl prompts` | Manage prompt templates |
|
|
58
|
+
| `chanl config` | CLI configuration |
|
|
59
|
+
| `chanl mcp` | MCP server status |
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
chanl config show # Show current config
|
|
65
|
+
chanl config set baseUrl <url> # Custom API endpoint
|
|
66
|
+
chanl config reset # Reset to production
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Default API: `https://platform.chanl.ai`
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
- [Chanl Platform](https://chanl.ai)
|
|
74
|
+
- [GitHub](https://github.com/chanl-ai/chanl-sdk)
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/bin/protobox.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { setOutputOptions } from "./utils/output.js";
|
|
4
|
+
import {
|
|
5
|
+
createAuthCommand,
|
|
6
|
+
createLoginCommand,
|
|
7
|
+
createLogoutCommand,
|
|
8
|
+
createConfigCommand,
|
|
9
|
+
createToolsCommand,
|
|
10
|
+
createToolsetsCommand,
|
|
11
|
+
createHealthCommand,
|
|
12
|
+
createMcpCommand,
|
|
13
|
+
createKnowledgeCommand,
|
|
14
|
+
createChatCommand,
|
|
15
|
+
createPromptsCommand,
|
|
16
|
+
createWorkspacesCommand
|
|
17
|
+
} from "./commands/index.js";
|
|
18
|
+
const require2 = createRequire(import.meta.url);
|
|
19
|
+
const { version: CLI_VERSION } = require2("../package.json");
|
|
20
|
+
function createProgram() {
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program.name("protobox").description("Command-line interface for Protobox \u2014 MCP servers, tools, and resources").version(CLI_VERSION, "-v, --version", "Show CLI version").option("--json", "Output results as JSON").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
|
|
23
|
+
const opts = thisCommand.opts();
|
|
24
|
+
setOutputOptions({
|
|
25
|
+
json: opts["json"] === true,
|
|
26
|
+
noColor: opts["color"] === false
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
program.addCommand(createLoginCommand());
|
|
30
|
+
program.addCommand(createLogoutCommand());
|
|
31
|
+
program.addCommand(createAuthCommand());
|
|
32
|
+
program.addCommand(createConfigCommand());
|
|
33
|
+
program.addCommand(createToolsCommand());
|
|
34
|
+
program.addCommand(createToolsetsCommand());
|
|
35
|
+
program.addCommand(createHealthCommand());
|
|
36
|
+
program.addCommand(createMcpCommand());
|
|
37
|
+
program.addCommand(createKnowledgeCommand());
|
|
38
|
+
program.addCommand(createChatCommand());
|
|
39
|
+
program.addCommand(createPromptsCommand());
|
|
40
|
+
program.addCommand(createWorkspacesCommand());
|
|
41
|
+
program.showHelpAfterError(true);
|
|
42
|
+
program.showSuggestionAfterError(true);
|
|
43
|
+
return program;
|
|
44
|
+
}
|
|
45
|
+
async function run(argv = process.argv) {
|
|
46
|
+
const program = createProgram();
|
|
47
|
+
try {
|
|
48
|
+
await program.parseAsync(argv);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
console.error("Error:", error.message);
|
|
52
|
+
}
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
createProgram,
|
|
58
|
+
run
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { select } from "@inquirer/prompts";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
import { ChanlSDK } from "@protoboxai/sdk";
|
|
7
|
+
import { configStore } from "../utils/config-store.js";
|
|
8
|
+
import {
|
|
9
|
+
printSuccess,
|
|
10
|
+
printError,
|
|
11
|
+
printInfo,
|
|
12
|
+
printLabel,
|
|
13
|
+
printBlank,
|
|
14
|
+
isJsonOutput,
|
|
15
|
+
printJson,
|
|
16
|
+
maskString
|
|
17
|
+
} from "../utils/output.js";
|
|
18
|
+
function createAuthCommand() {
|
|
19
|
+
const auth = new Command("auth").description("Authentication commands");
|
|
20
|
+
auth.command("status").description("Show current authentication status").action(handleAuthStatus);
|
|
21
|
+
return auth;
|
|
22
|
+
}
|
|
23
|
+
function createLoginCommand() {
|
|
24
|
+
return new Command("login").description("Authenticate with Chanl API").option("-k, --api-key <key>", "API key to use for authentication").option("--browser", "Force browser-based login (email/password or Google/Microsoft)").option("--base-url <url>", "Override the API base URL").addHelpText(
|
|
25
|
+
"after",
|
|
26
|
+
`
|
|
27
|
+
Login Methods:
|
|
28
|
+
Default Opens browser for sign-in (Google, Microsoft, or email)
|
|
29
|
+
--api-key <key> Use an API key directly (skips browser)
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
$ chanl login # Browser login (recommended)
|
|
33
|
+
$ chanl login --api-key ak_xxx # API key login
|
|
34
|
+
$ chanl login --base-url http://localhost:3100 # Custom API URL`
|
|
35
|
+
).action(handleLogin);
|
|
36
|
+
}
|
|
37
|
+
function createLogoutCommand() {
|
|
38
|
+
return new Command("logout").description("Remove stored credentials").action(handleLogout);
|
|
39
|
+
}
|
|
40
|
+
async function handleLogin(options) {
|
|
41
|
+
if (options.baseUrl) {
|
|
42
|
+
configStore.setBaseUrl(options.baseUrl);
|
|
43
|
+
}
|
|
44
|
+
if (options.apiKey) {
|
|
45
|
+
await handleApiKeyLogin(options.apiKey);
|
|
46
|
+
} else {
|
|
47
|
+
await handleBrowserLogin();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function handleApiKeyLogin(apiKey) {
|
|
51
|
+
const baseUrl = configStore.getBaseUrl();
|
|
52
|
+
const spinner = ora("Validating API key...").start();
|
|
53
|
+
try {
|
|
54
|
+
const sdk = new ChanlSDK({ apiKey, baseUrl });
|
|
55
|
+
const healthResponse = await sdk.health.getHealth();
|
|
56
|
+
if (!healthResponse.success) {
|
|
57
|
+
spinner.fail("Cannot connect to API");
|
|
58
|
+
printError("API is not reachable", `Check that the server is running at ${baseUrl}`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const authTestResponse = await sdk.tools.list();
|
|
63
|
+
if (!authTestResponse.success) {
|
|
64
|
+
spinner.fail("Authentication failed");
|
|
65
|
+
printError("Invalid API key or unauthorized access");
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
configStore.setApiKey(apiKey);
|
|
70
|
+
spinner.succeed("Authentication successful");
|
|
71
|
+
if (isJsonOutput()) {
|
|
72
|
+
printJson({ success: true, method: "api-key", apiKey: maskString(apiKey), baseUrl });
|
|
73
|
+
} else {
|
|
74
|
+
printSuccess(`API key saved to ${configStore.getPath()}`);
|
|
75
|
+
printInfo("Workspace context is determined by your API key");
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
spinner.fail("Authentication failed");
|
|
79
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
80
|
+
printError("Failed to validate API key", message);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function handleBrowserLogin() {
|
|
85
|
+
const baseUrl = configStore.getBaseUrl();
|
|
86
|
+
const appUrl = configStore.getAppUrl();
|
|
87
|
+
printBlank();
|
|
88
|
+
printInfo("Opening browser for authentication...");
|
|
89
|
+
try {
|
|
90
|
+
const { firebaseToken, port } = await startCallbackServer();
|
|
91
|
+
const authUrl = `${appUrl}/cli-auth?port=${port}`;
|
|
92
|
+
const open = (await import("open")).default;
|
|
93
|
+
await open(authUrl);
|
|
94
|
+
printInfo(`If the browser didn't open, visit:`);
|
|
95
|
+
console.log(chalk.cyan(` ${authUrl}`));
|
|
96
|
+
printBlank();
|
|
97
|
+
const spinner = ora("Waiting for authentication...").start();
|
|
98
|
+
const token = await firebaseToken;
|
|
99
|
+
spinner.text = "Exchanging token...";
|
|
100
|
+
const sdk = new ChanlSDK({ baseUrl });
|
|
101
|
+
const response = await sdk.auth.exchangeFirebaseToken(token);
|
|
102
|
+
if (!response.success || !response.data) {
|
|
103
|
+
spinner.fail("Token exchange failed");
|
|
104
|
+
printError("Failed to exchange token", response.message);
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const { accessToken, refreshToken, user, defaultWorkspaceId, workspaces } = response.data;
|
|
109
|
+
configStore.setJwtAuth(accessToken, refreshToken);
|
|
110
|
+
let selectedWorkspaceId = defaultWorkspaceId;
|
|
111
|
+
if (workspaces && workspaces.length > 1 && !isJsonOutput()) {
|
|
112
|
+
spinner.stop();
|
|
113
|
+
printBlank();
|
|
114
|
+
selectedWorkspaceId = await select({
|
|
115
|
+
message: "Select a workspace:",
|
|
116
|
+
choices: workspaces.map((ws) => ({
|
|
117
|
+
name: ws.name || ws.workspaceId,
|
|
118
|
+
value: ws.workspaceId,
|
|
119
|
+
description: ws.role
|
|
120
|
+
})),
|
|
121
|
+
default: defaultWorkspaceId
|
|
122
|
+
});
|
|
123
|
+
} else if (workspaces && workspaces.length === 1) {
|
|
124
|
+
selectedWorkspaceId = workspaces[0].workspaceId;
|
|
125
|
+
}
|
|
126
|
+
if (selectedWorkspaceId) {
|
|
127
|
+
configStore.setWorkspaceId(selectedWorkspaceId);
|
|
128
|
+
}
|
|
129
|
+
spinner.succeed("Authentication successful");
|
|
130
|
+
if (isJsonOutput()) {
|
|
131
|
+
printJson({
|
|
132
|
+
success: true,
|
|
133
|
+
method: "browser",
|
|
134
|
+
user: { email: user.email, displayName: user.displayName },
|
|
135
|
+
workspaceId: selectedWorkspaceId
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
printBlank();
|
|
139
|
+
printLabel("User", user.email);
|
|
140
|
+
if (user.displayName) printLabel("Name", user.displayName);
|
|
141
|
+
if (selectedWorkspaceId) printLabel("Workspace", selectedWorkspaceId);
|
|
142
|
+
printBlank();
|
|
143
|
+
printSuccess(`Credentials saved to ${configStore.getPath()}`);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
147
|
+
if (message === "LOGIN_TIMEOUT") {
|
|
148
|
+
printError("Login timed out", "No response from browser within 2 minutes");
|
|
149
|
+
} else if (message === "LOGIN_CANCELLED") {
|
|
150
|
+
printInfo("Login cancelled");
|
|
151
|
+
} else {
|
|
152
|
+
printError("Browser login failed", message);
|
|
153
|
+
}
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function startCallbackServer() {
|
|
158
|
+
return new Promise((resolveSetup) => {
|
|
159
|
+
let resolveToken;
|
|
160
|
+
let rejectToken;
|
|
161
|
+
const firebaseToken = new Promise((resolve, reject) => {
|
|
162
|
+
resolveToken = resolve;
|
|
163
|
+
rejectToken = reject;
|
|
164
|
+
});
|
|
165
|
+
const server = http.createServer((req, res) => {
|
|
166
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
167
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
168
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
169
|
+
if (req.method === "OPTIONS") {
|
|
170
|
+
res.writeHead(204);
|
|
171
|
+
res.end();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (req.method === "POST" && req.url === "/callback") {
|
|
175
|
+
let body = "";
|
|
176
|
+
req.on("data", (chunk) => {
|
|
177
|
+
body += chunk.toString();
|
|
178
|
+
});
|
|
179
|
+
req.on("end", () => {
|
|
180
|
+
try {
|
|
181
|
+
const data = JSON.parse(body);
|
|
182
|
+
if (data.error) {
|
|
183
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
184
|
+
res.end(JSON.stringify({ success: false }));
|
|
185
|
+
rejectToken(new Error(data.error));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!data.token) {
|
|
189
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
190
|
+
res.end(JSON.stringify({ error: "Missing token" }));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
194
|
+
res.end(JSON.stringify({ success: true }));
|
|
195
|
+
resolveToken(data.token);
|
|
196
|
+
} catch {
|
|
197
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
198
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
res.writeHead(404);
|
|
204
|
+
res.end();
|
|
205
|
+
});
|
|
206
|
+
server.listen(0, "127.0.0.1", () => {
|
|
207
|
+
const addr = server.address();
|
|
208
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
209
|
+
const timeout = setTimeout(() => {
|
|
210
|
+
server.close();
|
|
211
|
+
rejectToken(new Error("LOGIN_TIMEOUT"));
|
|
212
|
+
}, 12e4);
|
|
213
|
+
firebaseToken.then(() => {
|
|
214
|
+
clearTimeout(timeout);
|
|
215
|
+
setTimeout(() => server.close(), 500);
|
|
216
|
+
}).catch(() => {
|
|
217
|
+
clearTimeout(timeout);
|
|
218
|
+
server.close();
|
|
219
|
+
});
|
|
220
|
+
resolveSetup({ firebaseToken, port, server });
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function handleLogout() {
|
|
225
|
+
if (!configStore.isAuthenticated()) {
|
|
226
|
+
if (isJsonOutput()) {
|
|
227
|
+
printJson({ success: true, message: "Already logged out" });
|
|
228
|
+
} else {
|
|
229
|
+
printInfo("Already logged out");
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
configStore.clearAuth();
|
|
234
|
+
configStore.delete("workspaceId");
|
|
235
|
+
if (isJsonOutput()) {
|
|
236
|
+
printJson({ success: true, message: "Logged out successfully" });
|
|
237
|
+
} else {
|
|
238
|
+
printSuccess("Logged out successfully");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function handleAuthStatus() {
|
|
242
|
+
const authMethod = configStore.getAuthMethod();
|
|
243
|
+
const baseUrl = configStore.getBaseUrl();
|
|
244
|
+
const workspaceId = configStore.getWorkspaceId();
|
|
245
|
+
if (isJsonOutput()) {
|
|
246
|
+
printJson({
|
|
247
|
+
authenticated: authMethod !== null,
|
|
248
|
+
method: authMethod,
|
|
249
|
+
apiKey: authMethod === "api-key" ? maskString(configStore.getApiKey()) : null,
|
|
250
|
+
jwtToken: authMethod === "jwt" ? "(set)" : null,
|
|
251
|
+
workspaceId: workspaceId ?? null,
|
|
252
|
+
baseUrl
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
printBlank();
|
|
257
|
+
if (!authMethod) {
|
|
258
|
+
printLabel("Status", chalk.yellow("Not authenticated"));
|
|
259
|
+
printBlank();
|
|
260
|
+
printInfo("Run 'chanl login' to authenticate via browser");
|
|
261
|
+
printInfo("Run 'chanl login --api-key <key>' to use an API key");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
printLabel("Status", chalk.green("Authenticated"));
|
|
265
|
+
printLabel("Method", authMethod === "jwt" ? "Browser (JWT)" : "API Key");
|
|
266
|
+
if (authMethod === "api-key") {
|
|
267
|
+
printLabel("API Key", maskString(configStore.getApiKey()));
|
|
268
|
+
}
|
|
269
|
+
printLabel("Base URL", baseUrl);
|
|
270
|
+
if (workspaceId) {
|
|
271
|
+
printLabel("Workspace ID", workspaceId);
|
|
272
|
+
try {
|
|
273
|
+
const sdkConfig = { baseUrl };
|
|
274
|
+
if (authMethod === "api-key") {
|
|
275
|
+
sdkConfig.apiKey = configStore.getApiKey();
|
|
276
|
+
} else {
|
|
277
|
+
sdkConfig.jwtToken = configStore.getJwtToken();
|
|
278
|
+
}
|
|
279
|
+
const sdk = new ChanlSDK(sdkConfig);
|
|
280
|
+
const response = await sdk.workspace.getById(workspaceId);
|
|
281
|
+
if (response.success && response.data?.name) {
|
|
282
|
+
printLabel("Workspace Name", response.data.name);
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
printBlank();
|
|
288
|
+
}
|
|
289
|
+
export {
|
|
290
|
+
createAuthCommand,
|
|
291
|
+
createLoginCommand,
|
|
292
|
+
createLogoutCommand
|
|
293
|
+
};
|
|
294
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { createSdk } from "../utils/sdk-factory.js";
|
|
6
|
+
import {
|
|
7
|
+
printError,
|
|
8
|
+
printSuccess,
|
|
9
|
+
printInfo,
|
|
10
|
+
printBlank,
|
|
11
|
+
isJsonOutput,
|
|
12
|
+
printJson
|
|
13
|
+
} from "../utils/output.js";
|
|
14
|
+
function createChatCommand() {
|
|
15
|
+
const chat = new Command("chat").description("Start an interactive chat session with an agent").argument("<agent-id>", "ID of the agent to chat with").option("-m, --message <text>", "Send a single message and exit").option("-s, --stream", "Enable streaming responses (real-time text)").option("-w, --workspace <id>", "Workspace ID override").addHelpText(
|
|
16
|
+
"after",
|
|
17
|
+
`
|
|
18
|
+
What is Chat?
|
|
19
|
+
Chat lets you test an AI agent via text in your terminal. It creates a session
|
|
20
|
+
on the Chanl platform, which manages the full lifecycle including
|
|
21
|
+
transcription, billing, and post-chat evaluation.
|
|
22
|
+
|
|
23
|
+
Modes:
|
|
24
|
+
Interactive (default): Opens a prompt loop \u2014 type messages, see agent responses.
|
|
25
|
+
Type 'exit', 'quit', or press Ctrl+C to end.
|
|
26
|
+
|
|
27
|
+
Single message: Send one message with --message, print response, exit.
|
|
28
|
+
|
|
29
|
+
Streaming:
|
|
30
|
+
Use --stream for real-time text output. Text appears as the agent generates it.
|
|
31
|
+
Note: streaming responses do not include tool call metadata.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
$ chanl chat abc123 # Interactive chat
|
|
35
|
+
$ chanl chat abc123 --stream # Interactive with streaming
|
|
36
|
+
$ chanl chat abc123 --message "Hello" # Single message mode
|
|
37
|
+
$ chanl chat abc123 --message "Hi" --stream # Single message, streamed
|
|
38
|
+
$ chanl chat abc123 --message "Hi" --json # Single message, JSON output
|
|
39
|
+
$ chanl chat abc123 --workspace ws_456 # Override workspace`
|
|
40
|
+
).action(handleChat);
|
|
41
|
+
return chat;
|
|
42
|
+
}
|
|
43
|
+
async function handleChat(agentId, options) {
|
|
44
|
+
const sdk = createSdk();
|
|
45
|
+
if (!sdk) return;
|
|
46
|
+
if (options.message) {
|
|
47
|
+
await handleSingleMessage(sdk, agentId, options.message, options.stream);
|
|
48
|
+
} else {
|
|
49
|
+
await handleInteractiveChat(sdk, agentId, options.stream);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function handleSingleMessage(sdk, agentId, message, stream) {
|
|
53
|
+
const spinner = ora("Connecting to agent...").start();
|
|
54
|
+
try {
|
|
55
|
+
const chat = await sdk.chat.session(agentId);
|
|
56
|
+
spinner.text = "Sending message...";
|
|
57
|
+
try {
|
|
58
|
+
if (stream && !isJsonOutput()) {
|
|
59
|
+
spinner.stop();
|
|
60
|
+
printBlank();
|
|
61
|
+
console.log(chalk.dim("You >"), message);
|
|
62
|
+
process.stdout.write(chalk.cyan("Agent > "));
|
|
63
|
+
const reply = await chat.send(message, {
|
|
64
|
+
stream: true,
|
|
65
|
+
onChunk: (delta) => process.stdout.write(delta)
|
|
66
|
+
});
|
|
67
|
+
console.log();
|
|
68
|
+
if (reply.latencyMs) {
|
|
69
|
+
printBlank();
|
|
70
|
+
printInfo(`Response time: ${reply.latencyMs}ms`);
|
|
71
|
+
}
|
|
72
|
+
printBlank();
|
|
73
|
+
} else {
|
|
74
|
+
const reply = await chat.send(message);
|
|
75
|
+
spinner.stop();
|
|
76
|
+
if (isJsonOutput()) {
|
|
77
|
+
printJson({
|
|
78
|
+
sessionId: chat.sessionId,
|
|
79
|
+
agentId,
|
|
80
|
+
message,
|
|
81
|
+
response: reply.message,
|
|
82
|
+
toolCalls: reply.toolCalls,
|
|
83
|
+
latencyMs: reply.latencyMs
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
printBlank();
|
|
87
|
+
console.log(chalk.dim("You >"), message);
|
|
88
|
+
if (reply.toolCalls?.length) {
|
|
89
|
+
console.log(chalk.dim(" Tools:"));
|
|
90
|
+
printToolCalls(reply.toolCalls);
|
|
91
|
+
}
|
|
92
|
+
console.log(chalk.cyan("Agent >"), reply.message);
|
|
93
|
+
if (reply.latencyMs) {
|
|
94
|
+
printBlank();
|
|
95
|
+
printInfo(`Response time: ${reply.latencyMs}ms`);
|
|
96
|
+
}
|
|
97
|
+
printBlank();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
await chat.end();
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
spinner.fail("Chat failed");
|
|
105
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
106
|
+
printError("Error", errorMessage);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function handleInteractiveChat(sdk, agentId, stream) {
|
|
111
|
+
const spinner = ora("Connecting to agent...").start();
|
|
112
|
+
try {
|
|
113
|
+
const chat = await sdk.chat.session(agentId);
|
|
114
|
+
const agentName = chat.agentName || "Agent";
|
|
115
|
+
spinner.succeed(`Connected to ${agentName}${stream ? " (streaming)" : ""}`);
|
|
116
|
+
printInfo("Type 'exit' or 'quit' to end the session. Ctrl+C also works.");
|
|
117
|
+
printBlank();
|
|
118
|
+
const rl = createInterface({
|
|
119
|
+
input: process.stdin,
|
|
120
|
+
output: process.stdout
|
|
121
|
+
});
|
|
122
|
+
const prompt = () => {
|
|
123
|
+
rl.question(chalk.bold("You > "), async (input) => {
|
|
124
|
+
const trimmed = input.trim();
|
|
125
|
+
if (!trimmed) {
|
|
126
|
+
prompt();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
|
|
130
|
+
await endSessionCleanly(chat);
|
|
131
|
+
rl.close();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
if (stream) {
|
|
136
|
+
process.stdout.write(chalk.cyan("Agent > "));
|
|
137
|
+
const reply = await chat.send(trimmed, {
|
|
138
|
+
stream: true,
|
|
139
|
+
onChunk: (delta) => process.stdout.write(delta)
|
|
140
|
+
});
|
|
141
|
+
console.log();
|
|
142
|
+
if (reply.latencyMs) {
|
|
143
|
+
printInfo(`${reply.latencyMs}ms`);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
const reply = await chat.send(trimmed);
|
|
147
|
+
if (reply.toolCalls?.length) {
|
|
148
|
+
console.log(chalk.dim(" Tools:"));
|
|
149
|
+
printToolCalls(reply.toolCalls);
|
|
150
|
+
}
|
|
151
|
+
console.log(chalk.cyan("Agent >"), reply.message);
|
|
152
|
+
}
|
|
153
|
+
printBlank();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
156
|
+
printError("Send failed", errorMessage);
|
|
157
|
+
printBlank();
|
|
158
|
+
}
|
|
159
|
+
prompt();
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const handleSigint = async () => {
|
|
163
|
+
printBlank();
|
|
164
|
+
await endSessionCleanly(chat);
|
|
165
|
+
rl.close();
|
|
166
|
+
process.exit(0);
|
|
167
|
+
};
|
|
168
|
+
rl.on("SIGINT", () => {
|
|
169
|
+
void handleSigint();
|
|
170
|
+
});
|
|
171
|
+
rl.on("close", () => {
|
|
172
|
+
});
|
|
173
|
+
prompt();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
spinner.fail("Failed to connect");
|
|
176
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
177
|
+
printError("Error", errorMessage);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function printToolCalls(toolCalls) {
|
|
182
|
+
for (const tc of toolCalls) {
|
|
183
|
+
const statusIcon = tc.status === "success" ? chalk.green("\u2713") : tc.status === "error" ? chalk.red("\u2717") : tc.status === "timeout" ? chalk.yellow("\u23F1") : chalk.dim("\u2026");
|
|
184
|
+
const duration = tc.durationMs ? chalk.dim(` (${tc.durationMs}ms)`) : "";
|
|
185
|
+
const params = Object.keys(tc.parameters).length ? chalk.dim(` ${JSON.stringify(tc.parameters)}`) : "";
|
|
186
|
+
console.log(` ${statusIcon} ${chalk.yellow(tc.name)}${params}${duration}`);
|
|
187
|
+
if (tc.status === "error" && tc.error) {
|
|
188
|
+
console.log(` ${chalk.red(tc.error.message)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function endSessionCleanly(chat) {
|
|
193
|
+
try {
|
|
194
|
+
await chat.end();
|
|
195
|
+
printSuccess("Session ended");
|
|
196
|
+
} catch {
|
|
197
|
+
printInfo("Session closed");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
createChatCommand
|
|
202
|
+
};
|
|
203
|
+
//# sourceMappingURL=chat.js.map
|