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 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 with your ByteRover account via `/login` (TUI) or
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. Use `brv providers connect` to set up a provider and `brv providers switch` to change the active one.
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) <!-- TODO: ENG-1575 --> on GitHub
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
  [![Contributors](https://contrib.rocks/image?repo=campfirein/byterover-cli&max=40&columns=10)](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 %> --api-key <key> --format json',
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 authentication (get yours at https://app.byterover.dev/settings/keys)',
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 = (flags.format ?? 'text');
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
- if (format === 'text') {
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 errorMessage = error instanceof Error ? error.message : 'Login failed';
90
+ const message = formatConnectionError(error);
57
91
  if (format === 'json') {
58
- writeJsonResponse({ command: 'login', data: { error: errorMessage }, success: false });
92
+ this.emitError(format, message);
59
93
  }
60
94
  else {
61
- this.log(formatConnectionError(error));
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, logged-in ByteRover account required. Limited free usage.',
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: 'Connect any OpenAI-compatible endpoint (Ollama, LM Studio, etc.)',
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: serve index.html for unmatched routes
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(join(webuiDistDir, 'index.html'));
42
+ res.sendFile('index.html', { root: webuiDistDir });
42
43
  });
43
44
  }
44
45
  return app;