@ask-thane/thane-cli 0.1.17 → 0.1.19

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.
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AA0gBA,wBAAsB,OAAO,CAAC,cAAc,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAikBvE"}
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AA+hBA,wBAAsB,OAAO,CAAC,cAAc,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4vBvE"}
package/dist/chat.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
2
  import { emitKeypressEvents } from "node:readline";
3
- import { renderChannels, renderInbox, renderMembers, renderMessages, renderWorkspaces } from "./render.js";
3
+ import { createHostedChannel, hasHostedChat, reactHostedMessage, sendHostedMessage, syncHostedStore } from "./hosted.js";
4
+ import { renderChannels, renderInbox, renderMessages, renderUsers } from "./render.js";
4
5
  import { completeSlashCommand, renderSlashCommands, slashCommands } from "./slash-commands.js";
5
6
  import { ThaneStore } from "./store.js";
6
7
  import { checkForUpdate, renderUpdateStatus } from "./update.js";
@@ -299,6 +300,19 @@ function renderMenuLines(selectedIndex, availableRows, updateStatus) {
299
300
  })
300
301
  ];
301
302
  }
303
+ function renderWorkspacePickerLines(store, selectedIndex) {
304
+ const workspaces = store.listWorkspaces();
305
+ return [
306
+ `${BOLD}Workspaces${RESET}`,
307
+ `${DIM}Use arrows, Return to switch, Esc to close.${RESET}`,
308
+ "",
309
+ ...workspaces.map((workspace, index) => {
310
+ const active = workspace.id === store.activeWorkspace.id ? "*" : " ";
311
+ const row = `${active} ${workspace.slug} - ${workspace.name}`;
312
+ return index === selectedIndex ? `${INVERSE}${row}${RESET}` : row;
313
+ })
314
+ ];
315
+ }
302
316
  function renderReactionPickerLines(selectedIndex) {
303
317
  const options = QUICK_REACTIONS.map((reaction, index) => {
304
318
  const label = ` ${index + 1} ${reaction} `;
@@ -347,7 +361,7 @@ function requireHostedAuthToken(store) {
347
361
  }
348
362
  return token;
349
363
  }
350
- async function createWorkspaceInviteLink(store, role = "member") {
364
+ async function createWorkspaceInviteLink(store, role = "member", inviteeEmail) {
351
365
  store.requireWorkspaceAdmin();
352
366
  const token = requireHostedAuthToken(store);
353
367
  const response = await postThaneApiWithAuth("/v1/thane-cli/workspace-invites", token, {
@@ -355,8 +369,12 @@ async function createWorkspaceInviteLink(store, role = "member") {
355
369
  workspaceSlug: store.activeWorkspace.slug,
356
370
  workspaceName: store.activeWorkspace.name,
357
371
  role,
358
- expiresInHours: 24 * 7
372
+ expiresInHours: 24 * 7,
373
+ ...(inviteeEmail ? { inviteeEmail, maxUses: 1 } : {})
359
374
  });
375
+ if (response.invite.inviteeEmail) {
376
+ return `${response.invite.emailSent ? "Sent" : "Created"} invite for ${response.invite.inviteeEmail} (${response.invite.role}, expires ${response.invite.expiresAt})`;
377
+ }
360
378
  return `${response.invite.url} (${response.invite.role}, expires ${response.invite.expiresAt})`;
361
379
  }
362
380
  function renderScreen(inputText, state) {
@@ -449,12 +467,21 @@ function renderScreen(inputText, state) {
449
467
  }
450
468
  export async function runChat(initialChannel = "general") {
451
469
  let store = await ThaneStore.open();
470
+ try {
471
+ await syncHostedStore(store);
472
+ store = await ThaneStore.open();
473
+ }
474
+ catch (_error) {
475
+ // Stay usable offline; the footer will surface explicit command failures.
476
+ }
452
477
  let activeChannel = await selectConversation(store, initialChannel);
453
478
  let inputText = "";
454
479
  let status = "";
455
480
  let showHelp = false;
456
481
  let showMenu = false;
457
482
  let sidePanelLines;
483
+ let workspacePickerOpen = false;
484
+ let workspacePickerIndex = 0;
458
485
  let menuIndex = 0;
459
486
  let showReactionPicker = false;
460
487
  let reactionIndex = 0;
@@ -465,8 +492,22 @@ export async function runChat(initialChannel = "general") {
465
492
  let updateStatus = { state: "checking" };
466
493
  let targetMessageId;
467
494
  let isOpen = true;
495
+ let lastHostedSyncMs = 0;
468
496
  const refresh = async () => {
469
497
  store = await ThaneStore.open();
498
+ if (hasHostedChat(store) && Date.now() - lastHostedSyncMs > 2500) {
499
+ try {
500
+ await syncHostedStore(store, { workspaceId: store.activeWorkspace.id });
501
+ lastHostedSyncMs = Date.now();
502
+ store = await ThaneStore.open();
503
+ }
504
+ catch (error) {
505
+ status ||= `Hosted sync failed: ${error.message}`;
506
+ }
507
+ }
508
+ if (!store.findChannel(activeChannel.id)) {
509
+ activeChannel = await selectConversation(store, "general");
510
+ }
470
511
  const items = conversations(store, activeChannel.id);
471
512
  const activeIndex = items.findIndex((item) => item.id === activeChannel.id);
472
513
  if (focus !== "sidebar") {
@@ -513,6 +554,7 @@ export async function runChat(initialChannel = "general") {
513
554
  showHelp = false;
514
555
  showMenu = false;
515
556
  sidePanelLines = undefined;
557
+ workspacePickerOpen = false;
516
558
  showReactionPicker = false;
517
559
  focus = nextFocus;
518
560
  messageIndex = Math.max(0, threadedMessages(store.recent(activeChannel.id, 200)).length - 1);
@@ -578,7 +620,13 @@ export async function runChat(initialChannel = "general") {
578
620
  status = "No reaction target selected.";
579
621
  return;
580
622
  }
581
- await store.react(targetMessageId, emoji);
623
+ if (hasHostedChat(store)) {
624
+ await reactHostedMessage(store, { messageId: targetMessageId, emoji });
625
+ store = await ThaneStore.open();
626
+ }
627
+ else {
628
+ await store.react(targetMessageId, emoji);
629
+ }
582
630
  composerMode = "message";
583
631
  targetMessageId = undefined;
584
632
  showReactionPicker = false;
@@ -600,10 +648,18 @@ export async function runChat(initialChannel = "general") {
600
648
  status = "No reply target selected.";
601
649
  return;
602
650
  }
603
- const sent = await store.reply(targetMessageId, trimmed, "chat");
651
+ const root = selectedMessage();
652
+ const threadRootId = root?.threadRootId ?? root?.id ?? targetMessageId;
653
+ if (hasHostedChat(store)) {
654
+ await sendHostedMessage(store, { channelId: activeChannel.id, text: trimmed, source: "chat", threadRootId });
655
+ store = await ThaneStore.open();
656
+ }
657
+ const sent = hasHostedChat(store)
658
+ ? threadedMessages(store.recent(activeChannel.id, 200)).at(-1)
659
+ : await store.reply(targetMessageId, trimmed, "chat");
604
660
  composerMode = "message";
605
661
  targetMessageId = undefined;
606
- messageIndex = Math.max(0, threadedMessages(store.recent(activeChannel.id, 200)).findIndex((message) => message.id === sent.id));
662
+ messageIndex = Math.max(0, threadedMessages(store.recent(activeChannel.id, 200)).findIndex((message) => message.id === sent?.id));
607
663
  status = "";
608
664
  await store.markReadConversation(activeChannel.id);
609
665
  return;
@@ -616,6 +672,7 @@ export async function runChat(initialChannel = "general") {
616
672
  showMenu = true;
617
673
  showHelp = false;
618
674
  sidePanelLines = undefined;
675
+ workspacePickerOpen = false;
619
676
  status = "Command menu";
620
677
  return;
621
678
  }
@@ -638,12 +695,27 @@ export async function runChat(initialChannel = "general") {
638
695
  status = "Reset workspace art to generated default.";
639
696
  return;
640
697
  }
698
+ if (trimmed.startsWith("/invite ")) {
699
+ const email = trimmed.slice("/invite ".length).trim();
700
+ if (!email) {
701
+ status = "Usage: /invite <email>";
702
+ return;
703
+ }
704
+ status = await createWorkspaceInviteLink(store, "member", email);
705
+ showHelp = false;
706
+ showMenu = false;
707
+ sidePanelLines = undefined;
708
+ workspacePickerOpen = false;
709
+ showReactionPicker = false;
710
+ return;
711
+ }
641
712
  if (trimmed === "/invite-link" || trimmed === "/invite-link create" || trimmed === "/invite-link admin") {
642
713
  const role = trimmed.endsWith(" admin") ? "admin" : "member";
643
714
  status = `Invite link: ${await createWorkspaceInviteLink(store, role)}`;
644
715
  showHelp = false;
645
716
  showMenu = false;
646
717
  sidePanelLines = undefined;
718
+ workspacePickerOpen = false;
647
719
  showReactionPicker = false;
648
720
  return;
649
721
  }
@@ -651,17 +723,15 @@ export async function runChat(initialChannel = "general") {
651
723
  showHelp = !showHelp;
652
724
  showMenu = false;
653
725
  sidePanelLines = undefined;
726
+ workspacePickerOpen = false;
654
727
  status = showHelp ? "Command help" : "";
655
728
  return;
656
729
  }
657
730
  if (trimmed === "/workspaces") {
658
- const workspaces = renderWorkspaces(store.listWorkspaces(), store.activeWorkspace.id);
659
- sidePanelLines = [
660
- `${BOLD}Workspaces${RESET}`,
661
- `${DIM}Active workspace is marked with *. Switch with /workspace <slug>.${RESET}`,
662
- "",
663
- ...workspaces.split("\n")
664
- ];
731
+ const workspaces = store.listWorkspaces();
732
+ workspacePickerIndex = Math.max(0, workspaces.findIndex((workspace) => workspace.id === store.activeWorkspace.id));
733
+ sidePanelLines = renderWorkspacePickerLines(store, workspacePickerIndex);
734
+ workspacePickerOpen = true;
665
735
  showHelp = false;
666
736
  showMenu = false;
667
737
  showReactionPicker = false;
@@ -670,6 +740,7 @@ export async function runChat(initialChannel = "general") {
670
740
  }
671
741
  if (trimmed === "/channels") {
672
742
  sidePanelLines = [`${BOLD}Channels${RESET}`, "", ...renderChannels(store.listChannels()).split("\n")];
743
+ workspacePickerOpen = false;
673
744
  showHelp = false;
674
745
  showMenu = false;
675
746
  showReactionPicker = false;
@@ -677,11 +748,13 @@ export async function runChat(initialChannel = "general") {
677
748
  return;
678
749
  }
679
750
  if (trimmed === "/members") {
680
- sidePanelLines = [`${BOLD}Members${RESET}`, "", ...renderMembers(store.listMembers()).split("\n")];
751
+ const members = activeChannel.kind === "channel" ? store.channelMembers(activeChannel.name) : store.listUsers();
752
+ sidePanelLines = [`${BOLD}Members: ${channelLabel(activeChannel)}${RESET}`, "", ...renderUsers(members).split("\n")];
753
+ workspacePickerOpen = false;
681
754
  showHelp = false;
682
755
  showMenu = false;
683
756
  showReactionPicker = false;
684
- status = "Members in this workspace.";
757
+ status = `Members in ${channelLabel(activeChannel)}.`;
685
758
  return;
686
759
  }
687
760
  if (trimmed === "/inbox" || trimmed === "/inbox all") {
@@ -691,6 +764,7 @@ export async function runChat(initialChannel = "general") {
691
764
  "",
692
765
  ...renderInbox(store.inbox({ allWorkspaces })).split("\n")
693
766
  ];
767
+ workspacePickerOpen = false;
694
768
  showHelp = false;
695
769
  showMenu = false;
696
770
  showReactionPicker = false;
@@ -703,12 +777,89 @@ export async function runChat(initialChannel = "general") {
703
777
  "",
704
778
  ...renderMessages(store.recent(activeChannel.id, 12)).split("\n")
705
779
  ];
780
+ workspacePickerOpen = false;
706
781
  showHelp = false;
707
782
  showMenu = false;
708
783
  showReactionPicker = false;
709
784
  status = `Recent messages in ${channelLabel(activeChannel)}.`;
710
785
  return;
711
786
  }
787
+ if (trimmed.startsWith("/thread ")) {
788
+ const messageId = trimmed.slice("/thread ".length).trim();
789
+ sidePanelLines = [`${BOLD}Thread${RESET}`, "", ...renderMessages(store.thread(messageId)).split("\n")];
790
+ workspacePickerOpen = false;
791
+ showHelp = false;
792
+ showMenu = false;
793
+ showReactionPicker = false;
794
+ status = `Thread ${messageId}`;
795
+ return;
796
+ }
797
+ if (trimmed.startsWith("/reply ")) {
798
+ const match = trimmed.match(/^\/reply\s+(\S+)\s+([\s\S]+)$/);
799
+ if (!match?.[1] || !match[2]?.trim()) {
800
+ status = "Usage: /reply <message-id> <text>";
801
+ return;
802
+ }
803
+ const messageId = match[1];
804
+ const text = match[2].trim();
805
+ const root = store.thread(messageId)[0];
806
+ if (!root) {
807
+ status = `Message ${messageId} was not found.`;
808
+ return;
809
+ }
810
+ const target = store.findChannel(root.channel);
811
+ if (!target) {
812
+ status = `Channel ${root.channel} was not found.`;
813
+ return;
814
+ }
815
+ if (hasHostedChat(store)) {
816
+ await sendHostedMessage(store, { channelId: target.id, text, source: "chat", threadRootId: root.threadRootId ?? root.id });
817
+ store = await ThaneStore.open();
818
+ }
819
+ else {
820
+ await store.reply(messageId, text, "chat");
821
+ }
822
+ activeChannel = target;
823
+ sidePanelLines = [`${BOLD}Thread${RESET}`, "", ...renderMessages(store.thread(messageId)).split("\n")];
824
+ workspacePickerOpen = false;
825
+ showHelp = false;
826
+ showMenu = false;
827
+ showReactionPicker = false;
828
+ status = `Replied in thread ${messageId}.`;
829
+ return;
830
+ }
831
+ if (trimmed.startsWith("/react ")) {
832
+ const match = trimmed.match(/^\/react\s+(\S+)\s+(.+)$/);
833
+ if (!match?.[1] || !match[2]?.trim()) {
834
+ status = "Usage: /react <message-id> <emoji>";
835
+ return;
836
+ }
837
+ const messageId = match[1];
838
+ const emoji = match[2].trim();
839
+ if (hasHostedChat(store)) {
840
+ await reactHostedMessage(store, { messageId, emoji });
841
+ store = await ThaneStore.open();
842
+ }
843
+ else {
844
+ await store.react(messageId, emoji);
845
+ }
846
+ status = `Reacted ${emoji}`;
847
+ return;
848
+ }
849
+ if (trimmed.startsWith("/search ")) {
850
+ const query = trimmed.slice("/search ".length).trim();
851
+ if (!query) {
852
+ status = "Usage: /search <query>";
853
+ return;
854
+ }
855
+ sidePanelLines = [`${BOLD}Search: ${query}${RESET}`, "", ...renderMessages(store.search(query, 20)).split("\n")];
856
+ workspacePickerOpen = false;
857
+ showHelp = false;
858
+ showMenu = false;
859
+ showReactionPicker = false;
860
+ status = `Search results for ${query}.`;
861
+ return;
862
+ }
712
863
  if (trimmed === "/leave") {
713
864
  if (activeChannel.kind === "dm") {
714
865
  status = "DMs cannot be left.";
@@ -717,6 +868,7 @@ export async function runChat(initialChannel = "general") {
717
868
  const left = await store.leaveChannel(activeChannel.name);
718
869
  activeChannel = await selectConversation(store, "general");
719
870
  sidePanelLines = undefined;
871
+ workspacePickerOpen = false;
720
872
  showHelp = false;
721
873
  showMenu = false;
722
874
  showReactionPicker = false;
@@ -724,12 +876,18 @@ export async function runChat(initialChannel = "general") {
724
876
  return;
725
877
  }
726
878
  if (trimmed.startsWith("/join ")) {
727
- activeChannel = await selectConversation(store, trimmed.slice("/join ".length).trim());
879
+ const channelName = trimmed.slice("/join ".length).trim();
880
+ if (hasHostedChat(store)) {
881
+ await createHostedChannel(store, { name: channelName });
882
+ store = await ThaneStore.open();
883
+ }
884
+ activeChannel = await selectConversation(store, channelName);
728
885
  await store.markReadConversation(activeChannel.id);
729
886
  status = `Joined ${channelLabel(activeChannel)}`;
730
887
  showHelp = false;
731
888
  showMenu = false;
732
889
  sidePanelLines = undefined;
890
+ workspacePickerOpen = false;
733
891
  showReactionPicker = false;
734
892
  focus = "messages";
735
893
  messageIndex = Math.max(0, threadedMessages(store.recent(activeChannel.id, 200)).length - 1);
@@ -742,6 +900,7 @@ export async function runChat(initialChannel = "general") {
742
900
  showHelp = false;
743
901
  showMenu = false;
744
902
  sidePanelLines = undefined;
903
+ workspacePickerOpen = false;
745
904
  showReactionPicker = false;
746
905
  focus = "messages";
747
906
  messageIndex = Math.max(0, threadedMessages(store.recent(activeChannel.id, 200)).length - 1);
@@ -749,11 +908,16 @@ export async function runChat(initialChannel = "general") {
749
908
  }
750
909
  if (trimmed.startsWith("/workspace ")) {
751
910
  const workspace = await store.useWorkspace(trimmed.slice("/workspace ".length).trim());
911
+ if (hasHostedChat(store)) {
912
+ await syncHostedStore(store, { workspaceId: workspace.id });
913
+ store = await ThaneStore.open();
914
+ }
752
915
  activeChannel = await selectConversation(store, "general");
753
916
  status = `Switched to workspace ${workspace.slug}`;
754
917
  showHelp = false;
755
918
  showMenu = false;
756
919
  sidePanelLines = undefined;
920
+ workspacePickerOpen = false;
757
921
  showReactionPicker = false;
758
922
  return;
759
923
  }
@@ -761,13 +925,20 @@ export async function runChat(initialChannel = "general") {
761
925
  status = "Unknown command. Type /help.";
762
926
  return;
763
927
  }
764
- const sent = await store.sendMessage(activeChannel.id, trimmed, undefined, "chat");
928
+ if (hasHostedChat(store)) {
929
+ await sendHostedMessage(store, { channelId: activeChannel.id, text: trimmed, source: "chat" });
930
+ store = await ThaneStore.open();
931
+ }
932
+ const sent = hasHostedChat(store)
933
+ ? threadedMessages(store.recent(activeChannel.id, 200)).at(-1)
934
+ : await store.sendMessage(activeChannel.id, trimmed, undefined, "chat");
765
935
  const messages = threadedMessages(store.recent(activeChannel.id, 200));
766
- status = messages.some((message) => message.id === sent.id) ? "" : "";
767
- messageIndex = Math.max(0, messages.findIndex((message) => message.id === sent.id));
936
+ status = sent && messages.some((message) => message.id === sent.id) ? "" : "";
937
+ messageIndex = Math.max(0, sent ? messages.findIndex((message) => message.id === sent.id) : messages.length - 1);
768
938
  showHelp = false;
769
939
  showMenu = false;
770
940
  sidePanelLines = undefined;
941
+ workspacePickerOpen = false;
771
942
  await store.markReadConversation(activeChannel.id);
772
943
  };
773
944
  const completeInput = () => {
@@ -800,6 +971,7 @@ export async function runChat(initialChannel = "general") {
800
971
  showMenu = true;
801
972
  showHelp = false;
802
973
  sidePanelLines = undefined;
974
+ workspacePickerOpen = false;
803
975
  status = "Command menu";
804
976
  }
805
977
  else {
@@ -894,6 +1066,47 @@ export async function runChat(initialChannel = "general") {
894
1066
  await refresh();
895
1067
  return;
896
1068
  }
1069
+ if (workspacePickerOpen) {
1070
+ const workspaces = store.listWorkspaces();
1071
+ if (key.name === "escape") {
1072
+ workspacePickerOpen = false;
1073
+ sidePanelLines = undefined;
1074
+ status = "";
1075
+ }
1076
+ else if (key.name === "up") {
1077
+ workspacePickerIndex = workspacePickerIndex === 0 ? Math.max(0, workspaces.length - 1) : workspacePickerIndex - 1;
1078
+ sidePanelLines = renderWorkspacePickerLines(store, workspacePickerIndex);
1079
+ }
1080
+ else if (key.name === "down" || key.name === "tab") {
1081
+ workspacePickerIndex = workspacePickerIndex >= workspaces.length - 1 ? 0 : workspacePickerIndex + 1;
1082
+ sidePanelLines = renderWorkspacePickerLines(store, workspacePickerIndex);
1083
+ }
1084
+ else if (key.name === "return") {
1085
+ const selected = workspaces[workspacePickerIndex];
1086
+ if (selected) {
1087
+ const workspace = await store.useWorkspace(selected.slug);
1088
+ if (hasHostedChat(store)) {
1089
+ await syncHostedStore(store, { workspaceId: workspace.id });
1090
+ store = await ThaneStore.open();
1091
+ }
1092
+ activeChannel = await selectConversation(store, "general");
1093
+ workspacePickerOpen = false;
1094
+ sidePanelLines = undefined;
1095
+ status = `Switched to workspace ${workspace.slug}`;
1096
+ }
1097
+ }
1098
+ else if (key.sequence && key.sequence >= " " && !key.ctrl) {
1099
+ workspacePickerOpen = false;
1100
+ sidePanelLines = undefined;
1101
+ inputText = key.sequence;
1102
+ focusComposer();
1103
+ }
1104
+ if (!isOpen) {
1105
+ return;
1106
+ }
1107
+ await refresh();
1108
+ return;
1109
+ }
897
1110
  if (focus === "sidebar") {
898
1111
  const items = conversations(store, activeChannel.id);
899
1112
  if (key.name === "up") {
@@ -1002,6 +1215,7 @@ export async function runChat(initialChannel = "general") {
1002
1215
  showHelp = false;
1003
1216
  showMenu = false;
1004
1217
  sidePanelLines = undefined;
1218
+ workspacePickerOpen = false;
1005
1219
  showReactionPicker = false;
1006
1220
  if (composerMode !== "message") {
1007
1221
  composerMode = "message";