@pitboss/cli 0.1.3 → 0.2.1
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 +56 -17
- package/dist/mcp.js +189 -32
- 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,10 +19278,12 @@ 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
|
-
const
|
|
19285
|
+
const ni = nonInteractive();
|
|
19286
|
+
const apiUrl = opts.api || (ni ? requireFlag("api url", "--api <url>") : await dist_default6({ message: "API URL", default: "http://localhost:3010" }));
|
|
19249
19287
|
let agentId;
|
|
19250
19288
|
let agentName;
|
|
19251
19289
|
let repoId;
|
|
@@ -19269,14 +19307,14 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
19269
19307
|
}
|
|
19270
19308
|
} else {
|
|
19271
19309
|
const client = new CliApiClient({ apiUrl });
|
|
19272
|
-
const name = await dist_default6({
|
|
19310
|
+
const name = opts.name || (ni ? void 0 : await dist_default6({
|
|
19273
19311
|
message: "Agent name",
|
|
19274
19312
|
default: `${process.env.USER ?? "agent"}-claude-code`
|
|
19275
|
-
});
|
|
19276
|
-
const specialization = await dist_default7({
|
|
19313
|
+
}));
|
|
19314
|
+
const specialization = opts.specialization || (ni ? void 0 : await dist_default7({
|
|
19277
19315
|
message: "Specialization",
|
|
19278
19316
|
choices: SPECIALIZATION_CHOICES
|
|
19279
|
-
});
|
|
19317
|
+
}));
|
|
19280
19318
|
const specForApi = specialization === "custom" ? void 0 : specialization;
|
|
19281
19319
|
const s = ora("Accepting invite\u2026").start();
|
|
19282
19320
|
try {
|
|
@@ -19289,7 +19327,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
19289
19327
|
agentId = result.agent.id;
|
|
19290
19328
|
agentName = result.agent.name;
|
|
19291
19329
|
repoId = result.agent.repoId;
|
|
19292
|
-
agentSpecialization = specialization
|
|
19330
|
+
agentSpecialization = result.agent.specialization ?? void 0;
|
|
19293
19331
|
token2 = result.token;
|
|
19294
19332
|
s.succeed(source_default.green("Logged in"));
|
|
19295
19333
|
} catch (err) {
|
|
@@ -19581,7 +19619,8 @@ async function run(opts) {
|
|
|
19581
19619
|
role: invite.role,
|
|
19582
19620
|
specialization: invite.specialization ?? void 0,
|
|
19583
19621
|
workerType: invite.workerType,
|
|
19584
|
-
domains: invite.domains ?? []
|
|
19622
|
+
domains: invite.domains ?? [],
|
|
19623
|
+
.../^[^\p{Cc}\p{Cf}\u2028\u2029\u0085]{1,1024}$/u.test(root) ? { repoPath: root } : {}
|
|
19585
19624
|
});
|
|
19586
19625
|
if (accepted.status !== 201) {
|
|
19587
19626
|
const said = accepted.payload?.error?.message;
|
|
@@ -19827,7 +19866,7 @@ program2.command("send <threadId>").description("Send a message to a thread").op
|
|
|
19827
19866
|
program2.command("inbox").description("Show unread messages").option("-r, --read", "Mark all displayed messages as read").action(inboxCommand);
|
|
19828
19867
|
program2.command("repos").description("List all repos on this server").action(reposListCommand);
|
|
19829
19868
|
program2.command("agents").description("List agents in the current repo").action(agentsListCommand);
|
|
19830
|
-
program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `pitboss repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", "Agent runtime: claude, copilot, cursor, windsurf, gemini, gpt, mcp, human (default: human)").action(loginCommand);
|
|
19869
|
+
program2.command("login").description("Log in to a repo on this machine using an invite code or an agent token").option("--invite <code>", "Invite code from `pitboss repo invite`").option("--token <token>", "Per-agent token (from the cloud dashboard)").option("--api <url>", "API URL (skips prompt)").option("--working-dir <path>", "Override the working directory (defaults to CWD)").option("--worker-type <type>", "Agent runtime: claude, copilot, cursor, windsurf, gemini, gpt, mcp, human (default: human)").option("--name <name>", "Agent name (defaults to the invite's suggestion when not interactive)").option("--specialization <spec>", "Agent specialization: writer, reviewer, tester, architect, devops, orchestrator (defaults to the invite's suggestion)").action(loginCommand);
|
|
19831
19870
|
var repo = program2.command("repo").description("Repo operations");
|
|
19832
19871
|
repo.command("show [id]").description("Show a repo's details (defaults to the current repo)").action(repoShowCommand);
|
|
19833
19872
|
var repoContext = repo.command("context").description("View or edit the repo's pinned context (read by every agent on session start)");
|
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
|
};
|
|
@@ -13281,17 +13411,39 @@ function diffAttention(prev, tasks) {
|
|
|
13281
13411
|
for (const s of next.values()) counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
13282
13412
|
const parts = [...counts.entries()].map(([s, n]) => `${n} ${s}`).sort();
|
|
13283
13413
|
return {
|
|
13284
|
-
notices: [`relai: ${next.size} item(s) need you (${parts.join(", ")}). Call list_attention
|
|
13414
|
+
notices: [{ text: `relai: ${next.size} item(s) need you (${parts.join(", ")}). Call list_attention.`, ids: [...next.keys()] }],
|
|
13285
13415
|
next
|
|
13286
13416
|
};
|
|
13287
13417
|
}
|
|
13288
13418
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
13289
13419
|
const notices = [];
|
|
13290
13420
|
for (const [id, state] of next) {
|
|
13291
|
-
if (prev.get(id) !== state) notices.push(describe(byId.get(id), state));
|
|
13421
|
+
if (prev.get(id) !== state) notices.push({ text: describe(byId.get(id), state), ids: [id] });
|
|
13292
13422
|
}
|
|
13293
13423
|
return { notices, next };
|
|
13294
13424
|
}
|
|
13425
|
+
async function deliverAttention(prev, tasks, send) {
|
|
13426
|
+
const { notices, next } = diffAttention(prev, tasks);
|
|
13427
|
+
const undelivered = /* @__PURE__ */ new Set();
|
|
13428
|
+
for (const [i, n] of notices.entries()) {
|
|
13429
|
+
try {
|
|
13430
|
+
await send(n.text);
|
|
13431
|
+
} catch (err) {
|
|
13432
|
+
const held = notices.slice(i);
|
|
13433
|
+
console.error(`[relai-mcp] ${held.length} attention notice(s) held for the next poll:`, err instanceof Error ? err.message : err);
|
|
13434
|
+
held.forEach((h) => h.ids.forEach((id) => undelivered.add(id)));
|
|
13435
|
+
break;
|
|
13436
|
+
}
|
|
13437
|
+
}
|
|
13438
|
+
if (undelivered.size === 0) return next;
|
|
13439
|
+
if (prev === null) return null;
|
|
13440
|
+
for (const id of undelivered) {
|
|
13441
|
+
const before = prev.get(id);
|
|
13442
|
+
if (before) next.set(id, before);
|
|
13443
|
+
else next.delete(id);
|
|
13444
|
+
}
|
|
13445
|
+
return next;
|
|
13446
|
+
}
|
|
13295
13447
|
|
|
13296
13448
|
// ../../shared/git/src/index.ts
|
|
13297
13449
|
import { execFileSync } from "node:child_process";
|
|
@@ -13439,7 +13591,7 @@ var {
|
|
|
13439
13591
|
OWNER_ID,
|
|
13440
13592
|
TRANSPORT = "stdio"
|
|
13441
13593
|
} = process.env;
|
|
13442
|
-
var OWNER_MODE = Boolean(API_OWNER_TOKEN);
|
|
13594
|
+
var OWNER_MODE = Boolean(API_OWNER_TOKEN?.trim());
|
|
13443
13595
|
if (OWNER_MODE) {
|
|
13444
13596
|
if (!OWNER_ID || !OWNER_ID.startsWith("usr_")) {
|
|
13445
13597
|
console.error("[relai-mcp] owner mode requires OWNER_ID (a 'usr_\u2026' id) alongside API_OWNER_TOKEN");
|
|
@@ -13449,7 +13601,7 @@ if (OWNER_MODE) {
|
|
|
13449
13601
|
"[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
13602
|
);
|
|
13451
13603
|
} else {
|
|
13452
|
-
if (!API_SECRET) {
|
|
13604
|
+
if (!API_SECRET?.trim()) {
|
|
13453
13605
|
console.error("[relai-mcp] API_SECRET is required");
|
|
13454
13606
|
process.exit(1);
|
|
13455
13607
|
}
|
|
@@ -13472,19 +13624,24 @@ var toolConfig = OWNER_MODE ? { ownerMode: true, ownerId: OWNER_ID } : { ownerMo
|
|
|
13472
13624
|
if (OWNER_MODE) {
|
|
13473
13625
|
const OWNER_POLL_INTERVAL_MS = Number(process.env.OWNER_POLL_INTERVAL_MS ?? 6e4);
|
|
13474
13626
|
let seen = null;
|
|
13627
|
+
let polling = false;
|
|
13475
13628
|
async function pollAttention() {
|
|
13629
|
+
if (polling) return;
|
|
13630
|
+
polling = true;
|
|
13476
13631
|
try {
|
|
13477
13632
|
const [attention, active] = await Promise.all([
|
|
13478
13633
|
apiClient.getTasks({ status: "blocked,pending_verification,proposed" }),
|
|
13479
13634
|
apiClient.getTasks({ status: "in_progress" })
|
|
13480
13635
|
]);
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13485
|
-
|
|
13636
|
+
seen = await deliverAttention(
|
|
13637
|
+
seen,
|
|
13638
|
+
[...attention, ...active],
|
|
13639
|
+
(data) => server.server.sendLoggingMessage({ level: "warning", data })
|
|
13640
|
+
);
|
|
13486
13641
|
} catch (err) {
|
|
13487
13642
|
console.error("[relai-mcp] attention poll failed:", err instanceof Error ? err.message : err);
|
|
13643
|
+
} finally {
|
|
13644
|
+
polling = false;
|
|
13488
13645
|
}
|
|
13489
13646
|
}
|
|
13490
13647
|
void pollAttention();
|
|
@@ -13559,20 +13716,20 @@ async function main() {
|
|
|
13559
13716
|
const transport = new StdioServerTransport();
|
|
13560
13717
|
await server.connect(transport);
|
|
13561
13718
|
} else if (TRANSPORT === "http") {
|
|
13562
|
-
const { SSEServerTransport: SSEServerTransport2 } = await Promise.resolve().then(() => (init_sse(), sse_exports));
|
|
13563
13719
|
const http = await import("node:http");
|
|
13720
|
+
const { createHttpRequestListener: createHttpRequestListener2 } = await Promise.resolve().then(() => (init_http_transport(), http_transport_exports));
|
|
13721
|
+
const { resolveHttpCredential: resolveHttpCredential2, isBlankCredential: isBlankCredential2 } = await Promise.resolve().then(() => (init_http_auth(), http_auth_exports));
|
|
13564
13722
|
const port = Number(process.env.MCP_PORT ?? 3001);
|
|
13565
13723
|
const host = process.env.MCP_HOST ?? "127.0.0.1";
|
|
13566
|
-
const
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
});
|
|
13724
|
+
const credential = resolveHttpCredential2(process.env.MCP_HTTP_TOKEN, OWNER_MODE, API_OWNER_TOKEN, API_SECRET);
|
|
13725
|
+
if (isBlankCredential2(credential)) {
|
|
13726
|
+
console.error(
|
|
13727
|
+
"[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"
|
|
13728
|
+
);
|
|
13729
|
+
process.exit(1);
|
|
13730
|
+
}
|
|
13731
|
+
const listener = createHttpRequestListener2(server, credential);
|
|
13732
|
+
const httpServer = http.createServer(listener);
|
|
13576
13733
|
httpServer.listen(port, host, () => {
|
|
13577
13734
|
console.error(`[relai-mcp] HTTP/SSE transport listening on ${host}:${port}`);
|
|
13578
13735
|
});
|