@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.
package/package.json CHANGED
@@ -1,36 +1,31 @@
1
1
  {
2
2
  "name": "@andocorp/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Ando CLI - A terminal client for humans and agents",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ando": "./src/index.ts"
7
+ "ando": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --format esm && (echo '#!/usr/bin/env node'; tail -n +2 ./dist/index.js) > ./dist/index.tmp && mv ./dist/index.tmp ./dist/index.js && chmod +x ./dist/index.js",
10
11
  "compile": "bun build --compile ./src/index.ts --outfile ./dist/ando && chmod +x ./dist/ando",
11
12
  "compile:all": "node scripts/build-all-platforms.js",
12
13
  "dev": "bun run ./src/index.ts",
13
14
  "lint": "pnpm -w exec eslint apps/cli --no-warn-ignored",
15
+ "prepublishOnly": "npm run build",
14
16
  "test": "vitest run",
15
17
  "typecheck": "tsc --noEmit"
16
18
  },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/ando-labs/ando.git",
20
- "directory": "apps/cli"
21
- },
22
19
  "keywords": ["cli", "terminal", "chat", "collaboration"],
23
- "author": "Ando Labs",
20
+ "author": "Asari Inc.",
24
21
  "license": "MIT",
25
22
  "files": [
26
- "src/**/*.ts",
27
- "src/**/*.js"
23
+ "dist/**/*.js"
28
24
  ],
29
25
  "publishConfig": {
30
26
  "access": "public"
31
27
  },
32
28
  "dependencies": {
33
- "@ando/shared": "workspace:*",
34
29
  "convex": "^1.34.1",
35
30
  "emojibase-data": "^16.0.3"
36
31
  },
