byterover-cli 3.8.2 → 3.8.3
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/README.md +8 -34
- package/dist/oclif/commands/login.d.ts +15 -2
- package/dist/oclif/commands/login.js +106 -29
- package/dist/oclif/commands/providers/list.js +3 -0
- package/dist/server/core/domain/entities/provider-registry.js +2 -2
- package/dist/server/infra/webui/webui-middleware.js +4 -3
- package/dist/webui/assets/index-DFMY2d5W.css +1 -0
- package/dist/webui/assets/{index-Cti7S_1o.js → index-Dkyf6c5F.js} +1 -1
- package/dist/webui/index.html +2 -2
- package/dist/webui/sw.js +1 -1
- package/oclif.manifest.json +501 -498
- package/package.json +1 -1
- package/dist/webui/assets/index-Dpw6osIL.css +0 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ Or download our self-hosted PDF version of the paper [here](https://byterover.de
|
|
|
30
30
|
|
|
31
31
|
**Key Features:**
|
|
32
32
|
|
|
33
|
+
- 🌐 Web dashboard for curating and querying context (`brv webui`)
|
|
33
34
|
- 🖥️ Interactive TUI with REPL interface (React/Ink)
|
|
34
35
|
- 🧠 Context tree and knowledge storage management
|
|
35
36
|
- 🔀 Git-like version control for the context tree (branch, commit, merge, push/pull)
|
|
@@ -101,35 +102,6 @@ The REPL auto-configures on first run - no setup needed. Type `/` to discover al
|
|
|
101
102
|
/query How is authentication implemented?
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
## Web UI Development
|
|
105
|
-
|
|
106
|
-
The web UI supports a local-first development flow for the shared component library.
|
|
107
|
-
|
|
108
|
-
`npm run dev:ui` uses the git submodule at `packages/byterover-packages/ui` so edits to shared UI components hot-reload immediately in Vite.
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
# Clone with submodules, or initialize them after clone
|
|
112
|
-
git clone --recurse-submodules <repo-url>
|
|
113
|
-
# or
|
|
114
|
-
git submodule update --init --recursive
|
|
115
|
-
|
|
116
|
-
# Install dependencies
|
|
117
|
-
npm ci
|
|
118
|
-
|
|
119
|
-
# Start or restart the daemon
|
|
120
|
-
./bin/dev.js restart
|
|
121
|
-
|
|
122
|
-
# Start the web UI in local development mode
|
|
123
|
-
npm run dev:ui
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Notes:
|
|
127
|
-
|
|
128
|
-
- Edit shared components in `packages/byterover-packages/ui/src`.
|
|
129
|
-
- `npm run dev:ui` uses the submodule source.
|
|
130
|
-
- `npm run build:ui` uses the installed package path.
|
|
131
|
-
- If `/api/ui/config` or transport bootstrap fails, restart the Vite dev server after restarting the daemon.
|
|
132
|
-
|
|
133
105
|
## ByteRover Cloud
|
|
134
106
|
|
|
135
107
|
ByteRover Cloud is a hosted platform for teams to sync, share, and manage context knowledge across projects and machines.
|
|
@@ -139,12 +111,12 @@ Everything works locally by default - Cloud adds collaboration and persistence w
|
|
|
139
111
|
<a href="https://app.byterover.dev"><img src="https://img.shields.io/badge/Try%20ByteRover%20Cloud-Free-blue?style=for-the-badge" alt="Try ByteRover Cloud" /></a>
|
|
140
112
|
</p>
|
|
141
113
|
|
|
142
|
-
Sign in
|
|
143
|
-
an [API key](https://app.byterover.dev/settings/keys) (`brv login`) to get started.
|
|
114
|
+
Sign in from the dashboard, or run `brv login` with an [API key](https://app.byterover.dev/settings/keys).
|
|
144
115
|
|
|
145
116
|
- 🔄 **Team context sync** — push and pull shared knowledge across teammates
|
|
146
117
|
- 📂 **Shared spaces** — organize context across multiple projects and teams
|
|
147
118
|
- 💻 **Multi-machine access** — sync your context tree across devices with cloud backup
|
|
119
|
+
- 💻 **Multi-machine access** — sync your context tree across devices
|
|
148
120
|
- 🧠 **Built-in hosted LLM** — start immediately with limited free usage
|
|
149
121
|
- 👥 **Team management** — manage members, spaces, and permissions via the web app
|
|
150
122
|
- 📊 **Usage analytics** — track seat allocation and monthly credit consumption
|
|
@@ -153,10 +125,13 @@ an [API key](https://app.byterover.dev/settings/keys) (`brv login`) to get start
|
|
|
153
125
|
<details>
|
|
154
126
|
<summary><h2>CLI Usage</h2></summary>
|
|
155
127
|
|
|
128
|
+
Most users only need `brv webui`. The commands below are for advanced users and automation. Run `brv --help` for the full, up-to-date reference.
|
|
129
|
+
|
|
156
130
|
### Core Workflow
|
|
157
131
|
|
|
158
132
|
```bash
|
|
159
133
|
brv # Start interactive REPL
|
|
134
|
+
brv webui # Open the ByteRover dashboard (primary UI)
|
|
160
135
|
brv status # Show project and daemon status
|
|
161
136
|
brv curate # Add context to knowledge storage
|
|
162
137
|
brv curate view # View curate history
|
|
@@ -245,7 +220,7 @@ Run `brv --help` for the full command reference.
|
|
|
245
220
|
<details>
|
|
246
221
|
<summary><h2>Supported LLM Providers</h2></summary>
|
|
247
222
|
|
|
248
|
-
ByteRover CLI supports 18 LLM providers out of the box.
|
|
223
|
+
ByteRover CLI supports 18 LLM providers out of the box. Connect and switch providers from the dashboard, or use `brv providers connect` / `brv providers switch`.
|
|
249
224
|
|
|
250
225
|
| Provider | Description |
|
|
251
226
|
|----------|-------------|
|
|
@@ -310,13 +285,12 @@ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for deve
|
|
|
310
285
|
ByteRover CLI is built and maintained by the [ByteRover team](https://byterover.dev/).
|
|
311
286
|
|
|
312
287
|
- Join our [Discord](https://discord.com/invite/UMRrpNjh5W) to share projects, ask questions, or just say hi
|
|
313
|
-
- [Report issues](https://github.com/campfirein/byterover-cli/issues)
|
|
288
|
+
- [Report issues](https://github.com/campfirein/byterover-cli/issues) on GitHub
|
|
314
289
|
- If you enjoy ByteRover CLI, please give us a star on GitHub — it helps a lot!
|
|
315
290
|
- Follow [@kevinnguyendn](https://x.com/kevinnguyendn) on X
|
|
316
291
|
|
|
317
292
|
## Contributors
|
|
318
293
|
|
|
319
|
-
<!-- TODO: ENG-1575 -->
|
|
320
294
|
[](https://github.com/campfirein/byterover-cli/graphs/contributors)
|
|
321
295
|
|
|
322
296
|
## Star History
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { type AuthLoginWithApiKeyResponse } from '../../shared/transport/events/auth-events.js';
|
|
2
|
+
import { type AuthLoginCompletedEvent, type AuthLoginWithApiKeyResponse } from '../../shared/transport/events/auth-events.js';
|
|
3
3
|
import { type DaemonClientOptions } from '../lib/daemon-client.js';
|
|
4
|
+
export interface LoginOAuthOptions extends DaemonClientOptions {
|
|
5
|
+
/** Max time to wait for LOGIN_COMPLETED after the browser opens. */
|
|
6
|
+
oauthTimeoutMs?: number;
|
|
7
|
+
/** Invoked with the auth URL once the daemon has started the flow. */
|
|
8
|
+
onAuthUrl?: (authUrl: string) => void;
|
|
9
|
+
}
|
|
4
10
|
export default class Login extends Command {
|
|
5
11
|
static description: string;
|
|
6
12
|
static examples: string[];
|
|
7
13
|
static flags: {
|
|
8
|
-
'api-key': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
15
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
16
|
};
|
|
17
|
+
/** Gates the OAuth flow. DISPLAY/WAYLAND_DISPLAY deliberately not checked — unset on macOS/Windows, would false-positive. */
|
|
18
|
+
protected canOpenBrowser(): boolean;
|
|
11
19
|
protected loginWithApiKey(apiKey: string, options?: DaemonClientOptions): Promise<AuthLoginWithApiKeyResponse>;
|
|
20
|
+
protected loginWithOAuth(options?: LoginOAuthOptions): Promise<AuthLoginCompletedEvent>;
|
|
12
21
|
run(): Promise<void>;
|
|
22
|
+
private emitError;
|
|
23
|
+
private emitSuccess;
|
|
24
|
+
private runApiKey;
|
|
25
|
+
private runOAuth;
|
|
13
26
|
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import { AuthEvents } from '../../shared/transport/events/auth-events.js';
|
|
2
|
+
import { AuthEvents, } from '../../shared/transport/events/auth-events.js';
|
|
3
3
|
import { formatConnectionError, withDaemonRetry } from '../lib/daemon-client.js';
|
|
4
4
|
import { writeJsonResponse } from '../lib/json-response.js';
|
|
5
|
+
const DEFAULT_OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
6
|
export default class Login extends Command {
|
|
6
7
|
static description = 'Authenticate with ByteRover for cloud sync features (optional for local usage)';
|
|
7
8
|
static examples = [
|
|
9
|
+
'# Browser OAuth (default)',
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'',
|
|
12
|
+
'# API key (for CI / headless environments)',
|
|
8
13
|
'<%= config.bin %> <%= command.id %> --api-key <key>',
|
|
9
14
|
'',
|
|
10
15
|
'# JSON output (for automation)',
|
|
11
|
-
'<%= config.bin %> <%= command.id %> --
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --format json',
|
|
12
17
|
];
|
|
13
18
|
static flags = {
|
|
14
19
|
'api-key': Flags.string({
|
|
15
20
|
char: 'k',
|
|
16
|
-
description: 'API key for
|
|
17
|
-
required: true,
|
|
21
|
+
description: 'API key for headless/CI login (get yours at https://app.byterover.dev/settings/keys). Omit to use the browser OAuth flow.',
|
|
18
22
|
}),
|
|
19
23
|
format: Flags.string({
|
|
20
24
|
default: 'text',
|
|
@@ -22,44 +26,117 @@ export default class Login extends Command {
|
|
|
22
26
|
options: ['text', 'json'],
|
|
23
27
|
}),
|
|
24
28
|
};
|
|
29
|
+
/** Gates the OAuth flow. DISPLAY/WAYLAND_DISPLAY deliberately not checked — unset on macOS/Windows, would false-positive. */
|
|
30
|
+
canOpenBrowser() {
|
|
31
|
+
// Either stream not a TTY means piped/scripted/CI — no interactive user to complete OAuth.
|
|
32
|
+
if (process.stdout.isTTY !== true || process.stdin.isTTY !== true)
|
|
33
|
+
return false;
|
|
34
|
+
// SSH has a TTY but can't reach the user's local browser.
|
|
35
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY)
|
|
36
|
+
return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
25
39
|
async loginWithApiKey(apiKey, options) {
|
|
26
40
|
return withDaemonRetry(async (client) => client.requestWithAck(AuthEvents.LOGIN_WITH_API_KEY, { apiKey }), options);
|
|
27
41
|
}
|
|
42
|
+
async loginWithOAuth(options) {
|
|
43
|
+
const timeoutMs = options?.oauthTimeoutMs ?? DEFAULT_OAUTH_TIMEOUT_MS;
|
|
44
|
+
return withDaemonRetry(async (client) => {
|
|
45
|
+
// Subscribe *before* initiating, so a fast callback cannot race past us.
|
|
46
|
+
let unsubscribe;
|
|
47
|
+
let timer;
|
|
48
|
+
const completion = new Promise((resolve, reject) => {
|
|
49
|
+
timer = setTimeout(() => {
|
|
50
|
+
unsubscribe?.();
|
|
51
|
+
timer = undefined;
|
|
52
|
+
reject(new Error(`Login timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
unsubscribe = client.on(AuthEvents.LOGIN_COMPLETED, (data) => {
|
|
55
|
+
if (timer) {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = undefined;
|
|
58
|
+
}
|
|
59
|
+
unsubscribe?.();
|
|
60
|
+
resolve(data);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
const startResponse = await client.requestWithAck(AuthEvents.START_LOGIN);
|
|
65
|
+
options?.onAuthUrl?.(startResponse.authUrl);
|
|
66
|
+
return await completion;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (timer) {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
timer = undefined;
|
|
72
|
+
}
|
|
73
|
+
unsubscribe?.();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}, options);
|
|
77
|
+
}
|
|
28
78
|
async run() {
|
|
29
79
|
const { flags } = await this.parse(Login);
|
|
30
80
|
const apiKey = flags['api-key'];
|
|
31
|
-
const format =
|
|
81
|
+
const format = flags.format === 'json' ? 'json' : 'text';
|
|
82
|
+
if (!apiKey && !this.canOpenBrowser()) {
|
|
83
|
+
this.emitError(format, 'Cannot open a local browser here (non-interactive shell or SSH session). Use --api-key for headless login (get yours at https://app.byterover.dev/settings/keys).');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
32
86
|
try {
|
|
33
|
-
|
|
34
|
-
this.log('Logging in...');
|
|
35
|
-
}
|
|
36
|
-
const response = await this.loginWithApiKey(apiKey);
|
|
37
|
-
if (response.success) {
|
|
38
|
-
if (format === 'json') {
|
|
39
|
-
writeJsonResponse({ command: 'login', data: { userEmail: response.userEmail }, success: true });
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
this.log(`Logged in as ${response.userEmail}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
const errorMessage = response.error ?? 'Authentication failed';
|
|
47
|
-
if (format === 'json') {
|
|
48
|
-
writeJsonResponse({ command: 'login', data: { error: errorMessage }, success: false });
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
this.log(errorMessage);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
87
|
+
await (apiKey ? this.runApiKey(apiKey, format) : this.runOAuth(format));
|
|
54
88
|
}
|
|
55
89
|
catch (error) {
|
|
56
|
-
const
|
|
90
|
+
const message = formatConnectionError(error);
|
|
57
91
|
if (format === 'json') {
|
|
58
|
-
|
|
92
|
+
this.emitError(format, message);
|
|
59
93
|
}
|
|
60
94
|
else {
|
|
61
|
-
this.log(
|
|
95
|
+
this.log(message);
|
|
62
96
|
}
|
|
63
97
|
}
|
|
64
98
|
}
|
|
99
|
+
emitError(format, message) {
|
|
100
|
+
if (format === 'json') {
|
|
101
|
+
writeJsonResponse({ command: 'login', data: { error: message }, success: false });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.log(message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
emitSuccess(format, userEmail) {
|
|
108
|
+
if (format === 'json') {
|
|
109
|
+
writeJsonResponse({ command: 'login', data: { userEmail }, success: true });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this.log(userEmail ? `Logged in as ${userEmail}` : 'Logged in successfully');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async runApiKey(apiKey, format) {
|
|
116
|
+
if (format === 'text') {
|
|
117
|
+
this.log('Logging in...');
|
|
118
|
+
}
|
|
119
|
+
const response = await this.loginWithApiKey(apiKey);
|
|
120
|
+
if (response.success) {
|
|
121
|
+
this.emitSuccess(format, response.userEmail);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.emitError(format, response.error ?? 'Authentication failed');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async runOAuth(format) {
|
|
128
|
+
const onAuthUrl = (authUrl) => {
|
|
129
|
+
if (format === 'text') {
|
|
130
|
+
this.log('Opening browser for authentication...');
|
|
131
|
+
this.log(`If the browser did not open, visit: ${authUrl}`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const result = await this.loginWithOAuth({ onAuthUrl });
|
|
135
|
+
if (result.success) {
|
|
136
|
+
this.emitSuccess(format, result.user?.email);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.emitError(format, result.error ?? 'Authentication failed');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
65
142
|
}
|
|
@@ -29,6 +29,9 @@ export default class ProviderList extends Command {
|
|
|
29
29
|
const status = p.isCurrent ? chalk.green('(current)') : p.isConnected ? chalk.yellow('(connected)') : '';
|
|
30
30
|
const authBadge = p.authMethod === 'oauth' ? chalk.cyan('[OAuth]') : p.authMethod === 'api-key' ? chalk.dim('[API Key]') : '';
|
|
31
31
|
this.log(` ${p.name} [${p.id}] ${status} ${authBadge}`.trimEnd());
|
|
32
|
+
if (p.description) {
|
|
33
|
+
this.log(` ${chalk.dim(p.description)}`);
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
catch (error) {
|
|
@@ -26,7 +26,7 @@ export const PROVIDER_REGISTRY = {
|
|
|
26
26
|
byterover: {
|
|
27
27
|
baseUrl: '',
|
|
28
28
|
category: 'popular',
|
|
29
|
-
description: 'Built-in LLM,
|
|
29
|
+
description: 'Built-in LLM, ByteRover account required. Limited free usage.',
|
|
30
30
|
headers: {},
|
|
31
31
|
id: 'byterover',
|
|
32
32
|
modelsEndpoint: '',
|
|
@@ -187,7 +187,7 @@ export const PROVIDER_REGISTRY = {
|
|
|
187
187
|
baseUrl: '',
|
|
188
188
|
category: 'other',
|
|
189
189
|
defaultModel: 'llama3',
|
|
190
|
-
description: '
|
|
190
|
+
description: 'OpenAI-compatible endpoint (Ollama, LM Studio, etc.)',
|
|
191
191
|
envVars: ['OPENAI_COMPATIBLE_API_KEY'],
|
|
192
192
|
headers: {},
|
|
193
193
|
id: 'openai-compatible',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
3
|
/**
|
|
5
4
|
* Creates an Express app that serves the web UI and config endpoint.
|
|
6
5
|
*
|
|
@@ -36,9 +35,11 @@ export function createWebUiMiddleware({ getConfig, webuiDistDir }) {
|
|
|
36
35
|
// Serve static files from dist/webui/
|
|
37
36
|
if (existsSync(webuiDistDir)) {
|
|
38
37
|
app.use(express.static(webuiDistDir));
|
|
39
|
-
// SPA fallback
|
|
38
|
+
// SPA fallback. `root` scopes send's dotfile check to the relative
|
|
39
|
+
// path; without it, a dotfile anywhere in the absolute install path
|
|
40
|
+
// (e.g. ~/.nvm/...) triggers a 404.
|
|
40
41
|
app.get('*splat', (_req, res) => {
|
|
41
|
-
res.sendFile(
|
|
42
|
+
res.sendFile('index.html', { root: webuiDistDir });
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
return app;
|