@pitboss/cli 0.1.3 → 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.
- package/README.md +4 -4
- package/dist/cli.js +48 -10
- package/dist/mcp.js +155 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ relai login --invite inv_<code> --api <https://your-relai-host>
|
|
|
38
38
|
If you're standing one up yourself, see the [main repo](https://github.com/phillipsio/relai) for setup; then:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
relai init # guided setup — saves config to ~/.config/
|
|
41
|
+
relai init # guided setup — saves config to ~/.config/pitboss/config.json
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
After either path, `relai init` / `relai login` prints a ready-to-paste `mcpServers` block — drop it into your project's `.mcp.json` (or `~/.claude.json`) so your AI agent (Claude Code, Cursor, Copilot, etc.) can use the relai tools directly.
|
|
@@ -117,16 +117,16 @@ relai token revoke <tokenId> Revoke a specific token
|
|
|
117
117
|
|
|
118
118
|
## Multi-identity
|
|
119
119
|
|
|
120
|
-
Set `
|
|
120
|
+
Set `PITBOSS_CONFIG_DIR` to override the config location and run multiple agent identities side by side from one machine:
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
|
-
export
|
|
123
|
+
export PITBOSS_CONFIG_DIR=/tmp/relai-coworker
|
|
124
124
|
relai login --invite inv_<...>
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
## Config
|
|
128
128
|
|
|
129
|
-
`~/.config/
|
|
129
|
+
`~/.config/pitboss/config.json` (or `$PITBOSS_CONFIG_DIR/config.json`):
|
|
130
130
|
|
|
131
131
|
```json
|
|
132
132
|
{
|
package/dist/cli.js
CHANGED
|
@@ -18010,14 +18010,46 @@ var dist_default7 = createPrompt((config, done) => {
|
|
|
18010
18010
|
// src/config.ts
|
|
18011
18011
|
import { homedir } from "node:os";
|
|
18012
18012
|
import { join, dirname } from "node:path";
|
|
18013
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
18013
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, existsSync, renameSync, rmSync as rmSync2, lstatSync, statSync } from "node:fs";
|
|
18014
18014
|
import { randomBytes } from "node:crypto";
|
|
18015
|
+
var configDirOverride = () => process.env.PITBOSS_CONFIG_DIR || process.env.RELAI_CONFIG_DIR || void 0;
|
|
18016
|
+
function configDir() {
|
|
18017
|
+
return configDirOverride() ?? join(homedir(), ".config", "pitboss");
|
|
18018
|
+
}
|
|
18015
18019
|
function configPath() {
|
|
18016
|
-
|
|
18017
|
-
|
|
18020
|
+
return join(configDir(), "config.json");
|
|
18021
|
+
}
|
|
18022
|
+
function legacyHomePath(name) {
|
|
18023
|
+
return join(homedir(), ".config", "relai", name);
|
|
18024
|
+
}
|
|
18025
|
+
var configOverridden = () => configDirOverride() !== void 0;
|
|
18026
|
+
var warnedFor = /* @__PURE__ */ new Set();
|
|
18027
|
+
function readableFrom(current, legacy, overridden) {
|
|
18028
|
+
if (overridden || existsSync(current) || !existsSync(legacy)) return current;
|
|
18029
|
+
if (!warnedFor.has(legacy)) {
|
|
18030
|
+
warnedFor.add(legacy);
|
|
18031
|
+
console.error(`pitboss: reading ${legacy}; the next write moves it to ${current}.`);
|
|
18032
|
+
}
|
|
18033
|
+
return legacy;
|
|
18034
|
+
}
|
|
18035
|
+
function retireLegacy(legacy, current, overridden) {
|
|
18036
|
+
if (overridden) return;
|
|
18037
|
+
try {
|
|
18038
|
+
const found = lstatSync(legacy, { throwIfNoEntry: false });
|
|
18039
|
+
if (!found) return;
|
|
18040
|
+
const [a, b] = [statSync(dirname(legacy)), statSync(dirname(current))];
|
|
18041
|
+
if (a.dev === b.dev && a.ino === b.ino) return;
|
|
18042
|
+
rmSync2(legacy);
|
|
18043
|
+
} catch (err) {
|
|
18044
|
+
if (err.code === "ENOENT") return;
|
|
18045
|
+
console.error(`pitboss: could not remove ${legacy} (${String(err)}); it may still hold a token, delete it by hand.`);
|
|
18046
|
+
}
|
|
18047
|
+
}
|
|
18048
|
+
function configFileInUse() {
|
|
18049
|
+
return readableFrom(configPath(), legacyHomePath("config.json"), configOverridden());
|
|
18018
18050
|
}
|
|
18019
18051
|
function readConfig() {
|
|
18020
|
-
const file =
|
|
18052
|
+
const file = configFileInUse();
|
|
18021
18053
|
if (!existsSync(file)) return null;
|
|
18022
18054
|
try {
|
|
18023
18055
|
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
@@ -18029,10 +18061,12 @@ function readConfig() {
|
|
|
18029
18061
|
}
|
|
18030
18062
|
function writeConfig(config) {
|
|
18031
18063
|
const file = configPath();
|
|
18064
|
+
const migrating = !existsSync(file);
|
|
18032
18065
|
mkdirSync(dirname(file), { recursive: true });
|
|
18033
18066
|
const tmp = `${file}.relai-${randomBytes(8).toString("hex")}`;
|
|
18034
18067
|
writeFileSync2(tmp, JSON.stringify(config, null, 2), { mode: 384, flag: "wx" });
|
|
18035
18068
|
renameSync(tmp, file);
|
|
18069
|
+
if (migrating) retireLegacy(legacyHomePath("config.json"), file, configOverridden());
|
|
18036
18070
|
return file;
|
|
18037
18071
|
}
|
|
18038
18072
|
function requireConfig() {
|
|
@@ -18896,7 +18930,7 @@ function runtimeTargets(workerType, { home, repo: repo2 }) {
|
|
|
18896
18930
|
case "gpt":
|
|
18897
18931
|
return [join2(home, ".codex", "mcp.json")];
|
|
18898
18932
|
case "mcp":
|
|
18899
|
-
return [join2(home, ".config", "
|
|
18933
|
+
return [join2(home, ".config", "pitboss", "config.json")];
|
|
18900
18934
|
// A person, not a runtime. Nothing to configure.
|
|
18901
18935
|
case "human":
|
|
18902
18936
|
return [];
|
|
@@ -19123,12 +19157,12 @@ import { homedir as homedir3 } from "node:os";
|
|
|
19123
19157
|
import { join as join3, resolve, dirname as dirname2 } from "node:path";
|
|
19124
19158
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "node:fs";
|
|
19125
19159
|
import { createHash } from "node:crypto";
|
|
19160
|
+
var stateOverridden = () => Boolean(process.env.PITBOSS_AGENTS_STATE || process.env.RELAI_AGENTS_STATE);
|
|
19126
19161
|
function statePath() {
|
|
19127
|
-
|
|
19128
|
-
return join3(homedir3(), ".config", "relai", "agents.json");
|
|
19162
|
+
return process.env.PITBOSS_AGENTS_STATE || process.env.RELAI_AGENTS_STATE || join3(homedir3(), ".config", "pitboss", "agents.json");
|
|
19129
19163
|
}
|
|
19130
19164
|
function readAgentsState() {
|
|
19131
|
-
const p = statePath();
|
|
19165
|
+
const p = readableFrom(statePath(), legacyHomePath("agents.json"), stateOverridden());
|
|
19132
19166
|
if (!existsSync3(p)) return { agents: [] };
|
|
19133
19167
|
try {
|
|
19134
19168
|
const raw = JSON.parse(readFileSync4(p, "utf-8"));
|
|
@@ -19140,8 +19174,10 @@ function readAgentsState() {
|
|
|
19140
19174
|
}
|
|
19141
19175
|
function writeAgentsState(state) {
|
|
19142
19176
|
const p = statePath();
|
|
19177
|
+
const migrating = !existsSync3(p);
|
|
19143
19178
|
mkdirSync2(dirname2(p), { recursive: true });
|
|
19144
19179
|
writeFileSync3(p, JSON.stringify(state, null, 2));
|
|
19180
|
+
if (migrating) retireLegacy(legacyHomePath("agents.json"), p, stateOverridden());
|
|
19145
19181
|
}
|
|
19146
19182
|
function hashToken(token2) {
|
|
19147
19183
|
return createHash("sha256").update(token2).digest("hex").slice(0, 16);
|
|
@@ -19242,7 +19278,8 @@ async function loginCommand(opts) {
|
|
|
19242
19278
|
if (existing && opts.invite) {
|
|
19243
19279
|
console.log(source_default.yellow(`
|
|
19244
19280
|
Already logged in as ${source_default.bold(existing.agentName)} (${existing.agentId})`));
|
|
19245
|
-
console.log(source_default.dim(
|
|
19281
|
+
console.log(source_default.dim(`Delete ${configFileInUse()} to re-login.
|
|
19282
|
+
`));
|
|
19246
19283
|
return;
|
|
19247
19284
|
}
|
|
19248
19285
|
const apiUrl = opts.api ?? await dist_default6({ message: "API URL", default: "http://localhost:3010" });
|
|
@@ -19581,7 +19618,8 @@ async function run(opts) {
|
|
|
19581
19618
|
role: invite.role,
|
|
19582
19619
|
specialization: invite.specialization ?? void 0,
|
|
19583
19620
|
workerType: invite.workerType,
|
|
19584
|
-
domains: invite.domains ?? []
|
|
19621
|
+
domains: invite.domains ?? [],
|
|
19622
|
+
.../^[^\p{Cc}\p{Cf}\u2028\u2029\u0085]{1,1024}$/u.test(root) ? { repoPath: root } : {}
|
|
19585
19623
|
});
|
|
19586
19624
|
if (accepted.status !== 201) {
|
|
19587
19625
|
const said = accepted.payload?.error?.message;
|
package/dist/mcp.js
CHANGED
|
@@ -9766,10 +9766,6 @@ var require_content_type = __commonJS({
|
|
|
9766
9766
|
});
|
|
9767
9767
|
|
|
9768
9768
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.6.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sse.js
|
|
9769
|
-
var sse_exports = {};
|
|
9770
|
-
__export(sse_exports, {
|
|
9771
|
-
SSEServerTransport: () => SSEServerTransport
|
|
9772
|
-
});
|
|
9773
9769
|
import { randomUUID } from "node:crypto";
|
|
9774
9770
|
var import_raw_body, import_content_type, MAXIMUM_MESSAGE_SIZE, SSEServerTransport;
|
|
9775
9771
|
var init_sse = __esm({
|
|
@@ -9888,6 +9884,106 @@ data: ${JSON.stringify(message)}
|
|
|
9888
9884
|
}
|
|
9889
9885
|
});
|
|
9890
9886
|
|
|
9887
|
+
// ../mcp-server/src/http-auth.ts
|
|
9888
|
+
var http_auth_exports = {};
|
|
9889
|
+
__export(http_auth_exports, {
|
|
9890
|
+
isAuthorizedBearer: () => isAuthorizedBearer,
|
|
9891
|
+
isBlankCredential: () => isBlankCredential,
|
|
9892
|
+
resolveHttpCredential: () => resolveHttpCredential
|
|
9893
|
+
});
|
|
9894
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
9895
|
+
function sha256(value) {
|
|
9896
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
9897
|
+
}
|
|
9898
|
+
function isBlankCredential(value) {
|
|
9899
|
+
return value.trim().length === 0;
|
|
9900
|
+
}
|
|
9901
|
+
function isAuthorizedBearer(authHeader, credential) {
|
|
9902
|
+
if (isBlankCredential(credential) || !authHeader) return false;
|
|
9903
|
+
const spaceIndex = authHeader.indexOf(" ");
|
|
9904
|
+
if (spaceIndex === -1) return false;
|
|
9905
|
+
if (authHeader.slice(0, spaceIndex).toLowerCase() !== "bearer") return false;
|
|
9906
|
+
const token = authHeader.slice(spaceIndex + 1);
|
|
9907
|
+
return timingSafeEqual(sha256(token), sha256(credential));
|
|
9908
|
+
}
|
|
9909
|
+
function resolveHttpCredential(mcpHttpToken, ownerMode, ownerToken, apiSecret) {
|
|
9910
|
+
if (mcpHttpToken) return mcpHttpToken;
|
|
9911
|
+
return (ownerMode ? ownerToken : apiSecret) ?? "";
|
|
9912
|
+
}
|
|
9913
|
+
var init_http_auth = __esm({
|
|
9914
|
+
"../mcp-server/src/http-auth.ts"() {
|
|
9915
|
+
"use strict";
|
|
9916
|
+
}
|
|
9917
|
+
});
|
|
9918
|
+
|
|
9919
|
+
// ../mcp-server/src/http-transport.ts
|
|
9920
|
+
var http_transport_exports = {};
|
|
9921
|
+
__export(http_transport_exports, {
|
|
9922
|
+
createHttpRequestListener: () => createHttpRequestListener
|
|
9923
|
+
});
|
|
9924
|
+
function createHttpRequestListener(server2, credential) {
|
|
9925
|
+
let current;
|
|
9926
|
+
let takeoverQueue = Promise.resolve();
|
|
9927
|
+
function detach(transport) {
|
|
9928
|
+
transport.onclose = void 0;
|
|
9929
|
+
transport.onerror = void 0;
|
|
9930
|
+
transport.onmessage = void 0;
|
|
9931
|
+
}
|
|
9932
|
+
async function takeOver(res) {
|
|
9933
|
+
const replaced = current;
|
|
9934
|
+
current = void 0;
|
|
9935
|
+
if (replaced) {
|
|
9936
|
+
detach(replaced);
|
|
9937
|
+
try {
|
|
9938
|
+
await replaced.close();
|
|
9939
|
+
} catch {
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
9943
|
+
try {
|
|
9944
|
+
await server2.connect(transport);
|
|
9945
|
+
current = transport;
|
|
9946
|
+
} catch (err) {
|
|
9947
|
+
detach(transport);
|
|
9948
|
+
console.error("[relai-mcp] failed to establish SSE connection:", err instanceof Error ? err.message : err);
|
|
9949
|
+
if (!res.headersSent) {
|
|
9950
|
+
res.writeHead(500, { "content-type": "application/json" }).end(JSON.stringify({ error: { code: "connect_failed", message: "Failed to establish SSE connection" } }));
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
return async (req, res) => {
|
|
9955
|
+
if (!isAuthorizedBearer(req.headers.authorization, credential)) {
|
|
9956
|
+
console.error(`[relai-mcp] rejected unauthenticated HTTP request: ${req.method} ${req.url}`);
|
|
9957
|
+
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end(UNAUTHORIZED_BODY);
|
|
9958
|
+
return;
|
|
9959
|
+
}
|
|
9960
|
+
const path = (req.url ?? "").split("?")[0];
|
|
9961
|
+
if (req.method === "GET" && path === "/sse") {
|
|
9962
|
+
const attempt = takeoverQueue.then(() => takeOver(res), () => takeOver(res));
|
|
9963
|
+
takeoverQueue = attempt;
|
|
9964
|
+
await attempt;
|
|
9965
|
+
} else if (req.method === "POST" && path === "/messages") {
|
|
9966
|
+
res.writeHead(501, { "content-type": "application/json" }).end(NOT_IMPLEMENTED_BODY);
|
|
9967
|
+
} else {
|
|
9968
|
+
res.writeHead(404).end();
|
|
9969
|
+
}
|
|
9970
|
+
};
|
|
9971
|
+
}
|
|
9972
|
+
var UNAUTHORIZED_BODY, NOT_IMPLEMENTED_BODY;
|
|
9973
|
+
var init_http_transport = __esm({
|
|
9974
|
+
"../mcp-server/src/http-transport.ts"() {
|
|
9975
|
+
"use strict";
|
|
9976
|
+
init_sse();
|
|
9977
|
+
init_http_auth();
|
|
9978
|
+
UNAUTHORIZED_BODY = JSON.stringify({
|
|
9979
|
+
error: { code: "unauthorized", message: "Missing or invalid bearer token" }
|
|
9980
|
+
});
|
|
9981
|
+
NOT_IMPLEMENTED_BODY = JSON.stringify({
|
|
9982
|
+
error: { code: "not_implemented", message: "This server does not yet process incoming tool-call messages" }
|
|
9983
|
+
});
|
|
9984
|
+
}
|
|
9985
|
+
});
|
|
9986
|
+
|
|
9891
9987
|
// ../mcp-server/src/index.ts
|
|
9892
9988
|
import { readFileSync } from "node:fs";
|
|
9893
9989
|
import { join } from "node:path";
|
|
@@ -12177,6 +12273,12 @@ var ApiClient = class {
|
|
|
12177
12273
|
`/agents/${agentId}/tokens`
|
|
12178
12274
|
);
|
|
12179
12275
|
}
|
|
12276
|
+
deleteAgent(agentId) {
|
|
12277
|
+
return this.request("DELETE", `/agents/${agentId}`);
|
|
12278
|
+
}
|
|
12279
|
+
revokeToken(tokenId) {
|
|
12280
|
+
return this.request("DELETE", `/tokens/${tokenId}`);
|
|
12281
|
+
}
|
|
12180
12282
|
listInvites(repoId) {
|
|
12181
12283
|
return this.request(
|
|
12182
12284
|
"GET",
|
|
@@ -12824,7 +12926,39 @@ ${version.body}`
|
|
|
12824
12926
|
// Additive, never a swap: an owner-scoped agent is still an agent and needs
|
|
12825
12927
|
// everything above. buildOperatorTools assumes no agent identity, and its
|
|
12826
12928
|
// create_task bypasses propose/commit.
|
|
12827
|
-
...opts.ownerScoped ? buildProvisioningTools(client) : []
|
|
12929
|
+
...opts.ownerScoped ? [...buildProvisioningTools(client, { credentialMinting: true }), ...buildAgentManagementTools(client, agentId)] : []
|
|
12930
|
+
];
|
|
12931
|
+
}
|
|
12932
|
+
function buildAgentManagementTools(client, selfId) {
|
|
12933
|
+
const text = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
12934
|
+
return [
|
|
12935
|
+
{
|
|
12936
|
+
name: "remove_agent",
|
|
12937
|
+
description: "Permanently delete one of the owner's agents, with its tokens. Cannot be undone: the agent has to be invited again to come back. Use list_agents to find the id. You cannot remove yourself, and a repo's only orchestrator cannot be removed.",
|
|
12938
|
+
inputSchema: external_exports.object({ agentId: external_exports.string().min(1).describe("The agent to delete.") }),
|
|
12939
|
+
handler: async (input) => {
|
|
12940
|
+
if (input.agentId === selfId) {
|
|
12941
|
+
return { content: [{ type: "text", text: "Refused: you cannot remove yourself." }], isError: true };
|
|
12942
|
+
}
|
|
12943
|
+
await client.deleteAgent(input.agentId);
|
|
12944
|
+
return text({ removed: input.agentId });
|
|
12945
|
+
}
|
|
12946
|
+
},
|
|
12947
|
+
{
|
|
12948
|
+
name: "list_tokens",
|
|
12949
|
+
description: "List an agent's tokens: id, createdAt, lastUsedAt, revokedAt, whether each is owner-scoped, and which one is current. Never the token itself. Use it before revoke_token.",
|
|
12950
|
+
inputSchema: external_exports.object({ agentId: external_exports.string().min(1).describe("Whose tokens to list.") }),
|
|
12951
|
+
handler: async (input) => text({ tokens: await client.listAgentTokens(input.agentId) })
|
|
12952
|
+
},
|
|
12953
|
+
{
|
|
12954
|
+
name: "revoke_token",
|
|
12955
|
+
description: "Revoke one token by id, so whatever holds it stops working at once. The agent itself stays; it needs a new token or invite to reconnect. Revoking your own current token ends this session.",
|
|
12956
|
+
inputSchema: external_exports.object({ tokenId: external_exports.string().min(1).describe("From list_tokens.") }),
|
|
12957
|
+
handler: async (input) => {
|
|
12958
|
+
await client.revokeToken(input.tokenId);
|
|
12959
|
+
return text({ revoked: input.tokenId });
|
|
12960
|
+
}
|
|
12961
|
+
}
|
|
12828
12962
|
];
|
|
12829
12963
|
}
|
|
12830
12964
|
function selectTools(client, config) {
|
|
@@ -12879,11 +13013,7 @@ function buildProvisioningTools(client, opts = {}) {
|
|
|
12879
13013
|
return { content: [{ type: "text", text: JSON.stringify({ repo }, null, 2) }] };
|
|
12880
13014
|
}
|
|
12881
13015
|
},
|
|
12882
|
-
//
|
|
12883
|
-
// `claude --print --dangerously-skip-permissions` loop where nothing
|
|
12884
|
-
// confirms a call. Before this, no agent-mode tool took a repoId from the
|
|
12885
|
-
// model at all, so the model's reach was one repo even when the credential
|
|
12886
|
-
// reached every repo the owner owns.
|
|
13016
|
+
// Mints a bearer credential: owner mode and the god credential only.
|
|
12887
13017
|
...opts.credentialMinting ? [{
|
|
12888
13018
|
name: "invite_agent",
|
|
12889
13019
|
description: "Authorise a new agent to join one of your repos. This does NOT create the agent: it mints a single-use, expiring invite code and returns the exact command that redeems it. Two ways to finish. If you have a shell on the machine that agent will run on, create its working directory and run the command there. If you do not, give the user the command to paste. Either way the redeem is INTERACTIVE: `login` prompts for the agent name and specialization and does not yet read them from the invite, so expect to answer two questions and do not run it anywhere it cannot reach a terminal. The code is the only secret here and it is worthless once redeemed, which is why this is an invite rather than a token: a token would be long-lived and would sit in this conversation. Set workerType to the runtime that agent actually is, or it joins labelled 'human' and routing treats it wrongly.",
|
|
@@ -12907,8 +13037,8 @@ function buildProvisioningTools(client, opts = {}) {
|
|
|
12907
13037
|
text: JSON.stringify({
|
|
12908
13038
|
invite,
|
|
12909
13039
|
code,
|
|
12910
|
-
redeemCommand: `RELAI_CONFIG_DIR=~/.config/
|
|
12911
|
-
nextStep: "Run redeemCommand in the directory that agent will work from, or give it to the user to paste there. It will ask for an agent name and a specialization. The code is single-use and expires. One machine holds one identity per config dir, which is why the command sets
|
|
13040
|
+
redeemCommand: `PITBOSS_CONFIG_DIR=~/.config/pitboss/${slot} RELAI_CONFIG_DIR=~/.config/pitboss/${slot} pitboss login --api ${client.apiUrl} --invite ${code}${workerTypeFlag}`,
|
|
13041
|
+
nextStep: "Run redeemCommand in the directory that agent will work from, or give it to the user to paste there. It will ask for an agent name and a specialization. The code is single-use and expires. One machine holds one identity per config dir, which is why the command sets the config dir \u2014 drop it and a machine that already has an agent will report success without redeeming anything."
|
|
12912
13042
|
}, null, 2)
|
|
12913
13043
|
}]
|
|
12914
13044
|
};
|
|
@@ -13439,7 +13569,7 @@ var {
|
|
|
13439
13569
|
OWNER_ID,
|
|
13440
13570
|
TRANSPORT = "stdio"
|
|
13441
13571
|
} = process.env;
|
|
13442
|
-
var OWNER_MODE = Boolean(API_OWNER_TOKEN);
|
|
13572
|
+
var OWNER_MODE = Boolean(API_OWNER_TOKEN?.trim());
|
|
13443
13573
|
if (OWNER_MODE) {
|
|
13444
13574
|
if (!OWNER_ID || !OWNER_ID.startsWith("usr_")) {
|
|
13445
13575
|
console.error("[relai-mcp] owner mode requires OWNER_ID (a 'usr_\u2026' id) alongside API_OWNER_TOKEN");
|
|
@@ -13449,7 +13579,7 @@ if (OWNER_MODE) {
|
|
|
13449
13579
|
"[relai-mcp] owner mode: API_OWNER_TOKEN is a god-key credential \u2014 keep this server off the open internet (localhost bind + authenticating reverse proxy only)."
|
|
13450
13580
|
);
|
|
13451
13581
|
} else {
|
|
13452
|
-
if (!API_SECRET) {
|
|
13582
|
+
if (!API_SECRET?.trim()) {
|
|
13453
13583
|
console.error("[relai-mcp] API_SECRET is required");
|
|
13454
13584
|
process.exit(1);
|
|
13455
13585
|
}
|
|
@@ -13559,20 +13689,20 @@ async function main() {
|
|
|
13559
13689
|
const transport = new StdioServerTransport();
|
|
13560
13690
|
await server.connect(transport);
|
|
13561
13691
|
} else if (TRANSPORT === "http") {
|
|
13562
|
-
const { SSEServerTransport: SSEServerTransport2 } = await Promise.resolve().then(() => (init_sse(), sse_exports));
|
|
13563
13692
|
const http = await import("node:http");
|
|
13693
|
+
const { createHttpRequestListener: createHttpRequestListener2 } = await Promise.resolve().then(() => (init_http_transport(), http_transport_exports));
|
|
13694
|
+
const { resolveHttpCredential: resolveHttpCredential2, isBlankCredential: isBlankCredential2 } = await Promise.resolve().then(() => (init_http_auth(), http_auth_exports));
|
|
13564
13695
|
const port = Number(process.env.MCP_PORT ?? 3001);
|
|
13565
13696
|
const host = process.env.MCP_HOST ?? "127.0.0.1";
|
|
13566
|
-
const
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
});
|
|
13697
|
+
const credential = resolveHttpCredential2(process.env.MCP_HTTP_TOKEN, OWNER_MODE, API_OWNER_TOKEN, API_SECRET);
|
|
13698
|
+
if (isBlankCredential2(credential)) {
|
|
13699
|
+
console.error(
|
|
13700
|
+
"[relai-mcp] the HTTP transport credential (MCP_HTTP_TOKEN, API_SECRET, or API_OWNER_TOKEN) is empty or whitespace \u2014 refusing to start with a credential that would lock out every client"
|
|
13701
|
+
);
|
|
13702
|
+
process.exit(1);
|
|
13703
|
+
}
|
|
13704
|
+
const listener = createHttpRequestListener2(server, credential);
|
|
13705
|
+
const httpServer = http.createServer(listener);
|
|
13576
13706
|
httpServer.listen(port, host, () => {
|
|
13577
13707
|
console.error(`[relai-mcp] HTTP/SSE transport listening on ${host}:${port}`);
|
|
13578
13708
|
});
|