@obtoai/agent-bridge 0.1.0-beta.2 → 0.1.0-beta.21

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
@@ -1,20 +1,28 @@
1
1
  # @obtoai/agent-bridge
2
2
 
3
- A local daemon that lets a coding agent — [Claude Code](https://claude.ai/code) or [OpenAI Codex](https://developers.openai.com/codex) — running on your machine be driven from the [OBTO Agent Bridge](https://obto.co) web UI, even when you're away from the keyboard.
3
+ A local daemon that lets coding agents — [Claude Code](https://claude.ai/code), [OpenAI Codex](https://developers.openai.com/codex), or [opencode](https://opencode.ai) — running on your machine be driven from the [OBTO Agent Bridge](https://obto.co) web UI, even when you're away from the keyboard.
4
4
 
5
- You post a message on a thread from your phone or laptop. The daemon (running on your machine, no port forwarding required) receives it over a long-lived HTTPS stream, spawns or resumes an agent session in your project directory, and the response posts back to the bridge thread within seconds.
5
+ You post a message on a thread from your phone or laptop. The daemon (running on your machine, no port forwarding required) receives it over a long-lived HTTPS stream, spawns or resumes a session for the agent that thread is bound to, and the response posts back to the bridge thread within seconds.
6
+
7
+ **Three commands, you're driving Claude/Codex/opencode on your phone:**
8
+
9
+ ```bash
10
+ npm install -g @obtoai/agent-bridge
11
+ obto-bridge init # creates a free account inline — no card, no invite
12
+ obto-bridge start # daemon connects, you're live
13
+ ```
6
14
 
7
15
  ## Status
8
16
 
9
- **Closed beta.** Need an invite? Email **support@obto.co** with your name and a short note about what you'd use it for. We'll provision an account and mail you credentials.
17
+ **Public beta.** Self-serve, no card. `obto-bridge init` creates a free account inline — no waiting on an invite, no support email loop. A Pro tier with longer Hindsight memory retention is coming; until then everyone gets the same daemon features on the free tier.
10
18
 
11
19
  ## What you'll need
12
20
 
13
21
  - macOS, Linux, or Windows, **Node.js 18.17+**
14
- - One coding agent installed, with your own auth:
15
- - **Claude** — Claude Code / the Claude Agent SDK, billed to your Anthropic account; or
16
- - **Codex** — the `codex` CLI (`npm i -g @openai/codex`), signed in to your OpenAI/ChatGPT account
17
- - An invite from `support@obto.co` (gives you an `accountId`, browser username/password, and an API token)
22
+ - At least one coding agent installed on the machine (the daemon drives whichever ones it finds, with your own auth):
23
+ - **Claude** — Claude Code / the Claude Agent SDK, billed to your Anthropic account.
24
+ - **Codex** — the `codex` CLI (`npm i -g @openai/codex`), signed in to your OpenAI/ChatGPT account.
25
+ - **opencode** `npm i -g opencode-ai` (the `opencode` CLI; the daemon bundles the `@opencode-ai/sdk`). Auth is your own provider key (Anthropic by default; override with env vars below).
18
26
 
19
27
  ## Install
20
28
 
@@ -34,18 +42,31 @@ npx @obtoai/agent-bridge <command>
34
42
  obto-bridge init
35
43
  ```
36
44
 
37
- Walks you through a few questions: your account ID, API token, an agent name (to distinguish multiple machines on one account), which coding agent to drive (`claude` or `codex`), the project directory to work in, and whether to relay tool-permission requests via the bridge. (The server URL is a built-in default; advanced / self-hosted users can override it with the `BRIDGE_BASE_URL` env var.)
45
+ `init` asks for one thing your email. Username is derived from the email's local part (`divyansh.verma@gmail.com` → `divyansh-verma`); password is auto-generated as a strong 12-char string and **shown once** in stdout for you to save. It then creates the account inline via `POST /api/bridge/register`, saves the returned API token to `~/.obto-bridge/config.json` (mode 0600), and asks for an agent name (so multiple machines on the same account don't collide), the project directory the daemon should work in, a *fallback* agent (`claude` / `codex` / `opencode` used only for legacy events without an explicit agent), and whether to relay tool-permission requests via the bridge. Sign in at `https://agent-bridge.obto.co/api/view` as `@username` with the generated password to start a thread.
46
+
47
+ Overrides:
38
48
 
39
- Config lands at `~/.obto-bridge/config.json` (mode 0600). Safe to commit your account ID; **never commit the `apiToken`**.
49
+ - `obto-bridge init --username <name>` pick your own username instead of the derived one.
50
+ - `obto-bridge init --password <pwd>` — set your own password instead of auto-generating.
51
+ - `obto-bridge init --token obto_xxxxxxxx --account acc_xxxxxxxx` — skip registration entirely (paste-in for existing users or scripted/headless setups).
40
52
 
41
- ### claude vs codex
53
+ The API token is shown to you exactly once at registration time. **Save it.** If you lose it, rotate it from your account settings — it is not stored in readable form server-side. Safe to commit your `accountId`; **never commit the `apiToken`**. (Server URL is a built-in default; advanced / self-hosted users can override with the `BRIDGE_BASE_URL` env var.)
42
54
 
43
- Both drive real coding work on your machine; they differ in how they report back:
55
+ ### Agents (claude / codex / opencode)
44
56
 
45
- - **claude** the fuller integration. Posts status updates, questions, and results as it works (via an in-process MCP tool), and supports the human-in-the-loop tool-permission relay.
46
- - **codex** — runs the task and delivers one final answer per turn. No mid-task updates and no per-tool relay (the Codex SDK exposes neither); it runs unattended inside a sandbox (`workspace-write` by default, override with `BRIDGE_CODEX_SANDBOX`).
57
+ v1.1 makes the daemon **agent-agnostic per event**: at startup it detects which of `claude`, `codex`, and `opencode` are installed on the machine, advertises that to the bridge, and routes each incoming reply to the right driver based on what the thread is bound to in the UI. You can switch a thread's agent live from the thread header; each engine keeps its own session for that thread, so flipping claude→codex→claude resumes each side's context.
47
58
 
48
- One daemon drives one agent. To run both, use two daemons on two accounts.
59
+ How the three differ in how they report back:
60
+
61
+ - **claude** — the fullest integration. Posts status updates, mid-task questions, and final results as it works (via an in-process MCP tool), and supports the human-in-the-loop tool-permission relay.
62
+ - **codex** — runs the turn and delivers one final answer per turn. No mid-task updates and no per-tool relay (the Codex SDK exposes neither). Runs unattended inside a sandbox (`workspace-write` by default, override with `BRIDGE_CODEX_SANDBOX`).
63
+ - **opencode** — same capture-model shape as codex: one final answer per turn, no mid-task chatter. Defaults to provider `anthropic` and model `claude-sonnet-4-5`; override with `BRIDGE_OPENCODE_PROVIDER` and `BRIDGE_OPENCODE_MODEL`.
64
+
65
+ Picking a model is done in the bridge UI's **+ New thread** dialog and the thread-header switcher — not in the daemon config.
66
+
67
+ ### Multi-daemon (running across more than one machine)
68
+
69
+ You can run the same account's daemon on more than one machine (e.g. a Mac and a Windows box). Each daemon advertises its `agentId` (machine name) + capabilities on connect; threads are atomically **first-touch claimed** by whichever daemon gets the event first, and every other daemon skips the event cleanly. No duplicate replies, no special configuration — just install + start the daemon on each machine.
49
70
 
50
71
  ## Run
51
72
 
@@ -56,14 +77,16 @@ obto-bridge start
56
77
  You'll see two log lines and then the daemon waits silently:
57
78
 
58
79
  ```
59
- {"msg":"starting daemon","data":{"accountId":"acc_...","agentId":"my-mac",...}}
80
+ {"msg":"starting daemon","data":{"accountId":"acc_...","agentId":"my-mac","capabilities":["claude","codex"],...}}
60
81
  {"msg":"sse stream connected","data":{"status":200}}
61
82
  ```
62
83
 
84
+ `capabilities` is the list of agents this daemon will accept — the bridge UI offers exactly the union across your connected machines.
85
+
63
86
  Now open the bridge UI in any browser, log in with the browser credentials from your invite, and either:
64
87
 
65
- - Reply on an existing thread — daemon resumes the session bound to that thread
66
- - Start a new thread via the **+ New thread** button — daemon spawns a fresh session in your project directory
88
+ - Reply on an existing thread — daemon resumes the session bound to that thread (and to whichever agent the thread currently uses).
89
+ - Start a new thread via the **+ New thread** button — pick Claude, Codex, or Opencode; the daemon spawns a fresh session in your project directory.
67
90
 
68
91
  Within ~5–10 seconds you should see the agent's reply appear back on the thread.
69
92
 
@@ -72,40 +95,41 @@ Within ~5–10 seconds you should see the agent's reply appear back on the threa
72
95
  | Command | What it does |
73
96
  |---|---|
74
97
  | `obto-bridge whoami` | Verify your token works + show your account info |
75
- | `obto-bridge status` | List active thread→session bindings |
98
+ | `obto-bridge status` | List bindings per (thread, agent) — one row per engine that's ever driven a thread |
76
99
  | `obto-bridge logout` | Wipe `~/.obto-bridge/config.json` |
77
100
 
78
101
  ## How it actually works
79
102
 
80
103
  ```
81
- Your phone OBTO server Your machine
82
- ───────── ─────────── ────────────
104
+ Your phone OBTO server Your machine(s)
105
+ ───────── ─────────── ───────────────
83
106
  [reply form] ──► /api/reply ─► Mongo (durable)
84
- └─► RabbitMQ (publish bridge.<acct>.reply.<thread>)
107
+ └─► RabbitMQ (publish bridge.<acct>.reply.<thread>,
108
+ payload carries agent + agentId)
85
109
  ◄── /api/bridge/stream (SSE, Bearer auth)
86
- └─► daemon process
87
- └─► Claude Agent SDK
88
- └─► session JSONL in
89
- ~/.claude/projects/...
90
- /api/message ◄──── bridge_post (in-process MCP tool from daemon)
110
+ └─► daemon (dispatches per payload.agent)
111
+ ├─► Claude Agent SDK → ~/.claude/projects/...
112
+ ├─► @openai/codex-sdk → ~/.codex/sessions/...
113
+ └─► @opencode-ai/sdk → opencode server
114
+ /api/message ◄──── bridge_post (in-process MCP tool, Claude only)
91
115
  [poll: /api/messages] ◄──── (4s loop)
92
116
  ```
93
117
 
94
118
  Key bits:
95
119
 
96
120
  - The daemon **never** holds RabbitMQ credentials; broker access stays server-side. Per-account routing key isolation enforced by `BridgeAuth`.
97
- - The daemon's spawned Claude session uses an **in-process MCP server** (`mcp__bridge__bridge_post`) — not the platform's hosted MCP, so the daemon's tools don't depend on a long-lived OBTO MCP proxy session.
98
- - Each bridge **thread** binds to its own agent **session ID** at first message. Subsequent messages on the same thread resume the same session, so the agent keeps full context. Your interactive sessions are unaffected — they live in separate session stores.
121
+ - For the **claude** driver, the spawned Claude session uses an **in-process MCP server** (`mcp__bridge__bridge_post`) — not the platform's hosted MCP, so the daemon's tools don't depend on a long-lived OBTO MCP proxy session. For **codex** and **opencode**, the SDKs can't auto-approve a write tool when run unattended, so the daemon captures the final response and posts it to the thread on the agent's behalf.
122
+ - Each bridge **thread** binds to its own session ID **per agent**. Subsequent messages on the same thread + same agent resume the same engine-specific session, so the agent keeps full context. Switching the thread's agent in the UI starts (or resumes) the other engine's session — each side's state stays intact. Your interactive sessions are unaffected — they live in separate session stores.
99
123
  - Per-thread serialization means rapid bursts on the same thread are handled in order, never racing the same session.
100
- - With **codex**, there is no in-process MCP tool the Codex SDK can't auto-approve a write tool when run unattended, so the daemon captures Codex's final response and posts it to the thread on the agent's behalf.
124
+ - Multi-daemon races are killed by atomic first-touch claim against the thread record on the bridge.
101
125
 
102
126
  ## Agent costs
103
127
 
104
- The daemon runs your chosen agent on your machine with **your** credentials — Anthropic for `claude` (whatever Claude Code uses: `ANTHROPIC_API_KEY` or your Claude.ai session), or your OpenAI/ChatGPT account for `codex`. Every bridge-driven turn is a normal API call billed to you. We don't proxy.
128
+ The daemon runs your chosen agent on your machine with **your** credentials — Anthropic for `claude` (whatever Claude Code uses: `ANTHROPIC_API_KEY` or your Claude.ai session); your OpenAI/ChatGPT account for `codex`; whichever provider you've configured `opencode` to call (Anthropic by default for this daemon). Every bridge-driven turn is a normal API call billed to you. We don't proxy.
105
129
 
106
130
  ## Data handling
107
131
 
108
- **Your model traffic never touches us.** The daemon runs on your machine and calls Anthropic or OpenAI with *your own* credentials. Your prompts, your code, and the model's responses pass directly between your machine and the model provider, under your own API account and its terms. OBTO does not proxy, route, or see that traffic.
132
+ **Your model traffic never touches us.** The daemon runs on your machine and calls Anthropic, OpenAI, or whichever provider opencode is configured for, with *your own* credentials. Your prompts, your code, and the model's responses pass directly between your machine and the model provider, under your own API account and its terms. OBTO does not proxy, route, or see that traffic.
109
133
 
110
134
  **What the bridge stores.** For threads to work, the messages you and the agent post are saved in OBTO's database — that's what makes a thread durable and readable from your phone. Threads are strictly scoped to your account; one tenant can never see another's. Your daemon's API token is stored server-side only as a SHA-256 hash; the plaintext token never leaves your local config file.
111
135
 
@@ -14,7 +14,8 @@ const usage = () => {
14
14
  console.error('Usage: obto-bridge <command>');
15
15
  console.error('');
16
16
  console.error('Commands:');
17
- console.error(' init One-time setup: paste credentials, choose agent name + project dir.');
17
+ console.error(' init Create a free account (or paste an existing token via --token/--account)');
18
+ console.error(' and write ~/.obto-bridge/config.json.');
18
19
  console.error(' start Run the daemon (foreground).');
19
20
  console.error(' status Print active thread/session bindings.');
20
21
  console.error(' whoami Verify config and show your account info from the server.');
@@ -22,10 +23,12 @@ const usage = () => {
22
23
  console.error(' logout Wipe local credentials at ~/.obto-bridge/config.json.');
23
24
  console.error('');
24
25
  console.error('Flags:');
25
- console.error(' --version, -v Print the installed package version.');
26
- console.error(' --help, -h Show this help.');
27
- console.error('');
28
- console.error('Get an invite: support@obto.co');
26
+ console.error(' --version, -v Print the installed package version.');
27
+ console.error(' --help, -h Show this help.');
28
+ console.error(' --username <name> (init only) Override the username derived from email.');
29
+ console.error(' --password <pwd> (init only) Set your password instead of auto-generating one.');
30
+ console.error(' --token <obto_…> (init only) Skip self-serve register, use this token.');
31
+ console.error(' --account <acc_…> (init only) Pair with --token for paste-in mode.');
29
32
  };
30
33
 
31
34
  const cmd = process.argv[2];
package/cli/init.js CHANGED
@@ -1,13 +1,23 @@
1
1
  'use strict';
2
2
 
3
- // `obto-bridge init` — interactive setup wizard. Prompts for credentials,
4
- // writes ~/.obto-bridge/config.json with mode 0600, then validates the token
5
- // against the server via GET /api/bridge/whoami so the user knows immediately
6
- // if anything is wrong.
3
+ // `obto-bridge init` — interactive setup wizard.
4
+ //
5
+ // Default (>=0.1.0-beta.7): self-serve registration. Email is the only
6
+ // required input; username is derived from the email's local part, and a
7
+ // strong password is auto-generated and shown once. Posts to
8
+ // /api/bridge/register, saves the returned API token to
9
+ // ~/.obto-bridge/config.json (mode 0600).
10
+ //
11
+ // Overrides:
12
+ // --username <name> Use this instead of the derived username.
13
+ // --password <pwd> Use this instead of an auto-generated password.
14
+ // --token <obto_…> Skip registration entirely; paste an existing token.
15
+ // --account <acc_…> Pair with --token for paste-in mode.
7
16
 
8
17
  const fs = require('fs');
9
18
  const path = require('path');
10
19
  const os = require('os');
20
+ const crypto = require('crypto');
11
21
  const readline = require('readline');
12
22
 
13
23
  const CONFIG_DIR = path.join(os.homedir(), '.obto-bridge');
@@ -19,6 +29,17 @@ const DEFAULTS = {
19
29
  relayPermissions: true,
20
30
  };
21
31
 
32
+ // argv after `obto-bridge init`.
33
+ const argv = process.argv.slice(3);
34
+ const flagValue = (name) => {
35
+ const i = argv.indexOf(name);
36
+ return i !== -1 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : null;
37
+ };
38
+ const cliToken = flagValue('--token');
39
+ const cliAccount = flagValue('--account');
40
+ const cliUsername = flagValue('--username');
41
+ const cliPassword = flagValue('--password');
42
+
22
43
  const loadExisting = () => {
23
44
  try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); }
24
45
  catch (_) { return {}; }
@@ -35,6 +56,45 @@ const ask = (rl, prompt, def) =>
35
56
  });
