@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ando Corp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Ando CLI
2
2
 
3
- `ando` is a terminal client for humans and agents.
3
+ `ando` provides agent-first terminal commands for Ando. It is built on the
4
+ public `@andocorp/sdk` package and authenticates with an Ando API key.
4
5
 
5
6
  ## Installation
6
7
 
7
- ### npm (Recommended)
8
-
9
8
  ```sh
10
9
  npm install -g @andocorp/cli
11
10
  ```
@@ -16,75 +15,129 @@ Or run without installing:
16
15
  npx @andocorp/cli
17
16
  ```
18
17
 
19
- ### Alternative: Standalone Binaries
20
-
21
- Standalone binaries are available for users who prefer not to install Node.js/npm. Contact hello@ando.so for access.
22
-
23
- ## Commands
18
+ ## Authentication
24
19
 
25
- Open the interactive terminal UI:
20
+ Log in once:
26
21
 
27
22
  ```sh
28
- ando
23
+ ando login
29
24
  ```
30
25
 
31
- Show help:
26
+ The command opens a browser authorization page, prints a verification code to
27
+ confirm in the browser, and stores the returned API key in the system keyring
28
+ by default. Non-secret metadata is stored at `~/.config/ando/config.json`.
29
+
30
+ For remote shells or agents that cannot open a browser directly, start a
31
+ two-step login:
32
32
 
33
33
  ```sh
34
- ando help
35
- ando --help
34
+ ando login --no-browser
36
35
  ```
37
36
 
38
- List channels:
37
+ Open the printed URL, confirm the verification code, then run the printed
38
+ `ando login poll` command in the original shell to finish authentication.
39
+
40
+ For headless scripts and CI, provide an API key directly:
39
41
 
40
42
  ```sh
41
- ando list-channels
42
- ando list-channels --json
43
+ ando login --api-key ando_sk_...
43
44
  ```
44
45
 
45
- List direct-message conversations:
46
+ You can also set `ANDO_API_KEY` for one-off or CI usage.
46
47
 
47
- ```sh
48
- ando list-dms
49
- ando list-dms --json
50
- ```
48
+ Set `ANDO_KEYRING=0` to use file-based auth instead of the system keyring. In
49
+ that mode the API key is stored in `auth.json` under the Ando config directory.
50
+ Set `ANDO_HOME` to override the config and auth state directory; otherwise the
51
+ CLI uses the platform config directory.
52
+
53
+ Optional endpoint flags:
51
54
 
52
- List messages from a channel, DM, or conversation id:
55
+ - `--base-url`: login and SDK REST base URL
56
+ - `--api-host`: public API base URL for agent-first commands
57
+ - `--realtime-host`: realtime host for SDK subscriptions
58
+
59
+ Check local auth health or remove local credentials:
53
60
 
54
61
  ```sh
55
- ando list-messages --channel engineering
56
- ando list-messages --dm alex
57
- ando list-messages --conversation <conversation-id>
58
- ando list-messages --channel engineering --limit 20
59
- ando list-messages -c engineering -n 20 --json
62
+ ando doctor
63
+ ando doctor --json
64
+ ando whoami
65
+ ando whoami --json
66
+ ando logout
60
67
  ```
61
68
 
62
- Read a thread by message id:
69
+ `ando whoami` prints the member and workspace resolved by the active API key.
70
+ `ando logout` revokes browser-created CLI credentials before removing local auth
71
+ state. API keys supplied manually or through `ANDO_API_KEY` remain local-only.
72
+
73
+ ## Commands
74
+
75
+ The raw public API escape hatch mirrors the generated Ando API surface. It is
76
+ useful for scripts and agents that need an endpoint before a dedicated CLI
77
+ workflow exists.
63
78
 
64
79
  ```sh
