relaybraid 0.2.0

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.
Files changed (2) hide show
  1. package/package.json +21 -0
  2. package/src/index.mjs +147 -0
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "relaybraid",
3
+ "version": "0.2.0",
4
+ "description": "Relaybraid SDK for Node.js: run app actions from code.",
5
+ "type": "module",
6
+ "exports": "./src/index.mjs",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "license": "MIT",
14
+ "homepage": "https://relaybraid.com/docs/sdk",
15
+ "keywords": [
16
+ "automation",
17
+ "slack",
18
+ "integrations",
19
+ "sdk"
20
+ ]
21
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,147 @@
1
+ import { existsSync } from "node:fs";
2
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ const VERSION = "0.2.0";
6
+ const DEFAULT_ORIGIN = process.env.RELAYBRAID_ORIGIN || "https://relaybraid.com";
7
+ const CLIENT = `relaybraid-sdk/${VERSION} node/${process.versions.node} ${process.platform}`;
8
+
9
+ // The name of the coding agent running this process, when one is, read from AI_AGENT or the
10
+ // agent's own marker variable and sent as X-Relaybraid-Agent. Nothing else about the environment
11
+ // is sent. What the SDK sends and why: https://relaybraid.com/docs/telemetry
12
+ function agentName() {
13
+ if (process.env.AI_AGENT) return process.env.AI_AGENT;
14
+ if (process.env.CLAUDECODE) return "claude-code";
15
+ if (process.env.CURSOR_AGENT) return "cursor";
16
+ if (process.env.CODEX_CI) return "codex";
17
+ return null;
18
+ }
19
+
20
+ async function readJson(path) {
21
+ if (!existsSync(path)) return null;
22
+ try {
23
+ return JSON.parse(await readFile(path, "utf8"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ // The key the CLI saved, and the install id `init` minted for this project.
30
+ async function readProject(cwd) {
31
+ const credentials = await readJson(join(cwd, ".relaybraid", "credentials.json"));
32
+ const config = await readJson(join(cwd, ".relaybraid", "config.json"));
33
+ return { apiKey: credentials?.api_key ?? null, installId: credentials?.install_id ?? config?.install_id ?? null };
34
+ }
35
+
36
+ async function saveCredentials(cwd, apiKey, installId) {
37
+ const dir = join(cwd, ".relaybraid");
38
+ await mkdir(dir, { recursive: true });
39
+ await writeFile(join(dir, "credentials.json"), `${JSON.stringify({ api_key: apiKey, install_id: installId ?? null }, null, 2)}\n`, { mode: 0o600 });
40
+ const ignore = join(cwd, ".gitignore");
41
+ const current = existsSync(ignore) ? await readFile(ignore, "utf8") : "";
42
+ if (!current.includes(".relaybraid/credentials.json")) await appendFile(ignore, `${current.endsWith("\n") || current === "" ? "" : "\n"}.relaybraid/credentials.json\n`);
43
+ }
44
+
45
+ export class RelaybraidError extends Error {
46
+ constructor(message, { status, code }) {
47
+ super(message);
48
+ this.name = "RelaybraidError";
49
+ this.status = status;
50
+ this.code = code;
51
+ }
52
+ }
53
+
54
+ export class Relaybraid {
55
+ #apiKey;
56
+ #clientId;
57
+ #clientSecret;
58
+ #setup;
59
+ #origin;
60
+ #cwd;
61
+ #installId;
62
+ claimUrl = null;
63
+
64
+ // `setup` is the code shown in the docs you installed from; it links this project to that guide.
65
+ // `clientId` and `clientSecret` authenticate unattended code; otherwise the SDK reads the key the
66
+ // CLI saved, or RELAYBRAID_API_KEY.
67
+ constructor({ apiKey, clientId, clientSecret, setup, origin, cwd } = {}) {
68
+ this.#apiKey = apiKey ?? process.env.RELAYBRAID_API_KEY ?? null;
69
+ this.#clientId = clientId ?? process.env.RELAYBRAID_CLIENT_ID ?? null;
70
+ this.#clientSecret = clientSecret ?? process.env.RELAYBRAID_CLIENT_SECRET ?? null;
71
+ this.#setup = setup ?? null;
72
+ this.#origin = origin ?? DEFAULT_ORIGIN;
73
+ this.#cwd = cwd ?? process.cwd();
74
+ this.#installId = null;
75
+ }
76
+
77
+ // Creates a trial workspace (100 free action runs), saves its key in .relaybraid/credentials.json
78
+ // and exposes `claimUrl`, the link the account owner uses to keep the workspace.
79
+ static async trial({ setup, agent, intent, origin, cwd } = {}) {
80
+ const client = new Relaybraid({ setup, origin, cwd });
81
+ const existing = await readProject(client.#cwd);
82
+ if (existing.apiKey) {
83
+ client.#apiKey = existing.apiKey;
84
+ client.#installId = existing.installId;
85
+ return client;
86
+ }
87
+ const created = await client.#request("POST", "/api/sdk/trial", { setup: setup ?? null, agent: agent ?? agentName(), intent: intent ?? null }, { auth: false });
88
+ client.#apiKey = created.trial_key;
89
+ client.#installId = created.install_id ?? null;
90
+ client.claimUrl = created.claim_url;
91
+ await saveCredentials(client.#cwd, created.trial_key, created.install_id ?? null);
92
+ return client;
93
+ }
94
+
95
+ async #exchangeClientCredentials() {
96
+ const form = new URLSearchParams({ grant_type: "client_credentials", client_id: this.#clientId, client_secret: this.#clientSecret });
97
+ const response = await fetch(`${this.#origin}/oauth/token`, { method: "POST", headers: { "user-agent": CLIENT, "x-relaybraid-client": CLIENT, "content-type": "application/x-www-form-urlencoded" }, body: form.toString() });
98
+ const data = await response.json().catch(() => null);
99
+ if (!response.ok || !data?.access_token) throw new RelaybraidError(data?.error_description ?? "Client credentials were not accepted.", { status: response.status, code: data?.error ?? "invalid_client" });
100
+ return data.access_token;
101
+ }
102
+
103
+ async #request(method, path, body, { auth = true } = {}) {
104
+ const headers = { "user-agent": CLIENT, "x-relaybraid-client": CLIENT, accept: "application/json" };
105
+ const agent = agentName();
106
+ if (agent) headers["x-relaybraid-agent"] = agent;
107
+ if (this.#setup) headers["x-relaybraid-setup"] = this.#setup;
108
+ if (body !== undefined) headers["content-type"] = "application/json";
109
+ if (auth) {
110
+ if (!this.#apiKey && this.#clientId && this.#clientSecret) this.#apiKey = await this.#exchangeClientCredentials();
111
+ if (!this.#apiKey) {
112
+ const project = await readProject(this.#cwd);
113
+ this.#apiKey = project.apiKey;
114
+ this.#installId ??= project.installId;
115
+ }
116
+ if (!this.#apiKey) {
117
+ throw new RelaybraidError("No API key. Run the Relaybraid CLI's `login`, call Relaybraid.trial(), pass clientId and clientSecret, or set RELAYBRAID_API_KEY.", { status: 0, code: "no_api_key" });
118
+ }
119
+ headers.authorization = `Bearer ${this.#apiKey}`;
120
+ }
121
+ if (this.#installId) headers["x-relaybraid-install"] = this.#installId;
122
+ const response = await fetch(`${this.#origin}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
123
+ const text = await response.text();
124
+ let data = null;
125
+ try {
126
+ data = JSON.parse(text);
127
+ } catch {
128
+ data = null;
129
+ }
130
+ if (!response.ok) {
131
+ throw new RelaybraidError(data?.error?.message ?? `${method} ${path} failed with ${response.status}`, { status: response.status, code: data?.error?.code ?? "http_error" });
132
+ }
133
+ return data;
134
+ }
135
+
136
+ run(action, input = {}) {
137
+ return this.#request("POST", "/api/v1/actions/run", { action, input });
138
+ }
139
+
140
+ apps() {
141
+ return this.#request("GET", "/api/v1/apps");
142
+ }
143
+
144
+ me() {
145
+ return this.#request("GET", "/api/v1/me");
146
+ }
147
+ }