36
57
  });
37
58
 
59
+ // Derive a basicAuthUser-shaped username from email's local part. Mirrors
60
+ // the server-side derivation in registerSubmit so the username we sign in
61
+ // with matches what we display to the user inline.
62
+ const deriveUsername = (email) => {
63
+ const local = String(email || '').split('@')[0].toLowerCase();
64
+ let u = local.replace(/[^a-z0-9_-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
65
+ if (u.length < 3) u = 'user-' + crypto.randomBytes(3).toString('hex');
66
+ return u;
67
+ };
68
+
69
+ // 12-char password, no ambiguous chars (0/O, 1/l/I), grouped as 4-4-4
70
+ // for readability and easy copy-paste. Backed by crypto.randomBytes.
71
+ const generatePassword = () => {
72
+ const chars = 'abcdefghjkmnpqrstuvwxyz23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
73
+ const bytes = crypto.randomBytes(12);
74
+ let out = '';
75
+ for (let i = 0; i < 12; i++) out += chars[bytes[i] % chars.length];
76
+ return out.slice(0, 4) + '-' + out.slice(4, 8) + '-' + out.slice(8, 12);
77
+ };
78
+
79
+ const registerSelfServe = async ({ baseUrl, originHost, email, username, password }) => {
80
+ const url = baseUrl.replace(/\/$/, '') + '/api/bridge/register';
81
+ const res = await fetch(url, {
82
+ method: 'POST',
83
+ headers: {
84
+ Accept: 'application/json',
85
+ 'Content-Type': 'application/x-www-form-urlencoded',
86
+ 'OBTO-ORIGIN-HOST': originHost,
87
+ },
88
+ body: new URLSearchParams({ email, username, password }).toString(),
89
+ cache: 'no-store',
90
+ redirect: 'manual',
91
+ });
92
+ const text = await res.text();
93
+ let data;
94
+ try { data = JSON.parse(text); } catch (_) { data = { _rawBody: text }; }
95
+ return { status: res.status, ok: res.ok, data };
96
+ };
97
+
38
98
  const validateAgainstServer = async (cfg) => {
39
99
  const url = cfg.baseUrl.replace(/\/$/, '') + '/api/bridge/whoami';
40
100
  const res = await fetch(url, {
@@ -55,25 +115,92 @@ const validateAgainstServer = async (cfg) => {
55
115
  const main = async () => {
56
116
  console.log('OBTO Agent Bridge — setup');
57
117
  console.log('-------------------------');
58
- console.log('Need credentials? Email support@obto.co for an invite.');
59
118
  console.log('Config will be written to: ' + CONFIG_PATH);
60
119
  console.log('');
61
120
 
62
121
  const existing = loadExisting();
63
122
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
64
123
 
65
- // Server base URL and OBTO-ORIGIN-HOST are constants of the platform, not
66
- // per-user config — every daemon talks to the same bridge app. They are no
67
- // longer prompted. Advanced / self-hosted users can still override them via
68
- // the BRIDGE_BASE_URL / BRIDGE_ORIGIN_HOST env vars or by editing config.json.
69
124
  const baseUrl = process.env.BRIDGE_BASE_URL || existing.baseUrl || DEFAULTS.baseUrl;
70
125
  const originHost = process.env.BRIDGE_ORIGIN_HOST || existing.originHost || DEFAULTS.originHost;
71
- const accountId = await ask(rl, 'Account ID (acc_…)', existing.accountId || '');
72
- const apiToken = await ask(rl, 'API token (obto_…)', existing.apiToken || '');
73
- const agentId = await ask(rl, 'Agent name (e.g. my-mac)', existing.agentId || os.hostname().split('.')[0] || 'unnamed-agent');
126
+
127
+ let accountId = existing.accountId || '';
128
+ let apiToken = existing.apiToken || '';
129
+
130
+ // Pick the credential path. Precedence:
131
+ // 1. --token + --account flags (machine paste-in, scripted).
132
+ // 2. Existing config from a prior init.
133
+ // 3. Self-serve registration via /api/bridge/register (default).
134
+ const haveCliPaste = !!(cliToken && cliAccount);
135
+ const haveExisting = !!(existing.accountId && existing.apiToken);
136
+
137
+ let registeredUser = '';
138
+ let registeredPassword = '';
139
+
140
+ if (haveCliPaste) {
141
+ apiToken = cliToken;
142
+ accountId = cliAccount;
143
+ console.log('Using credentials from --token / --account flags.');
144
+ console.log('');
145
+ } else if (haveExisting) {
146
+ console.log('Existing config found:');
147
+ console.log(' Account: ' + existing.accountId);
148
+ console.log(' Token: ' + (existing.apiToken || '').slice(0, 10) + '…');
149
+ console.log('Reusing those credentials. To re-register from scratch, remove ' + CONFIG_PATH + ' first.');
150
+ console.log('');
151
+ } else {
152
+ console.log('Create a free account (no card needed):');
153
+ const email = await ask(rl, ' Email', '');
154
+ if (!email || email.indexOf('@') === -1) {
155
+ console.error('error: a valid email is required.');
156
+ rl.close();
157
+ process.exit(1);
158
+ }
159
+ const username = cliUsername || deriveUsername(email);
160
+ const password = cliPassword || generatePassword();
161
+ console.log('');
162
+ console.log(' Username: ' + username + (cliUsername ? '' : ' (derived from email)'));
163
+ if (!cliPassword) {
164
+ console.log(' Password: ' + password + ' (auto-generated)');
165
+ console.log('');
166
+ console.log(' ⚠ SAVE THIS PASSWORD — you will need it to sign in to the web UI.');
167
+ console.log(' It is shown once here and never again. Reset later from your account page.');
168
+ }
169
+ console.log('');
170
+ console.log('Creating account at ' + baseUrl + ' ...');
171
+ let r;
172
+ try {
173
+ r = await registerSelfServe({ baseUrl, originHost, email, username, password });
174
+ } catch (e) {
175
+ console.error('error: registration request failed: ' + (e && e.message ? e.message : e));
176
+ console.error(' (network problem? you can also paste an existing token via:');
177
+ console.error(' `obto-bridge init --token <obto_…> --account <acc_…>`)');
178
+ rl.close();
179
+ process.exit(1);
180
+ }
181
+ if (!r.ok || !r.data || !r.data.ok) {
182
+ const msg = (r.data && (r.data.error || r.data._rawBody)) || ('HTTP ' + r.status);
183
+ console.error('error: registration rejected: ' + msg);
184
+ console.error(' (username taken? rerun with `obto-bridge init --username <different>`)');
185
+ rl.close();
186
+ process.exit(1);
187
+ }
188
+ accountId = r.data.accountId;
189
+ apiToken = r.data.apiToken;
190
+ registeredUser = r.data.basicAuthUser || username;
191
+ registeredPassword = password;
192
+ console.log(' ✓ Free account created.');
193
+ console.log(' Account: ' + accountId);
194
+ console.log(' Username: ' + registeredUser + ' (sign in with this exact string — no @)');
195
+ console.log(' Plan: ' + (r.data.plan || 'free'));
196
+ console.log('');
197
+ }
198
+
199
+ const agentId = await ask(rl, 'Agent name (e.g. my-mac)', existing.agentId || os.hostname().split('.')[0] || 'unnamed-agent');
74
200
  const projectDir = await ask(rl, 'Project working dir', existing.projectDir || process.cwd());
75
- const agentAns = await ask(rl, 'Coding agent — claude or codex', existing.agent || 'claude');
76
- const agent = String(agentAns).trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
201
+ const agentAns = await ask(rl, 'Coding agent fallback — claude / codex / opencode', existing.agent || 'claude');
202
+ const agentLow = String(agentAns).trim().toLowerCase();
203
+ const agent = ['claude', 'codex', 'opencode'].indexOf(agentLow) !== -1 ? agentLow : 'claude';
77
204
  const relayAns = await ask(rl, 'Relay permission requests via bridge? (y/n)', existing.relayPermissions !== false ? 'y' : 'n');
78
205
  const relayPermissions = String(relayAns).toLowerCase().startsWith('y');
79
206
 
@@ -131,14 +258,19 @@ const main = async () => {
131
258
 
132
259
  if (result.ok && result.parsed && result.parsed.account) {
133
260
  const a = result.parsed.account;
134
- console.log(' ✓ Authenticated as @' + a.basicAuthUser + ' (' + a.accountId + ', status: ' + a.status + ')');
261
+ console.log(' ✓ Authenticated as ' + a.basicAuthUser + ' (' + a.accountId + ', status: ' + a.status + ')');
135
262
  console.log('');
136
- console.log('Next: obto-bridge start');
263
+ // The OBTO platform's root URL bounces unauthenticated users to /login.bto;
264
+ // /api/view is the canonical bridge entry point that serves either the
265
+ // sign-in form (unauthenticated) or the threads UI (authenticated).
266
+ const signInUrl = baseUrl.replace(/\/$/, '') + '/api/view';
267
+ console.log('Sign in at ' + signInUrl + ' as ' + a.basicAuthUser + (registeredPassword ? ' (password above)' : '') + '.');
268
+ console.log('Run: obto-bridge start');
137
269
  return;
138
270
  }
139
271
 
140
272
  if (result.status === 401) {
141
- console.error(' ✗ Server rejected the API token (HTTP 401). Double-check that you pasted the full token from your invite email.');
273
+ console.error(' ✗ Server rejected the API token (HTTP 401). Re-run `obto-bridge init` to reset.');
142
274
  process.exit(2);
143
275
  }
144
276
  if (result.status === 403) {
package/cli/status.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- // `obto-bridge status` — read local state.json and print thread→session bindings.
4
- // Read-only; useful for "did the daemon ever drive this thread?"
3
+ // `obto-bridge status` — read local state.json and print thread→session
4
+ // bindings. Read-only. v1.1: a thread keeps one session per agent.
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
@@ -29,20 +29,33 @@ if (threads.length === 0) {
29
29
  const fmtAge = (iso) => {
30
30
  if (!iso) return '?';
31
31
  const ms = Date.now() - new Date(iso).getTime();
32
- if (ms < 60_000) return Math.floor(ms / 1000) + 's ago';
33
- if (ms < 3_600_000) return Math.floor(ms / 60_000) + 'm ago';
34
- if (ms < 86_400_000) return Math.floor(ms / 3_600_000) + 'h ago';
35
- return Math.floor(ms / 86_400_000) + 'd ago';
32
+ if (ms < 60000) return Math.floor(ms / 1000) + 's ago';
33
+ if (ms < 3600000) return Math.floor(ms / 60000) + 'm ago';
34
+ if (ms < 86400000) return Math.floor(ms / 3600000) + 'h ago';
35
+ return Math.floor(ms / 86400000) + 'd ago';
36
36
  };
37
37
 
38
- console.log('Thread Session ID Last drive');
39
- console.log('-'.repeat(95));
38
+ console.log('Thread Agent Session ID Last drive');
39
+ console.log('-'.repeat(100));
40
40
  threads.forEach((t) => {
41
- const b = bindings[t];
42
- const sid = (b.sessionId || '').slice(0, 36);
43
- console.log(t.padEnd(36) + ' ' + sid.padEnd(38) + ' ' + fmtAge(b.lastDriveAt));
41
+ const b = bindings[t] || {};
42
+ // v1.1 per-agent sessions; tolerate a stray un-migrated v1 flat binding.
43
+ const sessions = b.sessions && typeof b.sessions === 'object'
44
+ ? b.sessions
45
+ : (b.sessionId ? { claude: b } : {});
46
+ const agents = Object.keys(sessions);
47
+ if (agents.length === 0) {
48
+ console.log(t.padEnd(36) + ' (no session yet)');
49
+ return;
50
+ }
51
+ agents.forEach((agent, i) => {
52
+ const s = sessions[agent] || {};
53
+ const sid = String(s.sessionId || '').slice(0, 36);
54
+ console.log(
55
+ (i === 0 ? t : '').padEnd(36) + ' ' +
56
+ agent.padEnd(7) + ' ' +
57
+ sid.padEnd(38) + ' ' +
58
+ fmtAge(s.lastDriveAt),
59
+ );
60
+ });
44
61
  });
45
- console.log('');
46
- console.log('JSONLs at: ' + (bindings[threads[0]] && bindings[threads[0]].projectDir
47
- ? '~/.claude/projects/' + bindings[threads[0]].projectDir.replace(/\//g, '-') + '/'
48
- : 'unknown'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obtoai/agent-bridge",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.21",
4
4
  "description": "Local consumer for the OBTO Agent Bridge. Receives bridge events over SSE and drives a coding agent (Claude Code or OpenAI Codex) on your machine.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "OBTO Inc.",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@anthropic-ai/claude-agent-sdk": "^0.2.126",
35
- "@openai/codex-sdk": "^0.130.0"
35
+ "@openai/codex-sdk": "^0.130.0",
36
+ "@opencode-ai/sdk": "^1.16.1"
36
37
  }
37
38
  }
@@ -78,10 +78,26 @@ const getMessages = (threadId, sinceCursor) => {
78
78
  const postAgentActivity = (threadId, state) =>
79
79
  postJson('/api/bridge/agent-activity', { threadId, state });
80
80
 
81
+ // Phase 2b — atomic first-touch claim. Called by the daemon when it sees a
82
+ // reply event for a thread whose `agentId` is null (unrouted). The bridge's
83
+ // claimThread does a conditional Mongo update — only one daemon wins.
84
+ // Returns { ok, won, winner }: `won` is the only thing the caller acts on.
85
+ const claimThread = (threadId, agentId) =>
86
+ postJson('/api/bridge/thread/claim', { threadId, agentId });
87
+
88
+ // Phase 6.1 — push external (non-bridge) sessions discovered by the local
89
+ // filesystem scanner so the bridge UI can render them alongside bridge-owned
90
+ // threads. Fire-and-forget; the bridge tolerates partial payloads and
91
+ // re-observes on the next 30s tick.
92
+ const postExternalSync = (agentId, sessions) =>
93
+ postJson('/api/bridge/external/sync', { agentId, sessions });
94
+
81
95
  module.exports = {
82
96
  getCfg,
83
97
  buildHeaders,
84
98
  postMessage,
85
99
  getMessages,
86
100
  postAgentActivity,
101
+ claimThread,
102
+ postExternalSync,
87
103
  };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // Phase 2b — what this machine can drive. The Claude Agent SDK is a hard
4
+ // dependency of the daemon (declared in package.json), so `claude` is always
5
+ // available. `codex` and `opencode` need their respective CLIs on PATH —
6
+ // we probe with `which` (POSIX) or `where` (Windows).
7
+ //
8
+ // Sent to the bridge as `?capabilities=claude,codex,...` on SSE connect; the
9
+ // bridge records them in `agent_bridge_daemons` so the UI picker can offer
10
+ // only what's actually installable across the account's machines.
11
+
12
+ const { spawnSync } = require('child_process');
13
+
14
+ const onPath = (cmd) => {
15
+ try {
16
+ const tool = process.platform === 'win32' ? 'where' : 'which';
17
+ const r = spawnSync(tool, [cmd], { stdio: 'ignore' });
18
+ return r.status === 0;
19
+ } catch (_) {
20
+ return false;
21
+ }
22
+ };
23
+
24
+ const detect = () => {
25
+ const out = ['claude']; // bundled SDK; always advertised
26
+ if (onPath('codex')) out.push('codex');
27
+ if (onPath('opencode')) out.push('opencode');
28
+ return out;
29
+ };
30
+
31
+ module.exports = { detect, onPath };