65
- ando thread --message-id <message-id>
66
- ando thread -m <message-id> --json
80
+ ando api ls
81
+ ando api ls --json
82
+ ando api searchMessages q==incident mode==semantic
83
+ ando api getMember memberId=<member-id>
84
+ ando api /v1/search/messages q==incident mode==semantic
85
+ ando api /v1/conversations/<conversation-id>/messages \
86
+ markdown_content="hello" \
87
+ Idempotency-Key:<key>
88
+ echo '{"markdown_content":"hello"}' | ando api /v1/conversations/<conversation-id>/messages --data -
89
+ ando api /v1/search/messages --help
90
+ ando api /v1/search/messages --spec
67
91
  ```
68
92
 
69
- Post a new message to a channel, DM, or conversation id:
93
+ `ando api` accepts `name==value` query parameters, `Header:Value` declared
94
+ request headers, `field=value` JSON string body fields, `field:=json` typed JSON
95
+ body fields, and `--data <json|->` for complete request bodies. The target can
96
+ be a public path or an operation ID from `ando api ls`; query and header names
97
+ are validated against the generated public API metadata. For operation ID
98
+ targets, supply required path parameters as `name=value`.
70
99
 
71
100
  ```sh
72
- ando post-message --channel engineering --text "hello"
73
- ando post-message --dm alex --text "want to pair?"
74
- ando post-message --conversation <conversation-id> --text "status update"
75
- ando post-message -c engineering -t "hello" --json
101
+ ando messages --channel engineering --limit 10
102
+ ando messages -c engineering -m 10
103
+ ando messages --dm alex --before <cursor> --json
104
+ ando messages --conversation <conversation-id>
76
105
  ```
77
106
 
78
- Reply inside an existing thread:
79
-
80
107
  ```sh
81
- ando reply --message-id <message-id> --text "on it"
82
- ando reply -m <message-id> -t "on it" --json
108
+ ando search "incident"
109
+ ando search "alice" --type members
110
+ ando search "standup" --type conversations
111
+ ando search "launch checklist" --type clipboard
112
+ ando search "migration" --type calls --after 2026-01-01T00:00:00.000Z
83
113
  ```
84
114
 
85
- Add a reaction to a message:
115
+ ```sh
116
+ ando get message <message-id>
117
+ ando get member <member-id>
118
+ ando get clipboard <clipboard-id>
119
+ ando get call <call-id>
120
+ ando get transcript <call-id> --limit 100 --cursor <cursor>
121
+ ```
86
122
 
87
123
  ```sh
