relayrun 0.1.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.
- package/README.md +99 -0
- package/dist/agent/diff.js +75 -0
- package/dist/agent/mock.js +175 -0
- package/dist/agent/orient.js +51 -0
- package/dist/agent/run.js +129 -0
- package/dist/agent/summarize.js +151 -0
- package/dist/cli.js +192 -0
- package/dist/emit.js +1 -0
- package/dist/redact.js +12 -0
- package/dist/repo.js +224 -0
- package/dist/runner.js +201 -0
- package/dist/sessionKey.js +108 -0
- package/package.json +41 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* MAKING AN EXPLICITLY SUPPLIED KEY ACTUALLY BE THE ONE THAT'S USED.
|
|
6
|
+
*
|
|
7
|
+
* This exists because the obvious implementation is silently wrong. Passing
|
|
8
|
+
* `env: { ANTHROPIC_API_KEY: key }` to the SDK looks like it supplies the key,
|
|
9
|
+
* typechecks, and produces a successful run — but on any machine where the
|
|
10
|
+
* operator has logged into Claude Code, the subprocess ignores the variable and
|
|
11
|
+
* authenticates with their stored OAuth token instead. Verified by pointing
|
|
12
|
+
* ANTHROPIC_BASE_URL at a local server and reading the headers: every request
|
|
13
|
+
* carried `Authorization: Bearer sk-ant-oat...`, the operator's own credential,
|
|
14
|
+
* with the supplied key nowhere.
|
|
15
|
+
*
|
|
16
|
+
* `apiKeyHelper` is the mechanism that does work. It's a script the CLI runs to
|
|
17
|
+
* obtain a credential, and its output is sent as `x-api-key`, taking precedence
|
|
18
|
+
* over the stored OAuth token.
|
|
19
|
+
*
|
|
20
|
+
* The script deliberately contains no secret. It echoes an environment
|
|
21
|
+
* variable, and the key travels in the process environment exactly as before.
|
|
22
|
+
*/
|
|
23
|
+
/** The env var the helper reads. Must match what the runner sets. */
|
|
24
|
+
export const SESSION_KEY_ENV = "RELAY_SESSION_API_KEY";
|
|
25
|
+
const SCRIPT = `#!/bin/sh\nprintf %s "$${SESSION_KEY_ENV}"\n`;
|
|
26
|
+
let helperPath = null;
|
|
27
|
+
/**
|
|
28
|
+
* Path to the helper script, created once per process on first use. Returns
|
|
29
|
+
* null if it can't be created (read-only tmp, a platform without /bin/sh).
|
|
30
|
+
*/
|
|
31
|
+
export function apiKeyHelperPath() {
|
|
32
|
+
if (helperPath)
|
|
33
|
+
return helperPath;
|
|
34
|
+
try {
|
|
35
|
+
// 0700 on both the directory and the script: it's executed by a subprocess
|
|
36
|
+
// of this process and nothing else needs to read it.
|
|
37
|
+
const dir = mkdtempSync(path.join(tmpdir(), "relay-key-"));
|
|
38
|
+
chmodSync(dir, 0o700);
|
|
39
|
+
const file = path.join(dir, "relay-api-key");
|
|
40
|
+
writeFileSync(file, SCRIPT, { mode: 0o700 });
|
|
41
|
+
chmodSync(file, 0o700);
|
|
42
|
+
process.once("exit", () => {
|
|
43
|
+
try {
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Best effort — it holds no secret, and tmp is cleared on reboot.
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
helperPath = file;
|
|
51
|
+
return helperPath;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error("could not create the API key helper:", err instanceof Error ? err.message : err);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const VERIFY_TIMEOUT_MS = 10_000;
|
|
59
|
+
/**
|
|
60
|
+
* Checks a key at startup rather than on first use.
|
|
61
|
+
*
|
|
62
|
+
* Without this, a mistyped or revoked key is accepted silently and the operator
|
|
63
|
+
* finds out only when an instruction arrives: the agent retries the 401 with
|
|
64
|
+
* backoff for over three minutes, then reports the failure as its answer.
|
|
65
|
+
* Measured — 191 seconds from instruction to "Failed to authenticate". One
|
|
66
|
+
* request up front turns that into an immediate, accurate "no".
|
|
67
|
+
*
|
|
68
|
+
* `/v1/models` is the cheapest authenticated endpoint there is.
|
|
69
|
+
*/
|
|
70
|
+
export async function verifyApiKey(key) {
|
|
71
|
+
const base = process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(`${base.replace(/\/$/, "")}/v1/models?limit=1`, {
|
|
74
|
+
headers: { "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
75
|
+
signal: AbortSignal.timeout(VERIFY_TIMEOUT_MS),
|
|
76
|
+
});
|
|
77
|
+
if (res.ok)
|
|
78
|
+
return { ok: true };
|
|
79
|
+
if (res.status === 401 || res.status === 403) {
|
|
80
|
+
return { ok: false, error: "Anthropic rejected that key" };
|
|
81
|
+
}
|
|
82
|
+
if (res.status === 429) {
|
|
83
|
+
// The key is real — it's the account that's over its limit. Accepting it
|
|
84
|
+
// is right: the rate limit may well have cleared by the first run.
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: `could not verify that key (Anthropic returned ${res.status})`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Offline, DNS failure, or the timeout above. Refuse rather than accept
|
|
94
|
+
// optimistically: if this machine can't reach Anthropic now, the agent
|
|
95
|
+
// can't either, and a key accepted here would fail confusingly later.
|
|
96
|
+
return { ok: false, error: "could not reach Anthropic to check that key" };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const KEY_SHAPE = /^[A-Za-z0-9_-]+$/;
|
|
100
|
+
const MAX_KEY_LENGTH = 300;
|
|
101
|
+
/** Shape-only check, so an obviously malformed key fails before a network call. */
|
|
102
|
+
export function looksLikeApiKey(key) {
|
|
103
|
+
return (key.startsWith("sk-ant-") && key.length <= MAX_KEY_LENGTH && KEY_SHAPE.test(key));
|
|
104
|
+
}
|
|
105
|
+
/** The only part of a key that may leave this machine. */
|
|
106
|
+
export function keyHint(key) {
|
|
107
|
+
return key.slice(-4);
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "relayrun",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run a Relay session against a repository on your own machine — everyone with the link watches the agent work, one person drives.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"agent",
|
|
8
|
+
"collaboration",
|
|
9
|
+
"pair-programming",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"relayrun": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.9.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
|
26
|
+
"ws": "8.21.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "26.1.2",
|
|
30
|
+
"@types/ws": "8.18.1",
|
|
31
|
+
"tsx": "4.23.1",
|
|
32
|
+
"typescript": "7.0.2",
|
|
33
|
+
"@relay/shared": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "tsx src/cli.ts --mock --server http://localhost:4000 --web http://localhost:3000",
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"start": "node dist/cli.js",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|