@andocorp/cli 0.1.0 → 0.1.2

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,235 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { createMe, createMessage } from "./test-helpers.js";
3
-
4
- const clientMocks = vi.hoisted(() => {
5
- return {
6
- action: vi.fn(),
7
- constructor: vi.fn(),
8
- mutation: vi.fn(),
9
- query: vi.fn(),
10
- setAuth: vi.fn(),
11
- };
12
- });
13
-
14
- vi.mock("convex/browser", () => {
15
- return {
16
- ConvexHttpClient: class {
17
- constructor(...args: unknown[]) {
18
- clientMocks.constructor(...args);
19
- }
20
-
21
- action = clientMocks.action;
22
- mutation = clientMocks.mutation;
23
- query = clientMocks.query;
24
- setAuth = clientMocks.setAuth;
25
- },
26
- };
27
- });
28
-
29
- vi.mock("../../convex/convex/_generated/api.js", () => {
30
- return {
31
- api: {
32
- app: {
33
- bootstrap: "bootstrap",
34
- createMessage: "createMessage",
35
- createReaction: "createReaction",
36
- getMessage: "getMessage",
37
- listConversationMessagesPaginated: "listConversationMessagesPaginated",
38
- listThreadReplies: "listThreadReplies",
39
- },
40
- auth: {
41
- discoverWorkspaces: "discoverWorkspaces",
42
- refreshSessionJwt: "refreshSessionJwt",
43
- selectWorkspace: "selectWorkspace",
44
- sendEmailOtp: "sendEmailOtp",
45
- },
46
- },
47
- };
48
- });
49
-
50
- describe("createAndoCliClient", () => {
51
- beforeEach(() => {
52
- clientMocks.action.mockReset();
53
- clientMocks.constructor.mockReset();
54
- clientMocks.mutation.mockReset();
55
- clientMocks.query.mockReset();
56
- clientMocks.setAuth.mockReset();
57
- });
58
-
59
- it("loads me from bootstrap", async () => {
60
- const { createAndoCliClient } = await import("./client.js");
61
- clientMocks.query.mockResolvedValue({
62
- me: createMe({
63
- created_at: 1_700_000_000_000 as unknown as Date,
64
- }),
65
- memberships: [],
66
- });
67
-
68
- const client = createAndoCliClient({
69
- convexUrl: "https://convex.example.com",
70
- sessionToken: "session-token",
71
- });
72
- const me = await client.getMe();
73
-
74
- expect(clientMocks.setAuth).toHaveBeenCalledWith("session-token");
75
- expect(clientMocks.query).toHaveBeenCalledWith("bootstrap", {});
76
- expect(me?.workspace.name).toBe("Ando");
77
- expect(me?.created_at).toBeInstanceOf(Date);
78
- });
79
-
80
- it("reuses the bootstrap result across me and memberships", async () => {
81
- const { createAndoCliClient } = await import("./client.js");
82
- clientMocks.query.mockResolvedValue({
83
- me: createMe(),
84
- memberships: [],
85
- });
86
-
87
- const client = createAndoCliClient({
88
- convexUrl: "https://convex.example.com",
89
- sessionToken: "session-token",
90
- });
91
-
92
- await client.getMe();
93
- await client.getAllMemberships();
94
-
95
- expect(clientMocks.query).toHaveBeenCalledTimes(1);
96
- expect(clientMocks.query).toHaveBeenCalledWith("bootstrap", {});
97
- });
98
-
99
- it("constructs the authenticated Convex client once per CLI client", async () => {
100
- const { createAndoCliClient } = await import("./client.js");
101
- clientMocks.query
102
- .mockResolvedValueOnce({
103
- me: createMe(),
104
- memberships: [],
105
- })
106
- .mockResolvedValueOnce({
107
- page: [],
108
- isDone: true,
109
- continueCursor: "",
110
- });
111
-
112
- const client = createAndoCliClient({
113
- convexUrl: "https://convex.example.com",
114
- sessionToken: "session-token",
115
- });
116
-
117
- await client.getMe();
118
- await client.getConversationMessages("conversation-1", 10);
119
-
120
- expect(clientMocks.constructor).toHaveBeenCalledTimes(2);
121
- expect(clientMocks.constructor).toHaveBeenNthCalledWith(
122
- 1,
123
- "https://convex.example.com"
124
- );
125
- expect(clientMocks.constructor).toHaveBeenNthCalledWith(
126
- 2,
127
- "https://convex.example.com"
128
- );
129
- expect(clientMocks.setAuth).toHaveBeenCalledTimes(1);
130
- expect(clientMocks.setAuth).toHaveBeenCalledWith("session-token");
131
- });
132
-
133
- it("reverses paginated message results", async () => {
134
- const { createAndoCliClient } = await import("./client.js");
135
- clientMocks.query.mockResolvedValue({
136
- page: [
137
- createMessage({ id: "message-3", created_at: 3 as unknown as Date }),
138
- createMessage({ id: "message-2", created_at: 2 as unknown as Date }),
139
- createMessage({ id: "message-1", created_at: 1 as unknown as Date }),
140
- ],
141
- isDone: false,
142
- continueCursor: "cursor-2",
143
- });
144
-
145
- const client = createAndoCliClient({
146
- convexUrl: "https://convex.example.com",
147
- sessionToken: "session-token",
148
- });
149
- const page = await client.getConversationMessages("conversation-1", 10);
150
-
151
- expect(clientMocks.query).toHaveBeenCalledWith(
152
- "listConversationMessagesPaginated",
153
- {
154
- conversationId: "conversation-1",
155
- paginationOpts: {
156
- cursor: null,
157
- numItems: 10,
158
- },
159
- }
160
- );
161
- expect(page.items.map((item) => item.id)).toEqual([
162
- "message-1",
163
- "message-2",
164
- "message-3",
165
- ]);
166
- expect(page.cursor).toBe("cursor-2");
167
- });
168
-
169
- it("posts a message with the Convex mutation payload the web app expects", async () => {
170
- const { createAndoCliClient } = await import("./client.js");
171
- clientMocks.mutation.mockResolvedValue(createMessage());
172
-
173
- const client = createAndoCliClient({
174
- convexUrl: "https://convex.example.com",
175
- sessionToken: "session-token",
176
- });
177
-
178
- await client.postMessage({
179
- conversationId: "conversation-1",
180
- markdownContent: "hello",
181
- threadRootId: "message-root",
182
- });
183
-
184
- expect(clientMocks.mutation).toHaveBeenCalledWith("createMessage", {
185
- conversationId: "conversation-1",
186
- clientRequestId: null,
187
- markdownContent: "hello",
188
- explicitContextMessageIds: [],
189
- imageUrls: [],
190
- fileIds: [],
191
- initiatorId: null,
192
- suppressedLinkPreviewUrls: [],
193
- callRootId: null,
194
- threadRootMessageId: "message-root",
195
- });
196
- });
197
-
198
- it("adapts discovered workspaces returned by auth discovery", async () => {
199
- const { createAndoCliClient } = await import("./client.js");
200
- clientMocks.action.mockResolvedValue({
201
- success: true,
202
- workspaces: [
203
- {
204
- id: "workspace-1",
205
- stytch_organization_id: "org-1",
206
- name: "Ando",
207
- slug: "ando",
208
- image_url: null,
209
- icon_image_file_id: null,
210
- description: null,
211
- invite_link_id: "invite-1",
212
- created_at: 1_700_000_000_000,
213
- updated_at: 1_700_000_000_000,
214
- member_count: 3,
215
- },
216
- ],
217
- intermediate_session_token: "intermediate-token",
218
- requires_workspace_creation: false,
219
- });
220
-
221
- const client = createAndoCliClient({
222
- convexUrl: "https://convex.example.com",
223
- });
224
- const discovery = await client.discoverWorkspaces({
225
- email: "alex@ando.so",
226
- code: "123456",
227
- });
228
-
229
- expect(clientMocks.action).toHaveBeenCalledWith("discoverWorkspaces", {
230
- email: "alex@ando.so",
231
- code: "123456",
232
- });
233
- expect(discovery.workspaces[0]?.created_at).toBeInstanceOf(Date);
234
- });
235
- });
package/src/client.ts DELETED
@@ -1,378 +0,0 @@
1
- import { ConvexHttpClient, ConvexClient } from "convex/browser";
2
- import { api } from "../../convex/convex/_generated/api.js";
3
- import * as Shared from "@ando/shared";
4
- import {
5
- adaptBootstrap,
6
- adaptConversationPage,
7
- adaptDiscoveredWorkspace,
8
- adaptMessage,
9
- } from "./adapters.js";
10
- import {
11
- CliConfig,
12
- ConversationPage,
13
- DiscoveredWorkspace,
14
- Me,
15
- Membership,
16
- Message,
17
- } from "./types.js";
18
-
19
- type AuthSessionResponse = {
20
- session_jwt: string | null;
21
- };
22
-
23
- type BootstrapQueryResult = {
24
- me: Record<string, unknown> | null;
25
- memberships: Record<string, unknown>[];
26
- };
27
-
28
- type ConversationMessagesQueryResult = {
29
- page: Shared.Contracts.ConversationMessageExtended[];
30
- isDone: boolean;
31
- continueCursor: string;
32
- };
33
-
34
- type AndoCliClient = {
35
- addReaction: (
36
- messageId: string,
37
- emoji: string
38
- ) => Promise<Shared.Contracts.CreateMessageReactionResponse>;
39
- createAuthenticatedCopy: (sessionToken: string) => AndoCliClient;
40
- discoverWorkspaces: (
41
- body: Shared.Contracts.DiscoverWorkspacesBody
42
- ) => Promise<{
43
- success: boolean;
44
- workspaces: DiscoveredWorkspace[];
45
- intermediate_session_token: string;
46
- requires_workspace_creation: boolean;
47
- }>;
48
- getAllMemberships: () => Promise<Membership[]>;
49
- getConversationMessages: (
50
- conversationId: string,
51
- limit?: number
52
- ) => Promise<ConversationPage>;
53
- getConversationMessagesPage: (params: {
54
- conversationId: string;
55
- cursor?: string | null;
56
- limit?: number;
57
- }) => Promise<ConversationPage>;
58
- getMe: () => Promise<Me | null>;
59
- getMessage: (messageId: string) => Promise<Message | null>;
60
- getThreadReplies: (messageId: string) => Promise<Message[]>;
61
- postMessage: (params: {
62
- conversationId: string;
63
- markdownContent: string;
64
- threadRootId?: string | null;
65
- }) => Promise<Message>;
66
- refreshSessionJwt: (sessionToken: string) => Promise<string | null>;
67
- selectWorkspace: (
68
- body: Shared.Contracts.SelectWorkspaceBody
69
- ) => Promise<Shared.Contracts.SelectWorkspaceResponse>;
70
- sendEmailOtp: (
71
- body: Shared.Contracts.SendEmailOtpBody
72
- ) => Promise<Shared.Contracts.SendEmailOtpSuccessResponse>;
73
- subscribeToConversationMessages: (params: {
74
- conversationId: string;
75
- limit?: number;
76
- onUpdate: (page: ConversationPage) => void;
77
- }) => () => void;
78
- subscribeToThreadReplies: (params: {
79
- threadRootId: string;
80
- onUpdate: (replies: Message[]) => void;
81
- }) => () => void;
82
- close: () => Promise<void>;
83
- };
84
-
85
- function createConvexHttpClient(convexUrl: string, sessionToken?: string) {
86
- const client = new ConvexHttpClient(convexUrl);
87
- if (typeof sessionToken === "string" && sessionToken.length > 0) {
88
- client.setAuth(sessionToken);
89
- }
90
-
91
- return client;
92
- }
93
-
94
- function createConvexReactiveClient(convexUrl: string, sessionToken?: string) {
95
- const client = new ConvexClient(convexUrl);
96
- if (typeof sessionToken === "string" && sessionToken.length > 0) {
97
- client.setAuth(async () => sessionToken);
98
- }
99
-
100
- return client;
101
- }
102
-
103
- function getAuthenticatedClient(config: CliConfig) {
104
- return createConvexHttpClient(config.convexUrl, config.sessionToken);
105
- }
106
-
107
- function getAuthenticatedReactiveClient(config: CliConfig) {
108
- return createConvexReactiveClient(config.convexUrl, config.sessionToken);
109
- }
110
-
111
- export function createAndoCliClient(
112
- config: Partial<CliConfig> & { convexUrl: string }
113
- ): AndoCliClient {
114
- const anonymousClient = createConvexHttpClient(config.convexUrl);
115
- let authenticatedClient: ConvexHttpClient | null = null;
116
- let reactiveClient: ConvexClient | null = null;
117
- let bootstrapPromise: Promise<{
118
- me: Me | null;
119
- memberships: Membership[];
120
- }> | null = null;
121
-
122
- function requireAuthenticatedConfig(): CliConfig {
123
- if (
124
- typeof config.sessionToken !== "string" ||
125
- config.sessionToken.length === 0
126
- ) {
127
- throw new Error(`OAuth session is required. Run "ando login" first.`);
128
- }
129
-
130
- return {
131
- convexUrl: config.convexUrl,
132
- sessionToken: config.sessionToken,
133
- };
134
- }
135
-
136
- function requireAuthenticatedClient() {
137
- if (authenticatedClient != null) {
138
- return authenticatedClient;
139
- }
140
-
141
- authenticatedClient = getAuthenticatedClient(requireAuthenticatedConfig());
142
- return authenticatedClient;
143
- }
144
-
145
- function requireReactiveClient() {
146
- if (reactiveClient != null) {
147
- return reactiveClient;
148
- }
149
-
150
- reactiveClient = getAuthenticatedReactiveClient(requireAuthenticatedConfig());
151
- return reactiveClient;
152
- }
153
-
154
- async function sendEmailOtp(body: Shared.Contracts.SendEmailOtpBody) {
155
- const response = (await anonymousClient.action(api.auth.sendEmailOtp, {
156
- email: body.email,
157
- })) as Shared.Contracts.SendEmailOtpResponse;
158
-
159
- return Shared.Contracts.assertSendEmailOtpSucceeded(response);
160
- }
161
-
162
- async function discoverWorkspaces(
163
- body: Shared.Contracts.DiscoverWorkspacesBody
164
- ) {
165
- const response = await anonymousClient.action(api.auth.discoverWorkspaces, {
166
- email: body.email,
167
- code: body.code,
168
- });
169
-
170
- return {
171
- ...response,
172
- workspaces: response.workspaces.map(adaptDiscoveredWorkspace),
173
- };
174
- }
175
-
176
- async function selectWorkspace(body: Shared.Contracts.SelectWorkspaceBody) {
177
- return await anonymousClient.action(api.auth.selectWorkspace, {
178
- workspace_id: body.workspace_id,
179
- intermediate_session_token: body.intermediate_session_token,
180
- });
181
- }
182
-
183
- async function refreshSessionJwt(sessionToken: string) {
184
- const response = (await anonymousClient.action(api.auth.refreshSessionJwt, {
185
- session_jwt: sessionToken,
186
- })) as AuthSessionResponse;
187
-
188
- return response.session_jwt;
189
- }
190
-
191
- async function getBootstrap() {
192
- if (bootstrapPromise != null) {
193
- return await bootstrapPromise;
194
- }
195
-
196
- bootstrapPromise = requireAuthenticatedClient()
197
- .query(api.app.bootstrap, {})
198
- .then((result) => adaptBootstrap(result as BootstrapQueryResult))
199
- .catch((error: unknown) => {
200
- bootstrapPromise = null;
201
- throw error;
202
- });
203
-
204
- return await bootstrapPromise;
205
- }
206
-
207
- async function getMe() {
208
- return (await getBootstrap()).me;
209
- }
210
-
211
- async function getAllMemberships() {
212
- return (await getBootstrap()).memberships;
213
- }
214
-
215
- async function getConversationMessagesPage(params: {
216
- conversationId: string;
217
- cursor?: string | null;
218
- limit?: number;
219
- }) {
220
- return adaptConversationPage(
221
- (await requireAuthenticatedClient().query(
222
- api.app.listConversationMessagesPaginated,
223
- {
224
- conversationId: params.conversationId,
225
- paginationOpts: {
226
- cursor: params.cursor ?? null,
227
- numItems: params.limit ?? 25,
228
- },
229
- }
230
- )) as ConversationMessagesQueryResult
231
- );
232
- }
233
-
234
- async function getConversationMessages(conversationId: string, limit = 25) {
235
- return await getConversationMessagesPage({
236
- conversationId,
237
- limit,
238
- });
239
- }
240
-
241
- async function getMessage(messageId: string) {
242
- const message = await requireAuthenticatedClient().query(
243
- api.app.getMessage,
244
- {
245
- messageId,
246
- }
247
- );
248
-
249
- return message == null ? null : adaptMessage(message);
250
- }
251
-
252
- async function getThreadReplies(messageId: string) {
253
- const replies = await requireAuthenticatedClient().query(
254
- api.app.listThreadReplies,
255
- {
256
- messageId,
257
- }
258
- );
259
-
260
- return replies.map(adaptMessage);
261
- }
262
-
263
- async function postMessage(params: {
264
- conversationId: string;
265
- markdownContent: string;
266
- threadRootId?: string | null;
267
- }) {
268
- const message = await requireAuthenticatedClient().mutation(
269
- api.app.createMessage,
270
- {
271
- conversationId: params.conversationId,
272
- clientRequestId: null,
273
- markdownContent: params.markdownContent,
274
- explicitContextMessageIds: [],
275
- imageUrls: [],
276
- fileIds: [],
277
- initiatorId: null,
278
- suppressedLinkPreviewUrls: [],
279
- callRootId: null,
280
- threadRootMessageId: params.threadRootId ?? null,
281
- }
282
- );
283
-
284
- return adaptMessage(message);
285
- }
286
-
287
- async function addReaction(messageId: string, emoji: string) {
288
- const reaction = await requireAuthenticatedClient().mutation(
289
- api.app.createReaction,
290
- {
291
- messageId,
292
- emoji,
293
- }
294
- );
295
-
296
- return {
297
- success: true,
298
- data: {
299
- ...reaction,
300
- created_at: new Date(reaction.created_at),
301
- },
302
- };
303
- }
304
-
305
- function subscribeToConversationMessages(params: {
306
- conversationId: string;
307
- limit?: number;
308
- onUpdate: (page: ConversationPage) => void;
309
- }) {
310
- const client = requireReactiveClient();
311
- const watch = client.watchQuery(api.app.listConversationMessagesPaginated, {
312
- conversationId: params.conversationId,
313
- paginationOpts: {
314
- cursor: null,
315
- numItems: params.limit ?? 25,
316
- },
317
- });
318
-
319
- return watch.onUpdate(() => {
320
- const result = watch.localQueryResult();
321
- if (result != null) {
322
- const page = adaptConversationPage(result as ConversationMessagesQueryResult);
323
- params.onUpdate(page);
324
- }
325
- });
326
- }
327
-
328
- function subscribeToThreadReplies(params: {
329
- threadRootId: string;
330
- onUpdate: (replies: Message[]) => void;
331
- }) {
332
- const client = requireReactiveClient();
333
- const watch = client.watchQuery(api.app.listThreadReplies, {
334
- messageId: params.threadRootId,
335
- });
336
-
337
- return watch.onUpdate(() => {
338
- const result = watch.localQueryResult();
339
- if (result != null) {
340
- const rawReplies =
341
- result as unknown as Shared.Contracts.ConversationMessageExtended[];
342
- const replies = rawReplies.map(adaptMessage);
343
- params.onUpdate(replies);
344
- }
345
- });
346
- }
347
-
348
- async function close() {
349
- if (reactiveClient != null) {
350
- await reactiveClient.close();
351
- reactiveClient = null;
352
- }
353
- }
354
-
355
- return {
356
- addReaction,
357
- close,
358
- createAuthenticatedCopy(sessionToken: string) {
359
- return createAndoCliClient({
360
- convexUrl: config.convexUrl,
361
- sessionToken,
362
- });
363
- },
364
- discoverWorkspaces,
365
- getAllMemberships,
366
- getConversationMessages,
367
- getConversationMessagesPage,
368
- getMe,
369
- getMessage,
370
- getThreadReplies,
371
- postMessage,
372
- refreshSessionJwt,
373
- selectWorkspace,
374
- sendEmailOtp,
375
- subscribeToConversationMessages,
376
- subscribeToThreadReplies,
377
- };
378
- }