@waniwani/cli 0.0.5

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/dist/index.js ADDED
@@ -0,0 +1,1234 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command15 } from "commander";
5
+
6
+ // src/commands/login.ts
7
+ import { spawn } from "child_process";
8
+ import { createServer } from "http";
9
+ import chalk3 from "chalk";
10
+ import { Command } from "commander";
11
+ import ora from "ora";
12
+
13
+ // src/lib/auth.ts
14
+ import { access, mkdir, readFile, writeFile } from "fs/promises";
15
+ import { homedir } from "os";
16
+ import { join } from "path";
17
+ import { z } from "zod";
18
+ var CONFIG_DIR = join(homedir(), ".waniwani");
19
+ var AUTH_FILE = join(CONFIG_DIR, "auth.json");
20
+ var AuthStoreSchema = z.object({
21
+ accessToken: z.string().nullable().default(null),
22
+ refreshToken: z.string().nullable().default(null),
23
+ expiresAt: z.string().nullable().default(null)
24
+ });
25
+ var API_BASE_URL = process.env.WANIWANI_API_URL || "https://waniwani.com";
26
+ async function ensureConfigDir() {
27
+ await mkdir(CONFIG_DIR, { recursive: true });
28
+ }
29
+ async function readAuthStore() {
30
+ await ensureConfigDir();
31
+ try {
32
+ await access(AUTH_FILE);
33
+ const content = await readFile(AUTH_FILE, "utf-8");
34
+ return AuthStoreSchema.parse(JSON.parse(content));
35
+ } catch {
36
+ return AuthStoreSchema.parse({});
37
+ }
38
+ }
39
+ async function writeAuthStore(store) {
40
+ await ensureConfigDir();
41
+ await writeFile(AUTH_FILE, JSON.stringify(store, null, 2), "utf-8");
42
+ }
43
+ var AuthManager = class {
44
+ storeCache = null;
45
+ async getStore() {
46
+ if (!this.storeCache) {
47
+ this.storeCache = await readAuthStore();
48
+ }
49
+ return this.storeCache;
50
+ }
51
+ async saveStore(store) {
52
+ this.storeCache = store;
53
+ await writeAuthStore(store);
54
+ }
55
+ async isLoggedIn() {
56
+ const store = await this.getStore();
57
+ return !!store.accessToken;
58
+ }
59
+ async getAccessToken() {
60
+ const store = await this.getStore();
61
+ return store.accessToken;
62
+ }
63
+ async getRefreshToken() {
64
+ const store = await this.getStore();
65
+ return store.refreshToken;
66
+ }
67
+ async setTokens(accessToken, refreshToken, expiresIn) {
68
+ const expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
69
+ const store = await this.getStore();
70
+ store.accessToken = accessToken;
71
+ store.refreshToken = refreshToken;
72
+ store.expiresAt = expiresAt;
73
+ await this.saveStore(store);
74
+ }
75
+ async clear() {
76
+ const emptyStore = AuthStoreSchema.parse({});
77
+ await this.saveStore(emptyStore);
78
+ }
79
+ async isTokenExpired() {
80
+ const store = await this.getStore();
81
+ if (!store.expiresAt) return true;
82
+ return new Date(store.expiresAt).getTime() - 5 * 60 * 1e3 < Date.now();
83
+ }
84
+ async tryRefreshToken() {
85
+ const refreshToken = await this.getRefreshToken();
86
+ if (!refreshToken) return false;
87
+ try {
88
+ const response = await fetch(`${API_BASE_URL}/api/auth/oauth2/token`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
91
+ body: new URLSearchParams({
92
+ grant_type: "refresh_token",
93
+ refresh_token: refreshToken,
94
+ client_id: "waniwani-cli"
95
+ }).toString()
96
+ });
97
+ if (!response.ok) {
98
+ await this.clear();
99
+ return false;
100
+ }
101
+ const data = await response.json();
102
+ await this.setTokens(
103
+ data.access_token,
104
+ data.refresh_token,
105
+ data.expires_in
106
+ );
107
+ return true;
108
+ } catch {
109
+ await this.clear();
110
+ return false;
111
+ }
112
+ }
113
+ };
114
+ var auth = new AuthManager();
115
+
116
+ // src/lib/errors.ts
117
+ import chalk from "chalk";
118
+ import { ZodError } from "zod";
119
+ var CLIError = class extends Error {
120
+ constructor(message, code, details) {
121
+ super(message);
122
+ this.code = code;
123
+ this.details = details;
124
+ this.name = "CLIError";
125
+ }
126
+ };
127
+ var AuthError = class extends CLIError {
128
+ constructor(message, details) {
129
+ super(message, "AUTH_ERROR", details);
130
+ }
131
+ };
132
+ var SandboxError = class extends CLIError {
133
+ constructor(message, details) {
134
+ super(message, "SANDBOX_ERROR", details);
135
+ }
136
+ };
137
+ var McpError = class extends CLIError {
138
+ constructor(message, details) {
139
+ super(message, "MCP_ERROR", details);
140
+ }
141
+ };
142
+ function handleError(error, json) {
143
+ if (error instanceof ZodError) {
144
+ const message = error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
145
+ outputError("VALIDATION_ERROR", `Invalid input: ${message}`, json);
146
+ } else if (error instanceof CLIError) {
147
+ outputError(error.code, error.message, json, error.details);
148
+ } else if (error instanceof Error) {
149
+ outputError("UNKNOWN_ERROR", error.message, json);
150
+ } else {
151
+ outputError("UNKNOWN_ERROR", String(error), json);
152
+ }
153
+ }
154
+ function outputError(code, message, json, details) {
155
+ if (json) {
156
+ console.error(
157
+ JSON.stringify({ success: false, error: { code, message, details } })
158
+ );
159
+ } else {
160
+ console.error(chalk.red(`Error [${code}]:`), message);
161
+ if (details) {
162
+ console.error(chalk.gray("Details:"), JSON.stringify(details, null, 2));
163
+ }
164
+ }
165
+ }
166
+
167
+ // src/lib/output.ts
168
+ import chalk2 from "chalk";
169
+ function formatOutput(data, json) {
170
+ if (json) {
171
+ console.log(JSON.stringify({ success: true, data }, null, 2));
172
+ } else {
173
+ prettyPrint(data);
174
+ }
175
+ }
176
+ function formatSuccess(message, json) {
177
+ if (json) {
178
+ console.log(JSON.stringify({ success: true, message }));
179
+ } else {
180
+ console.log(chalk2.green("\u2713"), message);
181
+ }
182
+ }
183
+ function formatTable(headers, rows, json) {
184
+ if (json) {
185
+ const data = rows.map(
186
+ (row) => Object.fromEntries(headers.map((header, i) => [header, row[i]]))
187
+ );
188
+ console.log(JSON.stringify({ success: true, data }, null, 2));
189
+ } else {
190
+ const colWidths = headers.map(
191
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || "").length))
192
+ );
193
+ const separator = colWidths.map((w) => "-".repeat(w + 2)).join("+");
194
+ const formatRow = (row) => row.map((cell, i) => ` ${(cell || "").padEnd(colWidths[i])} `).join("|");
195
+ console.log(chalk2.cyan(formatRow(headers)));
196
+ console.log(separator);
197
+ for (const row of rows) {
198
+ console.log(formatRow(row));
199
+ }
200
+ }
201
+ }
202
+ function formatList(items, json) {
203
+ if (json) {
204
+ const data = Object.fromEntries(
205
+ items.map((item) => [item.label, item.value])
206
+ );
207
+ console.log(JSON.stringify({ success: true, data }, null, 2));
208
+ } else {
209
+ const maxLabelLength = Math.max(...items.map((i) => i.label.length));
210
+ items.forEach((item) => {
211
+ console.log(
212
+ `${chalk2.gray(item.label.padEnd(maxLabelLength))} ${chalk2.white(item.value)}`
213
+ );
214
+ });
215
+ }
216
+ }
217
+ function prettyPrint(data, indent = 0) {
218
+ const prefix = " ".repeat(indent);
219
+ if (Array.isArray(data)) {
220
+ data.forEach((item, index) => {
221
+ console.log(`${prefix}${chalk2.gray(`[${index}]`)}`);
222
+ prettyPrint(item, indent + 1);
223
+ });
224
+ } else if (typeof data === "object" && data !== null) {
225
+ for (const [key, value] of Object.entries(data)) {
226
+ if (typeof value === "object" && value !== null) {
227
+ console.log(`${prefix}${chalk2.gray(key)}:`);
228
+ prettyPrint(value, indent + 1);
229
+ } else {
230
+ console.log(
231
+ `${prefix}${chalk2.gray(key)}: ${chalk2.white(String(value))}`
232
+ );
233
+ }
234
+ }
235
+ } else {
236
+ console.log(`${prefix}${chalk2.white(String(data))}`);
237
+ }
238
+ }
239
+
240
+ // src/commands/login.ts
241
+ var API_BASE_URL2 = process.env.WANIWANI_API_URL || "https://waniwani.com";
242
+ var CALLBACK_PORT = 54321;
243
+ var CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
244
+ var CLIENT_ID = "waniwani-cli";
245
+ function generateCodeVerifier() {
246
+ const array = new Uint8Array(32);
247
+ crypto.getRandomValues(array);
248
+ return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
249
+ }
250
+ async function generateCodeChallenge(verifier) {
251
+ const encoder = new TextEncoder();
252
+ const data = encoder.encode(verifier);
253
+ const hash = await crypto.subtle.digest("SHA-256", data);
254
+ return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
255
+ }
256
+ function generateState() {
257
+ const array = new Uint8Array(16);
258
+ crypto.getRandomValues(array);
259
+ return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
260
+ }
261
+ async function openBrowser(url) {
262
+ const [cmd, ...args] = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
263
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
264
+ }
265
+ async function waitForCallback(expectedState, timeoutMs = 3e5) {
266
+ return new Promise((resolve, reject) => {
267
+ let server = null;
268
+ const timeout = setTimeout(() => {
269
+ server?.close();
270
+ reject(new CLIError("Login timed out", "LOGIN_TIMEOUT"));
271
+ }, timeoutMs);
272
+ const cleanup = () => {
273
+ clearTimeout(timeout);
274
+ server?.close();
275
+ };
276
+ const htmlResponse = (title, message, color) => `<html>
277
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
278
+ <h1 style="color: ${color};">${title}</h1>
279
+ <p>${message}</p>
280
+ <p>You can close this window.</p>
281
+ </body>
282
+ </html>`;
283
+ try {
284
+ server = createServer((req, res) => {
285
+ const url = new URL(
286
+ req.url || "/",
287
+ `http://localhost:${CALLBACK_PORT}`
288
+ );
289
+ if (url.pathname === "/callback") {
290
+ const code = url.searchParams.get("code");
291
+ const state = url.searchParams.get("state");
292
+ const error = url.searchParams.get("error");
293
+ res.setHeader("Content-Type", "text/html");
294
+ if (error) {
295
+ res.statusCode = 400;
296
+ res.end(htmlResponse("Login Failed", `Error: ${error}`, "#ef4444"));
297
+ cleanup();
298
+ reject(new CLIError(`OAuth error: ${error}`, "OAUTH_ERROR"));
299
+ return;
300
+ }
301
+ if (state !== expectedState) {
302
+ res.statusCode = 400;
303
+ res.end(
304
+ htmlResponse(
305
+ "Login Failed",
306
+ "Invalid state parameter. Please try again.",
307
+ "#ef4444"
308
+ )
309
+ );
310
+ cleanup();
311
+ reject(new CLIError("Invalid state parameter", "INVALID_STATE"));
312
+ return;
313
+ }
314
+ if (!code) {
315
+ res.statusCode = 400;
316
+ res.end(
317
+ htmlResponse(
318
+ "Login Failed",
319
+ "No authorization code received.",
320
+ "#ef4444"
321
+ )
322
+ );
323
+ cleanup();
324
+ reject(new CLIError("No authorization code", "NO_CODE"));
325
+ return;
326
+ }
327
+ res.statusCode = 200;
328
+ res.end(
329
+ htmlResponse(
330
+ "Login Successful!",
331
+ "You can close this window and return to the terminal.",
332
+ "#22c55e"
333
+ )
334
+ );
335
+ setTimeout(() => {
336
+ cleanup();
337
+ resolve(code);
338
+ }, 100);
339
+ return;
340
+ }
341
+ res.statusCode = 404;
342
+ res.end("Not found");
343
+ });
344
+ server.on("error", (err) => {
345
+ cleanup();
346
+ if (err.code === "EADDRINUSE") {
347
+ reject(
348
+ new CLIError(
349
+ `Port ${CALLBACK_PORT} is already in use. Close any other WaniWani CLI instances and try again.`,
350
+ "PORT_IN_USE"
351
+ )
352
+ );
353
+ } else {
354
+ reject(err);
355
+ }
356
+ });
357
+ server.listen(CALLBACK_PORT);
358
+ } catch (err) {
359
+ cleanup();
360
+ reject(err);
361
+ }
362
+ });
363
+ }
364
+ async function exchangeCodeForToken(code, codeVerifier) {
365
+ const response = await fetch(`${API_BASE_URL2}/api/auth/oauth2/token`, {
366
+ method: "POST",
367
+ headers: {
368
+ "Content-Type": "application/x-www-form-urlencoded"
369
+ },
370
+ body: new URLSearchParams({
371
+ grant_type: "authorization_code",
372
+ code,
373
+ redirect_uri: CALLBACK_URL,
374
+ client_id: CLIENT_ID,
375
+ code_verifier: codeVerifier
376
+ }).toString()
377
+ });
378
+ if (!response.ok) {
379
+ const error = await response.json().catch(() => ({}));
380
+ throw new CLIError(
381
+ error.error_description || "Failed to exchange code for token",
382
+ "TOKEN_EXCHANGE_FAILED"
383
+ );
384
+ }
385
+ return response.json();
386
+ }
387
+ var loginCommand = new Command("login").description("Log in to WaniWani").option("--no-browser", "Don't open the browser automatically").action(async (options, command) => {
388
+ const globalOptions = command.optsWithGlobals();
389
+ const json = globalOptions.json ?? false;
390
+ try {
391
+ if (await auth.isLoggedIn()) {
392
+ if (json) {
393
+ formatOutput({ alreadyLoggedIn: true }, true);
394
+ } else {
395
+ console.log(
396
+ chalk3.yellow(
397
+ "Already logged in. Use 'waniwani logout' to log out first."
398
+ )
399
+ );
400
+ }
401
+ return;
402
+ }
403
+ if (!json) {
404
+ console.log(chalk3.bold("\nWaniWani CLI Login\n"));
405
+ }
406
+ const codeVerifier = generateCodeVerifier();
407
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
408
+ const state = generateState();
409
+ const authUrl = new URL(`${API_BASE_URL2}/oauth/authorize`);
410
+ authUrl.searchParams.set("client_id", CLIENT_ID);
411
+ authUrl.searchParams.set("redirect_uri", CALLBACK_URL);
412
+ authUrl.searchParams.set("response_type", "code");
413
+ authUrl.searchParams.set("code_challenge", codeChallenge);
414
+ authUrl.searchParams.set("code_challenge_method", "S256");
415
+ authUrl.searchParams.set("state", state);
416
+ if (!json) {
417
+ console.log("Opening browser for authentication...\n");
418
+ console.log(`If the browser doesn't open, visit:
419
+ `);
420
+ console.log(chalk3.cyan(` ${authUrl.toString()}`));
421
+ console.log();
422
+ }
423
+ const callbackPromise = waitForCallback(state);
424
+ if (options.browser !== false) {
425
+ await openBrowser(authUrl.toString());
426
+ }
427
+ const spinner = ora("Waiting for authorization...").start();
428
+ const code = await callbackPromise;
429
+ spinner.text = "Exchanging code for token...";
430
+ const tokenResponse = await exchangeCodeForToken(code, codeVerifier);
431
+ await auth.setTokens(
432
+ tokenResponse.access_token,
433
+ tokenResponse.refresh_token,
434
+ tokenResponse.expires_in
435
+ );
436
+ spinner.succeed("Logged in successfully!");
437
+ if (json) {
438
+ formatOutput({ success: true, loggedIn: true }, true);
439
+ } else {
440
+ console.log();
441
+ formatSuccess("You're now logged in to WaniWani!", false);
442
+ console.log();
443
+ console.log("Get started:");
444
+ console.log(
445
+ " waniwani mcp create my-server Create a new MCP sandbox"
446
+ );
447
+ console.log(' waniwani task "Add a tool" Send tasks to Claude');
448
+ console.log(
449
+ " waniwani org list View your organizations"
450
+ );
451
+ }
452
+ } catch (error) {
453
+ handleError(error, json);
454
+ process.exit(1);
455
+ }
456
+ });
457
+
458
+ // src/commands/logout.ts
459
+ import { Command as Command2 } from "commander";
460
+
461
+ // src/lib/config.ts
462
+ import { access as access2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
463
+ import { homedir as homedir2 } from "os";
464
+ import { join as join2 } from "path";
465
+ import { z as z2 } from "zod";
466
+ var CONFIG_DIR2 = join2(homedir2(), ".waniwani");
467
+ var CONFIG_FILE = join2(CONFIG_DIR2, "config.json");
468
+ var ConfigSchema = z2.object({
469
+ defaults: z2.object({
470
+ model: z2.string().default("claude-sonnet-4-20250514"),
471
+ maxSteps: z2.number().default(10)
472
+ }).default(() => ({ model: "claude-sonnet-4-20250514", maxSteps: 10 })),
473
+ activeMcpId: z2.string().nullable().default(null)
474
+ });
475
+ async function ensureConfigDir2() {
476
+ await mkdir2(CONFIG_DIR2, { recursive: true });
477
+ }
478
+ async function readConfig() {
479
+ await ensureConfigDir2();
480
+ try {
481
+ await access2(CONFIG_FILE);
482
+ const content = await readFile2(CONFIG_FILE, "utf-8");
483
+ return ConfigSchema.parse(JSON.parse(content));
484
+ } catch {
485
+ return ConfigSchema.parse({});
486
+ }
487
+ }
488
+ async function writeConfig(config2) {
489
+ await ensureConfigDir2();
490
+ await writeFile2(CONFIG_FILE, JSON.stringify(config2, null, 2), "utf-8");
491
+ }
492
+ var ConfigManager = class {
493
+ configCache = null;
494
+ async getConfig() {
495
+ if (!this.configCache) {
496
+ this.configCache = await readConfig();
497
+ }
498
+ return this.configCache;
499
+ }
500
+ async saveConfig(config2) {
501
+ this.configCache = config2;
502
+ await writeConfig(config2);
503
+ }
504
+ async getDefaults() {
505
+ const config2 = await this.getConfig();
506
+ return config2.defaults;
507
+ }
508
+ async setDefaults(defaults) {
509
+ const config2 = await this.getConfig();
510
+ config2.defaults = { ...config2.defaults, ...defaults };
511
+ await this.saveConfig(config2);
512
+ }
513
+ async getActiveMcpId() {
514
+ const config2 = await this.getConfig();
515
+ return config2.activeMcpId;
516
+ }
517
+ async setActiveMcpId(id) {
518
+ const config2 = await this.getConfig();
519
+ config2.activeMcpId = id;
520
+ await this.saveConfig(config2);
521
+ }
522
+ async clear() {
523
+ const emptyConfig = ConfigSchema.parse({});
524
+ await this.saveConfig(emptyConfig);
525
+ }
526
+ };
527
+ var config = new ConfigManager();
528
+
529
+ // src/commands/logout.ts
530
+ var logoutCommand = new Command2("logout").description("Log out from WaniWani").action(async (_, command) => {
531
+ const globalOptions = command.optsWithGlobals();
532
+ const json = globalOptions.json ?? false;
533
+ try {
534
+ if (!await auth.isLoggedIn()) {
535
+ if (json) {
536
+ formatOutput({ alreadyLoggedOut: true }, true);
537
+ } else {
538
+ console.log("Not currently logged in.");
539
+ }
540
+ return;
541
+ }
542
+ await auth.clear();
543
+ await config.clear();
544
+ if (json) {
545
+ formatOutput({ success: true }, true);
546
+ } else {
547
+ formatSuccess("You have been logged out.", false);
548
+ }
549
+ } catch (error) {
550
+ handleError(error, json);
551
+ process.exit(1);
552
+ }
553
+ });
554
+
555
+ // src/commands/mcp/index.ts
556
+ import { Command as Command10 } from "commander";
557
+
558
+ // src/commands/mcp/create.ts
559
+ import { Command as Command3 } from "commander";
560
+ import ora2 from "ora";
561
+
562
+ // src/lib/api.ts
563
+ var API_BASE_URL3 = process.env.WANIWANI_API_URL || "https://waniwani.com";
564
+ var ApiError = class extends CLIError {
565
+ constructor(message, code, statusCode, details) {
566
+ super(message, code, details);
567
+ this.statusCode = statusCode;
568
+ this.name = "ApiError";
569
+ }
570
+ };
571
+ async function request(method, path, options) {
572
+ const {
573
+ body,
574
+ requireAuth = true,
575
+ headers: extraHeaders = {}
576
+ } = options || {};
577
+ const headers = {
578
+ "Content-Type": "application/json",
579
+ ...extraHeaders
580
+ };
581
+ if (requireAuth) {
582
+ const token = await auth.getAccessToken();
583
+ if (!token) {
584
+ throw new AuthError(
585
+ "Not logged in. Run 'waniwani login' to authenticate."
586
+ );
587
+ }
588
+ headers.Authorization = `Bearer ${token}`;
589
+ }
590
+ const url = `${API_BASE_URL3}${path}`;
591
+ const response = await fetch(url, {
592
+ method,
593
+ headers,
594
+ body: body ? JSON.stringify(body) : void 0
595
+ });
596
+ if (response.status === 204) {
597
+ return void 0;
598
+ }
599
+ const data = await response.json();
600
+ if (!response.ok || data.error) {
601
+ const error = data.error || {
602
+ code: "API_ERROR",
603
+ message: `Request failed with status ${response.status}`
604
+ };
605
+ if (response.status === 401) {
606
+ const refreshed = await auth.tryRefreshToken();
607
+ if (refreshed) {
608
+ return request(method, path, options);
609
+ }
610
+ throw new AuthError(
611
+ "Session expired. Run 'waniwani login' to re-authenticate."
612
+ );
613
+ }
614
+ throw new ApiError(
615
+ error.message,
616
+ error.code,
617
+ response.status,
618
+ error.details
619
+ );
620
+ }
621
+ return data.data;
622
+ }
623
+ var api = {
624
+ get: (path, options) => request("GET", path, options),
625
+ post: (path, body, options) => request("POST", path, { body, ...options }),
626
+ delete: (path, options) => request("DELETE", path, options),
627
+ getBaseUrl: () => API_BASE_URL3
628
+ };
629
+
630
+ // src/commands/mcp/create.ts
631
+ var createCommand = new Command3("create").description("Create a new MCP sandbox from template").argument("<name>", "Name for the MCP project").action(async (name, _, command) => {
632
+ const globalOptions = command.optsWithGlobals();
633
+ const json = globalOptions.json ?? false;
634
+ try {
635
+ const spinner = ora2("Creating MCP sandbox...").start();
636
+ const result = await api.post("/api/admin/mcps", {
637
+ name
638
+ });
639
+ spinner.succeed("MCP sandbox created");
640
+ config.setActiveMcpId(result.id);
641
+ if (json) {
642
+ formatOutput(result, true);
643
+ } else {
644
+ console.log();
645
+ formatSuccess(`MCP sandbox "${name}" created successfully!`, false);
646
+ console.log();
647
+ console.log(` MCP ID: ${result.id}`);
648
+ console.log(` Sandbox ID: ${result.sandboxId}`);
649
+ console.log(` Preview URL: ${result.previewUrl}`);
650
+ console.log();
651
+ console.log(`Next steps:`);
652
+ console.log(` waniwani task "Add a tool that does X"`);
653
+ console.log(` waniwani mcp test`);
654
+ console.log(` waniwani mcp deploy`);
655
+ }
656
+ } catch (error) {
657
+ handleError(error, json);
658
+ process.exit(1);
659
+ }
660
+ });
661
+
662
+ // src/commands/mcp/deploy.ts
663
+ import { Command as Command4 } from "commander";
664
+ import ora3 from "ora";
665
+ var deployCommand = new Command4("deploy").description("Deploy MCP server to GitHub + Vercel from sandbox").option("--repo <name>", "GitHub repository name").option("--org <name>", "GitHub organization").option("--private", "Create private repository").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
666
+ const globalOptions = command.optsWithGlobals();
667
+ const json = globalOptions.json ?? false;
668
+ try {
669
+ let mcpId = options.mcpId;
670
+ if (!mcpId) {
671
+ mcpId = await config.getActiveMcpId();
672
+ if (!mcpId) {
673
+ throw new McpError(
674
+ "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
675
+ );
676
+ }
677
+ }
678
+ const spinner = ora3("Deploying to GitHub...").start();
679
+ const result = await api.post(
680
+ `/api/admin/mcps/${mcpId}/deploy`,
681
+ {
682
+ repoName: options.repo,
683
+ org: options.org,
684
+ private: options.private ?? false
685
+ }
686
+ );
687
+ spinner.succeed("Deployment complete!");
688
+ if (json) {
689
+ formatOutput(result, true);
690
+ } else {
691
+ console.log();
692
+ formatSuccess("MCP server deployed!", false);
693
+ console.log();
694
+ console.log(` Repository: ${result.repository.url}`);
695
+ if (result.deployment.url) {
696
+ console.log(` Deployment: ${result.deployment.url}`);
697
+ }
698
+ console.log();
699
+ if (result.deployment.note) {
700
+ console.log(`Note: ${result.deployment.note}`);
701
+ }
702
+ }
703
+ } catch (error) {
704
+ handleError(error, json);
705
+ process.exit(1);
706
+ }
707
+ });
708
+
709
+ // src/commands/mcp/list.ts
710
+ import chalk4 from "chalk";
711
+ import { Command as Command5 } from "commander";
712
+ import ora4 from "ora";
713
+ var listCommand = new Command5("list").description("List all MCPs in your organization").option("--all", "Include stopped/expired MCPs").action(async (options, command) => {
714
+ const globalOptions = command.optsWithGlobals();
715
+ const json = globalOptions.json ?? false;
716
+ try {
717
+ const spinner = ora4("Fetching MCPs...").start();
718
+ const mcps = await api.get(
719
+ `/api/admin/mcps${options.all ? "?all=true" : ""}`
720
+ );
721
+ spinner.stop();
722
+ const activeMcpId = await config.getActiveMcpId();
723
+ if (json) {
724
+ formatOutput(
725
+ {
726
+ mcps: mcps.map((m) => ({
727
+ ...m,
728
+ isActive: m.id === activeMcpId
729
+ })),
730
+ activeMcpId
731
+ },
732
+ true
733
+ );
734
+ } else {
735
+ if (mcps.length === 0) {
736
+ console.log("No MCPs found.");
737
+ console.log("\nCreate a new MCP sandbox: waniwani mcp create <name>");
738
+ return;
739
+ }
740
+ console.log(chalk4.bold("\nMCPs:\n"));
741
+ const rows = mcps.map((m) => {
742
+ const isActive = m.id === activeMcpId;
743
+ const statusColor = m.status === "active" ? chalk4.green : m.status === "stopped" ? chalk4.red : chalk4.yellow;
744
+ return [
745
+ isActive ? chalk4.cyan(`* ${m.id.slice(0, 8)}`) : ` ${m.id.slice(0, 8)}`,
746
+ m.name,
747
+ statusColor(m.status),
748
+ m.previewUrl,
749
+ m.createdAt ? new Date(m.createdAt).toLocaleString() : "N/A"
750
+ ];
751
+ });
752
+ formatTable(
753
+ ["ID", "Name", "Status", "Preview URL", "Created"],
754
+ rows,
755
+ false
756
+ );
757
+ console.log();
758
+ if (activeMcpId) {
759
+ console.log(`Active MCP: ${chalk4.cyan(activeMcpId.slice(0, 8))}`);
760
+ }
761
+ console.log("\nSelect an MCP: waniwani mcp use <name>");
762
+ }
763
+ } catch (error) {
764
+ handleError(error, json);
765
+ process.exit(1);
766
+ }
767
+ });
768
+
769
+ // src/commands/mcp/status.ts
770
+ import chalk5 from "chalk";
771
+ import { Command as Command6 } from "commander";
772
+ import ora5 from "ora";
773
+ var statusCommand = new Command6("status").description("Show current MCP sandbox status").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
774
+ const globalOptions = command.optsWithGlobals();
775
+ const json = globalOptions.json ?? false;
776
+ try {
777
+ let mcpId = options.mcpId;
778
+ if (!mcpId) {
779
+ mcpId = await config.getActiveMcpId();
780
+ if (!mcpId) {
781
+ throw new McpError(
782
+ "No active MCP. Run 'waniwani mcp create <name>' to create one or 'waniwani mcp use <name>' to select one."
783
+ );
784
+ }
785
+ }
786
+ const spinner = ora5("Fetching MCP status...").start();
787
+ const result = await api.get(`/api/admin/mcps/${mcpId}`);
788
+ spinner.stop();
789
+ if (json) {
790
+ formatOutput(result, true);
791
+ } else {
792
+ const statusColor = result.status === "active" ? chalk5.green : chalk5.red;
793
+ formatList(
794
+ [
795
+ { label: "MCP ID", value: result.id },
796
+ { label: "Name", value: result.name },
797
+ { label: "Status", value: statusColor(result.status) },
798
+ { label: "Sandbox ID", value: result.sandboxId },
799
+ { label: "Preview URL", value: result.previewUrl },
800
+ { label: "Created", value: result.createdAt },
801
+ { label: "Expires", value: result.expiresAt ?? "N/A" }
802
+ ],
803
+ false
804
+ );
805
+ }
806
+ } catch (error) {
807
+ handleError(error, json);
808
+ process.exit(1);
809
+ }
810
+ });
811
+
812
+ // src/commands/mcp/stop.ts
813
+ import { Command as Command7 } from "commander";
814
+ import ora6 from "ora";
815
+ var stopCommand = new Command7("stop").description("Stop and clean up the MCP sandbox").option("--mcp-id <id>", "Specific MCP ID").action(async (options, command) => {
816
+ const globalOptions = command.optsWithGlobals();
817
+ const json = globalOptions.json ?? false;
818
+ try {
819
+ let mcpId = options.mcpId;
820
+ if (!mcpId) {
821
+ mcpId = await config.getActiveMcpId();
822
+ if (!mcpId) {
823
+ throw new McpError("No active MCP. Use --mcp-id to specify one.");
824
+ }
825
+ }
826
+ const spinner = ora6("Stopping MCP sandbox...").start();
827
+ await api.delete(`/api/admin/mcps/${mcpId}`);
828
+ spinner.succeed("MCP sandbox stopped");
829
+ if (await config.getActiveMcpId() === mcpId) {
830
+ await config.setActiveMcpId(null);
831
+ }
832
+ if (json) {
833
+ formatOutput({ stopped: mcpId }, true);
834
+ } else {
835
+ formatSuccess("MCP sandbox stopped and cleaned up.", false);
836
+ }
837
+ } catch (error) {
838
+ handleError(error, json);
839
+ process.exit(1);
840
+ }
841
+ });
842
+
843
+ // src/commands/mcp/test.ts
844
+ import chalk6 from "chalk";
845
+ import { Command as Command8 } from "commander";
846
+ import ora7 from "ora";
847
+ var testCommand = new Command8("test").description("Test MCP tools via the sandbox").argument("[tool]", "Tool name to test (lists tools if omitted)").argument("[args...]", "JSON arguments for the tool").option("--mcp-id <id>", "Specific MCP ID").action(
848
+ async (tool, args, options, command) => {
849
+ const globalOptions = command.optsWithGlobals();
850
+ const json = globalOptions.json ?? false;
851
+ try {
852
+ let mcpId = options.mcpId;
853
+ if (!mcpId) {
854
+ mcpId = await config.getActiveMcpId();
855
+ if (!mcpId) {
856
+ throw new McpError(
857
+ "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
858
+ );
859
+ }
860
+ }
861
+ if (!tool) {
862
+ const spinner = ora7("Fetching available tools...").start();
863
+ const result = await api.post(
864
+ `/api/admin/mcps/${mcpId}/test`,
865
+ { action: "list" }
866
+ );
867
+ spinner.stop();
868
+ const tools = result.tools;
869
+ if (json) {
870
+ formatOutput({ tools }, true);
871
+ } else {
872
+ if (tools.length === 0) {
873
+ console.log("No tools available.");
874
+ } else {
875
+ console.log(chalk6.bold("\nAvailable Tools:\n"));
876
+ formatTable(
877
+ ["Name", "Description"],
878
+ tools.map((t) => [t.name, t.description || "No description"]),
879
+ false
880
+ );
881
+ console.log(
882
+ `
883
+ Test a tool: waniwani mcp test <tool-name> '{"arg": "value"}'`
884
+ );
885
+ }
886
+ }
887
+ } else {
888
+ let toolArgs = {};
889
+ if (args.length > 0) {
890
+ try {
891
+ toolArgs = JSON.parse(args.join(" "));
892
+ } catch {
893
+ throw new SandboxError(
894
+ `Invalid JSON arguments. Expected format: '{"key": "value"}'`
895
+ );
896
+ }
897
+ }
898
+ const spinner = ora7(`Calling tool "${tool}"...`).start();
899
+ const startTime = Date.now();
900
+ const result = await api.post(
901
+ `/api/admin/mcps/${mcpId}/test`,
902
+ {
903
+ action: "call",
904
+ tool,
905
+ args: toolArgs
906
+ }
907
+ );
908
+ const duration = result.duration || Date.now() - startTime;
909
+ spinner.stop();
910
+ const output = {
911
+ tool,
912
+ input: toolArgs,
913
+ result: result.result,
914
+ duration
915
+ };
916
+ if (json) {
917
+ formatOutput(output, true);
918
+ } else {
919
+ console.log(chalk6.bold("\nTool Result:\n"));
920
+ console.log(chalk6.gray("Tool:"), tool);
921
+ console.log(chalk6.gray("Input:"), JSON.stringify(toolArgs));
922
+ console.log(chalk6.gray("Duration:"), `${duration}ms`);
923
+ console.log(chalk6.gray("Result:"));
924
+ console.log(JSON.stringify(result.result, null, 2));
925
+ }
926
+ }
927
+ } catch (error) {
928
+ handleError(error, json);
929
+ process.exit(1);
930
+ }
931
+ }
932
+ );
933
+
934
+ // src/commands/mcp/use.ts
935
+ import { Command as Command9 } from "commander";
936
+ import ora8 from "ora";
937
+ var useCommand = new Command9("use").description("Select an MCP to use for subsequent commands").argument("<name>", "Name of the MCP to use").action(async (name, _, command) => {
938
+ const globalOptions = command.optsWithGlobals();
939
+ const json = globalOptions.json ?? false;
940
+ try {
941
+ const spinner = ora8("Fetching MCPs...").start();
942
+ const mcps = await api.get("/api/admin/mcps");
943
+ spinner.stop();
944
+ const mcp = mcps.find((m) => m.name === name);
945
+ if (!mcp) {
946
+ throw new McpError(
947
+ `MCP "${name}" not found. Run 'waniwani mcp list' to see available MCPs.`
948
+ );
949
+ }
950
+ if (mcp.status !== "active") {
951
+ throw new McpError(
952
+ `MCP "${name}" is ${mcp.status}. Only active MCPs can be used.`
953
+ );
954
+ }
955
+ await config.setActiveMcpId(mcp.id);
956
+ if (json) {
957
+ formatOutput({ selected: mcp }, true);
958
+ } else {
959
+ formatSuccess(`Now using MCP "${name}"`, false);
960
+ console.log();
961
+ console.log(` MCP ID: ${mcp.id}`);
962
+ console.log(` Preview URL: ${mcp.previewUrl}`);
963
+ console.log();
964
+ console.log("Next steps:");
965
+ console.log(' waniwani task "Add a tool"');
966
+ console.log(" waniwani mcp test");
967
+ console.log(" waniwani mcp status");
968
+ }
969
+ } catch (error) {
970
+ handleError(error, json);
971
+ process.exit(1);
972
+ }
973
+ });
974
+
975
+ // src/commands/mcp/index.ts
976
+ var mcpCommand = new Command10("mcp").description("MCP sandbox management commands").addCommand(createCommand).addCommand(listCommand).addCommand(useCommand).addCommand(statusCommand).addCommand(stopCommand).addCommand(testCommand).addCommand(deployCommand);
977
+
978
+ // src/commands/org/index.ts
979
+ import { Command as Command13 } from "commander";
980
+
981
+ // src/commands/org/list.ts
982
+ import chalk7 from "chalk";
983
+ import { Command as Command11 } from "commander";
984
+ import ora9 from "ora";
985
+ var listCommand2 = new Command11("list").description("List your organizations").action(async (_, command) => {
986
+ const globalOptions = command.optsWithGlobals();
987
+ const json = globalOptions.json ?? false;
988
+ try {
989
+ const spinner = ora9("Fetching organizations...").start();
990
+ const result = await api.get("/api/oauth/orgs");
991
+ spinner.stop();
992
+ const { orgs, activeOrgId } = result;
993
+ if (json) {
994
+ formatOutput(
995
+ {
996
+ orgs: orgs.map((o) => ({
997
+ ...o,
998
+ isActive: o.id === activeOrgId
999
+ })),
1000
+ activeOrgId
1001
+ },
1002
+ true
1003
+ );
1004
+ } else {
1005
+ if (orgs.length === 0) {
1006
+ console.log("No organizations found.");
1007
+ return;
1008
+ }
1009
+ console.log(chalk7.bold("\nOrganizations:\n"));
1010
+ const rows = orgs.map((o) => {
1011
+ const isActive = o.id === activeOrgId;
1012
+ return [
1013
+ isActive ? chalk7.cyan(`* ${o.name}`) : ` ${o.name}`,
1014
+ o.slug,
1015
+ o.role
1016
+ ];
1017
+ });
1018
+ formatTable(["Name", "Slug", "Role"], rows, false);
1019
+ console.log();
1020
+ if (activeOrgId) {
1021
+ const activeOrg = orgs.find((o) => o.id === activeOrgId);
1022
+ if (activeOrg) {
1023
+ console.log(`Active organization: ${chalk7.cyan(activeOrg.name)}`);
1024
+ }
1025
+ }
1026
+ console.log("\nSwitch organization: waniwani org switch <name>");
1027
+ }
1028
+ } catch (error) {
1029
+ handleError(error, json);
1030
+ process.exit(1);
1031
+ }
1032
+ });
1033
+
1034
+ // src/commands/org/switch.ts
1035
+ import { Command as Command12 } from "commander";
1036
+ import ora10 from "ora";
1037
+ var switchCommand = new Command12("switch").description("Switch to a different organization").argument("<name>", "Name or slug of the organization to switch to").action(async (name, _, command) => {
1038
+ const globalOptions = command.optsWithGlobals();
1039
+ const json = globalOptions.json ?? false;
1040
+ try {
1041
+ const spinner = ora10("Fetching organizations...").start();
1042
+ const { orgs } = await api.get("/api/oauth/orgs");
1043
+ const org = orgs.find((o) => o.name === name || o.slug === name);
1044
+ if (!org) {
1045
+ spinner.stop();
1046
+ throw new CLIError(
1047
+ `Organization "${name}" not found. Run 'waniwani org list' to see available organizations.`,
1048
+ "ORG_NOT_FOUND"
1049
+ );
1050
+ }
1051
+ spinner.text = "Switching organization...";
1052
+ await api.post("/api/oauth/orgs/switch", {
1053
+ orgId: org.id
1054
+ });
1055
+ spinner.succeed("Organization switched");
1056
+ config.setActiveMcpId(null);
1057
+ if (json) {
1058
+ formatOutput({ switched: org }, true);
1059
+ } else {
1060
+ formatSuccess(`Switched to organization "${org.name}"`, false);
1061
+ console.log();
1062
+ console.log("Note: Active MCP selection has been cleared.");
1063
+ console.log(
1064
+ "Run 'waniwani mcp list' to see MCPs in this organization."
1065
+ );
1066
+ }
1067
+ } catch (error) {
1068
+ handleError(error, json);
1069
+ process.exit(1);
1070
+ }
1071
+ });
1072
+
1073
+ // src/commands/org/index.ts
1074
+ var orgCommand = new Command13("org").description("Organization management commands").addCommand(listCommand2).addCommand(switchCommand);
1075
+
1076
+ // src/commands/task.ts
1077
+ import chalk8 from "chalk";
1078
+ import { Command as Command14 } from "commander";
1079
+ import ora11 from "ora";
1080
+ var taskCommand = new Command14("task").description("Send a task to Claude running in the sandbox").argument("<prompt>", "Task description/prompt").option("--mcp-id <id>", "Specific MCP ID").option("--model <model>", "Claude model to use", "claude-sonnet-4-20250514").option("--max-steps <n>", "Maximum tool use steps", "10").action(async (prompt, options, command) => {
1081
+ const globalOptions = command.optsWithGlobals();
1082
+ const json = globalOptions.json ?? false;
1083
+ try {
1084
+ let mcpId = options.mcpId;
1085
+ if (!mcpId) {
1086
+ mcpId = await config.getActiveMcpId();
1087
+ if (!mcpId) {
1088
+ throw new McpError(
1089
+ "No active MCP. Run 'waniwani mcp create <name>' or 'waniwani mcp use <name>'."
1090
+ );
1091
+ }
1092
+ }
1093
+ const token = await auth.getAccessToken();
1094
+ if (!token) {
1095
+ throw new AuthError(
1096
+ "Not logged in. Run 'waniwani login' to authenticate."
1097
+ );
1098
+ }
1099
+ const maxSteps = parseInt(options.maxSteps, 10);
1100
+ if (!json) {
1101
+ console.log();
1102
+ console.log(chalk8.bold("Task:"), prompt);
1103
+ console.log();
1104
+ }
1105
+ const spinner = ora11("Starting task...").start();
1106
+ const baseUrl = api.getBaseUrl();
1107
+ const response = await fetch(`${baseUrl}/api/admin/mcps/${mcpId}/task`, {
1108
+ method: "POST",
1109
+ headers: {
1110
+ "Content-Type": "application/json",
1111
+ Authorization: `Bearer ${token}`,
1112
+ Accept: "text/event-stream"
1113
+ },
1114
+ body: JSON.stringify({
1115
+ prompt,
1116
+ model: options.model,
1117
+ maxSteps
1118
+ })
1119
+ });
1120
+ if (!response.ok) {
1121
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1122
+ spinner.fail("Task failed");
1123
+ throw new Error(
1124
+ error.message || `Request failed with status ${response.status}`
1125
+ );
1126
+ }
1127
+ spinner.stop();
1128
+ const steps = [];
1129
+ let stepCount = 0;
1130
+ let maxStepsReached = false;
1131
+ const reader = response.body?.getReader();
1132
+ if (!reader) {
1133
+ throw new Error("No response body");
1134
+ }
1135
+ const decoder = new TextDecoder();
1136
+ let buffer = "";
1137
+ while (true) {
1138
+ const { done, value } = await reader.read();
1139
+ if (done) break;
1140
+ buffer += decoder.decode(value, { stream: true });
1141
+ const lines = buffer.split("\n");
1142
+ buffer = lines.pop() || "";
1143
+ for (const line of lines) {
1144
+ if (line.startsWith("event: ")) {
1145
+ continue;
1146
+ }
1147
+ if (line.startsWith("data: ")) {
1148
+ const data = line.slice(6);
1149
+ if (!data || data === "[DONE]") continue;
1150
+ try {
1151
+ const parsed = JSON.parse(data);
1152
+ if (parsed.type === "text") {
1153
+ const event = parsed;
1154
+ steps.push({ type: "text", text: event.content });
1155
+ if (!json && event.content) {
1156
+ console.log(chalk8.white(event.content));
1157
+ }
1158
+ } else if (parsed.type === "tool_call") {
1159
+ const event = parsed;
1160
+ steps.push({
1161
+ type: "tool_call",
1162
+ tool: event.tool,
1163
+ input: event.input,
1164
+ output: event.output
1165
+ });
1166
+ if (!json) {
1167
+ console.log(chalk8.cyan(`> Using tool: ${event.tool}`));
1168
+ if (event.input?.command) {
1169
+ console.log(chalk8.gray(` $ ${event.input.command}`));
1170
+ }
1171
+ if (event.output) {
1172
+ const outputLines = event.output.split("\n");
1173
+ if (outputLines.length > 10) {
1174
+ console.log(
1175
+ chalk8.gray(outputLines.slice(0, 5).join("\n"))
1176
+ );
1177
+ console.log(
1178
+ chalk8.gray(
1179
+ ` ... (${outputLines.length - 10} more lines)`
1180
+ )
1181
+ );
1182
+ console.log(chalk8.gray(outputLines.slice(-5).join("\n")));
1183
+ } else {
1184
+ console.log(chalk8.gray(event.output));
1185
+ }
1186
+ }
1187
+ console.log();
1188
+ }
1189
+ } else if (parsed.success !== void 0) {
1190
+ const doneEvent = parsed;
1191
+ stepCount = doneEvent.stepCount;
1192
+ maxStepsReached = doneEvent.maxStepsReached || false;
1193
+ }
1194
+ } catch {
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+ const result = {
1200
+ success: true,
1201
+ steps,
1202
+ stepCount,
1203
+ maxStepsReached
1204
+ };
1205
+ if (json) {
1206
+ formatOutput(result, true);
1207
+ } else {
1208
+ console.log();
1209
+ console.log(chalk8.green("\u2713"), `Task completed in ${stepCount} steps.`);
1210
+ if (maxStepsReached) {
1211
+ console.log(
1212
+ chalk8.yellow("\u26A0"),
1213
+ "Maximum steps reached. Task may be incomplete."
1214
+ );
1215
+ }
1216
+ }
1217
+ } catch (error) {
1218
+ handleError(error, json);
1219
+ process.exit(1);
1220
+ }
1221
+ });
1222
+
1223
+ // src/cli.ts
1224
+ var version = "0.1.0";
1225
+ var program = new Command15().name("waniwani").description("WaniWani CLI for MCP development workflow").version(version).option("--json", "Output results as JSON").option("--verbose", "Enable verbose logging");
1226
+ program.addCommand(loginCommand);
1227
+ program.addCommand(logoutCommand);
1228
+ program.addCommand(mcpCommand);
1229
+ program.addCommand(taskCommand);
1230
+ program.addCommand(orgCommand);
1231
+
1232
+ // src/index.ts
1233
+ program.parse(process.argv);
1234
+ //# sourceMappingURL=index.js.map