@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/LICENSE +21 -0
- package/README.md +95 -42
- package/dist/agent-commands.js +297 -0
- package/dist/api-command.js +187 -0
- package/dist/api-inputs.js +223 -0
- package/dist/api-operations.js +344 -0
- package/dist/args.js +71 -0
- package/dist/auth-commands.js +362 -0
- package/dist/cli-helpers.js +67 -0
- package/dist/cli-login-browser.js +60 -0
- package/dist/cli-login-errors.js +10 -0
- package/dist/cli-login-paths.js +8 -0
- package/dist/cli-login-revoke.js +100 -0
- package/dist/cli-login.js +335 -0
- package/dist/client.js +104 -0
- package/dist/commands.js +155 -0
- package/dist/config-credential-metadata.js +68 -0
- package/dist/config-keyring.js +61 -0
- package/dist/config-logout-credentials.js +171 -0
- package/dist/config-paths.js +41 -0
- package/dist/config-types.js +1 -0
- package/dist/config.js +333 -0
- package/dist/format.js +297 -0
- package/dist/help.js +70 -0
- package/dist/index.js +74 -11687
- package/dist/output.js +7 -0
- package/dist/session.js +58 -0
- package/dist/timeouts.js +1 -0
- package/dist/types.js +1 -0
- package/dist/watch-commands.js +120 -0
- package/package.json +24 -20
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
|
+
}
|