@opencomputer/blue 0.0.6 → 0.0.9
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 +86 -1
- package/dist/cli/index.js +263 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/connections/google.d.ts +35 -0
- package/dist/connections/google.js +211 -0
- package/dist/connections/google.js.map +1 -0
- package/dist/connections/local.d.ts +16 -0
- package/dist/connections/local.js +27 -0
- package/dist/connections/local.js.map +1 -0
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/agent.js +149 -0
- package/dist/core/agent.js.map +1 -1
- package/dist/core/artifact.d.ts +1 -0
- package/dist/core/artifact.js +17 -0
- package/dist/core/artifact.js.map +1 -1
- package/dist/dev/local-control-plane.d.ts +2 -0
- package/dist/dev/local-control-plane.js +66 -0
- package/dist/dev/local-control-plane.js.map +1 -1
- package/dist/landing/agent.d.ts +1 -0
- package/dist/landing/agent.js +15 -2
- package/dist/landing/agent.js.map +1 -1
- package/dist/landing/connection-callback.d.ts +5 -0
- package/dist/landing/connection-callback.js +68 -0
- package/dist/landing/connection-callback.js.map +1 -0
- package/dist/openrouter/auth.js +15 -4
- package/dist/openrouter/auth.js.map +1 -1
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.js.map +1 -1
- package/dist/runtime/local-runtime.js +3 -0
- package/dist/runtime/local-runtime.js.map +1 -1
- package/dist/sdk/client.d.ts +10 -1
- package/dist/sdk/client.js +32 -0
- package/dist/sdk/client.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -130,6 +130,79 @@ When running this repository directly, substitute
|
|
|
130
130
|
`../../node_modules/.bin/tsx ../../src/cli/index.ts` for `blue`. The installed
|
|
131
131
|
package exposes the shorter `blue` binary.
|
|
132
132
|
|
|
133
|
+
### Tools and connections
|
|
134
|
+
|
|
135
|
+
Tools are checked-in OpenCode functions under `agent/tools/`. Connections are
|
|
136
|
+
separate account authorizations: they can be replaced or revoked without
|
|
137
|
+
changing agent code, and credentials never become part of an agent artifact.
|
|
138
|
+
|
|
139
|
+
Add the built-in Gmail tools and their Google connection declaration:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
blue tools add gmail
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This creates `agent/tools/gmail.ts` with search, read, modify, and send tools,
|
|
146
|
+
plus `agent/connections/google.json` with the required scopes. Blue compiles
|
|
147
|
+
the tools into `.opencode/tools/`, so OpenCode discovers them and streams their
|
|
148
|
+
normal tool events.
|
|
149
|
+
|
|
150
|
+
Authorize a Google account for local development:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
blue connections connect gmail --local
|
|
154
|
+
blue dev
|
|
155
|
+
blue session "Find unread email that needs a reply"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Local OAuth credentials are stored in the ignored
|
|
159
|
+
`.blue/connections.json` file with mode `0600`. By default the CLI uses the
|
|
160
|
+
platform's public Google OAuth client ID; set `BLUE_GOOGLE_CLIENT_ID` and
|
|
161
|
+
optionally `BLUE_GOOGLE_CLIENT_SECRET` to use your own desktop OAuth client
|
|
162
|
+
without contacting the hosted provider configuration. For a deployed agent:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
blue deploy
|
|
166
|
+
blue connections connect gmail --remote
|
|
167
|
+
blue connections list
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Remote credentials are AES-GCM encrypted in the account Durable Object. The
|
|
171
|
+
MicroVM receives only its existing session-scoped runtime token. Custom tools
|
|
172
|
+
use that token to call Blue's provider proxy, which restricts requests to the
|
|
173
|
+
provider's supported API origins and refreshes OAuth tokens in the control
|
|
174
|
+
plane.
|
|
175
|
+
|
|
176
|
+
#### Gmail summarizer example
|
|
177
|
+
|
|
178
|
+
The `examples/gmail-summarizer` agent is a safe starting point for inbox
|
|
179
|
+
triage. In Google Cloud, enable the Gmail API, configure the OAuth consent
|
|
180
|
+
screen with your account as a test user, and create an OAuth client with the
|
|
181
|
+
Desktop app type. Then run:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
cd examples/gmail-summarizer
|
|
185
|
+
export BLUE_GOOGLE_CLIENT_ID="<client-id>.apps.googleusercontent.com"
|
|
186
|
+
export BLUE_GOOGLE_CLIENT_SECRET="<client-secret>"
|
|
187
|
+
blue connections connect gmail --local
|
|
188
|
+
blue session \
|
|
189
|
+
"Summarize email from the last 24 hours. Group it into urgent, needs reply, informational, and low priority. Do not modify or send anything."
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The browser consent redirects to a temporary loopback listener owned by the
|
|
193
|
+
CLI. The resulting local credential remains in `.blue/connections.json`, which
|
|
194
|
+
is ignored by Git. Run `blue connections list --local` to inspect it and
|
|
195
|
+
`blue connections disconnect <connection-id> --local` to revoke it from Blue's
|
|
196
|
+
local state.
|
|
197
|
+
|
|
198
|
+
If Google reports `ACCESS_TOKEN_SCOPE_INSUFFICIENT`, open the OAuth app's
|
|
199
|
+
Google Auth Platform **Data Access** settings and add
|
|
200
|
+
`https://www.googleapis.com/auth/gmail.modify`. Confirm that the Gmail API is
|
|
201
|
+
enabled and that the connecting account is an allowed test user, remove the
|
|
202
|
+
old local connection, and reconnect while approving Gmail access. Blue
|
|
203
|
+
validates the granted scopes during connection and will not store an
|
|
204
|
+
identity-only token.
|
|
205
|
+
|
|
133
206
|
### Local Slack
|
|
134
207
|
|
|
135
208
|
Install and authenticate the
|
|
@@ -220,7 +293,10 @@ The hosted Worker requires:
|
|
|
220
293
|
`BLUE_WORKOS_JWKS_URL`;
|
|
221
294
|
- `OPENROUTER_API_KEY` as a Worker secret;
|
|
222
295
|
- `BLUE_CHANNEL_ENCRYPTION_KEY` as a Worker secret containing a base64-encoded
|
|
223
|
-
32-byte random key;
|
|
296
|
+
32-byte random key; this currently protects channel and tool connection
|
|
297
|
+
credentials;
|
|
298
|
+
- `BLUE_GOOGLE_CLIENT_ID` and optionally `BLUE_GOOGLE_CLIENT_SECRET` for a
|
|
299
|
+
Google OAuth desktop client;
|
|
224
300
|
- `BLUE_RUNTIME_IMAGE_ARN` and `BLUE_RUNTIME_IMAGE_VERSION`;
|
|
225
301
|
- a `DEPLOYMENTS` service binding that returns signed platform artifact
|
|
226
302
|
uploads;
|
|
@@ -241,6 +317,10 @@ npx wrangler secret put BLUE_RUNTIME_IMAGE_VERSION \
|
|
|
241
317
|
--config wrangler.production.jsonc
|
|
242
318
|
npx wrangler secret put BLUE_CHANNEL_ENCRYPTION_KEY \
|
|
243
319
|
--config wrangler.production.jsonc
|
|
320
|
+
npx wrangler secret put BLUE_GOOGLE_CLIENT_ID \
|
|
321
|
+
--config wrangler.production.jsonc
|
|
322
|
+
npx wrangler secret put BLUE_GOOGLE_CLIENT_SECRET \
|
|
323
|
+
--config wrangler.production.jsonc
|
|
244
324
|
```
|
|
245
325
|
|
|
246
326
|
Generate the channel encryption secret with `openssl rand -base64 32`. Every
|
|
@@ -248,6 +328,11 @@ remote Slack installation receives a unique callback credential, stored by
|
|
|
248
328
|
Blue only as a hash. Slack CLI configures that callback automatically from the
|
|
249
329
|
agent manifest.
|
|
250
330
|
|
|
331
|
+
Google local authorization uses a desktop loopback redirect with PKCE.
|
|
332
|
+
Gmail's `gmail.modify` scope is restricted, so a public production OAuth app
|
|
333
|
+
must complete Google's verification requirements. OAuth access and refresh
|
|
334
|
+
tokens are never returned by the Blue API after connection.
|
|
335
|
+
|
|
251
336
|
Configure the platform Worker's AWS secrets plus
|
|
252
337
|
`BLUE_AWS_REGION`, `BLUE_ARTIFACT_BUCKET`, `BLUE_EXECUTION_ROLE_ARN`,
|
|
253
338
|
`BLUE_NETWORK_CONNECTOR_ARN`, `BLUE_S3_FILESYSTEM_ID`, and
|
package/dist/cli/index.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
4
|
import { existsSync, mkdirSync } from "node:fs";
|
|
4
5
|
import { rm } from "node:fs/promises";
|
|
6
|
+
import { createServer } from "node:http";
|
|
5
7
|
import { basename, dirname, resolve } from "node:path";
|
|
6
8
|
import process from "node:process";
|
|
7
9
|
import { createInterface } from "node:readline/promises";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { DEFAULT_BLUE_API_URL, accessTokenFor, login, logout, readCredentials, } from "../auth/cli-auth.js";
|
|
11
|
+
import { DEFAULT_BLUE_API_URL, accessTokenFor, login, logout, openBrowser, readCredentials, } from "../auth/cli-auth.js";
|
|
12
|
+
import { assertGoogleScopes, exchangeGoogleCode, GMAIL_SCOPES, googleAuthorizationUrl, resolveGoogleIdentity, } from "../connections/google.js";
|
|
13
|
+
import { readLocalConnections, writeLocalConnections, } from "../connections/local.js";
|
|
10
14
|
import { captureLocalSlackState, localSlackStatePath, readLocalSlackState, readRemoteSlackState, remoteSlackStatePath, renderSlackProjectManifest, writeRemoteSlackState, } from "../channels/slack-project.js";
|
|
11
|
-
import { addSlackChannel, findAgentRoot, initializeAgent, prepareAgent, } from "../core/agent.js";
|
|
15
|
+
import { addSlackChannel, addGmailTools, findAgentRoot, initializeAgent, prepareAgent, } from "../core/agent.js";
|
|
12
16
|
import { buildAgentArtifact } from "../core/artifact.js";
|
|
13
17
|
import { findDevState, requestDevManager, startDevManager, } from "../dev/manager.js";
|
|
14
18
|
import { startLocalControlPlane } from "../dev/local-control-plane.js";
|
|
15
19
|
import { authenticateOpenRouter, readOpenRouterCredential, } from "../openrouter/auth.js";
|
|
16
20
|
import { agentLandingPath } from "../landing/agent.js";
|
|
21
|
+
import { renderConnectionCallback } from "../landing/connection-callback.js";
|
|
17
22
|
import { BlueClient } from "../sdk/client.js";
|
|
18
23
|
const DEFAULT_EDGE = DEFAULT_BLUE_API_URL;
|
|
19
24
|
function argument(name) {
|
|
@@ -514,6 +519,7 @@ async function deployAgent(edge) {
|
|
|
514
519
|
agentId: built.agentId,
|
|
515
520
|
alias,
|
|
516
521
|
channels: built.channels,
|
|
522
|
+
connections: built.connections,
|
|
517
523
|
artifact: upload.artifact,
|
|
518
524
|
});
|
|
519
525
|
const identity = await client.whoami();
|
|
@@ -523,6 +529,7 @@ async function deployAgent(edge) {
|
|
|
523
529
|
`alias: ${deployment.alias}\n` +
|
|
524
530
|
`url: ${agentUrl}\n` +
|
|
525
531
|
`channels: ${deployment.channels.join(", ") || "none"}\n` +
|
|
532
|
+
`connections: ${deployment.connections.join(", ") || "none"}\n` +
|
|
526
533
|
`artifact: s3://${deployment.artifact.bucket}/${deployment.artifact.key}\n` +
|
|
527
534
|
`ready in: ${String(built.elapsedMs)}ms\n`);
|
|
528
535
|
}
|
|
@@ -558,6 +565,210 @@ async function agentRootForSlack() {
|
|
|
558
565
|
}
|
|
559
566
|
return root;
|
|
560
567
|
}
|
|
568
|
+
async function currentAgentRoot() {
|
|
569
|
+
const root = await findAgentRoot(argument("agent-dir") ?? process.cwd());
|
|
570
|
+
if (!root) {
|
|
571
|
+
throw new Error("No Blue agent repository was found");
|
|
572
|
+
}
|
|
573
|
+
return root;
|
|
574
|
+
}
|
|
575
|
+
async function googleOAuthCallback(begin) {
|
|
576
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
577
|
+
const codeChallenge = createHash("sha256")
|
|
578
|
+
.update(codeVerifier)
|
|
579
|
+
.digest("base64url");
|
|
580
|
+
let expectedState = "";
|
|
581
|
+
let resolveCallback;
|
|
582
|
+
let rejectCallback;
|
|
583
|
+
const callback = new Promise((resolvePromise, reject) => {
|
|
584
|
+
resolveCallback = resolvePromise;
|
|
585
|
+
rejectCallback = reject;
|
|
586
|
+
});
|
|
587
|
+
const server = createServer((request, response) => {
|
|
588
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
589
|
+
if (url.pathname !== "/oauth/callback") {
|
|
590
|
+
response.writeHead(404).end();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const error = url.searchParams.get("error");
|
|
594
|
+
const code = url.searchParams.get("code");
|
|
595
|
+
const state = url.searchParams.get("state");
|
|
596
|
+
response.writeHead(error || !code || state !== expectedState ? 400 : 200, {
|
|
597
|
+
"content-type": "text/html; charset=utf-8",
|
|
598
|
+
});
|
|
599
|
+
response.end(error || !code || state !== expectedState
|
|
600
|
+
? renderConnectionCallback({
|
|
601
|
+
provider: "Google",
|
|
602
|
+
status: "error",
|
|
603
|
+
})
|
|
604
|
+
: renderConnectionCallback({
|
|
605
|
+
provider: "Google",
|
|
606
|
+
status: "success",
|
|
607
|
+
}));
|
|
608
|
+
if (error) {
|
|
609
|
+
rejectCallback(new Error(`Google authorization failed: ${error}`));
|
|
610
|
+
}
|
|
611
|
+
else if (!code || !state || state !== expectedState) {
|
|
612
|
+
rejectCallback(new Error("Google returned an invalid OAuth callback"));
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
resolveCallback({ code, state });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
await new Promise((resolvePromise, reject) => {
|
|
619
|
+
server.once("error", reject);
|
|
620
|
+
server.listen(0, "127.0.0.1", () => {
|
|
621
|
+
server.off("error", reject);
|
|
622
|
+
resolvePromise();
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
const address = server.address();
|
|
626
|
+
if (!address || typeof address === "string") {
|
|
627
|
+
server.close();
|
|
628
|
+
throw new Error("Could not create the local OAuth callback");
|
|
629
|
+
}
|
|
630
|
+
const redirectUri = `http://127.0.0.1:${String(address.port)}/oauth/callback`;
|
|
631
|
+
try {
|
|
632
|
+
const started = await begin(redirectUri, codeChallenge);
|
|
633
|
+
expectedState = started.state;
|
|
634
|
+
process.stdout.write(`Open ${started.authorizationUrl}\n`);
|
|
635
|
+
openBrowser(started.authorizationUrl);
|
|
636
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
637
|
+
setTimeout(() => reject(new Error("Google authorization timed out")), 5 * 60_000).unref();
|
|
638
|
+
});
|
|
639
|
+
const result = await Promise.race([callback, timeout]);
|
|
640
|
+
return { ...result, codeVerifier, redirectUri };
|
|
641
|
+
}
|
|
642
|
+
finally {
|
|
643
|
+
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function connectGoogleLocal(edge) {
|
|
647
|
+
const root = await currentAgentRoot();
|
|
648
|
+
const clientId = process.env.BLUE_GOOGLE_CLIENT_ID ??
|
|
649
|
+
(await (await clientFor(edge)).connectionProvider("google")).clientId;
|
|
650
|
+
const config = {
|
|
651
|
+
clientId,
|
|
652
|
+
clientSecret: process.env.BLUE_GOOGLE_CLIENT_SECRET,
|
|
653
|
+
};
|
|
654
|
+
const state = randomBytes(24).toString("base64url");
|
|
655
|
+
const callback = await googleOAuthCallback(async (redirectUri, codeChallenge) => ({
|
|
656
|
+
state,
|
|
657
|
+
authorizationUrl: googleAuthorizationUrl({
|
|
658
|
+
config,
|
|
659
|
+
redirectUri,
|
|
660
|
+
scopes: [...GMAIL_SCOPES],
|
|
661
|
+
state,
|
|
662
|
+
codeChallenge,
|
|
663
|
+
}),
|
|
664
|
+
}));
|
|
665
|
+
const credential = await exchangeGoogleCode({
|
|
666
|
+
config,
|
|
667
|
+
code: callback.code,
|
|
668
|
+
codeVerifier: callback.codeVerifier,
|
|
669
|
+
redirectUri: callback.redirectUri,
|
|
670
|
+
});
|
|
671
|
+
assertGoogleScopes(credential, [...GMAIL_SCOPES]);
|
|
672
|
+
const identity = await resolveGoogleIdentity(credential);
|
|
673
|
+
const path = resolve(root, ".blue", "connections.json");
|
|
674
|
+
const connections = await readLocalConnections(path);
|
|
675
|
+
const label = argument("label") ?? "default";
|
|
676
|
+
const timestamp = new Date().toISOString();
|
|
677
|
+
const next = {
|
|
678
|
+
id: randomUUID(),
|
|
679
|
+
provider: "google",
|
|
680
|
+
label,
|
|
681
|
+
externalAccountId: identity.id,
|
|
682
|
+
displayName: identity.email,
|
|
683
|
+
scopes: credential.scopes.length
|
|
684
|
+
? credential.scopes
|
|
685
|
+
: [...GMAIL_SCOPES],
|
|
686
|
+
clientId,
|
|
687
|
+
clientSecret: config.clientSecret,
|
|
688
|
+
credential,
|
|
689
|
+
createdAt: timestamp,
|
|
690
|
+
updatedAt: timestamp,
|
|
691
|
+
};
|
|
692
|
+
await writeLocalConnections(path, [
|
|
693
|
+
next,
|
|
694
|
+
...connections.filter((connection) => connection.provider !== "google" || connection.label !== label),
|
|
695
|
+
]);
|
|
696
|
+
process.stdout.write(`connected: local\nprovider: google\naccount: ${identity.email}\nlabel: ${label}\n`);
|
|
697
|
+
}
|
|
698
|
+
async function connectGoogleRemote(edge) {
|
|
699
|
+
await currentAgentRoot();
|
|
700
|
+
const agentId = argument("agent") ?? (await inferDeployedAgent());
|
|
701
|
+
if (!agentId) {
|
|
702
|
+
throw new Error("Run this inside an agent repository or pass --agent");
|
|
703
|
+
}
|
|
704
|
+
const client = await clientFor(edge);
|
|
705
|
+
let connectionId;
|
|
706
|
+
try {
|
|
707
|
+
const callback = await googleOAuthCallback(async (redirectUri, codeChallenge) => {
|
|
708
|
+
const started = await client.startConnection({
|
|
709
|
+
provider: "google",
|
|
710
|
+
agentId,
|
|
711
|
+
label: argument("label") ?? "default",
|
|
712
|
+
scopes: [...GMAIL_SCOPES],
|
|
713
|
+
redirectUri,
|
|
714
|
+
codeChallenge,
|
|
715
|
+
});
|
|
716
|
+
connectionId = started.connection.id;
|
|
717
|
+
return {
|
|
718
|
+
authorizationUrl: started.authorizationUrl,
|
|
719
|
+
state: started.state,
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
if (!connectionId)
|
|
723
|
+
throw new Error("Connection was not reserved");
|
|
724
|
+
const connection = await client.completeConnection(connectionId, callback);
|
|
725
|
+
process.stdout.write(`connected: remote\nconnection: ${connection.id}\n` +
|
|
726
|
+
`provider: ${connection.provider}\n` +
|
|
727
|
+
`account: ${connection.displayName ?? connection.externalAccountId ?? "unknown"}\n` +
|
|
728
|
+
`agent: ${connection.agentId}@${connection.alias}\n` +
|
|
729
|
+
`label: ${connection.label}\n`);
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (connectionId) {
|
|
733
|
+
await client.disconnectConnection(connectionId).catch(() => undefined);
|
|
734
|
+
}
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function listConnections(edge) {
|
|
739
|
+
let found = false;
|
|
740
|
+
const root = await findAgentRoot(process.cwd());
|
|
741
|
+
if (!hasFlag("remote") && root) {
|
|
742
|
+
const local = await readLocalConnections(resolve(root, ".blue", "connections.json"));
|
|
743
|
+
for (const connection of local) {
|
|
744
|
+
found = true;
|
|
745
|
+
process.stdout.write(`${connection.id} ${connection.provider.padEnd(10)} local ` +
|
|
746
|
+
`${connection.label.padEnd(12)} ${connection.displayName}\n`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!hasFlag("local")) {
|
|
750
|
+
const remote = await (await clientFor(edge)).listConnections();
|
|
751
|
+
for (const connection of remote) {
|
|
752
|
+
found = true;
|
|
753
|
+
process.stdout.write(`${connection.id} ${connection.provider.padEnd(10)} ` +
|
|
754
|
+
`${connection.status.padEnd(11)} ${connection.label.padEnd(12)} ` +
|
|
755
|
+
`${connection.displayName ?? connection.externalAccountId ?? "pending"}\n`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (!found)
|
|
759
|
+
process.stdout.write("No connections.\n");
|
|
760
|
+
}
|
|
761
|
+
async function disconnectLocalConnection(connectionId) {
|
|
762
|
+
const root = await currentAgentRoot();
|
|
763
|
+
const path = resolve(root, ".blue", "connections.json");
|
|
764
|
+
const connections = await readLocalConnections(path);
|
|
765
|
+
const next = connections.filter((connection) => connection.id !== connectionId);
|
|
766
|
+
if (next.length === connections.length) {
|
|
767
|
+
throw new Error("Local connection not found");
|
|
768
|
+
}
|
|
769
|
+
await writeLocalConnections(path, next);
|
|
770
|
+
process.stdout.write(`disconnected: ${connectionId}\n`);
|
|
771
|
+
}
|
|
561
772
|
async function connectLocalSlack() {
|
|
562
773
|
const root = await agentRootForSlack();
|
|
563
774
|
await runSlackCli(root, ["app", "install", "--environment", "local"], "local");
|
|
@@ -736,7 +947,9 @@ async function dev() {
|
|
|
736
947
|
port,
|
|
737
948
|
agentId: prepared.name,
|
|
738
949
|
slackEnabled: builtInSlackChannel(prepared.root),
|
|
950
|
+
googleEnabled: existsSync(resolve(prepared.root, "agent", "connections", "google.json")),
|
|
739
951
|
statePath: resolve(prepared.root, ".blue", "dev", "control-plane.json"),
|
|
952
|
+
connectionStatePath: resolve(prepared.root, ".blue", "connections.json"),
|
|
740
953
|
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
741
954
|
model: process.env.BLUE_MODEL,
|
|
742
955
|
});
|
|
@@ -809,6 +1022,12 @@ Usage:
|
|
|
809
1022
|
blue demo [prompt] [--agent-dir path] [--harness mock|opencode]
|
|
810
1023
|
blue run <prompt> [--agent <agent>@<alias>]
|
|
811
1024
|
blue auth openrouter
|
|
1025
|
+
blue tools add gmail [--agent-dir path]
|
|
1026
|
+
blue connections connect gmail --local
|
|
1027
|
+
blue connections connect gmail --remote [--agent agent@alias]
|
|
1028
|
+
blue connections list [--local|--remote]
|
|
1029
|
+
blue connections disconnect <connection-id> --local
|
|
1030
|
+
blue connections disconnect <connection-id> --remote
|
|
812
1031
|
blue channels add slack [--agent-dir path]
|
|
813
1032
|
blue channels connect slack --local
|
|
814
1033
|
blue channels connect slack --remote [--agent agent@alias]
|
|
@@ -910,6 +1129,48 @@ async function main() {
|
|
|
910
1129
|
process.stdout.write("OpenRouter connected locally.\n");
|
|
911
1130
|
return;
|
|
912
1131
|
}
|
|
1132
|
+
if (command === "tools") {
|
|
1133
|
+
const [action, toolName] = positional(3);
|
|
1134
|
+
if (action === "add" && toolName === "gmail") {
|
|
1135
|
+
process.stdout.write(`created: ${await addGmailTools(argument("agent-dir") ?? process.cwd())}\n`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
throw new Error("Usage: blue tools add gmail");
|
|
1139
|
+
}
|
|
1140
|
+
if (command === "connections") {
|
|
1141
|
+
const [action, provider, connectionId] = positional(3);
|
|
1142
|
+
if (action === "connect" && (provider === "gmail" || provider === "google")) {
|
|
1143
|
+
if (hasFlag("local") && hasFlag("remote")) {
|
|
1144
|
+
throw new Error("Choose either --local or --remote");
|
|
1145
|
+
}
|
|
1146
|
+
if (!hasFlag("local") && !hasFlag("remote")) {
|
|
1147
|
+
throw new Error("Pass --local or --remote");
|
|
1148
|
+
}
|
|
1149
|
+
if (hasFlag("local")) {
|
|
1150
|
+
await connectGoogleLocal(hostedEdge());
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
await connectGoogleRemote(hostedEdge());
|
|
1154
|
+
}
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (action === "list") {
|
|
1158
|
+
await listConnections(hostedEdge());
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (action === "disconnect" && provider) {
|
|
1162
|
+
if (hasFlag("local")) {
|
|
1163
|
+
await disconnectLocalConnection(provider);
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
const connection = await (await clientFor(hostedEdge())).disconnectConnection(provider);
|
|
1167
|
+
process.stdout.write(`disconnected: ${connection.id}\n`);
|
|
1168
|
+
}
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
void connectionId;
|
|
1172
|
+
throw new Error("Unsupported connections command");
|
|
1173
|
+
}
|
|
913
1174
|
if (command === "channels") {
|
|
914
1175
|
const [action, channel, connectionId] = positional(3);
|
|
915
1176
|
if (action === "add" && channel === "slack") {
|