@opencomputer/blue 0.0.5 → 0.0.7
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 +56 -1
- package/dist/cli/index.js +286 -16
- package/dist/cli/index.js.map +1 -1
- package/dist/connections/google.d.ts +34 -0
- package/dist/connections/google.js +192 -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/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,49 @@ 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
|
+
|
|
133
176
|
### Local Slack
|
|
134
177
|
|
|
135
178
|
Install and authenticate the
|
|
@@ -220,7 +263,10 @@ The hosted Worker requires:
|
|
|
220
263
|
`BLUE_WORKOS_JWKS_URL`;
|
|
221
264
|
- `OPENROUTER_API_KEY` as a Worker secret;
|
|
222
265
|
- `BLUE_CHANNEL_ENCRYPTION_KEY` as a Worker secret containing a base64-encoded
|
|
223
|
-
32-byte random key;
|
|
266
|
+
32-byte random key; this currently protects channel and tool connection
|
|
267
|
+
credentials;
|
|
268
|
+
- `BLUE_GOOGLE_CLIENT_ID` and optionally `BLUE_GOOGLE_CLIENT_SECRET` for a
|
|
269
|
+
Google OAuth desktop client;
|
|
224
270
|
- `BLUE_RUNTIME_IMAGE_ARN` and `BLUE_RUNTIME_IMAGE_VERSION`;
|
|
225
271
|
- a `DEPLOYMENTS` service binding that returns signed platform artifact
|
|
226
272
|
uploads;
|
|
@@ -241,6 +287,10 @@ npx wrangler secret put BLUE_RUNTIME_IMAGE_VERSION \
|
|
|
241
287
|
--config wrangler.production.jsonc
|
|
242
288
|
npx wrangler secret put BLUE_CHANNEL_ENCRYPTION_KEY \
|
|
243
289
|
--config wrangler.production.jsonc
|
|
290
|
+
npx wrangler secret put BLUE_GOOGLE_CLIENT_ID \
|
|
291
|
+
--config wrangler.production.jsonc
|
|
292
|
+
npx wrangler secret put BLUE_GOOGLE_CLIENT_SECRET \
|
|
293
|
+
--config wrangler.production.jsonc
|
|
244
294
|
```
|
|
245
295
|
|
|
246
296
|
Generate the channel encryption secret with `openssl rand -base64 32`. Every
|
|
@@ -248,6 +298,11 @@ remote Slack installation receives a unique callback credential, stored by
|
|
|
248
298
|
Blue only as a hash. Slack CLI configures that callback automatically from the
|
|
249
299
|
agent manifest.
|
|
250
300
|
|
|
301
|
+
Google local authorization uses a desktop loopback redirect with PKCE.
|
|
302
|
+
Gmail's `gmail.modify` scope is restricted, so a public production OAuth app
|
|
303
|
+
must complete Google's verification requirements. OAuth access and refresh
|
|
304
|
+
tokens are never returned by the Blue API after connection.
|
|
305
|
+
|
|
251
306
|
Configure the platform Worker's AWS secrets plus
|
|
252
307
|
`BLUE_AWS_REGION`, `BLUE_ARTIFACT_BUCKET`, `BLUE_EXECUTION_ROLE_ARN`,
|
|
253
308
|
`BLUE_NETWORK_CONNECTOR_ARN`, `BLUE_S3_FILESYSTEM_ID`, and
|
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
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 { 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";
|
|
@@ -514,6 +518,7 @@ async function deployAgent(edge) {
|
|
|
514
518
|
agentId: built.agentId,
|
|
515
519
|
alias,
|
|
516
520
|
channels: built.channels,
|
|
521
|
+
connections: built.connections,
|
|
517
522
|
artifact: upload.artifact,
|
|
518
523
|
});
|
|
519
524
|
const identity = await client.whoami();
|
|
@@ -523,6 +528,7 @@ async function deployAgent(edge) {
|
|
|
523
528
|
`alias: ${deployment.alias}\n` +
|
|
524
529
|
`url: ${agentUrl}\n` +
|
|
525
530
|
`channels: ${deployment.channels.join(", ") || "none"}\n` +
|
|
531
|
+
`connections: ${deployment.connections.join(", ") || "none"}\n` +
|
|
526
532
|
`artifact: s3://${deployment.artifact.bucket}/${deployment.artifact.key}\n` +
|
|
527
533
|
`ready in: ${String(built.elapsedMs)}ms\n`);
|
|
528
534
|
}
|
|
@@ -558,6 +564,203 @@ async function agentRootForSlack() {
|
|
|
558
564
|
}
|
|
559
565
|
return root;
|
|
560
566
|
}
|
|
567
|
+
async function currentAgentRoot() {
|
|
568
|
+
const root = await findAgentRoot(argument("agent-dir") ?? process.cwd());
|
|
569
|
+
if (!root) {
|
|
570
|
+
throw new Error("No Blue agent repository was found");
|
|
571
|
+
}
|
|
572
|
+
return root;
|
|
573
|
+
}
|
|
574
|
+
async function googleOAuthCallback(begin) {
|
|
575
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
576
|
+
const codeChallenge = createHash("sha256")
|
|
577
|
+
.update(codeVerifier)
|
|
578
|
+
.digest("base64url");
|
|
579
|
+
let expectedState = "";
|
|
580
|
+
let resolveCallback;
|
|
581
|
+
let rejectCallback;
|
|
582
|
+
const callback = new Promise((resolvePromise, reject) => {
|
|
583
|
+
resolveCallback = resolvePromise;
|
|
584
|
+
rejectCallback = reject;
|
|
585
|
+
});
|
|
586
|
+
const server = createServer((request, response) => {
|
|
587
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
588
|
+
if (url.pathname !== "/oauth/callback") {
|
|
589
|
+
response.writeHead(404).end();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const error = url.searchParams.get("error");
|
|
593
|
+
const code = url.searchParams.get("code");
|
|
594
|
+
const state = url.searchParams.get("state");
|
|
595
|
+
response.writeHead(error || !code || state !== expectedState ? 400 : 200, {
|
|
596
|
+
"content-type": "text/html; charset=utf-8",
|
|
597
|
+
});
|
|
598
|
+
response.end(error || !code || state !== expectedState
|
|
599
|
+
? "<h1>Connection failed</h1><p>Return to the Blue CLI.</p>"
|
|
600
|
+
: "<h1>Connected</h1><p>You can close this tab.</p>");
|
|
601
|
+
if (error) {
|
|
602
|
+
rejectCallback(new Error(`Google authorization failed: ${error}`));
|
|
603
|
+
}
|
|
604
|
+
else if (!code || !state || state !== expectedState) {
|
|
605
|
+
rejectCallback(new Error("Google returned an invalid OAuth callback"));
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
resolveCallback({ code, state });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
await new Promise((resolvePromise, reject) => {
|
|
612
|
+
server.once("error", reject);
|
|
613
|
+
server.listen(0, "127.0.0.1", () => {
|
|
614
|
+
server.off("error", reject);
|
|
615
|
+
resolvePromise();
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
const address = server.address();
|
|
619
|
+
if (!address || typeof address === "string") {
|
|
620
|
+
server.close();
|
|
621
|
+
throw new Error("Could not create the local OAuth callback");
|
|
622
|
+
}
|
|
623
|
+
const redirectUri = `http://127.0.0.1:${String(address.port)}/oauth/callback`;
|
|
624
|
+
try {
|
|
625
|
+
const started = await begin(redirectUri, codeChallenge);
|
|
626
|
+
expectedState = started.state;
|
|
627
|
+
process.stdout.write(`Open ${started.authorizationUrl}\n`);
|
|
628
|
+
openBrowser(started.authorizationUrl);
|
|
629
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
630
|
+
setTimeout(() => reject(new Error("Google authorization timed out")), 5 * 60_000).unref();
|
|
631
|
+
});
|
|
632
|
+
const result = await Promise.race([callback, timeout]);
|
|
633
|
+
return { ...result, codeVerifier, redirectUri };
|
|
634
|
+
}
|
|
635
|
+
finally {
|
|
636
|
+
await new Promise((resolvePromise) => server.close(() => resolvePromise()));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function connectGoogleLocal(edge) {
|
|
640
|
+
const root = await currentAgentRoot();
|
|
641
|
+
const clientId = process.env.BLUE_GOOGLE_CLIENT_ID ??
|
|
642
|
+
(await (await clientFor(edge)).connectionProvider("google")).clientId;
|
|
643
|
+
const config = {
|
|
644
|
+
clientId,
|
|
645
|
+
clientSecret: process.env.BLUE_GOOGLE_CLIENT_SECRET,
|
|
646
|
+
};
|
|
647
|
+
const state = randomBytes(24).toString("base64url");
|
|
648
|
+
const callback = await googleOAuthCallback(async (redirectUri, codeChallenge) => ({
|
|
649
|
+
state,
|
|
650
|
+
authorizationUrl: googleAuthorizationUrl({
|
|
651
|
+
config,
|
|
652
|
+
redirectUri,
|
|
653
|
+
scopes: [...GMAIL_SCOPES],
|
|
654
|
+
state,
|
|
655
|
+
codeChallenge,
|
|
656
|
+
}),
|
|
657
|
+
}));
|
|
658
|
+
const credential = await exchangeGoogleCode({
|
|
659
|
+
config,
|
|
660
|
+
code: callback.code,
|
|
661
|
+
codeVerifier: callback.codeVerifier,
|
|
662
|
+
redirectUri: callback.redirectUri,
|
|
663
|
+
});
|
|
664
|
+
const identity = await resolveGoogleIdentity(credential);
|
|
665
|
+
const path = resolve(root, ".blue", "connections.json");
|
|
666
|
+
const connections = await readLocalConnections(path);
|
|
667
|
+
const label = argument("label") ?? "default";
|
|
668
|
+
const timestamp = new Date().toISOString();
|
|
669
|
+
const next = {
|
|
670
|
+
id: randomUUID(),
|
|
671
|
+
provider: "google",
|
|
672
|
+
label,
|
|
673
|
+
externalAccountId: identity.id,
|
|
674
|
+
displayName: identity.email,
|
|
675
|
+
scopes: credential.scopes.length
|
|
676
|
+
? credential.scopes
|
|
677
|
+
: [...GMAIL_SCOPES],
|
|
678
|
+
clientId,
|
|
679
|
+
clientSecret: config.clientSecret,
|
|
680
|
+
credential,
|
|
681
|
+
createdAt: timestamp,
|
|
682
|
+
updatedAt: timestamp,
|
|
683
|
+
};
|
|
684
|
+
await writeLocalConnections(path, [
|
|
685
|
+
next,
|
|
686
|
+
...connections.filter((connection) => connection.provider !== "google" || connection.label !== label),
|
|
687
|
+
]);
|
|
688
|
+
process.stdout.write(`connected: local\nprovider: google\naccount: ${identity.email}\nlabel: ${label}\n`);
|
|
689
|
+
}
|
|
690
|
+
async function connectGoogleRemote(edge) {
|
|
691
|
+
await currentAgentRoot();
|
|
692
|
+
const agentId = argument("agent") ?? (await inferDeployedAgent());
|
|
693
|
+
if (!agentId) {
|
|
694
|
+
throw new Error("Run this inside an agent repository or pass --agent");
|
|
695
|
+
}
|
|
696
|
+
const client = await clientFor(edge);
|
|
697
|
+
let connectionId;
|
|
698
|
+
try {
|
|
699
|
+
const callback = await googleOAuthCallback(async (redirectUri, codeChallenge) => {
|
|
700
|
+
const started = await client.startConnection({
|
|
701
|
+
provider: "google",
|
|
702
|
+
agentId,
|
|
703
|
+
label: argument("label") ?? "default",
|
|
704
|
+
scopes: [...GMAIL_SCOPES],
|
|
705
|
+
redirectUri,
|
|
706
|
+
codeChallenge,
|
|
707
|
+
});
|
|
708
|
+
connectionId = started.connection.id;
|
|
709
|
+
return {
|
|
710
|
+
authorizationUrl: started.authorizationUrl,
|
|
711
|
+
state: started.state,
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
if (!connectionId)
|
|
715
|
+
throw new Error("Connection was not reserved");
|
|
716
|
+
const connection = await client.completeConnection(connectionId, callback);
|
|
717
|
+
process.stdout.write(`connected: remote\nconnection: ${connection.id}\n` +
|
|
718
|
+
`provider: ${connection.provider}\n` +
|
|
719
|
+
`account: ${connection.displayName ?? connection.externalAccountId ?? "unknown"}\n` +
|
|
720
|
+
`agent: ${connection.agentId}@${connection.alias}\n` +
|
|
721
|
+
`label: ${connection.label}\n`);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (connectionId) {
|
|
725
|
+
await client.disconnectConnection(connectionId).catch(() => undefined);
|
|
726
|
+
}
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function listConnections(edge) {
|
|
731
|
+
let found = false;
|
|
732
|
+
const root = await findAgentRoot(process.cwd());
|
|
733
|
+
if (!hasFlag("remote") && root) {
|
|
734
|
+
const local = await readLocalConnections(resolve(root, ".blue", "connections.json"));
|
|
735
|
+
for (const connection of local) {
|
|
736
|
+
found = true;
|
|
737
|
+
process.stdout.write(`${connection.id} ${connection.provider.padEnd(10)} local ` +
|
|
738
|
+
`${connection.label.padEnd(12)} ${connection.displayName}\n`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (!hasFlag("local")) {
|
|
742
|
+
const remote = await (await clientFor(edge)).listConnections();
|
|
743
|
+
for (const connection of remote) {
|
|
744
|
+
found = true;
|
|
745
|
+
process.stdout.write(`${connection.id} ${connection.provider.padEnd(10)} ` +
|
|
746
|
+
`${connection.status.padEnd(11)} ${connection.label.padEnd(12)} ` +
|
|
747
|
+
`${connection.displayName ?? connection.externalAccountId ?? "pending"}\n`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (!found)
|
|
751
|
+
process.stdout.write("No connections.\n");
|
|
752
|
+
}
|
|
753
|
+
async function disconnectLocalConnection(connectionId) {
|
|
754
|
+
const root = await currentAgentRoot();
|
|
755
|
+
const path = resolve(root, ".blue", "connections.json");
|
|
756
|
+
const connections = await readLocalConnections(path);
|
|
757
|
+
const next = connections.filter((connection) => connection.id !== connectionId);
|
|
758
|
+
if (next.length === connections.length) {
|
|
759
|
+
throw new Error("Local connection not found");
|
|
760
|
+
}
|
|
761
|
+
await writeLocalConnections(path, next);
|
|
762
|
+
process.stdout.write(`disconnected: ${connectionId}\n`);
|
|
763
|
+
}
|
|
561
764
|
async function connectLocalSlack() {
|
|
562
765
|
const root = await agentRootForSlack();
|
|
563
766
|
await runSlackCli(root, ["app", "install", "--environment", "local"], "local");
|
|
@@ -573,27 +776,44 @@ async function connectRemoteSlack(edge) {
|
|
|
573
776
|
if (!agentId) {
|
|
574
777
|
throw new Error("Run this inside an agent repository or pass --agent");
|
|
575
778
|
}
|
|
576
|
-
const
|
|
779
|
+
const client = await clientFor(edge);
|
|
780
|
+
const previous = await readRemoteSlackState(root);
|
|
781
|
+
const result = await client.createSlackConnection({
|
|
577
782
|
agentId,
|
|
578
783
|
});
|
|
579
|
-
|
|
784
|
+
const nextState = {
|
|
580
785
|
version: 1,
|
|
581
786
|
connectionId: result.connection.id,
|
|
582
787
|
agentId: `${result.connection.agentId}@${result.connection.alias}`,
|
|
583
788
|
webhookUrl: result.webhookUrl,
|
|
584
789
|
apiUrl: edge,
|
|
585
|
-
}
|
|
586
|
-
await
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
`
|
|
790
|
+
};
|
|
791
|
+
await writeRemoteSlackState(root, nextState);
|
|
792
|
+
try {
|
|
793
|
+
await runSlackCli(root, ["deploy", "--app", "deployed"], "remote");
|
|
794
|
+
const connected = (await client.listChannelConnections()).find((connection) => connection.id === result.connection.id);
|
|
795
|
+
if (!connected || connected.status !== "connected") {
|
|
796
|
+
throw new Error("Slack CLI finished without completing the connection");
|
|
797
|
+
}
|
|
798
|
+
if (previous && previous.connectionId !== connected.id) {
|
|
799
|
+
await client.disconnectSlack(previous.connectionId).catch(() => undefined);
|
|
800
|
+
}
|
|
801
|
+
process.stdout.write(`connected: remote\n` +
|
|
802
|
+
`connection: ${connected.id}\n` +
|
|
803
|
+
`agent: ${connected.agentId}@${connected.alias}\n` +
|
|
804
|
+
`app: ${connected.appId ?? "unknown"}\n` +
|
|
805
|
+
`workspace: ${connected.teamName ?? connected.teamId ?? "unknown"}\n`);
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
await client.disconnectSlack(result.connection.id).catch(() => undefined);
|
|
809
|
+
if (previous) {
|
|
810
|
+
await writeRemoteSlackState(root, previous);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
await rm(remoteSlackStatePath(root), { force: true });
|
|
814
|
+
}
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
597
817
|
}
|
|
598
818
|
async function listRemoteChannels(edge) {
|
|
599
819
|
let found = false;
|
|
@@ -719,7 +939,9 @@ async function dev() {
|
|
|
719
939
|
port,
|
|
720
940
|
agentId: prepared.name,
|
|
721
941
|
slackEnabled: builtInSlackChannel(prepared.root),
|
|
942
|
+
googleEnabled: existsSync(resolve(prepared.root, "agent", "connections", "google.json")),
|
|
722
943
|
statePath: resolve(prepared.root, ".blue", "dev", "control-plane.json"),
|
|
944
|
+
connectionStatePath: resolve(prepared.root, ".blue", "connections.json"),
|
|
723
945
|
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
724
946
|
model: process.env.BLUE_MODEL,
|
|
725
947
|
});
|
|
@@ -792,6 +1014,12 @@ Usage:
|
|
|
792
1014
|
blue demo [prompt] [--agent-dir path] [--harness mock|opencode]
|
|
793
1015
|
blue run <prompt> [--agent <agent>@<alias>]
|
|
794
1016
|
blue auth openrouter
|
|
1017
|
+
blue tools add gmail [--agent-dir path]
|
|
1018
|
+
blue connections connect gmail --local
|
|
1019
|
+
blue connections connect gmail --remote [--agent agent@alias]
|
|
1020
|
+
blue connections list [--local|--remote]
|
|
1021
|
+
blue connections disconnect <connection-id> --local
|
|
1022
|
+
blue connections disconnect <connection-id> --remote
|
|
795
1023
|
blue channels add slack [--agent-dir path]
|
|
796
1024
|
blue channels connect slack --local
|
|
797
1025
|
blue channels connect slack --remote [--agent agent@alias]
|
|
@@ -893,6 +1121,48 @@ async function main() {
|
|
|
893
1121
|
process.stdout.write("OpenRouter connected locally.\n");
|
|
894
1122
|
return;
|
|
895
1123
|
}
|
|
1124
|
+
if (command === "tools") {
|
|
1125
|
+
const [action, toolName] = positional(3);
|
|
1126
|
+
if (action === "add" && toolName === "gmail") {
|
|
1127
|
+
process.stdout.write(`created: ${await addGmailTools(argument("agent-dir") ?? process.cwd())}\n`);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
throw new Error("Usage: blue tools add gmail");
|
|
1131
|
+
}
|
|
1132
|
+
if (command === "connections") {
|
|
1133
|
+
const [action, provider, connectionId] = positional(3);
|
|
1134
|
+
if (action === "connect" && (provider === "gmail" || provider === "google")) {
|
|
1135
|
+
if (hasFlag("local") && hasFlag("remote")) {
|
|
1136
|
+
throw new Error("Choose either --local or --remote");
|
|
1137
|
+
}
|
|
1138
|
+
if (!hasFlag("local") && !hasFlag("remote")) {
|
|
1139
|
+
throw new Error("Pass --local or --remote");
|
|
1140
|
+
}
|
|
1141
|
+
if (hasFlag("local")) {
|
|
1142
|
+
await connectGoogleLocal(hostedEdge());
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
await connectGoogleRemote(hostedEdge());
|
|
1146
|
+
}
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (action === "list") {
|
|
1150
|
+
await listConnections(hostedEdge());
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (action === "disconnect" && provider) {
|
|
1154
|
+
if (hasFlag("local")) {
|
|
1155
|
+
await disconnectLocalConnection(provider);
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
const connection = await (await clientFor(hostedEdge())).disconnectConnection(provider);
|
|
1159
|
+
process.stdout.write(`disconnected: ${connection.id}\n`);
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
void connectionId;
|
|
1164
|
+
throw new Error("Unsupported connections command");
|
|
1165
|
+
}
|
|
896
1166
|
if (command === "channels") {
|
|
897
1167
|
const [action, channel, connectionId] = positional(3);
|
|
898
1168
|
if (action === "add" && channel === "slack") {
|