88
- ando react --message-id <message-id> --emoji 👍
89
- ando react -m <message-id> -e 👍 --json
124
+ ando thread <thread-root-message-id>
125
+ ando thread <thread-root-message-id> --limit 50 --after <cursor>
126
+ ando thread -m <message-id> --json
90
127
  ```
128
+
129
+ Every agent-first command supports `--json` for machine-readable output.
130
+ Without `--json`, commands print tab-separated rows.
131
+
132
+ ## Environment
133
+
134
+ - `ANDO_API_KEY`: API key for SDK and public API requests
135
+ - `ANDO_KEYRING`: set to `0` to use file-based auth instead of keyring
136
+ - `ANDO_HOME`: config and auth state directory
137
+ - `ANDO_BASE_URL`: login and SDK REST base URL
138
+ - `ANDO_API_HOST`: public API base URL for agent-first commands
139
+ - `ANDO_REALTIME_HOST`: realtime host for SDK subscriptions
140
+ - `XDG_CONFIG_HOME`: fallback base directory for config/auth state
141
+
142
+ Flags take precedence over environment variables, and environment variables
143
+ take precedence over saved config.
@@ -0,0 +1,297 @@
1
+ import { getStringFlag, hasFlag } from "./args.js";
2
+ import { resolveConversation } from "./cli-helpers.js";
3
+ import { printJson } from "./output.js";
4
+ async function resolveConversationId(cliClient, parsedArgs) {
5
+ const channelQuery = getStringFlag(parsedArgs, "channel", "c");
6
+ const dmQuery = getStringFlag(parsedArgs, "dm", "d");
7
+ const conversationQuery = getStringFlag(parsedArgs, "conversation");
8
+ if (conversationQuery != null && channelQuery == null && dmQuery == null) {
9
+ return conversationQuery;
10
+ }
11
+ const query = channelQuery ?? dmQuery ?? conversationQuery;
12
+ if (query == null) {
13
+ throw new Error("messages requires --channel, --dm, or --conversation.");
14
+ }
15
+ const memberships = await cliClient.getAllMemberships();
16
+ const membership = resolveConversation(memberships, {
17
+ allowChannels: dmQuery == null,
18
+ allowDms: channelQuery == null,
19
+ query,
20
+ });
21
+ return membership.conversation.id;
22
+ }
23
+ export async function runMessagesCommand(params) {
24
+ const conversationId = await resolveConversationId(params.cliClient, params.parsedArgs);
25
+ const limit = getStringFlag(params.parsedArgs, "limit", "m") ??
26
+ getStringFlag(params.parsedArgs, "n", "n");
27
+ const before = getStringFlag(params.parsedArgs, "before");
28
+ const messageParams = {
29
+ conversationId,
30
+ };
31
+ if (limit != null) {
32
+ messageParams.limit = limit;
33
+ }
34
+ if (before != null) {
35
+ messageParams.before = before;
36
+ }
37
+ const response = await params.apiClient.listConversationMessages(messageParams);
38
+ if (hasFlag(params.parsedArgs, "json")) {
39
+ printJson(response);
40
+ return;
41
+ }
42
+ printMessageResults(response.items);
43
+ }
44
+ function printMessageResults(items) {
45
+ for (const item of items) {
46
+ const author = item.author_name ?? "unknown";
47
+ const content = (item.content ?? "").replace(/\s+/g, " ").trim();
48
+ process.stdout.write(`${item.id}\t${item.created_at}\t${author}\t${content}\n`);
49
+ }
50
+ }
51
+ async function runSearchMessages(apiClient, parsedArgs, query, jsonFlag) {
52
+ const searchParams = { q: query };
53
+ const author = getStringFlag(parsedArgs, "author");
54
+ const conversation = getStringFlag(parsedArgs, "conversation");
55
+ const thread = getStringFlag(parsedArgs, "thread");
56
+ const after = getStringFlag(parsedArgs, "after");
57
+ const before = getStringFlag(parsedArgs, "before");
58
+ const mode = parseSearchMode(getStringFlag(parsedArgs, "mode"));
59
+ if (author != null) {
60
+ searchParams.author = author;
61
+ }
62
+ if (conversation != null) {
63
+ searchParams.conversation = conversation;
64
+ }
65
+ if (thread != null) {
66
+ searchParams.thread = thread;
67
+ }
68
+ if (after != null) {
69
+ searchParams.after = after;
70
+ }
71
+ if (before != null) {
72
+ searchParams.before = before;
73
+ }
74
+ if (mode != null) {
75
+ searchParams.mode = mode;
76
+ }
77
+ const response = await apiClient.searchMessages(searchParams);
78
+ if (jsonFlag) {
79
+ printJson(response);
80
+ return;
81
+ }
82
+ printMessageResults(response.items);
83
+ }
84
+ async function runSearchMembers(apiClient, query, jsonFlag) {
85
+ const response = await apiClient.searchMembers({ q: query });
86
+ if (jsonFlag) {
87
+ printJson(response);
88
+ return;
89
+ }
90
+ for (const item of response.items) {
91
+ const title = item.title == null || item.title === "" ? "" : ` (${item.title})`;
92
+ process.stdout.write(`${item.id}\t${item.display_name ?? ""}\t${item.email ?? ""}${title}\n`);
93
+ }
94
+ }
95
+ async function runSearchConversations(apiClient, query, jsonFlag) {
96
+ const response = await apiClient.searchConversations({ q: query });
97
+ if (jsonFlag) {
98
+ printJson(response);
99
+ return;
100
+ }
101
+ for (const item of response.items) {
102
+ process.stdout.write(`${item.id}\t${item.type}\t${item.name ?? ""}\t${item.human_members_count} members\n`);
103
+ }
104
+ }
105
+ async function runSearchClipboard(apiClient, query, jsonFlag) {
106
+ const response = await apiClient.searchClipboards({ q: query });
107
+ if (jsonFlag) {
108
+ printJson(response);
109
+ return;
110
+ }
111
+ for (const item of response.items) {
112
+ process.stdout.write(`${item.id}\t${item.created_at}\t${item.title ?? ""}\t${item.item_count} items\n`);
113
+ }
114
+ }
115
+ async function runSearchCalls(apiClient, parsedArgs, query, jsonFlag) {
116
+ const searchParams = { q: query };
117
+ const conversation = getStringFlag(parsedArgs, "conversation");
118
+ const after = getStringFlag(parsedArgs, "after");
119
+ const before = getStringFlag(parsedArgs, "before");
120
+ if (conversation != null) {
121
+ searchParams.conversation = conversation;
122
+ }
123
+ if (after != null) {
124
+ searchParams.after = after;
125
+ }
126
+ if (before != null) {
127
+ searchParams.before = before;
128
+ }
129
+ const response = await apiClient.searchCalls(searchParams);
130
+ if (jsonFlag) {
131
+ printJson(response);
132
+ return;
133
+ }
134
+ for (const item of response.items) {
135
+ process.stdout.write(`${item.id}\t${item.status}\t${item.conversation_name ?? ""}\t${item.duration_seconds ?? "-"}s\n`);
136
+ }
137
+ }
138
+ export async function runSearchCommand(params) {
139
+ const query = params.parsedArgs.positionals[1];
140
+ const type = (getStringFlag(params.parsedArgs, "type", "t") ?? "messages").toLowerCase();
141
+ const jsonFlag = hasFlag(params.parsedArgs, "json");
142
+ if (query == null || query.trim() === "") {
143
+ throw new Error('search requires a query, e.g. ando search "incident".');
144
+ }
145
+ switch (type) {
146
+ case "messages": {
147
+ await runSearchMessages(params.apiClient, params.parsedArgs, query, jsonFlag);
148
+ return;
149
+ }
150
+ case "members": {
151
+ await runSearchMembers(params.apiClient, query, jsonFlag);
152
+ return;
153
+ }
154
+ case "conversations": {
155
+ await runSearchConversations(params.apiClient, query, jsonFlag);
156
+ return;
157
+ }
158
+ case "clipboard": {
159
+ await runSearchClipboard(params.apiClient, query, jsonFlag);
160
+ return;
161
+ }
162
+ case "calls": {
163
+ await runSearchCalls(params.apiClient, params.parsedArgs, query, jsonFlag);
164
+ return;
165
+ }
166
+ default:
167
+ throw new Error(`Unknown search --type "${type}". Expected messages, members, conversations, clipboard, or calls.`);
168
+ }
169
+ }
170
+ function parseSearchMode(value) {
171
+ if (value == null) {
172
+ return undefined;
173
+ }
174
+ if (value === "full-text" || value === "semantic") {
175
+ return value;
176
+ }
177
+ throw new Error(`Unknown --mode "${value}". Expected full-text or semantic.`);
178
+ }
179
+ async function runGetMessage(apiClient, id, jsonFlag) {
180
+ const response = await apiClient.getConversationMessageResult(id);
181
+ if (jsonFlag) {
182
+ printJson(response);
183
+ return;
184
+ }
185
+ printMessageResults([response]);
186
+ }
187
+ async function runGetMember(apiClient, id, jsonFlag) {
188
+ const response = await apiClient.getMember(id);
189
+ if (jsonFlag) {
190
+ printJson(response);
191
+ return;
192
+ }
193
+ const title = response.title == null ? "" : ` (${response.title})`;
194
+ process.stdout.write(`${response.id}\t${response.display_name ?? ""}\t${response.email ?? ""}${title}\n`);
195
+ }
196
+ async function runGetClipboard(apiClient, id, jsonFlag) {
197
+ const response = await apiClient.getClipboard(id);
198
+ if (jsonFlag) {
199
+ printJson(response);
200
+ return;
201
+ }
202
+ process.stdout.write(`${response.id}\t${response.title ?? ""}\t${response.created_at}\n`);
203
+ for (const item of response.items) {
204
+ process.stdout.write(` - ${item.type}\t${item.id}\t${item.name ?? item.display_name ?? item.author_name ?? ""}\n`);
205
+ }
206
+ }
207
+ async function runGetCall(apiClient, id, jsonFlag) {
208
+ const response = await apiClient.getCall(id);
209
+ if (jsonFlag) {
210
+ printJson(response);
211
+ return;
212
+ }
213
+ process.stdout.write(`${response.id}\t${response.status}\t${response.conversation_name ?? ""}\t${response.duration_seconds ?? "-"}s\n`);
214
+ for (const participant of response.participants) {
215
+ process.stdout.write(` - ${participant.id}\t${participant.name ?? ""}\n`);
216
+ }
217
+ }
218
+ async function runGetTranscript(apiClient, parsedArgs, id, jsonFlag) {
219
+ const transcriptParams = {
220
+ callId: id,
221
+ };
222
+ const limit = getStringFlag(parsedArgs, "limit");
223
+ const cursor = getStringFlag(parsedArgs, "cursor");
224
+ if (limit != null) {
225
+ transcriptParams.limit = limit;
226
+ }
227
+ if (cursor != null) {
228
+ transcriptParams.cursor = cursor;
229
+ }
230
+ const response = await apiClient.getCallTranscript(transcriptParams);
231
+ if (jsonFlag) {
232
+ printJson(response);
233
+ return;
234
+ }
235
+ for (const segment of response.segments) {
236
+ process.stdout.write(`${segment.start_ms}\t${segment.end_ms}\t${segment.speaker_name ?? ""}\t${segment.content}\n`);
237
+ }
238
+ }
239
+ export async function runGetCommand(params) {
240
+ const entity = params.parsedArgs.positionals[1];
241
+ const id = params.parsedArgs.positionals[2];
242
+ const jsonFlag = hasFlag(params.parsedArgs, "json");
243
+ if (entity == null || id == null || id.trim() === "") {
244
+ throw new Error("get requires an entity type and id, e.g. ando get message <id>.");
245
+ }
246
+ switch (entity) {
247
+ case "message": {
248
+ await runGetMessage(params.apiClient, id, jsonFlag);
249
+ return;
250
+ }
251
+ case "member": {
252
+ await runGetMember(params.apiClient, id, jsonFlag);
253
+ return;
254
+ }
255
+ case "clipboard": {
256
+ await runGetClipboard(params.apiClient, id, jsonFlag);
257
+ return;
258
+ }
259
+ case "call": {
260
+ await runGetCall(params.apiClient, id, jsonFlag);
261
+ return;
262
+ }
263
+ case "transcript": {
264
+ await runGetTranscript(params.apiClient, params.parsedArgs, id, jsonFlag);
265
+ return;
266
+ }
267
+ default:
268
+ throw new Error(`Unknown get entity "${entity}". Expected message, member, clipboard, call, or transcript.`);
269
+ }
270
+ }
271
+ export async function runAgentThreadCommand(params) {
272
+ const positionalId = params.parsedArgs.positionals[1];
273
+ const messageId = positionalId ?? getStringFlag(params.parsedArgs, "message-id", "m");
274
+ if (messageId == null || messageId.trim() === "") {
275
+ throw new Error("thread requires a thread root message id.");
276
+ }
277
+ const threadParams = {
278
+ messageId,
279
+ };
280
+ const limit = getStringFlag(params.parsedArgs, "limit");
281
+ const after = getStringFlag(params.parsedArgs, "after");
282
+ if (limit != null) {
283
+ threadParams.limit = limit;
284
+ }
285
+ if (after != null) {
286
+ threadParams.after = after;
287
+ }
288
+ const response = await params.apiClient.listThreadReplies(threadParams);
289
+ if (hasFlag(params.parsedArgs, "json")) {
290
+ printJson(response);
291
+ return;
292
+ }
293
+ for (const reply of response.items) {
294
+ const content = (reply.content ?? "").replace(/\s+/g, " ").trim();
295
+ process.stdout.write(`${reply.id}\t${reply.created_at}\t${reply.author_name ?? ""}\t${content}\n`);
296
+ }
297
+ }
@@ -0,0 +1,187 @@
1
+ import { requestAndoPublicApi, } from "@andocorp/sdk";
2
+ import { buildBodyFromAssignments, defaultReadStdin, flagValue, parseInlineInputs, readDataFlagBody, } from "./api-inputs.js";
3
+ import { applyPathParameterAssignments, buildOperationSpec, buildOperationUrl, listOperations, matchingOperations, operationCanUseConcretePath, printEndpointHelp, selectOperation, selectedMatches, validateOperationInputs, } from "./api-operations.js";
4
+ import { hasFlag } from "./args.js";
5
+ import { printJson } from "./output.js";
6
+ import { getConfiguredApiHost } from "./session.js";
7
+ import { DEFAULT_CLI_REQUEST_TIMEOUT_MS } from "./timeouts.js";
8
+ function getRequestedMethod(parsedArgs) {
9
+ return flagValue(parsedArgs, "method", "X")?.toUpperCase();
10
+ }
11
+ function printApiCommandHelp() {
12
+ process.stdout.write(`
13
+ ando api
14
+
15
+ Usage:
16
+ ando api ls [--json]
17
+ ando api <path|operation-id> [--method|-X <method>] [--data|-d <json|-] [name==value] [Header:Value] [field=value] [field:=json]
18
+ ando api <path|operation-id> --help
19
+ ando api <path|operation-id> --spec
20
+
21
+ Examples:
22
+ ando api ls
23
+ ando api searchMessages q==incident mode==semantic
24
+ ando api getMember memberId=<member-id>
25
+ ando api /v1/search/messages q==incident mode==semantic
26
+ ando api /v1/conversations/<conversation-id>/messages markdown_content="hello" Idempotency-Key:<key>
27
+ echo '{"markdown_content":"hello"}' | ando api /v1/conversations/<conversation-id>/messages --data -
28
+
29
+ Input syntax:
30
+ name==value query string parameter
31
+ Header:Value declared request header
32
+ field=value JSON body string field
33
+ field:=json JSON body field parsed as JSON
34
+ --data <json> complete JSON request body
35
+ --data - read complete JSON request body from stdin
36
+
37
+ Targets can be public paths or operation IDs from "ando api ls".
38
+ For operation ID targets, supply required path parameters as name=value.
39
+ `.trim() + "\n");
40
+ }
41
+ function formatErrorDetail(value) {
42
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
43
+ }
44
+ function printResponse(response) {
45
+ if (response.empty) {
46
+ process.stdout.write(`${response.status} ${response.statusText}\n`);
47
+ return;
48
+ }
49
+ if (typeof response.body === "string") {
50
+ process.stdout.write(response.body.endsWith("\n") ? response.body : `${response.body}\n`);
51
+ return;
52
+ }
53
+ printJson(response.body);
54
+ }
55
+ function requestBodyFromInputs(params) {
56
+ if (params.dataBody !== undefined && params.assignmentBody !== undefined) {
57
+ throw new Error("Use either --data or inline body fields, not both.");
58
+ }
59
+ return params.dataBody !== undefined ? params.dataBody : params.assignmentBody;
60
+ }
61
+ function rejectMixedBodySourcesBeforeRead(params) {
62
+ if (params.inlineBodyFieldCount > 0 && hasFlag(params.parsedArgs, "data", "d")) {
63
+ throw new Error("Use either --data or inline body fields, not both.");
64
+ }
65
+ }
66
+ function selectPathParameterMatch(params) {
67
+ if (params.requestedMethod != null) {
68
+ return selectOperation(params.rawPath, params.matches, params.requestedMethod);
69
+ }
70
+ const match = params.matches[0];
71
+ if (match == null) {
72
+ throw new Error(`No public API operation matches ${params.rawPath}.`);
73
+ }
74
+ return match;
75
+ }
76
+ function createRawApiTimeoutSignal() {
77
+ const controller = new AbortController();
78
+ const timeout = setTimeout(() => {
79
+ controller.abort(new Error(`[ando-cli] api request timed out after ${DEFAULT_CLI_REQUEST_TIMEOUT_MS}ms`));
80
+ }, DEFAULT_CLI_REQUEST_TIMEOUT_MS);
81
+ return {
82
+ signal: controller.signal,
83
+ clear: () => {
84
+ clearTimeout(timeout);
85
+ },
86
+ };
87
+ }
88
+ async function prepareApiRequest(params) {
89
+ const inlineInputs = parseInlineInputs(params.parsedArgs.positionals.slice(2));
90
+ const pathParameterMatch = selectPathParameterMatch({
91
+ matches: params.matches,
92
+ rawPath: params.rawPath,
93
+ requestedMethod: params.requestedMethod,
94
+ });
95
+ const pathParameterInputs = applyPathParameterAssignments({
96
+ bodyAssignments: inlineInputs.bodyAssignments,
97
+ match: pathParameterMatch,
98
+ });
99
+ rejectMixedBodySourcesBeforeRead({
100
+ parsedArgs: params.parsedArgs,
101
+ inlineBodyFieldCount: pathParameterInputs.bodyAssignments.length,
102
+ });
103
+ const dataBody = await readDataFlagBody({
104
+ parsedArgs: params.parsedArgs,
105
+ readStdin: params.readStdin,
106
+ });
107
+ const assignmentBody = pathParameterInputs.bodyAssignments.length > 0
108
+ ? buildBodyFromAssignments(pathParameterInputs.bodyAssignments)
109
+ : undefined;
110
+ const body = requestBodyFromInputs({ assignmentBody, dataBody });
111
+ const method = params.requestedMethod ?? (body === undefined ? "GET" : "POST");
112
+ const selectedMatch = selectOperation(params.rawPath, params.matches, method);
113
+ const { match } = applyPathParameterAssignments({
114
+ bodyAssignments: inlineInputs.bodyAssignments,
115
+ match: selectedMatch,
116
+ });
117
+ if (!operationCanUseConcretePath(match)) {
118
+ throw new Error(`Supply concrete path values before calling ${match.id}. Use ${match.operation.pathParameters.map((name) => `${name}=<value>`).join(" ")}.`);
119
+ }
120
+ validateOperationInputs({
121
+ body,
122
+ headers: inlineInputs.headers,
123
+ inlineQuery: inlineInputs.query,
124
+ match,
125
+ });
126
+ const config = await params.loadConfig();
127
+ return {
128
+ apiKey: config.apiKey,
129
+ body,
130
+ headers: inlineInputs.headers,
131
+ method,
132
+ url: buildOperationUrl({
133
+ apiHost: getConfiguredApiHost(params.parsedArgs, config.apiHost ?? null),
134
+ concretePath: match.concretePath,
135
+ pathQueryString: match.queryString,
136
+ inlineQuery: inlineInputs.query,
137
+ }),
138
+ };
139
+ }
140
+ async function runPreparedRequest(params) {
141
+ const timeout = createRawApiTimeoutSignal();
142
+ try {
143
+ const response = await params.requestApi({
144
+ ...params.request,
145
+ signal: params.request.signal ?? timeout.signal,
146
+ });
147
+ if (!response.ok) {
148
+ const detail = response.empty ? "" : `\n${formatErrorDetail(response.body)}`;
149
+ throw new Error(`Public API request failed: ${response.status} ${response.statusText}${detail}`);
150
+ }
151
+ printResponse(response);
152
+ }
153
+ finally {
154
+ timeout.clear();
155
+ }
156
+ }
157
+ export async function runApiCommand({ parsedArgs, loadConfig, requestApi = requestAndoPublicApi, readStdin = defaultReadStdin, }) {
158
+ const rawPath = parsedArgs.positionals[1];
159
+ if (rawPath == null || rawPath === "help") {
160
+ printApiCommandHelp();
161
+ return;
162
+ }
163
+ if (rawPath === "ls" || rawPath === "list") {
164
+ listOperations(parsedArgs);
165
+ return;
166
+ }
167
+ const matches = matchingOperations(rawPath);
168
+ const requestedMethod = getRequestedMethod(parsedArgs);
169
+ const visibleMatches = selectedMatches(matches, requestedMethod);
170
+ if (hasFlag(parsedArgs, "help", "h")) {
171
+ printEndpointHelp(rawPath, visibleMatches);
172
+ return;
173
+ }
174
+ if (hasFlag(parsedArgs, "spec")) {
175
+ printJson(buildOperationSpec(visibleMatches));
176
+ return;
177
+ }
178
+ const request = await prepareApiRequest({
179
+ loadConfig,
180
+ matches,
181
+ parsedArgs,
182
+ rawPath,
183
+ readStdin,
184
+ requestedMethod,
185
+ });
186
+ await runPreparedRequest({ request, requestApi });
187
+ }