@@ -1,50 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { adaptBootstrap, adaptConversationPage } from "./adapters.js";
3
- import { createMe, createMembership, createMessage } from "./test-helpers.js";
4
-
5
- describe("adapters", () => {
6
- it("adapts bootstrap payloads into date-backed objects", () => {
7
- const result = adaptBootstrap({
8
- me: createMe({
9
- created_at: 1_700_000_000_000 as unknown as Date,
10
- }) as unknown as Record<string, unknown>,
11
- memberships: [
12
- createMembership({
13
- created_at: 1_700_000_000_000 as unknown as Date,
14
- }) as unknown as Record<string, unknown>,
15
- ],
16
- });
17
-
18
- expect(result.me?.created_at).toBeInstanceOf(Date);
19
- expect(result.memberships[0]?.created_at).toBeInstanceOf(Date);
20
- });
21
-
22
- it("reverses paginated conversation results into chronological order", () => {
23
- const newest = createMessage({
24
- id: "message-3",
25
- created_at: 3 as unknown as Date,
26
- });
27
- const middle = createMessage({
28
- id: "message-2",
29
- created_at: 2 as unknown as Date,
30
- });
31
- const oldest = createMessage({
32
- id: "message-1",
33
- created_at: 1 as unknown as Date,
34
- });
35
-
36
- const page = adaptConversationPage({
37
- page: [newest, middle, oldest],
38
- isDone: false,
39
- continueCursor: "cursor-2",
40
- });
41
-
42
- expect(page.items.map((item) => item.id)).toEqual([
43
- "message-1",
44
- "message-2",
45
- "message-3",
46
- ]);
47
- expect(page.cursor).toBe("cursor-2");
48
- expect(page.hasMore).toBe(true);
49
- });
50
- });
package/src/adapters.ts DELETED
@@ -1,215 +0,0 @@
1
- import * as Shared from "@ando/shared";
2
- import { ConversationPage, DiscoveredWorkspace, Me, Membership, Message } from "./types.js";
3
-
4
- type BootstrapResult = {
5
- me: Record<string, unknown> | null;
6
- memberships: Record<string, unknown>[];
7
- };
8
-
9
- type ConvexPaginatedResult<T> = {
10
- page: T[];
11
- isDone: boolean;
12
- continueCursor: string;
13
- };
14
-
15
- function adaptTimestamp(value: Date | number | string | null | undefined): Date {
16
- if (value instanceof Date) {
17
- return value;
18
- }
19
-
20
- if (typeof value === "number") {
21
- return new Date(value);
22
- }
23
-
24
- if (typeof value === "string") {
25
- return new Date(value);
26
- }
27
-
28
- return new Date(0);
29
- }
30
-
31
- function adaptNullableTimestamp(value: Date | number | string | null | undefined) {
32
- if (value == null) {
33
- return null;
34
- }
35
-
36
- return adaptTimestamp(value);
37
- }
38
-
39
- export function adaptDiscoveredWorkspace(
40
- workspace: Shared.Contracts.DiscoveredWorkspace
41
- ): DiscoveredWorkspace {
42
- return {
43
- ...workspace,
44
- created_at: adaptTimestamp(workspace.created_at),
45
- updated_at: adaptTimestamp(workspace.updated_at),
46
- };
47
- }
48
-
49
- function adaptFile(file: Shared.Contracts.FileExtended): Shared.Contracts.FileExtended {
50
- return {
51
- ...file,
52
- created_at: adaptTimestamp(file.created_at),
53
- updated_at: adaptTimestamp(file.updated_at),
54
- };
55
- }
56
-
57
- function adaptPreviewMember(member: Shared.Models.Member): Shared.Models.Member {
58
- return {
59
- ...member,
60
- created_at: adaptTimestamp(member.created_at),
61
- updated_at: adaptTimestamp(member.updated_at),
62
- last_active_at: adaptTimestamp(member.last_active_at),
63
- last_caught_up_at: adaptTimestamp(member.last_caught_up_at),
64
- archived_at: adaptNullableTimestamp(member.archived_at),
65
- onboarding_completed_at: adaptNullableTimestamp(member.onboarding_completed_at),
66
- last_summary_at: adaptNullableTimestamp(member.last_summary_at),
67
- };
68
- }
69
-
70
- export function adaptMember(member: Shared.Contracts.MemberExtended): Shared.Contracts.MemberExtended {
71
- return {
72
- ...member,
73
- created_at: adaptTimestamp(member.created_at),
74
- updated_at: adaptTimestamp(member.updated_at),
75
- last_active_at: adaptTimestamp(member.last_active_at),
76
- last_caught_up_at: adaptTimestamp(member.last_caught_up_at),
77
- archived_at: adaptNullableTimestamp(member.archived_at),
78
- onboarding_completed_at: adaptNullableTimestamp(member.onboarding_completed_at),
79
- last_summary_at: adaptNullableTimestamp(member.last_summary_at),
80
- profile_image_file:
81
- member.profile_image_file == null ? null : adaptFile(member.profile_image_file),
82
- };
83
- }
84
-
85
- export function adaptConversation(
86
- conversation: Shared.Contracts.ConversationExtended
87
- ): Shared.Contracts.ConversationExtended {
88
- return {
89
- ...conversation,
90
- created_at: adaptTimestamp(conversation.created_at),
91
- updated_at: adaptTimestamp(conversation.updated_at),
92
- last_message_at: adaptNullableTimestamp(conversation.last_message_at),
93
- last_summary_at: adaptNullableTimestamp(conversation.last_summary_at),
94
- archived_at: adaptNullableTimestamp(conversation.archived_at),
95
- preview_members: (conversation.preview_members ?? []).map(adaptPreviewMember),
96
- creator_member:
97
- conversation.creator_member == null
98
- ? null
99
- : adaptPreviewMember(conversation.creator_member),
100
- };
101
- }
102
-
103
- function adaptThreadSubscription(
104
- subscription: Shared.Contracts.ThreadSubscriptionExtended
105
- ): Shared.Contracts.ThreadSubscriptionExtended {
106
- return {
107
- ...subscription,
108
- created_at: adaptTimestamp(subscription.created_at),
109
- updated_at: adaptTimestamp(subscription.updated_at),
110
- last_seen_at: adaptNullableTimestamp(subscription.last_seen_at),
111
- thread_root: {
112
- ...subscription.thread_root,
113
- created_at: adaptTimestamp(subscription.thread_root.created_at),
114
- updated_at: adaptTimestamp(subscription.thread_root.updated_at),
115
- edited_at: adaptNullableTimestamp(subscription.thread_root.edited_at),
116
- pinned_at: adaptNullableTimestamp(subscription.thread_root.pinned_at),
117
- last_reply_at: adaptNullableTimestamp(subscription.thread_root.last_reply_at),
118
- archived_at: adaptNullableTimestamp(subscription.thread_root.archived_at),
119
- },
120
- };
121
- }
122
-
123
- export function adaptMembership(
124
- membership: Shared.Contracts.MembershipExtended
125
- ): Membership {
126
- return {
127
- ...membership,
128
- created_at: adaptTimestamp(membership.created_at),
129
- updated_at: adaptTimestamp(membership.updated_at),
130
- last_seen_at: adaptTimestamp(membership.last_seen_at),
131
- member: adaptMember(membership.member),
132
- conversation: adaptConversation(membership.conversation),
133
- thread_subscriptions: (membership.thread_subscriptions ?? []).map(
134
- adaptThreadSubscription
135
- ),
136
- };
137
- }
138
-
139
- export function adaptMessage(
140
- message: Shared.Contracts.ConversationMessageExtended
141
- ): Message {
142
- return {
143
- ...message,
144
- created_at: adaptTimestamp(message.created_at),
145
- updated_at: adaptTimestamp(message.updated_at),
146
- edited_at: adaptNullableTimestamp(message.edited_at),
147
- pinned_at: adaptNullableTimestamp(message.pinned_at),
148
- last_reply_at: adaptNullableTimestamp(message.last_reply_at),
149
- archived_at: adaptNullableTimestamp(message.archived_at),
150
- author: adaptMember(message.author),
151
- message_reactions: (message.message_reactions ?? []).map((reaction) => ({
152
- ...reaction,
153
- created_at: adaptTimestamp(reaction.created_at),
154
- })),
155
- replies_preview_members: (message.replies_preview_members ?? []).map(
156
- adaptPreviewMember
157
- ),
158
- files: (message.files ?? []).map(adaptFile),
159
- thread_root_message:
160
- message.thread_root_message == null
161
- ? null
162
- : {
163
- ...message.thread_root_message,
164
- created_at: adaptTimestamp(message.thread_root_message.created_at),
165
- updated_at: adaptTimestamp(message.thread_root_message.updated_at),
166
- edited_at: adaptNullableTimestamp(message.thread_root_message.edited_at),
167
- pinned_at: adaptNullableTimestamp(message.thread_root_message.pinned_at),
168
- last_reply_at: adaptNullableTimestamp(message.thread_root_message.last_reply_at),
169
- archived_at: adaptNullableTimestamp(message.thread_root_message.archived_at),
170
- },
171
- };
172
- }
173
-
174
- function adaptWorkspace(
175
- workspace: Shared.Contracts.WorkspaceExtended
176
- ): Shared.Contracts.WorkspaceExtended {
177
- return {
178
- ...workspace,
179
- created_at: adaptTimestamp(workspace.created_at),
180
- updated_at: adaptTimestamp(workspace.updated_at),
181
- workspace_invite_link: {
182
- ...workspace.workspace_invite_link,
183
- created_at: adaptTimestamp(workspace.workspace_invite_link.created_at),
184
- },
185
- };
186
- }
187
-
188
- export function adaptMe(member: Shared.Contracts.MeExtended): Me {
189
- return {
190
- ...adaptMember(member),
191
- workspace: adaptWorkspace(member.workspace),
192
- };
193
- }
194
-
195
- export function adaptBootstrap(result: BootstrapResult) {
196
- return {
197
- me:
198
- result.me == null
199
- ? null
200
- : adaptMe(result.me as unknown as Shared.Contracts.MeExtended),
201
- memberships: result.memberships.map((membership) =>
202
- adaptMembership(membership as unknown as Shared.Contracts.MembershipExtended)
203
- ),
204
- };
205
- }
206
-
207
- export function adaptConversationPage(
208
- result: ConvexPaginatedResult<Shared.Contracts.ConversationMessageExtended>
209
- ): ConversationPage {
210
- return {
211
- items: [...result.page].reverse().map(adaptMessage),
212
- cursor: result.isDone ? null : result.continueCursor,
213
- hasMore: !result.isDone,
214
- };
215
- }
package/src/args.test.ts DELETED
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { getStringFlag, hasFlag, parseArgs } from "./args.js";
3
-
4
- describe("args", () => {
5
- it("parses long and short flags with values", () => {
6
- const parsed = parseArgs([
7
- "list-messages",
8
- "--channel",
9
- "engineering",
10
- "-n",
11
- "20",
12
- "--json",
13
- ]);
14
-
15
- expect(parsed.positionals).toEqual(["list-messages"]);
16
- expect(getStringFlag(parsed, "channel", "c")).toBe("engineering");
17
- expect(getStringFlag(parsed, "limit", "n")).toBe("20");
18
- expect(hasFlag(parsed, "json")).toBe(true);
19
- });
20
-
21
- it("parses compact short flags", () => {
22
- const parsed = parseArgs(["thread", "-vhm", "message-1"]);
23
-
24
- expect(hasFlag(parsed, "verbose", "v")).toBe(true);
25
- expect(hasFlag(parsed, "help", "h")).toBe(true);
26
- expect(getStringFlag(parsed, "message-id", "m")).toBe("message-1");
27
- });
28
- });
package/src/args.ts DELETED
@@ -1,98 +0,0 @@
1
- import { ParsedArgs } from "./types.js";
2
-
3
- export function parseArgs(argv: string[]): ParsedArgs {
4
- const flags = new Map<string, string | true>();
5
- const positionals: string[] = [];
6
-
7
- for (let index = 0; index < argv.length; index += 1) {
8
- const value = argv[index];
9
- if (value == null) {
10
- continue;
11
- }
12
-
13
- if (!value.startsWith("-")) {
14
- positionals.push(value);
15
- continue;
16
- }
17
-
18
- if (value.startsWith("--")) {
19
- const [key, inlineValue] = value.slice(2).split("=", 2);
20
- if (inlineValue != null) {
21
- flags.set(key, inlineValue);
22
- continue;
23
- }
24
-
25
- const nextValue = argv[index + 1];
26
- if (nextValue != null && !nextValue.startsWith("-")) {
27
- flags.set(key, nextValue);
28
- index += 1;
29
- } else {
30
- flags.set(key, true);
31
- }
32
- continue;
33
- }
34
-
35
- const shortFlags = value.slice(1);
36
- if (shortFlags.length > 1) {
37
- for (let shortIndex = 0; shortIndex < shortFlags.length - 1; shortIndex += 1) {
38
- const shortFlag = shortFlags[shortIndex];
39
- if (shortFlag != null) {
40
- flags.set(shortFlag, true);
41
- }
42
- }
43
-
44
- const lastFlag = shortFlags[shortFlags.length - 1];
45
- const nextValue = argv[index + 1];
46
- if (lastFlag == null) {
47
- continue;
48
- }
49
-
50
- if (nextValue != null && !nextValue.startsWith("-")) {
51
- flags.set(lastFlag, nextValue);
52
- index += 1;
53
- } else {
54
- flags.set(lastFlag, true);
55
- }
56
- continue;
57
- }
58
-
59
- const nextValue = argv[index + 1];
60
- if (nextValue != null && !nextValue.startsWith("-")) {
61
- flags.set(shortFlags, nextValue);
62
- index += 1;
63
- } else {
64
- flags.set(shortFlags, true);
65
- }
66
- }
67
-
68
- return {
69
- flags,
70
- positionals,
71
- };
72
- }
73
-
74
- export function getStringFlag(
75
- parsedArgs: ParsedArgs,
76
- longName: string,
77
- shortName?: string
78
- ) {
79
- const longValue = parsedArgs.flags.get(longName);
80
- if (typeof longValue === "string") {
81
- return longValue;
82
- }
83
-
84
- if (shortName == null) {
85
- return undefined;
86
- }
87
-
88
- const shortValue = parsedArgs.flags.get(shortName);
89
- return typeof shortValue === "string" ? shortValue : undefined;
90
- }
91
-
92
- export function hasFlag(
93
- parsedArgs: ParsedArgs,
94
- longName: string,
95
- shortName?: string
96
- ) {
97
- return parsedArgs.flags.has(longName) || (shortName != null && parsedArgs.flags.has(shortName));
98
- }
@@ -1,82 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import * as Shared from "@ando/shared";
3
- import { compareByRecentActivity, resolveConversation, resolveWorkspace } from "./cli-helpers.js";
4
- import { createConversation, createMembership } from "./test-helpers.js";
5
-
6
- describe("cli helpers", () => {
7
- it("resolves a conversation by exact name", () => {
8
- const memberships = [
9
- createMembership({
10
- id: "membership-a",
11
- conversation: createConversation({ id: "conversation-a", name: "engineering" }),
12
- }),
13
- ];
14
-
15
- const result = resolveConversation(memberships, {
16
- allowChannels: true,
17
- allowDms: true,
18
- query: "engineering",
19
- });
20
-
21
- expect(result.id).toBe("membership-a");
22
- });
23
-
24
- it("throws when a partial conversation match is ambiguous", () => {
25
- const memberships = [
26
- createMembership({
27
- id: "membership-a",
28
- conversation: createConversation({ id: "conversation-a", name: "eng-platform" }),
29
- }),
30
- createMembership({
31
- id: "membership-b",
32
- conversation: createConversation({ id: "conversation-b", name: "eng-mobile" }),
33
- }),
34
- ];
35
-
36
- expect(() =>
37
- resolveConversation(memberships, {
38
- allowChannels: true,
39
- allowDms: true,
40
- query: "eng",
41
- })
42
- ).toThrow(/ambiguous/);
43
- });
44
-
45
- it("sorts more recent memberships first", () => {
46
- const oldMembership = createMembership({
47
- id: "old",
48
- conversation: createConversation({
49
- id: "conversation-old",
50
- last_message_at: new Date("2026-04-08T12:00:00.000Z"),
51
- }),
52
- });
53
- const newMembership = createMembership({
54
- id: "new",
55
- conversation: createConversation({
56
- id: "conversation-new",
57
- last_message_at: new Date("2026-04-09T12:00:00.000Z"),
58
- }),
59
- });
60
-
61
- expect(compareByRecentActivity(newMembership, oldMembership)).toBeLessThan(0);
62
- });
63
-
64
- it("resolves workspaces by slug", () => {
65
- const workspace = {
66
- id: "workspace-1",
67
- stytch_organization_id: "org-1",
68
- name: "Ando",
69
- slug: "ando",
70
- image_url: null,
71
- icon_image_file_id: null,
72
- description: null,
73
- invite_link_id: "invite-1",
74
- created_at: new Date("2026-04-09T12:00:00.000Z"),
75
- updated_at: new Date("2026-04-09T12:00:00.000Z"),
76
- member_count: 3,
77
- } satisfies Shared.Contracts.DiscoveredWorkspace;
78
-
79
- const result = resolveWorkspace([workspace], "ando");
80
- expect(result.id).toBe("workspace-1");
81
- });
82
- });
@@ -1,149 +0,0 @@
1
- import * as Shared from "@ando/shared";
2
- import { DiscoveredWorkspace, Membership } from "./types.js";
3
-
4
- export function isChannel(membership: Membership) {
5
- return membership.conversation.type === Shared.Models.ConversationType.CHANNEL;
6
- }
7
-
8
- function normalizeForMatch(value: string) {
9
- return value.trim().toLowerCase();
10
- }
11
-
12
- export function compareByRecentActivity(left: Membership, right: Membership) {
13
- const leftDate = new Date(
14
- left.conversation.last_message_at ?? left.conversation.updated_at
15
- ).getTime();
16
- const rightDate = new Date(
17
- right.conversation.last_message_at ?? right.conversation.updated_at
18
- ).getTime();
19
-
20
- return rightDate - leftDate;
21
- }
22
-
23
- export function getWorkspaceLabel(workspace: DiscoveredWorkspace) {
24
- return `${workspace.name} (${workspace.slug})`;
25
- }
26
-
27
- export function resolveWorkspace(
28
- workspaces: DiscoveredWorkspace[],
29
- query: string | null
30
- ) {
31
- if (workspaces.length === 0) {
32
- throw new Error("No workspaces were returned for this account.");
33
- }
34
-
35
- if (query == null || query.trim() === "") {
36
- if (workspaces.length === 1) {
37
- return workspaces[0] ?? null;
38
- }
39
-
40
- throw new Error("Multiple workspaces are available. Pass --workspace or choose one interactively.");
41
- }
42
-
43
- const normalizedQuery = normalizeForMatch(query);
44
- const exactMatch =
45
- workspaces.find((workspace) => {
46
- return (
47
- workspace.id === query ||
48
- normalizeForMatch(workspace.slug) === normalizedQuery ||
49
- normalizeForMatch(workspace.name) === normalizedQuery
50
- );
51
- }) ?? null;
52
-
53
- if (exactMatch != null) {
54
- return exactMatch;
55
- }
56
-
57
- const partialMatches = workspaces.filter((workspace) => {
58
- return [workspace.id, workspace.slug, workspace.name].some((value) =>
59
- normalizeForMatch(value).includes(normalizedQuery)
60
- );
61
- });
62
-
63
- if (partialMatches.length === 1) {
64
- return partialMatches[0] ?? null;
65
- }
66
-
67
- if (partialMatches.length === 0) {
68
- throw new Error(`No workspace matched "${query}".`);
69
- }
70
-
71
- throw new Error(
72
- `Workspace "${query}" is ambiguous: ${partialMatches
73
- .slice(0, 8)
74
- .map(getWorkspaceLabel)
75
- .join(", ")}`
76
- );
77
- }
78
-
79
- export function resolveConversation(
80
- memberships: Membership[],
81
- options: {
82
- allowDms: boolean;
83
- allowChannels: boolean;
84
- query: string;
85
- }
86
- ) {
87
- const filtered = memberships
88
- .filter((membership) => {
89
- if (!options.allowChannels && isChannel(membership)) {
90
- return false;
91
- }
92
-
93
- if (!options.allowDms && !isChannel(membership)) {
94
- return false;
95
- }
96
-
97
- return true;
98
- })
99
- .sort(compareByRecentActivity);
100
-
101
- const normalizedQuery = normalizeForMatch(options.query);
102
- const exactMatch =
103
- filtered.find((membership) => {
104
- return (
105
- membership.conversation.id === options.query ||
106
- normalizeForMatch(membership.conversation.name ?? "") === normalizedQuery
107
- );
108
- }) ?? null;
109
-
110
- if (exactMatch != null) {
111
- return exactMatch;
112
- }
113
-
114
- const partialMatches = filtered.filter((membership) => {
115
- const fields = [
116
- membership.conversation.id,
117
- membership.conversation.name ?? "",
118
- getConversationLabel(membership),
119
- ];
120
-
121
- return fields.some((value) =>
122
- normalizeForMatch(value).includes(normalizedQuery)
123
- );
124
- });
125
-
126
- if (partialMatches.length === 1) {
127
- return partialMatches[0] ?? null;
128
- }
129
-
130
- if (partialMatches.length === 0) {
131
- throw new Error(`No conversation matched "${options.query}".`);
132
- }
133
-
134
- throw new Error(
135
- `Conversation "${options.query}" is ambiguous: ${partialMatches
136
- .slice(0, 8)
137
- .map((membership) => getConversationLabel(membership))
138
- .join(", ")}`
139
- );
140
- }
141
-
142
- export function getConversationLabel(membership: Membership) {
143
- const prefix =
144
- membership.conversation.type === Shared.Models.ConversationType.CHANNEL
145
- ? "#"
146
- : "@";
147
-
148
- return `${prefix}${membership.conversation.name || membership.conversation.id}`;
149
- }