starkbot-cli 0.2.2 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +257 -154
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -182,15 +182,16 @@ var init_flash_client = __esm({
182
182
  import { createServer } from "http";
183
183
  import { createInterface } from "readline";
184
184
  import { randomBytes } from "crypto";
185
- import { URL } from "url";
185
+ import { URL as URL2 } from "url";
186
186
  import open from "open";
187
187
  async function runOAuthFlow() {
188
188
  return new Promise((resolve, reject) => {
189
189
  let server;
190
190
  let timeout;
191
+ let loginSpin = null;
191
192
  const csrfNonce = randomBytes(16).toString("hex");
192
193
  server = createServer((req, res) => {
193
- const url = new URL(req.url ?? "/", `http://localhost`);
194
+ const url = new URL2(req.url ?? "/", `http://localhost`);
194
195
  if (url.pathname === "/callback") {
195
196
  const returnedNonce = url.searchParams.get("nonce");
196
197
  if (returnedNonce !== csrfNonce) {
@@ -215,6 +216,7 @@ async function runOAuthFlow() {
215
216
  </body>
216
217
  </html>
217
218
  `);
219
+ loginSpin?.stop();
218
220
  clearTimeout(timeout);
219
221
  server.close();
220
222
  resolve({
@@ -225,6 +227,7 @@ async function runOAuthFlow() {
225
227
  } else {
226
228
  res.writeHead(400, { "Content-Type": "text/html" });
227
229
  res.end("<h1>Login failed</h1><p>Missing JWT or username in callback.</p>");
230
+ loginSpin?.stop();
228
231
  clearTimeout(timeout);
229
232
  server.close();
230
233
  reject(new Error("OAuth callback missing jwt or username"));
@@ -256,16 +259,17 @@ async function runOAuthFlow() {
256
259
  res();
257
260
  });
258
261
  });
259
- const loginSpin = spinner("Opening browser for X login...");
262
+ loginSpin = spinner("Opening browser for X login...");
260
263
  loginSpin.start();
261
264
  await open(url);
262
265
  loginSpin.text = "Waiting for login in browser...";
263
266
  timeout = setTimeout(() => {
264
- loginSpin.fail("Login timed out (5 minutes)");
267
+ loginSpin?.fail("Login timed out (5 minutes)");
265
268
  server.close();
266
269
  reject(new Error("Login timed out"));
267
270
  }, 5 * 60 * 1e3);
268
271
  } catch (err) {
272
+ loginSpin?.stop();
269
273
  printError("Failed to start login");
270
274
  server.close();
271
275
  reject(err);
@@ -318,6 +322,7 @@ function clearCredentials() {
318
322
  }
319
323
  }
320
324
  function isJwtExpired(creds) {
325
+ if (creds.mode === "external") return false;
321
326
  if (!creds.jwt) return true;
322
327
  try {
323
328
  const parts = creds.jwt.split(".");
@@ -337,6 +342,12 @@ function requireCredentials() {
337
342
  if (!creds) {
338
343
  throw new Error("Not logged in. Run `starkbot login` first.");
339
344
  }
345
+ if (creds.mode === "external") {
346
+ if (!creds.gateway_token || !creds.instance_domain) {
347
+ throw new Error("External credentials incomplete. Run `starkbot login` to reconfigure.");
348
+ }
349
+ return creds;
350
+ }
340
351
  if (isJwtExpired(creds)) {
341
352
  throw new Error("Session expired. Run `starkbot login` to re-authenticate.");
342
353
  }
@@ -351,17 +362,168 @@ var init_credentials = __esm({
351
362
  }
352
363
  });
353
364
 
365
+ // src/lib/gateway-client.ts
366
+ var GatewayClient;
367
+ var init_gateway_client = __esm({
368
+ "src/lib/gateway-client.ts"() {
369
+ "use strict";
370
+ GatewayClient = class {
371
+ baseUrl;
372
+ token;
373
+ sessionId;
374
+ constructor(baseUrl, token, sessionId) {
375
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
376
+ this.token = token;
377
+ this.sessionId = sessionId;
378
+ }
379
+ headers() {
380
+ return {
381
+ Authorization: `Bearer ${this.token}`,
382
+ "Content-Type": "application/json"
383
+ };
384
+ }
385
+ chatBody(message) {
386
+ return {
387
+ message,
388
+ ...this.sessionId ? { session_id: this.sessionId } : {}
389
+ };
390
+ }
391
+ /** Send a message and get the full response */
392
+ async chat(message) {
393
+ const url = `${this.baseUrl}/api/gateway/chat`;
394
+ const resp = await fetch(url, {
395
+ method: "POST",
396
+ headers: this.headers(),
397
+ body: JSON.stringify(this.chatBody(message))
398
+ });
399
+ if (!resp.ok) {
400
+ const text = await resp.text().catch(() => "");
401
+ throw new Error(`HTTP ${resp.status}: ${text}`);
402
+ }
403
+ return resp.json();
404
+ }
405
+ /** Send a message and stream SSE events */
406
+ async chatStream(message, onEvent) {
407
+ const url = `${this.baseUrl}/api/gateway/chat/stream`;
408
+ const resp = await fetch(url, {
409
+ method: "POST",
410
+ headers: this.headers(),
411
+ body: JSON.stringify(this.chatBody(message))
412
+ });
413
+ if (!resp.ok) {
414
+ const text = await resp.text().catch(() => "");
415
+ throw new Error(`HTTP ${resp.status}: ${text}`);
416
+ }
417
+ if (!resp.body) throw new Error("No response body");
418
+ const reader = resp.body.getReader();
419
+ const decoder = new TextDecoder();
420
+ let buffer = "";
421
+ while (true) {
422
+ const { done, value } = await reader.read();
423
+ if (done) break;
424
+ buffer += decoder.decode(value, { stream: true });
425
+ let pos;
426
+ while ((pos = buffer.indexOf("\n\n")) !== -1) {
427
+ const frame = buffer.slice(0, pos);
428
+ buffer = buffer.slice(pos + 2);
429
+ for (const line of frame.split("\n")) {
430
+ if (line.startsWith("data: ")) {
431
+ try {
432
+ const event = JSON.parse(line.slice(6));
433
+ const isDone = event.type === "done";
434
+ onEvent(event);
435
+ if (isDone) return;
436
+ } catch {
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ /** Create a new session */
444
+ async newSession() {
445
+ const url = `${this.baseUrl}/api/gateway/sessions/new`;
446
+ const resp = await fetch(url, {
447
+ method: "POST",
448
+ headers: this.headers()
449
+ });
450
+ if (!resp.ok) {
451
+ const text = await resp.text().catch(() => "");
452
+ throw new Error(`HTTP ${resp.status}: ${text}`);
453
+ }
454
+ return resp.json();
455
+ }
456
+ /** List sessions */
457
+ async listSessions() {
458
+ const url = `${this.baseUrl}/api/gateway/sessions`;
459
+ const resp = await fetch(url, {
460
+ method: "GET",
461
+ headers: this.headers()
462
+ });
463
+ if (!resp.ok) {
464
+ const text = await resp.text().catch(() => "");
465
+ throw new Error(`HTTP ${resp.status}: ${text}`);
466
+ }
467
+ return resp.json();
468
+ }
469
+ /** Get message history for a session */
470
+ async getHistory(sessionId) {
471
+ const url = `${this.baseUrl}/api/gateway/sessions/${sessionId}/messages`;
472
+ const resp = await fetch(url, {
473
+ method: "GET",
474
+ headers: this.headers()
475
+ });
476
+ if (!resp.ok) {
477
+ const text = await resp.text().catch(() => "");
478
+ throw new Error(`HTTP ${resp.status}: ${text}`);
479
+ }
480
+ return resp.json();
481
+ }
482
+ /** Simple health check */
483
+ async ping() {
484
+ try {
485
+ const resp = await fetch(`${this.baseUrl}/api/health`, {
486
+ headers: this.headers()
487
+ });
488
+ return resp.ok;
489
+ } catch {
490
+ return false;
491
+ }
492
+ }
493
+ };
494
+ }
495
+ });
496
+
354
497
  // src/commands/login.ts
355
498
  var login_exports = {};
356
499
  __export(login_exports, {
357
500
  loginCommand: () => loginCommand
358
501
  });
502
+ import inquirer from "inquirer";
359
503
  async function loginCommand() {
360
504
  const existing = loadCredentials();
361
505
  if (existing && !isJwtExpired(existing)) {
362
506
  printWarning(`Already logged in as @${existing.username}. Use \`starkbot logout\` first to switch accounts.`);
363
507
  return;
364
508
  }
509
+ const { loginMethod } = await inquirer.prompt([
510
+ {
511
+ type: "list",
512
+ name: "loginMethod",
513
+ message: "How would you like to connect?",
514
+ choices: [
515
+ { name: "Login with X", value: "x" },
516
+ { name: "Custom external channel", value: "external" }
517
+ ]
518
+ }
519
+ ]);
520
+ if (loginMethod === "external") {
521
+ await externalLogin();
522
+ } else {
523
+ await xLogin();
524
+ }
525
+ }
526
+ async function xLogin() {
365
527
  const result = await runOAuthFlow();
366
528
  const spin = spinner("Fetching account info...");
367
529
  spin.start();
@@ -373,7 +535,8 @@ async function loginCommand() {
373
535
  username: me.user.username,
374
536
  tenant_id: me.tenant.id,
375
537
  instance_domain: me.tenant.domain ?? void 0,
376
- jwt_expires_at: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString()
538
+ jwt_expires_at: new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString(),
539
+ mode: "x"
377
540
  });
378
541
  spin.stop();
379
542
  printSuccess(`Logged in as @${me.user.username}`);
@@ -388,17 +551,71 @@ async function loginCommand() {
388
551
  saveCredentials({
389
552
  jwt: result.jwt,
390
553
  username: result.username,
391
- tenant_id: result.tenant_id ?? ""
554
+ tenant_id: result.tenant_id ?? "",
555
+ mode: "x"
392
556
  });
393
557
  printSuccess(`Logged in as @${result.username}`);
394
558
  }
395
559
  }
560
+ async function externalLogin() {
561
+ const { instanceUrl, apiToken } = await inquirer.prompt([
562
+ {
563
+ type: "input",
564
+ name: "instanceUrl",
565
+ message: "Instance URL (e.g. https://mybot.example.com):",
566
+ validate: (val) => {
567
+ if (!val.trim()) return "Instance URL is required";
568
+ try {
569
+ new URL(val.trim());
570
+ return true;
571
+ } catch {
572
+ return "Please enter a valid URL";
573
+ }
574
+ }
575
+ },
576
+ {
577
+ type: "password",
578
+ name: "apiToken",
579
+ message: "API token:",
580
+ mask: "*",
581
+ validate: (val) => val.trim() ? true : "API token is required"
582
+ }
583
+ ]);
584
+ const url = new URL(instanceUrl.trim());
585
+ const domain = url.host;
586
+ const baseUrl = url.origin;
587
+ const token = apiToken.trim();
588
+ const spin = spinner("Testing connection...");
589
+ spin.start();
590
+ try {
591
+ const gw = new GatewayClient(baseUrl, token);
592
+ const ok = await gw.ping();
593
+ spin.stop();
594
+ if (!ok) {
595
+ printError("Instance is not responding. Check the URL and try again.");
596
+ return;
597
+ }
598
+ saveCredentials({
599
+ jwt: "",
600
+ username: "external",
601
+ tenant_id: "",
602
+ gateway_token: token,
603
+ instance_domain: domain,
604
+ mode: "external"
605
+ });
606
+ printSuccess(`Connected to ${domain}`);
607
+ } catch (err) {
608
+ spin.stop();
609
+ printError(`Connection failed: ${err.message}`);
610
+ }
611
+ }
396
612
  var init_login = __esm({
397
613
  "src/commands/login.ts"() {
398
614
  "use strict";
399
615
  init_auth();
400
616
  init_credentials();
401
617
  init_flash_client();
618
+ init_gateway_client();
402
619
  init_ui();
403
620
  }
404
621
  });
@@ -508,7 +725,7 @@ var subscribe_exports = {};
508
725
  __export(subscribe_exports, {
509
726
  subscribeCommand: () => subscribeCommand
510
727
  });
511
- import inquirer from "inquirer";
728
+ import inquirer2 from "inquirer";
512
729
  async function subscribeCommand() {
513
730
  const creds = requireCredentials();
514
731
  const client = new FlashClient(creds.jwt);
@@ -520,7 +737,7 @@ async function subscribeCommand() {
520
737
  printSuccess(`You already have an active subscription (${me.subscription.days_remaining} days remaining).`);
521
738
  return;
522
739
  }
523
- const { action } = await inquirer.prompt([
740
+ const { action } = await inquirer2.prompt([
524
741
  {
525
742
  type: "list",
526
743
  name: "action",
@@ -548,7 +765,7 @@ async function subscribeCommand() {
548
765
  printError(err.message);
549
766
  }
550
767
  } else if (action === "voucher") {
551
- const { code } = await inquirer.prompt([
768
+ const { code } = await inquirer2.prompt([
552
769
  {
553
770
  type: "input",
554
771
  name: "code",
@@ -645,144 +862,12 @@ var init_provision = __esm({
645
862
  }
646
863
  });
647
864
 
648
- // src/lib/gateway-client.ts
649
- var GatewayClient;
650
- var init_gateway_client = __esm({
651
- "src/lib/gateway-client.ts"() {
652
- "use strict";
653
- GatewayClient = class {
654
- baseUrl;
655
- token;
656
- sessionId;
657
- constructor(baseUrl, token, sessionId) {
658
- this.baseUrl = baseUrl.replace(/\/+$/, "");
659
- this.token = token;
660
- this.sessionId = sessionId;
661
- }
662
- headers() {
663
- return {
664
- Authorization: `Bearer ${this.token}`,
665
- "Content-Type": "application/json"
666
- };
667
- }
668
- chatBody(message) {
669
- return {
670
- message,
671
- ...this.sessionId ? { session_id: this.sessionId } : {}
672
- };
673
- }
674
- /** Send a message and get the full response */
675
- async chat(message) {
676
- const url = `${this.baseUrl}/api/gateway/chat`;
677
- const resp = await fetch(url, {
678
- method: "POST",
679
- headers: this.headers(),
680
- body: JSON.stringify(this.chatBody(message))
681
- });
682
- if (!resp.ok) {
683
- const text = await resp.text().catch(() => "");
684
- throw new Error(`HTTP ${resp.status}: ${text}`);
685
- }
686
- return resp.json();
687
- }
688
- /** Send a message and stream SSE events */
689
- async chatStream(message, onEvent) {
690
- const url = `${this.baseUrl}/api/gateway/chat/stream`;
691
- const resp = await fetch(url, {
692
- method: "POST",
693
- headers: this.headers(),
694
- body: JSON.stringify(this.chatBody(message))
695
- });
696
- if (!resp.ok) {
697
- const text = await resp.text().catch(() => "");
698
- throw new Error(`HTTP ${resp.status}: ${text}`);
699
- }
700
- if (!resp.body) throw new Error("No response body");
701
- const reader = resp.body.getReader();
702
- const decoder = new TextDecoder();
703
- let buffer = "";
704
- while (true) {
705
- const { done, value } = await reader.read();
706
- if (done) break;
707
- buffer += decoder.decode(value, { stream: true });
708
- let pos;
709
- while ((pos = buffer.indexOf("\n\n")) !== -1) {
710
- const frame = buffer.slice(0, pos);
711
- buffer = buffer.slice(pos + 2);
712
- for (const line of frame.split("\n")) {
713
- if (line.startsWith("data: ")) {
714
- try {
715
- const event = JSON.parse(line.slice(6));
716
- const isDone = event.type === "done";
717
- onEvent(event);
718
- if (isDone) return;
719
- } catch {
720
- }
721
- }
722
- }
723
- }
724
- }
725
- }
726
- /** Create a new session */
727
- async newSession() {
728
- const url = `${this.baseUrl}/api/gateway/sessions/new`;
729
- const resp = await fetch(url, {
730
- method: "POST",
731
- headers: this.headers()
732
- });
733
- if (!resp.ok) {
734
- const text = await resp.text().catch(() => "");
735
- throw new Error(`HTTP ${resp.status}: ${text}`);
736
- }
737
- return resp.json();
738
- }
739
- /** List sessions */
740
- async listSessions() {
741
- const url = `${this.baseUrl}/api/gateway/sessions`;
742
- const resp = await fetch(url, {
743
- method: "GET",
744
- headers: this.headers()
745
- });
746
- if (!resp.ok) {
747
- const text = await resp.text().catch(() => "");
748
- throw new Error(`HTTP ${resp.status}: ${text}`);
749
- }
750
- return resp.json();
751
- }
752
- /** Get message history for a session */
753
- async getHistory(sessionId) {
754
- const url = `${this.baseUrl}/api/gateway/sessions/${sessionId}/messages`;
755
- const resp = await fetch(url, {
756
- method: "GET",
757
- headers: this.headers()
758
- });
759
- if (!resp.ok) {
760
- const text = await resp.text().catch(() => "");
761
- throw new Error(`HTTP ${resp.status}: ${text}`);
762
- }
763
- return resp.json();
764
- }
765
- /** Simple health check */
766
- async ping() {
767
- try {
768
- const resp = await fetch(`${this.baseUrl}/api/health`, {
769
- headers: this.headers()
770
- });
771
- return resp.ok;
772
- } catch {
773
- return false;
774
- }
775
- }
776
- };
777
- }
778
- });
779
-
780
865
  // src/commands/connect.ts
781
866
  var connect_exports = {};
782
867
  __export(connect_exports, {
783
868
  connectCommand: () => connectCommand
784
869
  });
785
- import inquirer2 from "inquirer";
870
+ import inquirer3 from "inquirer";
786
871
  async function connectCommand(opts = {}) {
787
872
  const creds = requireCredentials();
788
873
  if (opts.token) {
@@ -839,7 +924,7 @@ async function connectCommand(opts = {}) {
839
924
  printWarning("Could not auto-fetch gateway credentials \u2014 you can enter them manually.");
840
925
  console.log(dim(` (${err.message})
841
926
  `));
842
- const answers = await inquirer2.prompt([
927
+ const answers = await inquirer3.prompt([
843
928
  {
844
929
  type: "input",
845
930
  name: "token",
@@ -1253,7 +1338,7 @@ __export(instances_exports, {
1253
1338
  instancesNewCommand: () => instancesNewCommand,
1254
1339
  instancesSelectCommand: () => instancesSelectCommand
1255
1340
  });
1256
- import inquirer3 from "inquirer";
1341
+ import inquirer4 from "inquirer";
1257
1342
  async function instancesListCommand() {
1258
1343
  const creds = requireCredentials();
1259
1344
  const client = new FlashClient(creds.jwt);
@@ -1335,7 +1420,7 @@ async function instancesSelectCommand(tenantId) {
1335
1420
  name: `${inst.display_name ?? inst.id} ${dim(`(${inst.status}${inst.domain ? ` - ${inst.domain}` : ""})`)}${inst.id === creds.tenant_id ? success(" *active*") : ""}`,
1336
1421
  value: inst.id
1337
1422
  }));
1338
- const answer = await inquirer3.prompt([
1423
+ const answer = await inquirer4.prompt([
1339
1424
  {
1340
1425
  type: "list",
1341
1426
  name: "tenantId",
@@ -1409,7 +1494,7 @@ async function instancesDeleteCommand(tenantId) {
1409
1494
  await doDelete(client, creds.tenant_id, creds);
1410
1495
  }
1411
1496
  async function doDelete(client, tenantId, creds) {
1412
- const answer = await inquirer3.prompt([
1497
+ const answer = await inquirer4.prompt([
1413
1498
  {
1414
1499
  type: "confirm",
1415
1500
  name: "confirm",
@@ -1553,16 +1638,16 @@ var wizard_exports = {};
1553
1638
  __export(wizard_exports, {
1554
1639
  wizardCommand: () => wizardCommand
1555
1640
  });
1556
- import inquirer4 from "inquirer";
1641
+ import inquirer5 from "inquirer";
1557
1642
  async function wizardCommand() {
1558
1643
  banner();
1559
1644
  let creds = loadCredentials();
1560
1645
  if (!creds || isJwtExpired(creds)) {
1561
- console.log(bold(" Step 1: Login with X\n"));
1646
+ console.log(bold(" Step 1: Login\n"));
1562
1647
  await loginCommand();
1563
1648
  console.log();
1564
1649
  } else {
1565
- const { accountAction } = await inquirer4.prompt([
1650
+ const { accountAction } = await inquirer5.prompt([
1566
1651
  {
1567
1652
  type: "list",
1568
1653
  name: "accountAction",
@@ -1575,12 +1660,30 @@ async function wizardCommand() {
1575
1660
  ]);
1576
1661
  if (accountAction === "signout") {
1577
1662
  await logoutCommand();
1578
- console.log(bold("\n Login with X\n"));
1663
+ console.log(bold("\n Login\n"));
1579
1664
  await loginCommand();
1580
1665
  console.log();
1581
1666
  creds = loadCredentials();
1582
1667
  }
1583
1668
  }
1669
+ creds = loadCredentials();
1670
+ if (creds?.mode === "external") {
1671
+ if (creds.gateway_token && creds.instance_domain) {
1672
+ const { startChat } = await inquirer5.prompt([
1673
+ {
1674
+ type: "confirm",
1675
+ name: "startChat",
1676
+ message: "Start chatting with your bot?",
1677
+ default: true
1678
+ }
1679
+ ]);
1680
+ if (startChat) {
1681
+ console.log();
1682
+ await chatReplCommand();
1683
+ }
1684
+ }
1685
+ return;
1686
+ }
1584
1687
  const currentCreds = requireCredentials();
1585
1688
  const client = new FlashClient(currentCreds.jwt);
1586
1689
  const spin = spinner("Checking account...");
@@ -1607,7 +1710,7 @@ async function wizardCommand() {
1607
1710
  });
1608
1711
  choices.push({ name: "Create new instance", value: "__new__" });
1609
1712
  console.log(bold("\n Select an instance\n"));
1610
- const { instanceChoice } = await inquirer4.prompt([
1713
+ const { instanceChoice } = await inquirer5.prompt([
1611
1714
  {
1612
1715
  type: "list",
1613
1716
  name: "instanceChoice",
@@ -1617,7 +1720,7 @@ async function wizardCommand() {
1617
1720
  }
1618
1721
  ]);
1619
1722
  if (instanceChoice === "__new__") {
1620
- const { displayName } = await inquirer4.prompt([
1723
+ const { displayName } = await inquirer5.prompt([
1621
1724
  {
1622
1725
  type: "input",
1623
1726
  name: "displayName",
@@ -1665,7 +1768,7 @@ async function wizardCommand() {
1665
1768
  }
1666
1769
  const finalCreds = loadCredentials();
1667
1770
  if (finalCreds?.gateway_token && finalCreds?.instance_domain) {
1668
- const { startChat } = await inquirer4.prompt([
1771
+ const { startChat } = await inquirer5.prompt([
1669
1772
  {
1670
1773
  type: "confirm",
1671
1774
  name: "startChat",
@@ -1698,7 +1801,7 @@ init_ui();
1698
1801
  import { Command } from "commander";
1699
1802
  var program = new Command();
1700
1803
  program.name("starkbot").description("CLI for Starkbot \u2014 login, provision, and chat with your bot").version("0.1.0").addHelpCommand("help", "Show help for a command");
1701
- program.command("login").description("Login with your X (Twitter) account").action(async () => {
1804
+ program.command("login").description("Login with X or connect to an external instance").action(async () => {
1702
1805
  const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
1703
1806
  await loginCommand2();
1704
1807
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starkbot-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for Starkbot — login, provision, and chat with your bot from the terminal",
5
5
  "type": "module",
6
6
  "bin": {