@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/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`
|
|
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
|
-
|
|
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
|
-
|
|
20
|
+
Log in once:
|
|
26
21
|
|
|
27
22
|
```sh
|
|
28
|
-
ando
|
|
23
|
+
ando login
|
|
29
24
|
```
|
|
30
25
|
|
|
31
|
-
|
|
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
|
|
35
|
-
ando --help
|
|
34
|
+
ando login --no-browser
|
|
36
35
|
```
|
|
37
36
|
|
|
38
|
-
|
|
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
|
|
42
|
-
ando list-channels --json
|
|
43
|
+
ando login --api-key ando_sk_...
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
You can also set `ANDO_API_KEY` for one-off or CI usage.
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
ando
|
|
57
|
-
ando
|
|
58
|
-
ando
|
|
59
|
-
ando
|
|
62
|
+
ando doctor
|
|
63
|
+
ando doctor --json
|
|
64
|
+
ando whoami
|
|
65
|
+
ando whoami --json
|
|
66
|
+
ando logout
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
|
|
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
|
|
66
|
-
ando
|
|
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
|
-
|
|
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
|
|
73
|
-
ando
|
|
74
|
-
ando
|
|
75
|
-
ando
|
|
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
|
|
82
|
-
ando
|
|
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
|
-
|
|
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
|
|
89
|
-
ando
|
|
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
|
+
}
|