@andocorp/cli 0.1.3 → 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.
package/dist/format.js ADDED
@@ -0,0 +1,297 @@
1
+ const LINK_START_MARKER = "\u0000";
2
+ const LINK_END_MARKER = "\u0001";
3
+ const LINK_SEPARATOR_MARKER = "\u0002";
4
+ const ANSI_RESET = "\u001b[0m";
5
+ const ANSI_LINK = "\u001b[34m";
6
+ const ANSI_CURRENT_MEMBER = "\u001b[38;2;0;224;224m";
7
+ const ANSI_ESCAPE_PATTERN = new RegExp(`${ANSI_RESET[0]}\\[[0-9;]*m`, "g");
8
+ const ANSI_ESCAPE_AT_START_PATTERN = new RegExp(`^${ANSI_RESET[0]}\\[[0-9;]*m`);
9
+ function pad(value) {
10
+ return value.toString().padStart(2, "0");
11
+ }
12
+ function stripLinkMarkers(text) {
13
+ let result = "";
14
+ let index = 0;
15
+ while (index < text.length) {
16
+ const character = text[index];
17
+ if (character == null) {
18
+ break;
19
+ }
20
+ if (character === LINK_START_MARKER) {
21
+ index += 1;
22
+ while (index < text.length && text[index] !== LINK_SEPARATOR_MARKER) {
23
+ index += 1;
24
+ }
25
+ if (text[index] === LINK_SEPARATOR_MARKER) {
26
+ index += 1;
27
+ }
28
+ while (index < text.length && text[index] !== LINK_END_MARKER) {
29
+ const linkCharacter = text[index];
30
+ if (linkCharacter != null) {
31
+ result += linkCharacter;
32
+ }
33
+ index += 1;
34
+ }
35
+ if (text[index] === LINK_END_MARKER) {
36
+ index += 1;
37
+ }
38
+ continue;
39
+ }
40
+ if (character !== LINK_END_MARKER && character !== LINK_SEPARATOR_MARKER) {
41
+ result += character;
42
+ }
43
+ index += 1;
44
+ }
45
+ return result;
46
+ }
47
+ function stripAnsi(text) {
48
+ return text.replace(ANSI_ESCAPE_PATTERN, "");
49
+ }
50
+ export function getVisibleText(text) {
51
+ return stripAnsi(stripLinkMarkers(text));
52
+ }
53
+ export function getVisibleTextLength(text) {
54
+ return getVisibleText(text).length;
55
+ }
56
+ export function renderTerminalText(text) {
57
+ let rendered = "";
58
+ let index = 0;
59
+ while (index < text.length) {
60
+ const character = text[index];
61
+ if (character == null) {
62
+ break;
63
+ }
64
+ if (character === LINK_START_MARKER) {
65
+ index += 1;
66
+ while (index < text.length && text[index] !== LINK_SEPARATOR_MARKER) {
67
+ index += 1;
68
+ }
69
+ if (text[index] === LINK_SEPARATOR_MARKER) {
70
+ index += 1;
71
+ }
72
+ rendered += ANSI_LINK;
73
+ continue;
74
+ }
75
+ if (character === LINK_END_MARKER) {
76
+ rendered += ANSI_RESET;
77
+ index += 1;
78
+ continue;
79
+ }
80
+ if (character !== LINK_SEPARATOR_MARKER) {
81
+ rendered += character;
82
+ }
83
+ index += 1;
84
+ }
85
+ return rendered;
86
+ }
87
+ function formatLink(url, label) {
88
+ return `${LINK_START_MARKER}${url}${LINK_SEPARATOR_MARKER}${label}${LINK_END_MARKER}`;
89
+ }
90
+ function isUrlLikeText(text) {
91
+ try {
92
+ const url = new URL(text);
93
+ return url.protocol === "http:" || url.protocol === "https:";
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ function getLinkLabel(part) {
100
+ const childText = part.label.trim();
101
+ if (childText === "") {
102
+ return part.url;
103
+ }
104
+ if (childText === part.url || isUrlLikeText(childText)) {
105
+ return childText;
106
+ }
107
+ return childText;
108
+ }
109
+ function parseMarkdownParts(markdown) {
110
+ const parts = [];
111
+ const pattern = /(!?)\[([^\]]*)\]\(([^)]+)\)|\n/g;
112
+ let lastIndex = 0;
113
+ let match;
114
+ while ((match = pattern.exec(markdown)) != null) {
115
+ if (match.index > lastIndex) {
116
+ parts.push({
117
+ type: "text",
118
+ value: markdown.slice(lastIndex, match.index),
119
+ });
120
+ }
121
+ const raw = match[0];
122
+ if (raw === "\n") {
123
+ parts.push({ type: "linebreak" });
124
+ }
125
+ else if (match[1] === "!") {
126
+ parts.push({
127
+ type: "image",
128
+ alt: match[2] ?? "",
129
+ });
130
+ }
131
+ else {
132
+ parts.push({
133
+ type: "link",
134
+ label: match[2] ?? "",
135
+ url: match[3] ?? "",
136
+ });
137
+ }
138
+ lastIndex = pattern.lastIndex;
139
+ }
140
+ if (lastIndex < markdown.length) {
141
+ parts.push({
142
+ type: "text",
143
+ value: markdown.slice(lastIndex),
144
+ });
145
+ }
146
+ return parts;
147
+ }
148
+ function getMessageBodyText(markdown) {
149
+ return parseMarkdownParts(markdown)
150
+ .map((part) => {
151
+ switch (part.type) {
152
+ case "text":
153
+ return part.value;
154
+ case "linebreak":
155
+ return "\n";
156
+ case "image":
157
+ return part.alt || "Image";
158
+ case "link":
159
+ return formatLink(part.url, getLinkLabel(part));
160
+ }
161
+ })
162
+ .join("");
163
+ }
164
+ function formatDateTime(value) {
165
+ if (value == null) {
166
+ return "";
167
+ }
168
+ const date = new Date(value);
169
+ if (Number.isNaN(date.getTime())) {
170
+ return "";
171
+ }
172
+ return `${pad(date.getDate())}/${pad(date.getMonth() + 1)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
173
+ }
174
+ function truncate(text, width) {
175
+ if (width <= 0) {
176
+ return "";
177
+ }
178
+ const visibleText = getVisibleText(text);
179
+ if (visibleText.length <= width) {
180
+ return text;
181
+ }
182
+ if (width <= 1) {
183
+ return visibleText.slice(0, width);
184
+ }
185
+ let visibleCount = 0;
186
+ let result = "";
187
+ for (let index = 0; index < text.length; index += 1) {
188
+ const character = text[index];
189
+ if (character == null) {
190
+ break;
191
+ }
192
+ if (character === "\u001b") {
193
+ const ansiMatch = ANSI_ESCAPE_AT_START_PATTERN.exec(text.slice(index));
194
+ if (ansiMatch != null) {
195
+ result += ansiMatch[0];
196
+ index += ansiMatch[0].length - 1;
197
+ continue;
198
+ }
199
+ }
200
+ if (character === LINK_START_MARKER || character === LINK_END_MARKER) {
201
+ result += character;
202
+ if (character === LINK_START_MARKER) {
203
+ index += 1;
204
+ while (index < text.length) {
205
+ const linkCharacter = text[index];
206
+ if (linkCharacter == null) {
207
+ break;
208
+ }
209
+ result += linkCharacter;
210
+ if (linkCharacter === LINK_SEPARATOR_MARKER) {
211
+ break;
212
+ }
213
+ index += 1;
214
+ }
215
+ }
216
+ continue;
217
+ }
218
+ if (visibleCount >= width - 1) {
219
+ break;
220
+ }
221
+ result += character;
222
+ visibleCount += 1;
223
+ }
224
+ return `${result}…`;
225
+ }
226
+ export function getMessageBody(message) {
227
+ const body = message.markdown_content?.trim();
228
+ if (body) {
229
+ return getMessageBodyText(body).replace(/\s+/g, " ").trim();
230
+ }
231
+ if (message.image_urls.length > 0) {
232
+ return `[${message.image_urls.length} image attachment${message.image_urls.length === 1 ? "" : "s"}]`;
233
+ }
234
+ if (message.files.length > 0) {
235
+ return `[${message.files.length} file attachment${message.files.length === 1 ? "" : "s"}]`;
236
+ }
237
+ if (message.call_id != null) {
238
+ return "[Jam started]";
239
+ }
240
+ return "[empty message]";
241
+ }
242
+ export function formatReactionSummary(message) {
243
+ const reactions = "message_reactions" in message ? message.message_reactions : message.reactions;
244
+ if (reactions.length === 0) {
245
+ return "";
246
+ }
247
+ const groupedEmojis = [];
248
+ let currentEmoji = "";
249
+ let currentCount = 0;
250
+ const pushCurrentEmoji = () => {
251
+ if (currentCount === 0) {
252
+ return;
253
+ }
254
+ groupedEmojis.push(currentEmoji.repeat(currentCount));
255
+ };
256
+ for (const reaction of reactions) {
257
+ const count = "count" in reaction && typeof reaction.count === "number" ? reaction.count : 1;
258
+ if (reaction.emoji_text === currentEmoji) {
259
+ currentCount += count;
260
+ continue;
261
+ }
262
+ pushCurrentEmoji();
263
+ currentEmoji = reaction.emoji_text;
264
+ currentCount = count;
265
+ }
266
+ pushCurrentEmoji();
267
+ return groupedEmojis.join(" ");
268
+ }
269
+ function formatMessageFooter(message) {
270
+ const parts = [];
271
+ const reactions = formatReactionSummary(message);
272
+ if (message.replies_count > 0) {
273
+ parts.push(`[${message.replies_count} ${message.replies_count === 1 ? "reply" : "replies"}]`);
274
+ }
275
+ if (reactions !== "") {
276
+ parts.push(reactions);
277
+ }
278
+ return parts.join(" ");
279
+ }
280
+ function getMessageAuthorLabel(message) {
281
+ return (message.author.display_name ??
282
+ ("email" in message.author ? message.author.email : null) ??
283
+ "Unknown");
284
+ }
285
+ export function formatMessageLine(message, options) {
286
+ const author = truncate(getMessageAuthorLabel(message), 20);
287
+ const parts = [
288
+ formatDateTime(message.created_at),
289
+ options?.currentMemberId != null && message.author.id === options.currentMemberId
290
+ ? `${ANSI_CURRENT_MEMBER}${author}${ANSI_RESET}`
291
+ : author,
292
+ getMessageBody(message),
293
+ ];
294
+ const footer = formatMessageFooter(message);
295
+ const footerSuffix = footer === "" ? "" : ` | ${footer}`;
296
+ return renderTerminalText(`${parts.filter(Boolean).join(" | ")}${footerSuffix}`);
297
+ }
package/dist/help.js ADDED
@@ -0,0 +1,70 @@
1
+ import { getConfigPath } from "./config.js";
2
+ export function buildHelpText() {
3
+ return `
4
+ ando
5
+
6
+ Usage:
7
+ ando help
8
+ ando login [--no-browser] [--base-url <url>] [--api-host <url>] [--realtime-host <host>]
9
+ ando login poll
10
+ ando login --api-key <key> [--base-url <url>] [--api-host <url>] [--realtime-host <host>]
11
+ ando logout
12
+ ando doctor [--json]
13
+ ando whoami [--json]
14
+
15
+ Agent-first commands:
16
+ ando api ls [--json]
17
+ ando api <path|operation-id> [--method|-X <method>] [--data|-d <json|->]
18
+ [name==value] [Header:Value] [field=value] [field:=json]
19
+ ando api <path|operation-id> --help
20
+ ando api <path|operation-id> --spec
21
+ ando messages (--channel <query> | --dm <query> | --conversation <id>) [--limit|-m <n>] [--before <cursor>] [--json]
22
+ ando search <query> [--type messages|members|conversations|clipboard|calls]
23
+ [--author <id>[,<id>]] [--conversation <id>[,<id>]] [--thread <id>]
24
+ [--after <iso>] [--before <iso>] [--mode full-text|semantic] [--json]
25
+ ando get message <message-id> [--json]
26
+ ando get member <member-id> [--json]
27
+ ando get clipboard <clipboard-id> [--json]
28
+ ando get call <call-id> [--json]
29
+ ando get transcript <call-id> [--limit <n>] [--cursor <cursor>] [--json]
30
+ ando thread <thread-root-message-id> [--limit <n>] [--after <cursor>] [--json]
31
+ ando thread -m <message-id> [--json]
32
+ ando watch messages [--delivery mentions] [--limit|-n <n>] [--timeout <ms>] [--json]
33
+
34
+ Authentication:
35
+ login opens a browser authorization page, shows a verification code, and stores
36
+ the returned Ando API key in the system keyring by default. Non-secret config
37
+ metadata is stored in:
38
+ ${getConfigPath()}
39
+ logout revokes browser-created CLI credentials before removing local auth
40
+ state. API keys supplied manually or through ANDO_API_KEY remain local-only.
41
+ Use --api-key or ANDO_API_KEY for headless scripts and CI. Set ANDO_KEYRING=0
42
+ to use file-based auth instead of the system keyring.
43
+
44
+ Environment:
45
+ ANDO_API_KEY API key for SDK and public API requests
46
+ ANDO_KEYRING Set to 0 to use file-based auth instead of keyring
47
+ ANDO_HOME Config and auth state directory
48
+ ANDO_BASE_URL Login and SDK REST base URL (default: https://api.ando.so)
49
+ ANDO_API_HOST Public API base URL for agent-first commands (default: https://api.ando.so/v1)
50
+ ANDO_REALTIME_HOST Realtime host for SDK subscriptions
51
+
52
+ Examples:
53
+ ando login
54
+ ando login --no-browser
55
+ ando login --api-key ando_sk_...
56
+ ando doctor
57
+ ando whoami
58
+ ando logout
59
+ ando api ls
60
+ ando api searchMessages q==incident mode==semantic
61
+ ando api getMember memberId=<member-id>
62
+ ando api /v1/search/messages q==incident mode==semantic
63
+ echo '{"markdown_content":"hello"}' | ando api /v1/conversations/<conversation-id>/messages --data -
64
+ ando messages -c engineering -m 20
65
+ ando search "incident" --type messages --mode semantic
66
+ ando get message <message-id>
67
+ ando thread <thread-root-message-id> --limit 50
68
+ ando watch messages --limit 1 --json
69
+ `.trim();
70
+ }