@bctrl/cli 0.1.6 → 0.1.8
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 +15 -0
- package/README.md +113 -95
- package/dist/api/auth.d.ts +10 -1
- package/dist/api/auth.js +10 -1
- package/dist/api/client.d.ts +2 -1
- package/dist/api/client.js +37 -37
- package/dist/api/device-auth.d.ts +39 -0
- package/dist/api/device-auth.js +93 -0
- package/dist/api/errors.d.ts +1 -0
- package/dist/api/errors.js +8 -0
- package/dist/commands/ai/index.js +10 -11
- package/dist/commands/api-key/index.js +3 -8
- package/dist/commands/auth/device-login.d.ts +6 -0
- package/dist/commands/auth/device-login.js +19 -0
- package/dist/commands/auth/login.d.ts +11 -0
- package/dist/commands/auth/login.js +140 -20
- package/dist/commands/auth/logout.d.ts +2 -0
- package/dist/commands/auth/logout.js +19 -2
- package/dist/commands/auth/status.d.ts +1 -0
- package/dist/commands/auth/status.js +22 -5
- package/dist/commands/auth/token.js +6 -0
- package/dist/commands/browser-extension/index.js +7 -15
- package/dist/commands/file/index.js +8 -7
- package/dist/commands/help/index.js +5 -4
- package/dist/commands/notification-recipient/index.d.ts +3 -0
- package/dist/commands/notification-recipient/index.js +72 -0
- package/dist/commands/proxy/index.js +6 -4
- package/dist/commands/run/index.js +31 -32
- package/dist/commands/runtime/index.js +185 -106
- package/dist/commands/shared/help.d.ts +26 -3
- package/dist/commands/shared/help.js +87 -23
- package/dist/commands/shared/io.d.ts +1 -0
- package/dist/commands/shared/io.js +4 -2
- package/dist/commands/shared/operation.d.ts +50 -3
- package/dist/commands/shared/operation.js +171 -25
- package/dist/commands/shared/output.js +3 -7
- package/dist/commands/space/index.js +35 -32
- package/dist/commands/space/list.js +6 -5
- package/dist/commands/subaccount/index.js +12 -14
- package/dist/commands/usage/index.js +9 -5
- package/dist/commands/vault/index.js +15 -21
- package/dist/config/auth-store.d.ts +75 -9
- package/dist/config/auth-store.js +133 -27
- package/dist/config/config.d.ts +2 -2
- package/dist/config/config.js +9 -17
- package/dist/config/paths.d.ts +16 -0
- package/dist/config/paths.js +34 -0
- package/dist/config/pending-auth.d.ts +19 -0
- package/dist/config/pending-auth.js +72 -0
- package/dist/config/secret-store.d.ts +24 -0
- package/dist/config/secret-store.js +123 -0
- package/dist/factory.js +2 -1
- package/dist/generated/help.d.ts +7424 -2768
- package/dist/generated/help.js +10116 -4014
- package/dist/generated/openapi-routes.d.ts +52 -8
- package/dist/generated/openapi-routes.js +20 -9
- package/dist/generated/openapi-types.d.ts +2688 -883
- package/dist/root.js +2 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +58 -47
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BCTRL
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
6
|
+
with or without fee is hereby granted, provided that the above copyright notice
|
|
7
|
+
and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
11
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
13
|
+
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
14
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
15
|
+
THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,95 +1,113 @@
|
|
|
1
|
-
# BCTRL CLI
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @bctrl/cli
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
bctrl
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
bctrl
|
|
95
|
-
|
|
1
|
+
# BCTRL CLI
|
|
2
|
+
|
|
3
|
+
Command-line tools for BCTRL cloud browser automation. Use it to create browser runtimes, start live sessions, submit hosted invocations, inspect runs and files, and manage account resources from a terminal or script.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @bctrl/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 22.14 or newer.
|
|
12
|
+
|
|
13
|
+
## Authenticate
|
|
14
|
+
|
|
15
|
+
For an interactive terminal, start the browser approval flow and wait for completion:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bctrl auth login --url --wait
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For CI, agents, or one-off shells, use an API key:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export BCTRL_API_KEY="bctrl_..."
|
|
25
|
+
bctrl auth status
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`BCTRL_API_KEY` takes precedence over credentials stored by `bctrl auth login`.
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
Create a browser runtime, start it, run an extraction task, then stop it:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bctrl runtime create --name research-browser --json
|
|
36
|
+
bctrl runtime start <runtime-id> --json
|
|
37
|
+
|
|
38
|
+
bctrl runtime invocation create <runtime-id> \
|
|
39
|
+
--action extract \
|
|
40
|
+
--instruction "Extract the page title." \
|
|
41
|
+
--json
|
|
42
|
+
|
|
43
|
+
bctrl runtime invocation wait <runtime-id> <invocation-id> --json
|
|
44
|
+
bctrl runtime stop <runtime-id>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For full request bodies, pass JSON with `--body`. Inline JSON, `@file`, and `-` for stdin are supported:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bctrl runtime invocation create <runtime-id> \
|
|
51
|
+
--body '{"action":"observe","instruction":"Summarize the current page."}' \
|
|
52
|
+
--json
|
|
53
|
+
|
|
54
|
+
cat invocation.json | bctrl runtime invocation create <runtime-id> --body - --json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use `--params` for path and query overrides when you need the exact API surface:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
bctrl run list --params '{"limit":25}' --json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Output
|
|
64
|
+
|
|
65
|
+
Print full JSON:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
bctrl runtime list --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Print selected fields:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bctrl runtime list --json id,status,name
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Filter with jq syntax:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bctrl runtime list --json --jq '.data[] | select(.status == "active")'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Render with a template:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bctrl runtime list \
|
|
87
|
+
--json \
|
|
88
|
+
--template '{{#each data}}{{id}} {{status}}{{newline}}{{/each}}'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Common Commands
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bctrl space list
|
|
95
|
+
bctrl runtime create --name browser-task
|
|
96
|
+
bctrl runtime start <runtime-id>
|
|
97
|
+
bctrl runtime target create <runtime-id> --uri https://example.com --activate
|
|
98
|
+
bctrl runtime invocation create <runtime-id> --action act --instruction "Click the sign in button"
|
|
99
|
+
bctrl run list --json
|
|
100
|
+
bctrl runtime stop <runtime-id>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run any command with `--help` for the exact arguments and flags:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
bctrl runtime invocation create --help
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Documentation
|
|
110
|
+
|
|
111
|
+
- CLI guide: https://platform.bctrl.ai/cli
|
|
112
|
+
- Command reference: https://platform.bctrl.ai/cli/reference
|
|
113
|
+
- API reference: https://platform.bctrl.ai/api-reference
|
package/dist/api/auth.d.ts
CHANGED
|
@@ -7,7 +7,16 @@ export declare const AuthWhoamiSchema: z.ZodObject<{
|
|
|
7
7
|
}>;
|
|
8
8
|
organizationId: z.ZodString;
|
|
9
9
|
subaccountId: z.ZodNullable<z.ZodString>;
|
|
10
|
-
defaultSpaceId: z.ZodString
|
|
10
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
11
|
+
effectiveScope: z.ZodObject<{
|
|
12
|
+
scope: z.ZodEnum<{
|
|
13
|
+
organization: "organization";
|
|
14
|
+
subaccount: "subaccount";
|
|
15
|
+
}>;
|
|
16
|
+
organizationId: z.ZodString;
|
|
17
|
+
subaccountId: z.ZodNullable<z.ZodString>;
|
|
18
|
+
defaultSpaceId: z.ZodNullable<z.ZodString>;
|
|
19
|
+
}, z.core.$strict>;
|
|
11
20
|
keyId: z.ZodString;
|
|
12
21
|
plan: z.ZodString;
|
|
13
22
|
}, z.core.$strict>;
|
package/dist/api/auth.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CliError } from '../runtime/errors.js';
|
|
3
3
|
import { apiErrorFromResponse } from './errors.js';
|
|
4
|
+
const AuthEffectiveScopeSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
scope: z.enum(['organization', 'subaccount']),
|
|
7
|
+
organizationId: z.string().min(1),
|
|
8
|
+
subaccountId: z.string().min(1).nullable(),
|
|
9
|
+
defaultSpaceId: z.string().min(1).nullable(),
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
4
12
|
export const AuthWhoamiSchema = z
|
|
5
13
|
.object({
|
|
6
14
|
email: z.string().email().nullable().optional(),
|
|
7
15
|
scope: z.enum(['organization', 'subaccount']),
|
|
8
16
|
organizationId: z.string().min(1),
|
|
9
17
|
subaccountId: z.string().min(1).nullable(),
|
|
10
|
-
defaultSpaceId: z.string().min(1),
|
|
18
|
+
defaultSpaceId: z.string().min(1).nullable(),
|
|
19
|
+
effectiveScope: AuthEffectiveScopeSchema,
|
|
11
20
|
keyId: z.string().min(1),
|
|
12
21
|
plan: z.string().min(1),
|
|
13
22
|
})
|
package/dist/api/client.d.ts
CHANGED
|
@@ -16,8 +16,9 @@ export type BctrlApiClient = {
|
|
|
16
16
|
export type RequestOptions = {
|
|
17
17
|
query?: Record<string, string | number | boolean | undefined>;
|
|
18
18
|
idempotencyKey?: string;
|
|
19
|
+
actingSubaccountId?: string;
|
|
19
20
|
};
|
|
20
21
|
export type JsonRequestOptions = RequestOptions & {
|
|
21
22
|
body?: unknown;
|
|
22
23
|
};
|
|
23
|
-
export declare function createBctrlApiClient(config: BctrlConfig): BctrlApiClient;
|
|
24
|
+
export declare function createBctrlApiClient(config: BctrlConfig, env?: NodeJS.ProcessEnv): BctrlApiClient;
|
package/dist/api/client.js
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
import { AuthError, CliError } from '../runtime/errors.js';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { clearCredential } from '../config/auth-store.js';
|
|
3
|
+
import { apiErrorFromResponse, isUnauthorizedApiError } from './errors.js';
|
|
4
|
+
export function createBctrlApiClient(config, env = process.env) {
|
|
4
5
|
return {
|
|
5
|
-
get: (path, options) => requestJson(config, 'GET', path, options),
|
|
6
|
-
post: (path, options) => requestJson(config, 'POST', path, options),
|
|
7
|
-
patch: (path, options) => requestJson(config, 'PATCH', path, options),
|
|
8
|
-
put: (path, options) => requestJson(config, 'PUT', path, options),
|
|
9
|
-
delete: (path, options) => requestJson(config, 'DELETE', path, options),
|
|
10
|
-
download: (path, options) => requestBinary(config, 'GET', path, options),
|
|
11
|
-
streamText: (path, options) => requestTextStream(config, path, options),
|
|
12
|
-
uploadFile: (path, options) => uploadFile(config, path, options),
|
|
6
|
+
get: (path, options) => requestJson(config, env, 'GET', path, options),
|
|
7
|
+
post: (path, options) => requestJson(config, env, 'POST', path, options),
|
|
8
|
+
patch: (path, options) => requestJson(config, env, 'PATCH', path, options),
|
|
9
|
+
put: (path, options) => requestJson(config, env, 'PUT', path, options),
|
|
10
|
+
delete: (path, options) => requestJson(config, env, 'DELETE', path, options),
|
|
11
|
+
download: (path, options) => requestBinary(config, env, 'GET', path, options),
|
|
12
|
+
streamText: (path, options) => requestTextStream(config, env, path, options),
|
|
13
|
+
uploadFile: (path, options) => uploadFile(config, env, path, options),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
async function requestJson(config, method, path, options) {
|
|
16
|
+
async function requestJson(config, env, method, path, options) {
|
|
16
17
|
if (!config.activeToken) {
|
|
17
18
|
throw new AuthError();
|
|
18
19
|
}
|
|
19
|
-
const headers =
|
|
20
|
-
accept: 'application/json',
|
|
21
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
22
|
-
'user-agent': 'BCTRL CLI',
|
|
23
|
-
};
|
|
20
|
+
const headers = requestHeaders(config.activeToken.token, options, 'application/json');
|
|
24
21
|
if (options?.idempotencyKey) {
|
|
25
22
|
headers['idempotency-key'] = options.idempotencyKey;
|
|
26
23
|
}
|
|
@@ -35,7 +32,7 @@ async function requestJson(config, method, path, options) {
|
|
|
35
32
|
body,
|
|
36
33
|
});
|
|
37
34
|
if (!response.ok) {
|
|
38
|
-
throw await apiErrorFromResponse(response);
|
|
35
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
39
36
|
}
|
|
40
37
|
if (response.status === 204) {
|
|
41
38
|
return { ok: true };
|
|
@@ -43,36 +40,29 @@ async function requestJson(config, method, path, options) {
|
|
|
43
40
|
const text = await response.text();
|
|
44
41
|
return (text ? JSON.parse(text) : { ok: true });
|
|
45
42
|
}
|
|
46
|
-
async function requestBinary(config, method, path, options) {
|
|
43
|
+
async function requestBinary(config, env, method, path, options) {
|
|
47
44
|
if (!config.activeToken) {
|
|
48
45
|
throw new AuthError();
|
|
49
46
|
}
|
|
50
47
|
const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
|
|
51
48
|
method,
|
|
52
|
-
headers:
|
|
53
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
54
|
-
'user-agent': 'BCTRL CLI',
|
|
55
|
-
},
|
|
49
|
+
headers: requestHeaders(config.activeToken.token, options),
|
|
56
50
|
});
|
|
57
51
|
if (!response.ok) {
|
|
58
|
-
throw await apiErrorFromResponse(response);
|
|
52
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
59
53
|
}
|
|
60
54
|
return new Uint8Array(await response.arrayBuffer());
|
|
61
55
|
}
|
|
62
|
-
async function requestTextStream(config, path, options) {
|
|
56
|
+
async function requestTextStream(config, env, path, options) {
|
|
63
57
|
if (!config.activeToken) {
|
|
64
58
|
throw new AuthError();
|
|
65
59
|
}
|
|
66
60
|
const response = await fetch(buildUrl(config.apiBaseUrl, path, options?.query), {
|
|
67
61
|
method: 'GET',
|
|
68
|
-
headers:
|
|
69
|
-
accept: 'text/event-stream',
|
|
70
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
71
|
-
'user-agent': 'BCTRL CLI',
|
|
72
|
-
},
|
|
62
|
+
headers: requestHeaders(config.activeToken.token, options, 'text/event-stream'),
|
|
73
63
|
});
|
|
74
64
|
if (!response.ok) {
|
|
75
|
-
throw await apiErrorFromResponse(response);
|
|
65
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
76
66
|
}
|
|
77
67
|
if (!response.body) {
|
|
78
68
|
throw new CliError('BCTRL API response did not include a stream body');
|
|
@@ -99,7 +89,7 @@ async function requestTextStream(config, path, options) {
|
|
|
99
89
|
},
|
|
100
90
|
};
|
|
101
91
|
}
|
|
102
|
-
async function uploadFile(config, path, options) {
|
|
92
|
+
async function uploadFile(config, env, path, options) {
|
|
103
93
|
if (!config.activeToken) {
|
|
104
94
|
throw new AuthError();
|
|
105
95
|
}
|
|
@@ -110,18 +100,28 @@ async function uploadFile(config, path, options) {
|
|
|
110
100
|
}
|
|
111
101
|
const response = await fetch(buildUrl(config.apiBaseUrl, path, options.query), {
|
|
112
102
|
method: 'POST',
|
|
113
|
-
headers:
|
|
114
|
-
accept: 'application/json',
|
|
115
|
-
authorization: `Bearer ${config.activeToken.token}`,
|
|
116
|
-
'user-agent': 'BCTRL CLI',
|
|
117
|
-
},
|
|
103
|
+
headers: requestHeaders(config.activeToken.token, options, 'application/json'),
|
|
118
104
|
body: form,
|
|
119
105
|
});
|
|
120
106
|
if (!response.ok) {
|
|
121
|
-
throw await apiErrorFromResponse(response);
|
|
107
|
+
throw await reconcileStoredCredentialAuthError(config, env, await apiErrorFromResponse(response));
|
|
122
108
|
}
|
|
123
109
|
return (await response.json());
|
|
124
110
|
}
|
|
111
|
+
async function reconcileStoredCredentialAuthError(config, env, error) {
|
|
112
|
+
if (config.activeToken?.source === 'stored' && isUnauthorizedApiError(error)) {
|
|
113
|
+
await clearCredential(env, config.apiBaseUrl);
|
|
114
|
+
}
|
|
115
|
+
return error;
|
|
116
|
+
}
|
|
117
|
+
function requestHeaders(token, options, accept) {
|
|
118
|
+
return {
|
|
119
|
+
...(accept ? { accept } : {}),
|
|
120
|
+
authorization: `Bearer ${token}`,
|
|
121
|
+
'user-agent': 'BCTRL CLI',
|
|
122
|
+
...(options?.actingSubaccountId ? { 'BCTRL-Subaccount-Id': options.actingSubaccountId } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
125
|
function buildUrl(baseUrl, path, query) {
|
|
126
126
|
const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
127
127
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const DeviceStartResponseSchema: z.ZodObject<{
|
|
3
|
+
deviceCode: z.ZodString;
|
|
4
|
+
userCode: z.ZodString;
|
|
5
|
+
verificationUri: z.ZodString;
|
|
6
|
+
verificationUriComplete: z.ZodString;
|
|
7
|
+
expiresIn: z.ZodNumber;
|
|
8
|
+
interval: z.ZodNumber;
|
|
9
|
+
}, z.core.$strict>;
|
|
10
|
+
declare const DeviceSessionRevokeResponseSchema: z.ZodObject<{
|
|
11
|
+
revoked: z.ZodBoolean;
|
|
12
|
+
}, z.core.$strict>;
|
|
13
|
+
export type DeviceStartResponse = z.infer<typeof DeviceStartResponseSchema>;
|
|
14
|
+
type DeviceSessionRevokeResponse = z.infer<typeof DeviceSessionRevokeResponseSchema>;
|
|
15
|
+
/**
|
|
16
|
+
* Poll outcome. The control-plane returns the RFC 8628 polling states as a 200
|
|
17
|
+
* status union; a 429 (rate limiter) is surfaced as `rate_limited` so the poll
|
|
18
|
+
* loop can back off rather than treating it as a hard failure.
|
|
19
|
+
*/
|
|
20
|
+
export type DevicePollResult = {
|
|
21
|
+
status: 'authorization_pending';
|
|
22
|
+
} | {
|
|
23
|
+
status: 'access_denied';
|
|
24
|
+
} | {
|
|
25
|
+
status: 'expired_token';
|
|
26
|
+
} | {
|
|
27
|
+
status: 'rate_limited';
|
|
28
|
+
} | {
|
|
29
|
+
status: 'complete';
|
|
30
|
+
token: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function startDeviceAuth(apiBaseUrl: string, body?: {
|
|
33
|
+
clientName?: string;
|
|
34
|
+
clientKind?: 'cli' | 'mcp';
|
|
35
|
+
deviceName?: string;
|
|
36
|
+
}): Promise<DeviceStartResponse>;
|
|
37
|
+
export declare function pollDeviceAuth(apiBaseUrl: string, deviceCode: string): Promise<DevicePollResult>;
|
|
38
|
+
export declare function revokeDeviceSession(apiBaseUrl: string, token: string): Promise<DeviceSessionRevokeResponse>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CliError } from '../runtime/errors.js';
|
|
3
|
+
import { apiErrorFromResponse } from './errors.js';
|
|
4
|
+
// Hand-written client for the OAuth 2.0 device-authorization endpoints (RFC 8628).
|
|
5
|
+
// These are unauthenticated (the CLI has no credential yet) and intentionally kept
|
|
6
|
+
// out of the generated public OpenAPI/SDK (docs-hidden).
|
|
7
|
+
const DeviceStartResponseSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
deviceCode: z.string().min(1),
|
|
10
|
+
userCode: z.string().min(1),
|
|
11
|
+
verificationUri: z.string().min(1),
|
|
12
|
+
verificationUriComplete: z.string().min(1),
|
|
13
|
+
expiresIn: z.number().int().positive(),
|
|
14
|
+
interval: z.number().int().positive(),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
const DeviceTokenResponseSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
status: z.enum(['authorization_pending', 'access_denied', 'expired_token', 'complete']),
|
|
20
|
+
token: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
const DeviceSessionRevokeResponseSchema = z.object({ revoked: z.boolean() }).strict();
|
|
24
|
+
function deviceEndpoint(apiBaseUrl, path) {
|
|
25
|
+
return new URL(`${apiBaseUrl.replace(/\/+$/, '')}${path}`);
|
|
26
|
+
}
|
|
27
|
+
export async function startDeviceAuth(apiBaseUrl, body = {}) {
|
|
28
|
+
const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device'), {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
accept: 'application/json',
|
|
32
|
+
'content-type': 'application/json',
|
|
33
|
+
'user-agent': 'BCTRL CLI',
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw await apiErrorFromResponse(response, 'BCTRL device login');
|
|
39
|
+
}
|
|
40
|
+
const parsed = DeviceStartResponseSchema.safeParse(await response.json());
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
throw new CliError(`BCTRL device login returned an unexpected response: ${parsed.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
export async function pollDeviceAuth(apiBaseUrl, deviceCode) {
|
|
47
|
+
const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device/token'), {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
accept: 'application/json',
|
|
51
|
+
'content-type': 'application/json',
|
|
52
|
+
'user-agent': 'BCTRL CLI',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({ deviceCode }),
|
|
55
|
+
});
|
|
56
|
+
// The poll route is rate-limited; a 429 is expected under fast polling and just
|
|
57
|
+
// means "back off and keep waiting", not a terminal error.
|
|
58
|
+
if (response.status === 429) {
|
|
59
|
+
return { status: 'rate_limited' };
|
|
60
|
+
}
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw await apiErrorFromResponse(response, 'BCTRL device login');
|
|
63
|
+
}
|
|
64
|
+
const parsed = DeviceTokenResponseSchema.safeParse(await response.json());
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
throw new CliError(`BCTRL device login returned an unexpected response: ${parsed.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
if (parsed.data.status === 'complete') {
|
|
69
|
+
if (!parsed.data.token) {
|
|
70
|
+
throw new CliError('BCTRL device login completed without returning a token.');
|
|
71
|
+
}
|
|
72
|
+
return { status: 'complete', token: parsed.data.token };
|
|
73
|
+
}
|
|
74
|
+
return { status: parsed.data.status };
|
|
75
|
+
}
|
|
76
|
+
export async function revokeDeviceSession(apiBaseUrl, token) {
|
|
77
|
+
const response = await fetch(deviceEndpoint(apiBaseUrl, '/auth/device/session'), {
|
|
78
|
+
method: 'DELETE',
|
|
79
|
+
headers: {
|
|
80
|
+
accept: 'application/json',
|
|
81
|
+
authorization: `Bearer ${token}`,
|
|
82
|
+
'user-agent': 'BCTRL CLI',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw await apiErrorFromResponse(response, 'BCTRL device logout');
|
|
87
|
+
}
|
|
88
|
+
const parsed = DeviceSessionRevokeResponseSchema.safeParse(await response.json());
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
throw new CliError(`BCTRL device logout returned an unexpected response: ${parsed.error.message}`);
|
|
91
|
+
}
|
|
92
|
+
return parsed.data;
|
|
93
|
+
}
|
package/dist/api/errors.d.ts
CHANGED
package/dist/api/errors.js
CHANGED
|
@@ -16,6 +16,14 @@ export async function apiErrorFromResponse(response, context = 'BCTRL API reques
|
|
|
16
16
|
apiError,
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
+
export function isUnauthorizedApiError(error) {
|
|
20
|
+
if (!(error instanceof CliError))
|
|
21
|
+
return false;
|
|
22
|
+
return (error.apiError?.status === 401 &&
|
|
23
|
+
(error.apiError.code === 'auth.required' ||
|
|
24
|
+
error.apiError.code === 'auth.invalid' ||
|
|
25
|
+
error.apiError.code === undefined));
|
|
26
|
+
}
|
|
19
27
|
function parseErrorBody(bodyText) {
|
|
20
28
|
if (!bodyText)
|
|
21
29
|
return {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readJsonFile } from '../shared/io.js';
|
|
3
2
|
import { addOutputFlags } from '../shared/output.js';
|
|
4
|
-
import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
3
|
+
import { actingSubaccountOption, buildOperationInput, createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, createOperationViewCommand, outputFlags, requestOperationAndPrint, } from '../shared/operation.js';
|
|
5
4
|
export function createAiCommand(factory) {
|
|
6
5
|
const command = new Command('ai').description('Manage AI models and credentials');
|
|
7
6
|
command.addCommand(createAiModelsCommand(factory));
|
|
@@ -50,11 +49,13 @@ function createAiCredentialsCommand(factory) {
|
|
|
50
49
|
}));
|
|
51
50
|
command.addCommand(createAiCredentialWriteCommand(factory, 'create', 'ai.credentials.create'));
|
|
52
51
|
command.addCommand(createAiCredentialWriteCommand(factory, 'patch', 'ai.credentials.update', ['credentialId']));
|
|
53
|
-
command.addCommand(addOutputFlags(new Command('test').description('Test an AI credential').argument('<credentialId>'))
|
|
54
|
-
|
|
52
|
+
command.addCommand(addOutputFlags(new Command('test').description('Test an AI credential').argument('<credentialId>'))
|
|
53
|
+
.option('--params <json>', 'Path/query parameters as a JSON object (inline, @file, or - for stdin)')
|
|
54
|
+
.action(async (credentialId, options) => {
|
|
55
|
+
await requestOperationAndPrint(factory, 'ai.credentials.test', await buildOperationInput('ai.credentials.test', options, {
|
|
55
56
|
pathParams: { credentialId },
|
|
56
57
|
output: outputFlags(options),
|
|
57
|
-
});
|
|
58
|
+
}));
|
|
58
59
|
}));
|
|
59
60
|
command.addCommand(createOperationDeleteCommand(factory, {
|
|
60
61
|
operationId: 'ai.credentials.delete',
|
|
@@ -76,22 +77,20 @@ function createAiCredentialWriteCommand(factory, name, operationId, argNames = [
|
|
|
76
77
|
.option('--status <status>', 'Credential status')
|
|
77
78
|
.option('--default-model <model>', 'Default model')
|
|
78
79
|
.option('--base-url <url>', 'Custom provider base URL')
|
|
79
|
-
.option('--subaccount-id <id>', 'Subaccount scope when using a parent/org key')
|
|
80
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
80
|
+
.option('--subaccount-id <id>', 'Subaccount scope when using a parent/org key'),
|
|
81
81
|
body: async (_args, options) => {
|
|
82
|
-
if (typeof options.input === 'string') {
|
|
83
|
-
return (await readJsonFile(options.input));
|
|
84
|
-
}
|
|
85
82
|
return {
|
|
86
83
|
name: options.name,
|
|
87
84
|
provider: options.provider,
|
|
88
85
|
apiKey: options.apiKey,
|
|
89
86
|
status: options.status,
|
|
90
|
-
subaccountId: options.subaccountId,
|
|
91
87
|
defaultModel: options.defaultModel,
|
|
92
88
|
baseUrl: options.baseUrl,
|
|
93
89
|
};
|
|
94
90
|
},
|
|
91
|
+
actingSubaccountId: operationId === 'ai.credentials.create'
|
|
92
|
+
? (_args, options) => actingSubaccountOption()(options)
|
|
93
|
+
: undefined,
|
|
95
94
|
});
|
|
96
95
|
}
|
|
97
96
|
function parseBooleanFlag(value) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { readJsonFile } from '../shared/io.js';
|
|
3
2
|
import { createOperationDeleteCommand, createOperationJsonBodyCommand, createOperationListCommand, } from '../shared/operation.js';
|
|
4
3
|
export function createApiKeyCommand(factory) {
|
|
5
4
|
const command = new Command('api-key').description('Manage API keys');
|
|
@@ -8,12 +7,12 @@ export function createApiKeyCommand(factory) {
|
|
|
8
7
|
description: 'List API keys',
|
|
9
8
|
configure: (cmd) => cmd
|
|
10
9
|
.option('--subaccount-id <id>', 'Filter by subaccount id')
|
|
11
|
-
.option('--
|
|
10
|
+
.option('--type <type>', 'Filter by API key type: organization or subaccount')
|
|
12
11
|
.option('-L, --limit <number>', 'Maximum number of results to return')
|
|
13
12
|
.option('--cursor <cursor>', 'Pagination cursor'),
|
|
14
13
|
query: (options) => ({
|
|
15
14
|
subaccountId: typeof options.subaccountId === 'string' ? options.subaccountId : undefined,
|
|
16
|
-
|
|
15
|
+
type: typeof options.type === 'string' ? options.type : undefined,
|
|
17
16
|
limit: typeof options.limit === 'string' ? Number(options.limit) : undefined,
|
|
18
17
|
cursor: typeof options.cursor === 'string' ? options.cursor : undefined,
|
|
19
18
|
}),
|
|
@@ -25,12 +24,8 @@ export function createApiKeyCommand(factory) {
|
|
|
25
24
|
configure: (cmd) => cmd
|
|
26
25
|
.option('--name <name>', 'API key name')
|
|
27
26
|
.option('--subaccount-id <id>', 'Create a subaccount-scoped key')
|
|
28
|
-
.option('--expires-at <iso>', 'Expiration timestamp')
|
|
29
|
-
.option('--input <path>', 'Read full JSON request body from file, or - for stdin'),
|
|
27
|
+
.option('--expires-at <iso>', 'Expiration timestamp'),
|
|
30
28
|
body: async (_args, options) => {
|
|
31
|
-
if (typeof options.input === 'string') {
|
|
32
|
-
return (await readJsonFile(options.input));
|
|
33
|
-
}
|
|
34
29
|
return {
|
|
35
30
|
name: options.name,
|
|
36
31
|
subaccountId: options.subaccountId,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { startDeviceAuth as defaultStartDeviceAuth, type DeviceStartResponse } from '../../api/device-auth.js';
|
|
2
|
+
export type DeviceLoginStartDeps = {
|
|
3
|
+
apiBaseUrl: string;
|
|
4
|
+
startDeviceAuth?: typeof defaultStartDeviceAuth;
|
|
5
|
+
};
|
|
6
|
+
export declare function startDeviceLoginSession(deps: DeviceLoginStartDeps): Promise<DeviceStartResponse>;
|