@pitboss/cli 0.2.1 → 0.3.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 +43 -107
- package/dist/cli.js +638 -95
- package/dist/mcp.js +91 -25
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2161,9 +2161,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2161
2161
|
_chainOrCallSubCommandHook(promise, subCommand, event) {
|
|
2162
2162
|
let result = promise;
|
|
2163
2163
|
if (this._lifeCycleHooks[event] !== void 0) {
|
|
2164
|
-
this._lifeCycleHooks[event].forEach((
|
|
2164
|
+
this._lifeCycleHooks[event].forEach((hook2) => {
|
|
2165
2165
|
result = this._chainOrCall(result, () => {
|
|
2166
|
-
return
|
|
2166
|
+
return hook2(this, subCommand);
|
|
2167
2167
|
});
|
|
2168
2168
|
});
|
|
2169
2169
|
}
|
|
@@ -10864,7 +10864,7 @@ var require_lib2 = __commonJS({
|
|
|
10864
10864
|
return matches;
|
|
10865
10865
|
};
|
|
10866
10866
|
exports.analyse = analyse;
|
|
10867
|
-
var detectFile = (filepath, opts = {}) => new Promise((
|
|
10867
|
+
var detectFile = (filepath, opts = {}) => new Promise((resolve4, reject) => {
|
|
10868
10868
|
let fd;
|
|
10869
10869
|
const fs = (0, node_1.default)();
|
|
10870
10870
|
const handler = (err, buffer) => {
|
|
@@ -10874,7 +10874,7 @@ var require_lib2 = __commonJS({
|
|
|
10874
10874
|
if (err) {
|
|
10875
10875
|
reject(err);
|
|
10876
10876
|
} else if (buffer) {
|
|
10877
|
-
|
|
10877
|
+
resolve4((0, exports.detect)(buffer));
|
|
10878
10878
|
} else {
|
|
10879
10879
|
reject(new Error("No error and no buffer received"));
|
|
10880
10880
|
}
|
|
@@ -14704,10 +14704,6 @@ var require_lib3 = __commonJS({
|
|
|
14704
14704
|
}
|
|
14705
14705
|
});
|
|
14706
14706
|
|
|
14707
|
-
// src/index.ts
|
|
14708
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
14709
|
-
import { dirname as dirname4, join as join5 } from "node:path";
|
|
14710
|
-
|
|
14711
14707
|
// ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
|
|
14712
14708
|
var import_index = __toESM(require_commander(), 1);
|
|
14713
14709
|
var {
|
|
@@ -17285,13 +17281,13 @@ var PromisePolyfill = class extends Promise {
|
|
|
17285
17281
|
// Available starting from Node 22
|
|
17286
17282
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
|
|
17287
17283
|
static withResolver() {
|
|
17288
|
-
let
|
|
17284
|
+
let resolve4;
|
|
17289
17285
|
let reject;
|
|
17290
17286
|
const promise = new Promise((res, rej) => {
|
|
17291
|
-
|
|
17287
|
+
resolve4 = res;
|
|
17292
17288
|
reject = rej;
|
|
17293
17289
|
});
|
|
17294
|
-
return { promise, resolve:
|
|
17290
|
+
return { promise, resolve: resolve4, reject };
|
|
17295
17291
|
}
|
|
17296
17292
|
};
|
|
17297
17293
|
|
|
@@ -17328,7 +17324,7 @@ function createPrompt(view) {
|
|
|
17328
17324
|
});
|
|
17329
17325
|
output.mute();
|
|
17330
17326
|
const screen = new ScreenManager(rl);
|
|
17331
|
-
const { promise, resolve:
|
|
17327
|
+
const { promise, resolve: resolve4, reject } = PromisePolyfill.withResolver();
|
|
17332
17328
|
const cancel = () => reject(new CancelPromptError());
|
|
17333
17329
|
if (signal) {
|
|
17334
17330
|
const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
|
|
@@ -17359,7 +17355,7 @@ function createPrompt(view) {
|
|
|
17359
17355
|
try {
|
|
17360
17356
|
const nextView = view(config, (value) => {
|
|
17361
17357
|
if (effectsSettled) {
|
|
17362
|
-
|
|
17358
|
+
resolve4(value);
|
|
17363
17359
|
} else {
|
|
17364
17360
|
pendingDone = { value };
|
|
17365
17361
|
}
|
|
@@ -17382,7 +17378,7 @@ function createPrompt(view) {
|
|
|
17382
17378
|
if (pendingDone !== null) {
|
|
17383
17379
|
const { value } = pendingDone;
|
|
17384
17380
|
pendingDone = null;
|
|
17385
|
-
|
|
17381
|
+
resolve4(value);
|
|
17386
17382
|
}
|
|
17387
17383
|
});
|
|
17388
17384
|
};
|
|
@@ -17537,7 +17533,7 @@ var ExternalEditor = class {
|
|
|
17537
17533
|
}
|
|
17538
17534
|
runAsync(callback) {
|
|
17539
17535
|
this.createTempFile();
|
|
17540
|
-
const promise = new Promise((
|
|
17536
|
+
const promise = new Promise((resolve4, reject) => {
|
|
17541
17537
|
try {
|
|
17542
17538
|
const editorProcess = spawn(this.editor.bin, this.editorArgs(), {
|
|
17543
17539
|
shell: false,
|
|
@@ -17548,7 +17544,7 @@ var ExternalEditor = class {
|
|
|
17548
17544
|
});
|
|
17549
17545
|
editorProcess.once("exit", (code) => {
|
|
17550
17546
|
this.lastExitStatus = code ?? 0;
|
|
17551
|
-
|
|
17547
|
+
resolve4();
|
|
17552
17548
|
});
|
|
17553
17549
|
} catch (launchError) {
|
|
17554
17550
|
reject(new LaunchEditorError(launchError));
|
|
@@ -17623,7 +17619,7 @@ var editorTheme = {
|
|
|
17623
17619
|
}
|
|
17624
17620
|
};
|
|
17625
17621
|
var dist_default4 = createPrompt((config, done) => {
|
|
17626
|
-
const { waitForUserInput = true, file: { postfix = config.postfix ?? ".txt", ...fileProps } = {}, validate:
|
|
17622
|
+
const { waitForUserInput = true, file: { postfix = config.postfix ?? ".txt", ...fileProps } = {}, validate: validate3 = () => true } = config;
|
|
17627
17623
|
const theme = makeTheme(editorTheme, config.theme);
|
|
17628
17624
|
const [status, setStatus] = useState("idle");
|
|
17629
17625
|
const [value = "", setValue] = useState(config.default);
|
|
@@ -17635,7 +17631,7 @@ var dist_default4 = createPrompt((config, done) => {
|
|
|
17635
17631
|
const answer = await editAsync(value, { postfix, ...fileProps });
|
|
17636
17632
|
rl.resume();
|
|
17637
17633
|
setStatus("loading");
|
|
17638
|
-
const isValid = await
|
|
17634
|
+
const isValid = await validate3(answer);
|
|
17639
17635
|
if (isValid === true) {
|
|
17640
17636
|
setError(void 0);
|
|
17641
17637
|
setStatus("done");
|
|
@@ -17762,7 +17758,7 @@ var dist_default6 = createPrompt((config, done) => {
|
|
|
17762
17758
|
const [errorMsg, setError] = useState();
|
|
17763
17759
|
const [value, setValue] = useState("");
|
|
17764
17760
|
const prefix = usePrefix({ status, theme });
|
|
17765
|
-
async function
|
|
17761
|
+
async function validate3(value2) {
|
|
17766
17762
|
const { required, pattern, patternError = "Invalid input" } = config;
|
|
17767
17763
|
if (required && !value2) {
|
|
17768
17764
|
return "You must provide a value";
|
|
@@ -17782,7 +17778,7 @@ var dist_default6 = createPrompt((config, done) => {
|
|
|
17782
17778
|
if (isEnterKey(key)) {
|
|
17783
17779
|
const answer = value || defaultValue;
|
|
17784
17780
|
setStatus("loading");
|
|
17785
|
-
const isValid = await
|
|
17781
|
+
const isValid = await validate3(answer);
|
|
17786
17782
|
if (isValid === true) {
|
|
17787
17783
|
setValue(answer);
|
|
17788
17784
|
setStatus("done");
|
|
@@ -18908,29 +18904,189 @@ async function startCommand() {
|
|
|
18908
18904
|
}
|
|
18909
18905
|
|
|
18910
18906
|
// src/commands/token.ts
|
|
18911
|
-
import { execFileSync } from "node:child_process";
|
|
18912
|
-
import { existsSync as existsSync2, readFileSync as
|
|
18907
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
18908
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
18913
18909
|
import { homedir as homedir2 } from "node:os";
|
|
18910
|
+
import { join as join4 } from "node:path";
|
|
18911
|
+
|
|
18912
|
+
// src/lib/wake.ts
|
|
18913
|
+
import { execFileSync } from "node:child_process";
|
|
18914
|
+
import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
18915
|
+
import { createHash, randomBytes as randomBytes2 } from "node:crypto";
|
|
18916
|
+
import { join as join2, resolve } from "node:path";
|
|
18917
|
+
var EVENT_ID = /^evt_[A-Za-z0-9_-]{1,64}$/;
|
|
18918
|
+
function parseFrame(frame) {
|
|
18919
|
+
let id = null;
|
|
18920
|
+
let data = "";
|
|
18921
|
+
for (const line of frame.split("\n")) {
|
|
18922
|
+
if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
18923
|
+
else if (line.startsWith("data:")) data += line.slice(5).trimStart();
|
|
18924
|
+
}
|
|
18925
|
+
if (!data) return { id, event: null };
|
|
18926
|
+
try {
|
|
18927
|
+
return { id, event: JSON.parse(data) };
|
|
18928
|
+
} catch {
|
|
18929
|
+
return { id, event: null };
|
|
18930
|
+
}
|
|
18931
|
+
}
|
|
18932
|
+
async function readWake(stream, { idleMs = 1e3, stallMs = 75e3 } = {}) {
|
|
18933
|
+
const reader = stream.getReader();
|
|
18934
|
+
const decoder = new TextDecoder();
|
|
18935
|
+
const wake = { first: null, lastId: null, count: 0, truncated: false };
|
|
18936
|
+
let buffer = "";
|
|
18937
|
+
try {
|
|
18938
|
+
for (; ; ) {
|
|
18939
|
+
const wait = wake.first ? idleMs : stallMs;
|
|
18940
|
+
let timer;
|
|
18941
|
+
const timeout = new Promise((r) => {
|
|
18942
|
+
timer = setTimeout(() => r("timeout"), wait);
|
|
18943
|
+
});
|
|
18944
|
+
const next = await Promise.race([reader.read(), timeout]);
|
|
18945
|
+
clearTimeout(timer);
|
|
18946
|
+
if (next === "timeout") {
|
|
18947
|
+
if (wake.first) return wake;
|
|
18948
|
+
throw new Error(`event stream stalled: no data for ${Math.round(stallMs / 1e3)}s`);
|
|
18949
|
+
}
|
|
18950
|
+
if (next.done) return wake;
|
|
18951
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
18952
|
+
let sep2;
|
|
18953
|
+
while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
|
|
18954
|
+
const { id, event } = parseFrame(buffer.slice(0, sep2));
|
|
18955
|
+
buffer = buffer.slice(sep2 + 2);
|
|
18956
|
+
if (id && EVENT_ID.test(id)) wake.lastId = id;
|
|
18957
|
+
if (event?.kind === "replay.truncated" && !wake.first) {
|
|
18958
|
+
wake.truncated = true;
|
|
18959
|
+
return wake;
|
|
18960
|
+
}
|
|
18961
|
+
if (!event || event.kind === "replay.truncated") continue;
|
|
18962
|
+
wake.first ??= event;
|
|
18963
|
+
wake.count++;
|
|
18964
|
+
}
|
|
18965
|
+
}
|
|
18966
|
+
} finally {
|
|
18967
|
+
reader.cancel().catch(() => {
|
|
18968
|
+
});
|
|
18969
|
+
}
|
|
18970
|
+
}
|
|
18971
|
+
function wakeKey(agentId, folder) {
|
|
18972
|
+
let path3 = resolve(folder);
|
|
18973
|
+
try {
|
|
18974
|
+
path3 = realpathSync.native(path3);
|
|
18975
|
+
} catch {
|
|
18976
|
+
path3 = resolve(folder);
|
|
18977
|
+
}
|
|
18978
|
+
return `${agentId}.${createHash("sha256").update(path3).digest("hex").slice(0, 12)}`;
|
|
18979
|
+
}
|
|
18980
|
+
function readCursor(dir, key) {
|
|
18981
|
+
try {
|
|
18982
|
+
const raw = readFileSync3(join2(dir, `${key}.cursor`), "utf-8").trim();
|
|
18983
|
+
return EVENT_ID.test(raw) ? raw : null;
|
|
18984
|
+
} catch {
|
|
18985
|
+
return null;
|
|
18986
|
+
}
|
|
18987
|
+
}
|
|
18988
|
+
function writeAtomic(file, body) {
|
|
18989
|
+
const tmp = `${file}.${randomBytes2(6).toString("hex")}`;
|
|
18990
|
+
writeFileSync3(tmp, body, { mode: 384, flag: "wx" });
|
|
18991
|
+
renameSync2(tmp, file);
|
|
18992
|
+
}
|
|
18993
|
+
function writeCursor(dir, key, id) {
|
|
18994
|
+
if (!EVENT_ID.test(id)) return;
|
|
18995
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
18996
|
+
writeAtomic(join2(dir, `${key}.cursor`), `${id}
|
|
18997
|
+
`);
|
|
18998
|
+
}
|
|
18999
|
+
function followupCount(dir, key) {
|
|
19000
|
+
try {
|
|
19001
|
+
const n = Number(readFileSync3(join2(dir, `${key}.followups`), "utf-8").trim());
|
|
19002
|
+
return Number.isInteger(n) && n > 0 ? n : 0;
|
|
19003
|
+
} catch {
|
|
19004
|
+
return 0;
|
|
19005
|
+
}
|
|
19006
|
+
}
|
|
19007
|
+
function recordFollowup(dir, key) {
|
|
19008
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
19009
|
+
writeAtomic(join2(dir, `${key}.followups`), `${followupCount(dir, key) + 1}
|
|
19010
|
+
`);
|
|
19011
|
+
}
|
|
19012
|
+
function resetFollowups(dir, key) {
|
|
19013
|
+
rmSync3(join2(dir, `${key}.followups`), { force: true });
|
|
19014
|
+
}
|
|
19015
|
+
function resetAgentFollowups(dir, agentId) {
|
|
19016
|
+
let names;
|
|
19017
|
+
try {
|
|
19018
|
+
names = readdirSync(dir);
|
|
19019
|
+
} catch {
|
|
19020
|
+
return;
|
|
19021
|
+
}
|
|
19022
|
+
for (const name of names) {
|
|
19023
|
+
if (name.startsWith(`${agentId}.`) && /^agent_[A-Za-z0-9_-]+\.[0-9a-f]{12}\.followups$/.test(name)) rmSync3(join2(dir, name), { force: true });
|
|
19024
|
+
}
|
|
19025
|
+
}
|
|
19026
|
+
function isAlive(pid) {
|
|
19027
|
+
try {
|
|
19028
|
+
process.kill(pid, 0);
|
|
19029
|
+
return true;
|
|
19030
|
+
} catch (err) {
|
|
19031
|
+
return err.code === "EPERM";
|
|
19032
|
+
}
|
|
19033
|
+
}
|
|
19034
|
+
function isWatcherCommand(command) {
|
|
19035
|
+
return /(^|\s)watch --once(\s|$)/.test(command);
|
|
19036
|
+
}
|
|
19037
|
+
function psCommand(pid) {
|
|
19038
|
+
if (process.platform === "win32") return "watch --once";
|
|
19039
|
+
try {
|
|
19040
|
+
return execFileSync("ps", ["-o", "command=", "-p", String(pid)], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
19041
|
+
} catch {
|
|
19042
|
+
return "";
|
|
19043
|
+
}
|
|
19044
|
+
}
|
|
19045
|
+
function watcherAlive(dir, key, { commandOf = psCommand } = {}) {
|
|
19046
|
+
let names;
|
|
19047
|
+
try {
|
|
19048
|
+
names = readdirSync(dir);
|
|
19049
|
+
} catch {
|
|
19050
|
+
return false;
|
|
19051
|
+
}
|
|
19052
|
+
return names.some((name) => {
|
|
19053
|
+
const m = /^(agent_[A-Za-z0-9_-]+\.[0-9a-f]{12})\.(\d+)\.pid$/.exec(name);
|
|
19054
|
+
if (!m || m[1] !== key) return false;
|
|
19055
|
+
const pid = Number(m[2]);
|
|
19056
|
+
if (!isAlive(pid)) {
|
|
19057
|
+
rmSync3(join2(dir, name), { force: true });
|
|
19058
|
+
return false;
|
|
19059
|
+
}
|
|
19060
|
+
return isWatcherCommand(commandOf(pid));
|
|
19061
|
+
});
|
|
19062
|
+
}
|
|
19063
|
+
function registerWatcher(dir, key) {
|
|
19064
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
19065
|
+
const file = join2(dir, `${key}.${process.pid}.pid`);
|
|
19066
|
+
writeAtomic(file, `${process.pid}
|
|
19067
|
+
`);
|
|
19068
|
+
return () => rmSync3(file, { force: true });
|
|
19069
|
+
}
|
|
18914
19070
|
|
|
18915
19071
|
// src/lib/runtimes.ts
|
|
18916
|
-
import { join as
|
|
19072
|
+
import { join as join3 } from "node:path";
|
|
18917
19073
|
var RUNTIMES = ["claude", "cursor", "windsurf", "gemini", "copilot", "gpt", "mcp"];
|
|
18918
19074
|
function runtimeTargets(workerType, { home, repo: repo2 }) {
|
|
18919
19075
|
switch (workerType) {
|
|
18920
19076
|
case "claude":
|
|
18921
|
-
return [
|
|
19077
|
+
return [join3(repo2, ".mcp.json")];
|
|
18922
19078
|
case "cursor":
|
|
18923
|
-
return [
|
|
19079
|
+
return [join3(repo2, ".cursor", "mcp.json")];
|
|
18924
19080
|
case "windsurf":
|
|
18925
|
-
return [
|
|
19081
|
+
return [join3(home, ".codeium", "windsurf", "mcp_config.json")];
|
|
18926
19082
|
case "gemini":
|
|
18927
|
-
return [
|
|
19083
|
+
return [join3(home, ".gemini", "settings.json")];
|
|
18928
19084
|
case "copilot":
|
|
18929
|
-
return [
|
|
19085
|
+
return [join3(home, ".config", "github-copilot", "mcp.json")];
|
|
18930
19086
|
case "gpt":
|
|
18931
|
-
return [
|
|
19087
|
+
return [join3(home, ".codex", "mcp.json")];
|
|
18932
19088
|
case "mcp":
|
|
18933
|
-
return [
|
|
19089
|
+
return [join3(home, ".config", "pitboss", "config.json")];
|
|
18934
19090
|
// A person, not a runtime. Nothing to configure.
|
|
18935
19091
|
case "human":
|
|
18936
19092
|
return [];
|
|
@@ -18960,9 +19116,16 @@ function mergeMcpServer(existing, name, entry) {
|
|
|
18960
19116
|
mcpServers: { ...servers, [name]: entry }
|
|
18961
19117
|
};
|
|
18962
19118
|
}
|
|
19119
|
+
function dropLegacyPitbossEntry(config) {
|
|
19120
|
+
const legacy = config.mcpServers.relai;
|
|
19121
|
+
const ours = Array.isArray(legacy?.args) && legacy.args.some((a) => typeof a === "string" && /^(@pitboss\/cli|@getrelai\/mcp-server)(@|$)/.test(a));
|
|
19122
|
+
if (!ours) return config;
|
|
19123
|
+
const { relai: _dropped, ...rest } = config.mcpServers;
|
|
19124
|
+
return { ...config, mcpServers: rest };
|
|
19125
|
+
}
|
|
18963
19126
|
function allRuntimeTargets({ home, repo: repo2 }) {
|
|
18964
19127
|
const fromRuntimes = RUNTIMES.flatMap((w) => runtimeTargets(w, { home, repo: repo2 }));
|
|
18965
|
-
return [.../* @__PURE__ */ new Set([...fromRuntimes,
|
|
19128
|
+
return [.../* @__PURE__ */ new Set([...fromRuntimes, join3(home, ".claude.json")])];
|
|
18966
19129
|
}
|
|
18967
19130
|
function holdsRelaiToken(existing, token2) {
|
|
18968
19131
|
if (typeof token2 !== "string" || token2 === "") return false;
|
|
@@ -18976,18 +19139,20 @@ function holdsRelaiToken(existing, token2) {
|
|
|
18976
19139
|
if (typeof scope !== "object" || scope === null || Array.isArray(scope)) return false;
|
|
18977
19140
|
const servers = scope.mcpServers;
|
|
18978
19141
|
if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return false;
|
|
18979
|
-
|
|
18980
|
-
|
|
18981
|
-
|
|
18982
|
-
|
|
18983
|
-
|
|
19142
|
+
return ["pitboss", "relai"].some((name) => {
|
|
19143
|
+
const server = servers[name];
|
|
19144
|
+
if (typeof server !== "object" || server === null || Array.isArray(server)) return false;
|
|
19145
|
+
const env2 = server.env;
|
|
19146
|
+
if (typeof env2 !== "object" || env2 === null || Array.isArray(env2)) return false;
|
|
19147
|
+
return env2.API_SECRET === token2;
|
|
19148
|
+
});
|
|
18984
19149
|
});
|
|
18985
19150
|
}
|
|
18986
19151
|
|
|
18987
19152
|
// src/commands/token.ts
|
|
18988
19153
|
function gitRoot(cwd) {
|
|
18989
19154
|
try {
|
|
18990
|
-
return
|
|
19155
|
+
return execFileSync2("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim() || cwd;
|
|
18991
19156
|
} catch {
|
|
18992
19157
|
return cwd;
|
|
18993
19158
|
}
|
|
@@ -18998,7 +19163,7 @@ function configsHoldingToken(oldToken) {
|
|
|
18998
19163
|
for (const target of allRuntimeTargets({ home: homedir2(), repo: repo2 })) {
|
|
18999
19164
|
if (target === configPath() || !existsSync2(target)) continue;
|
|
19000
19165
|
try {
|
|
19001
|
-
if (holdsRelaiToken(JSON.parse(
|
|
19166
|
+
if (holdsRelaiToken(JSON.parse(readFileSync4(target, "utf-8")), oldToken)) found.push(target);
|
|
19002
19167
|
} catch {
|
|
19003
19168
|
found.push(`${target} ${source_default.dim("(unreadable, check by hand)")}`);
|
|
19004
19169
|
}
|
|
@@ -19015,6 +19180,7 @@ async function tokenRotateCommand() {
|
|
|
19015
19180
|
let revoked;
|
|
19016
19181
|
try {
|
|
19017
19182
|
({ token: token2, tokenId, revoked } = await client.rotateToken(config.agentId));
|
|
19183
|
+
resetAgentFollowups(join4(configDir(), "wake"), config.agentId);
|
|
19018
19184
|
s.succeed(source_default.green("New token issued"));
|
|
19019
19185
|
} catch (err) {
|
|
19020
19186
|
s.fail(source_default.red("Rotate failed"));
|
|
@@ -19110,10 +19276,10 @@ ${live.length} live, ${rows.length - live.length} revoked.`);
|
|
|
19110
19276
|
import { resolve as resolvePath } from "node:path";
|
|
19111
19277
|
|
|
19112
19278
|
// ../../shared/git/src/index.ts
|
|
19113
|
-
import { execFileSync as
|
|
19279
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
19114
19280
|
function getGitRoot(cwd) {
|
|
19115
19281
|
try {
|
|
19116
|
-
const out =
|
|
19282
|
+
const out = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
19117
19283
|
cwd,
|
|
19118
19284
|
stdio: ["ignore", "pipe", "ignore"],
|
|
19119
19285
|
encoding: "utf-8"
|
|
@@ -19125,7 +19291,7 @@ function getGitRoot(cwd) {
|
|
|
19125
19291
|
}
|
|
19126
19292
|
function getOriginUrl(cwd) {
|
|
19127
19293
|
try {
|
|
19128
|
-
const out =
|
|
19294
|
+
const out = execFileSync3("git", ["remote", "get-url", "origin"], {
|
|
19129
19295
|
cwd,
|
|
19130
19296
|
stdio: ["ignore", "pipe", "ignore"],
|
|
19131
19297
|
encoding: "utf-8"
|
|
@@ -19154,18 +19320,18 @@ function repoNameFromUrl(url) {
|
|
|
19154
19320
|
|
|
19155
19321
|
// src/lib/agents-state.ts
|
|
19156
19322
|
import { homedir as homedir3 } from "node:os";
|
|
19157
|
-
import { join as
|
|
19158
|
-
import { readFileSync as
|
|
19159
|
-
import { createHash } from "node:crypto";
|
|
19323
|
+
import { join as join5, resolve as resolve2, dirname as dirname2 } from "node:path";
|
|
19324
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "node:fs";
|
|
19325
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
19160
19326
|
var stateOverridden = () => Boolean(process.env.PITBOSS_AGENTS_STATE || process.env.RELAI_AGENTS_STATE);
|
|
19161
19327
|
function statePath() {
|
|
19162
|
-
return process.env.PITBOSS_AGENTS_STATE || process.env.RELAI_AGENTS_STATE ||
|
|
19328
|
+
return process.env.PITBOSS_AGENTS_STATE || process.env.RELAI_AGENTS_STATE || join5(homedir3(), ".config", "pitboss", "agents.json");
|
|
19163
19329
|
}
|
|
19164
19330
|
function readAgentsState() {
|
|
19165
19331
|
const p = readableFrom(statePath(), legacyHomePath("agents.json"), stateOverridden());
|
|
19166
19332
|
if (!existsSync3(p)) return { agents: [] };
|
|
19167
19333
|
try {
|
|
19168
|
-
const raw = JSON.parse(
|
|
19334
|
+
const raw = JSON.parse(readFileSync5(p, "utf-8"));
|
|
19169
19335
|
if (!raw || !Array.isArray(raw.agents)) return { agents: [] };
|
|
19170
19336
|
return raw;
|
|
19171
19337
|
} catch {
|
|
@@ -19175,12 +19341,12 @@ function readAgentsState() {
|
|
|
19175
19341
|
function writeAgentsState(state) {
|
|
19176
19342
|
const p = statePath();
|
|
19177
19343
|
const migrating = !existsSync3(p);
|
|
19178
|
-
|
|
19179
|
-
|
|
19344
|
+
mkdirSync3(dirname2(p), { recursive: true });
|
|
19345
|
+
writeFileSync4(p, JSON.stringify(state, null, 2));
|
|
19180
19346
|
if (migrating) retireLegacy(legacyHomePath("agents.json"), p, stateOverridden());
|
|
19181
19347
|
}
|
|
19182
19348
|
function hashToken(token2) {
|
|
19183
|
-
return
|
|
19349
|
+
return createHash2("sha256").update(token2).digest("hex").slice(0, 16);
|
|
19184
19350
|
}
|
|
19185
19351
|
var WorkingDirCollisionError = class extends Error {
|
|
19186
19352
|
constructor(existing, newAgentId, repoName) {
|
|
@@ -19197,9 +19363,9 @@ var WorkingDirCollisionError = class extends Error {
|
|
|
19197
19363
|
repoName;
|
|
19198
19364
|
};
|
|
19199
19365
|
function claimWorkingDir(claim) {
|
|
19200
|
-
const absDir =
|
|
19366
|
+
const absDir = resolve2(claim.workingDir);
|
|
19201
19367
|
const state = readAgentsState();
|
|
19202
|
-
const existing = state.agents.find((a) =>
|
|
19368
|
+
const existing = state.agents.find((a) => resolve2(a.workingDir) === absDir);
|
|
19203
19369
|
if (existing && existing.agentId !== claim.agentId) {
|
|
19204
19370
|
throw new WorkingDirCollisionError(existing, claim.agentId, "");
|
|
19205
19371
|
}
|
|
@@ -19349,8 +19515,8 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
19349
19515
|
process.exit(1);
|
|
19350
19516
|
}
|
|
19351
19517
|
if (repoUrl) {
|
|
19352
|
-
const
|
|
19353
|
-
if (!
|
|
19518
|
+
const gitRoot3 = getGitRoot(requestedDir);
|
|
19519
|
+
if (!gitRoot3) {
|
|
19354
19520
|
const repoName = repoNameFromUrl(repoUrl);
|
|
19355
19521
|
console.error(source_default.red(`Not in a git repo. Clone first:`));
|
|
19356
19522
|
console.error(` git clone ${repoUrl}`);
|
|
@@ -19358,7 +19524,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
19358
19524
|
console.error(` relai login \u2026`);
|
|
19359
19525
|
process.exit(1);
|
|
19360
19526
|
}
|
|
19361
|
-
const origin = getOriginUrl(
|
|
19527
|
+
const origin = getOriginUrl(gitRoot3);
|
|
19362
19528
|
if (origin && normalizeRepoUrl(origin) !== normalizeRepoUrl(repoUrl)) {
|
|
19363
19529
|
console.error(
|
|
19364
19530
|
source_default.red(`You're in ${origin}, but this agent is for ${repoUrl}.`)
|
|
@@ -19368,7 +19534,7 @@ Already logged in as ${source_default.bold(existing.agentName)} (${existing.agen
|
|
|
19368
19534
|
);
|
|
19369
19535
|
process.exit(1);
|
|
19370
19536
|
}
|
|
19371
|
-
workingDir =
|
|
19537
|
+
workingDir = gitRoot3;
|
|
19372
19538
|
}
|
|
19373
19539
|
try {
|
|
19374
19540
|
claimWorkingDir({
|
|
@@ -19432,10 +19598,9 @@ ${source_default.cyan(JSON.stringify({
|
|
|
19432
19598
|
}
|
|
19433
19599
|
|
|
19434
19600
|
// src/commands/join.ts
|
|
19435
|
-
import { execFileSync as
|
|
19436
|
-
import { existsSync as
|
|
19437
|
-
import {
|
|
19438
|
-
import { basename, dirname as dirname3, join as join4 } from "node:path";
|
|
19601
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
19602
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync8, appendFileSync, realpathSync as realpathSync2 } from "node:fs";
|
|
19603
|
+
import { basename, dirname as dirname5, join as join8 } from "node:path";
|
|
19439
19604
|
import { homedir as homedir4 } from "node:os";
|
|
19440
19605
|
|
|
19441
19606
|
// src/lib/mcp-entry.ts
|
|
@@ -19444,11 +19609,135 @@ var MCP_SERVER_ENTRY = {
|
|
|
19444
19609
|
args: ["-y", "@pitboss/cli", "mcp"]
|
|
19445
19610
|
};
|
|
19446
19611
|
|
|
19612
|
+
// src/lib/agent-files.ts
|
|
19613
|
+
var REFUSED_MARKER = "PITBOSS-CONFIG-REFUSED";
|
|
19614
|
+
var shq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19615
|
+
function watchCommandLine(cli, dir) {
|
|
19616
|
+
return `${cli} watch --once --dir ${shq(dir)}`;
|
|
19617
|
+
}
|
|
19618
|
+
function pinnedCli(version) {
|
|
19619
|
+
return `npx -y @pitboss/cli@${version}`;
|
|
19620
|
+
}
|
|
19621
|
+
var isOurs = (command) => /@pitboss\/cli\S* hook /.test(String(command ?? ""));
|
|
19622
|
+
function objectField(obj, key) {
|
|
19623
|
+
const v = obj[key];
|
|
19624
|
+
if (v === void 0) return {};
|
|
19625
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) throw new Error(`existing "${key}" is not an object; refusing to overwrite it`);
|
|
19626
|
+
return v;
|
|
19627
|
+
}
|
|
19628
|
+
function base(existing) {
|
|
19629
|
+
if (existing === null) return {};
|
|
19630
|
+
if (typeof existing !== "object" || Array.isArray(existing)) throw new Error("existing settings are not a JSON object; refusing to overwrite them");
|
|
19631
|
+
return existing;
|
|
19632
|
+
}
|
|
19633
|
+
function claudeLocalSettings(existing, cli) {
|
|
19634
|
+
const settings = base(existing);
|
|
19635
|
+
const hooks = objectField(settings, "hooks");
|
|
19636
|
+
const servers = Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : [];
|
|
19637
|
+
const withHook = (event) => {
|
|
19638
|
+
const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
19639
|
+
const kept = groups.filter((g) => !(g.hooks ?? []).some((h) => isOurs(h.command)));
|
|
19640
|
+
return [...kept, { hooks: [{ type: "command", command: `${cli} hook claude --event=${event}` }] }];
|
|
19641
|
+
};
|
|
19642
|
+
return {
|
|
19643
|
+
...settings,
|
|
19644
|
+
enabledMcpjsonServers: servers.includes("pitboss") ? servers : [...servers, "pitboss"],
|
|
19645
|
+
hooks: { ...hooks, SessionStart: withHook("SessionStart"), Stop: withHook("Stop") }
|
|
19646
|
+
};
|
|
19647
|
+
}
|
|
19648
|
+
function cursorHooks(existing, cli, dir) {
|
|
19649
|
+
const file = base(existing);
|
|
19650
|
+
const hooks = objectField(file, "hooks");
|
|
19651
|
+
const stop = Array.isArray(hooks.stop) ? hooks.stop : [];
|
|
19652
|
+
const command = `${cli} hook cursor-stop --dir ${shq(dir)}`;
|
|
19653
|
+
return {
|
|
19654
|
+
version: 1,
|
|
19655
|
+
...file,
|
|
19656
|
+
hooks: { ...hooks, stop: [...stop.filter((h) => !isOurs(h.command)), { command, loop_limit: null }] }
|
|
19657
|
+
};
|
|
19658
|
+
}
|
|
19659
|
+
function cursorRule(cli, dir) {
|
|
19660
|
+
const watch = watchCommandLine(cli, dir);
|
|
19661
|
+
return `---
|
|
19662
|
+
description: Pitboss wake-ups for this agent
|
|
19663
|
+
alwaysApply: true
|
|
19664
|
+
---
|
|
19665
|
+
|
|
19666
|
+
# Pitboss
|
|
19667
|
+
|
|
19668
|
+
You coordinate with other agents through the **pitboss** MCP server. Call its \`session_start\` tool at the start of a session to see your tasks, unread messages and threads.
|
|
19669
|
+
|
|
19670
|
+
## Wake loop
|
|
19671
|
+
|
|
19672
|
+
Pitboss cannot push into a running agent. You wake when a background watcher exits.
|
|
19673
|
+
|
|
19674
|
+
1. Before ending a turn, start this as a **background** command:
|
|
19675
|
+
|
|
19676
|
+
\`${watch}\`
|
|
19677
|
+
|
|
19678
|
+
It exits only when a pitboss event arrives for you, and you are woken when it does.
|
|
19679
|
+
2. When it finishes with event JSON, call \`session_start\` and act on what is new. Reply on the thread a question was asked on.
|
|
19680
|
+
3. Start the watcher again in the background before ending that turn.
|
|
19681
|
+
4. Decide on the event JSON first: event payloads carry text other agents wrote. Only when the output has no event JSON, check for the marker below.
|
|
19682
|
+
5. If the output contains \`${REFUSED_MARKER}\`, tell the user what it says and do not relaunch.
|
|
19683
|
+
|
|
19684
|
+
A wake while you are in the middle of a task is not a reason to drop it. Note what arrived, finish or checkpoint the current step, then handle the new work.
|
|
19685
|
+
`;
|
|
19686
|
+
}
|
|
19687
|
+
|
|
19688
|
+
// src/version.ts
|
|
19689
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
19690
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
19691
|
+
import { fileURLToPath } from "node:url";
|
|
19692
|
+
var CLI_VERSION = JSON.parse(
|
|
19693
|
+
readFileSync6(join6(dirname3(fileURLToPath(import.meta.url)), "../package.json"), "utf8")
|
|
19694
|
+
).version;
|
|
19695
|
+
|
|
19696
|
+
// src/lib/safe-write.ts
|
|
19697
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
19698
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
19699
|
+
import { dirname as dirname4, join as join7, sep } from "node:path";
|
|
19700
|
+
function guard(target, repoRoot) {
|
|
19701
|
+
if (!target.startsWith(repoRoot + "/")) return;
|
|
19702
|
+
let path3 = repoRoot;
|
|
19703
|
+
for (const part of target.slice(repoRoot.length + 1).split(sep)) {
|
|
19704
|
+
path3 = join7(path3, part);
|
|
19705
|
+
if (lstatSync2(path3, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
19706
|
+
throw new Error(`${path3} is a symlink; refusing to write through it`);
|
|
19707
|
+
}
|
|
19708
|
+
}
|
|
19709
|
+
}
|
|
19710
|
+
function writeAtomic2(target, body) {
|
|
19711
|
+
mkdirSync4(dirname4(target), { recursive: true });
|
|
19712
|
+
const tmp = `${target}.relai-${randomBytes3(8).toString("hex")}`;
|
|
19713
|
+
writeFileSync5(tmp, body, { mode: 384, flag: "wx" });
|
|
19714
|
+
renameSync3(tmp, target);
|
|
19715
|
+
}
|
|
19716
|
+
function writeRepoFile(target, body, repoRoot) {
|
|
19717
|
+
guard(target, repoRoot);
|
|
19718
|
+
writeAtomic2(target, body);
|
|
19719
|
+
}
|
|
19720
|
+
function writeMergedJson(target, merge, repoRoot) {
|
|
19721
|
+
guard(target, repoRoot);
|
|
19722
|
+
let existing = null;
|
|
19723
|
+
if (existsSync4(target)) {
|
|
19724
|
+
const raw = readFileSync7(target, "utf-8").trim();
|
|
19725
|
+
if (raw) {
|
|
19726
|
+
try {
|
|
19727
|
+
existing = JSON.parse(raw);
|
|
19728
|
+
} catch {
|
|
19729
|
+
throw new Error(`${target} is not valid JSON; fix or move it, then run join again`);
|
|
19730
|
+
}
|
|
19731
|
+
}
|
|
19732
|
+
}
|
|
19733
|
+
writeAtomic2(target, JSON.stringify(merge(existing), null, 2) + "\n");
|
|
19734
|
+
}
|
|
19735
|
+
|
|
19447
19736
|
// src/commands/join.ts
|
|
19448
19737
|
var DEFAULT_API = "https://api.pitboss.dev";
|
|
19449
19738
|
function git(args, cwd) {
|
|
19450
19739
|
try {
|
|
19451
|
-
return
|
|
19740
|
+
return execFileSync4("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
19452
19741
|
} catch {
|
|
19453
19742
|
return null;
|
|
19454
19743
|
}
|
|
@@ -19500,40 +19789,52 @@ async function handshake(api, repoId, team) {
|
|
|
19500
19789
|
}
|
|
19501
19790
|
return { attempted: team.length, delivered };
|
|
19502
19791
|
}
|
|
19503
|
-
function
|
|
19504
|
-
|
|
19505
|
-
if (
|
|
19506
|
-
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
|
|
19792
|
+
function wakeFiles(workerType, root) {
|
|
19793
|
+
const cli = pinnedCli(CLI_VERSION);
|
|
19794
|
+
if (workerType === "claude") {
|
|
19795
|
+
return [{ path: join8(root, ".claude", "settings.local.json"), write: (p) => writeMergedJson(p, (e) => claudeLocalSettings(e, cli), root) }];
|
|
19796
|
+
}
|
|
19797
|
+
if (workerType === "cursor") {
|
|
19798
|
+
return [
|
|
19799
|
+
{ path: join8(root, ".cursor", "hooks.json"), write: (p) => writeMergedJson(p, (e) => cursorHooks(e, cli, root), root) },
|
|
19800
|
+
{
|
|
19801
|
+
path: join8(root, ".cursor", "rules", "pitboss.mdc"),
|
|
19802
|
+
write: (p) => {
|
|
19803
|
+
if (existsSync5(p) && !readFileSync8(p, "utf-8").includes("description: Pitboss wake-ups")) {
|
|
19804
|
+
throw new Error(`${p} exists and was not written by pitboss; leaving it alone`);
|
|
19805
|
+
}
|
|
19806
|
+
writeRepoFile(p, cursorRule(cli, root), root);
|
|
19807
|
+
}
|
|
19512
19808
|
}
|
|
19513
|
-
|
|
19809
|
+
];
|
|
19514
19810
|
}
|
|
19515
|
-
|
|
19516
|
-
|
|
19517
|
-
|
|
19518
|
-
|
|
19811
|
+
return [];
|
|
19812
|
+
}
|
|
19813
|
+
function trackedIn(root, rel) {
|
|
19814
|
+
const icase = git(["config", "--bool", "core.ignorecase"], root) === "true";
|
|
19815
|
+
return git(["ls-files", "--error-unmatch", "--", icase ? `:(icase)${rel}` : rel], root) !== null;
|
|
19519
19816
|
}
|
|
19520
19817
|
function isTracked(rawTarget) {
|
|
19521
|
-
const target =
|
|
19522
|
-
|
|
19818
|
+
const target = existsSync5(rawTarget) ? realpathSync2.native(rawTarget) : rawTarget;
|
|
19819
|
+
let dir = dirname5(target);
|
|
19820
|
+
while (!existsSync5(dir)) dir = dirname5(dir);
|
|
19523
19821
|
const root = git(["rev-parse", "--show-toplevel"], dir);
|
|
19524
19822
|
if (!root) return false;
|
|
19525
|
-
|
|
19823
|
+
const realRoot = realpathSync2.native(root);
|
|
19824
|
+
const realDir = realpathSync2.native(dir);
|
|
19825
|
+
const rel = (realDir + target.slice(dir.length)).slice(realRoot.length + 1);
|
|
19826
|
+
return trackedIn(root, rel);
|
|
19526
19827
|
}
|
|
19527
19828
|
function excludeIfUntracked(repoRoot, target) {
|
|
19528
19829
|
if (!target.startsWith(repoRoot + "/")) return;
|
|
19529
|
-
const rel = target.slice(repoRoot.length + 1);
|
|
19830
|
+
const rel = realpathSync2.native(target).slice(realpathSync2.native(repoRoot).length + 1);
|
|
19530
19831
|
if (git(["check-ignore", "-q", rel], repoRoot) !== null) return;
|
|
19531
|
-
if (
|
|
19832
|
+
if (trackedIn(repoRoot, rel)) return;
|
|
19532
19833
|
const dir = git(["rev-parse", "--git-common-dir"], repoRoot);
|
|
19533
19834
|
if (!dir) return;
|
|
19534
|
-
const exclude =
|
|
19535
|
-
|
|
19536
|
-
const body =
|
|
19835
|
+
const exclude = join8(dir.startsWith("/") ? dir : join8(repoRoot, dir), "info", "exclude");
|
|
19836
|
+
mkdirSync5(dirname5(exclude), { recursive: true });
|
|
19837
|
+
const body = existsSync5(exclude) ? readFileSync8(exclude, "utf-8") : "";
|
|
19537
19838
|
if (!body.split("\n").some((l) => l.trim() === rel)) appendFileSync(exclude, `
|
|
19538
19839
|
${rel}
|
|
19539
19840
|
`);
|
|
@@ -19612,6 +19913,7 @@ async function run(opts) {
|
|
|
19612
19913
|
const skipped = [];
|
|
19613
19914
|
const failed = [];
|
|
19614
19915
|
let wroteCliConfig = "";
|
|
19916
|
+
const wakeWarnings = [];
|
|
19615
19917
|
for (const invite of invites) {
|
|
19616
19918
|
const accepted = await postJson(`${api}/auth/accept-invite`, {
|
|
19617
19919
|
code: invite.code,
|
|
@@ -19646,7 +19948,7 @@ async function run(opts) {
|
|
|
19646
19948
|
writeConfig({ apiUrl: api, apiToken: agent.token, agentId: agent.data.id, agentName: invite.name, repoId, specialization: invite.specialization ?? void 0 });
|
|
19647
19949
|
wroteCliConfig = invite.name;
|
|
19648
19950
|
} else {
|
|
19649
|
-
|
|
19951
|
+
writeMergedJson(dest, (e) => dropLegacyPitbossEntry(mergeMcpServer(e, "pitboss", { ...MCP_SERVER_ENTRY, env: env2 })), root);
|
|
19650
19952
|
}
|
|
19651
19953
|
written.push(dest);
|
|
19652
19954
|
excludeIfUntracked(root, dest);
|
|
@@ -19655,6 +19957,21 @@ async function run(opts) {
|
|
|
19655
19957
|
failed.push({ name: invite.name, id: agent.data.id, why: err instanceof Error ? err.message : String(err) });
|
|
19656
19958
|
continue;
|
|
19657
19959
|
}
|
|
19960
|
+
if (written.length) {
|
|
19961
|
+
for (const file of wakeFiles(invite.workerType, root)) {
|
|
19962
|
+
if (isTracked(file.path)) {
|
|
19963
|
+
skipped.push({ name: invite.name, target: file.path });
|
|
19964
|
+
continue;
|
|
19965
|
+
}
|
|
19966
|
+
try {
|
|
19967
|
+
file.write(file.path);
|
|
19968
|
+
written.push(file.path);
|
|
19969
|
+
excludeIfUntracked(root, file.path);
|
|
19970
|
+
} catch (err) {
|
|
19971
|
+
wakeWarnings.push(`${invite.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
19972
|
+
}
|
|
19973
|
+
}
|
|
19974
|
+
}
|
|
19658
19975
|
connected.push({ name: invite.name, workerType: invite.workerType, targets: written });
|
|
19659
19976
|
team.push({ name: invite.name, id: agent.data.id, token: agent.token });
|
|
19660
19977
|
const where = written.length ? "" : source_default.yellow(" no config written");
|
|
@@ -19669,10 +19986,14 @@ async function run(opts) {
|
|
|
19669
19986
|
for (const f of failed) console.log(source_default.red(` ${f.name} (${f.id}): ${f.why}`));
|
|
19670
19987
|
}
|
|
19671
19988
|
if (skipped.length) {
|
|
19672
|
-
console.log(source_default.red("\n Refused to write
|
|
19989
|
+
console.log(source_default.red("\n Refused to write into a file git tracks:"));
|
|
19673
19990
|
for (const s of skipped) console.log(source_default.red(` ${s.name} -> ${s.target.replace(home, "~")}`));
|
|
19674
19991
|
console.log(source_default.dim(" Untrack it (git rm --cached <file>) and run join again, or configure that agent by hand."));
|
|
19675
19992
|
}
|
|
19993
|
+
if (wakeWarnings.length) {
|
|
19994
|
+
console.log(source_default.yellow("\n Wake-up hooks not installed, so these agents only see new work when prompted:"));
|
|
19995
|
+
for (const w of wakeWarnings) console.log(source_default.yellow(` ${w}`));
|
|
19996
|
+
}
|
|
19676
19997
|
const shook = await handshake(api, repoId, team);
|
|
19677
19998
|
if (shook.attempted > 0) {
|
|
19678
19999
|
const ok = shook.delivered === shook.attempted;
|
|
@@ -19705,6 +20026,67 @@ async function mcpCommand() {
|
|
|
19705
20026
|
await import(new URL("mcp.js", import.meta.url).href);
|
|
19706
20027
|
}
|
|
19707
20028
|
|
|
20029
|
+
// src/commands/watch.ts
|
|
20030
|
+
import { join as join10 } from "node:path";
|
|
20031
|
+
|
|
20032
|
+
// src/lib/agent-creds.ts
|
|
20033
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
20034
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9 } from "node:fs";
|
|
20035
|
+
import { dirname as dirname6, join as join9, resolve as resolve3 } from "node:path";
|
|
20036
|
+
var SERVER_NAMES = ["pitboss", "relai"];
|
|
20037
|
+
var AGENT_ID = /^agent_[A-Za-z0-9_-]{1,64}$/;
|
|
20038
|
+
function validate2(raw, source) {
|
|
20039
|
+
const { apiUrl, token: token2, agentId } = raw;
|
|
20040
|
+
if (typeof apiUrl !== "string" || typeof token2 !== "string" || typeof agentId !== "string" || !token2) {
|
|
20041
|
+
return { ok: false, reason: `${source} is missing API_URL, API_SECRET or AGENT_ID` };
|
|
20042
|
+
}
|
|
20043
|
+
if (!/^https?:\/\/[^\s]+$/.test(apiUrl)) return { ok: false, reason: `${source}: API_URL must be an http(s) URL` };
|
|
20044
|
+
if (!AGENT_ID.test(agentId)) return { ok: false, reason: `${source}: AGENT_ID is not an agent id` };
|
|
20045
|
+
if (/[\s\p{Cc}]/u.test(token2)) return { ok: false, reason: `${source}: API_SECRET contains whitespace or control characters` };
|
|
20046
|
+
return { ok: true, creds: { apiUrl: apiUrl.replace(/\/+$/, ""), token: token2, agentId }, source };
|
|
20047
|
+
}
|
|
20048
|
+
function fromMcpFile(file) {
|
|
20049
|
+
if (!existsSync6(file)) return null;
|
|
20050
|
+
let parsed;
|
|
20051
|
+
try {
|
|
20052
|
+
parsed = JSON.parse(readFileSync9(file, "utf-8"));
|
|
20053
|
+
} catch {
|
|
20054
|
+
return null;
|
|
20055
|
+
}
|
|
20056
|
+
const servers = parsed?.mcpServers;
|
|
20057
|
+
if (!servers || typeof servers !== "object") return null;
|
|
20058
|
+
for (const name of SERVER_NAMES) {
|
|
20059
|
+
const env2 = servers[name]?.env;
|
|
20060
|
+
if (env2 && typeof env2 === "object") return env2;
|
|
20061
|
+
}
|
|
20062
|
+
return null;
|
|
20063
|
+
}
|
|
20064
|
+
function gitRoot2(dir) {
|
|
20065
|
+
try {
|
|
20066
|
+
return execFileSync5("git", ["rev-parse", "--show-toplevel"], { cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
20067
|
+
} catch {
|
|
20068
|
+
return null;
|
|
20069
|
+
}
|
|
20070
|
+
}
|
|
20071
|
+
function resolveAgentCreds({ dir, env: env2, cliConfig }) {
|
|
20072
|
+
if (env2.API_URL && env2.API_SECRET && env2.AGENT_ID) {
|
|
20073
|
+
return validate2({ apiUrl: env2.API_URL, token: env2.API_SECRET, agentId: env2.AGENT_ID }, "environment");
|
|
20074
|
+
}
|
|
20075
|
+
const start = resolve3(dir);
|
|
20076
|
+
const stop = gitRoot2(start) ?? start;
|
|
20077
|
+
for (let cur = start; ; cur = dirname6(cur)) {
|
|
20078
|
+
for (const file of [join9(cur, ".mcp.json"), join9(cur, ".cursor", "mcp.json")]) {
|
|
20079
|
+
const found = fromMcpFile(file);
|
|
20080
|
+
if (found) return validate2({ apiUrl: found.API_URL, token: found.API_SECRET, agentId: found.AGENT_ID }, file);
|
|
20081
|
+
}
|
|
20082
|
+
if (cur === stop || dirname6(cur) === cur) break;
|
|
20083
|
+
}
|
|
20084
|
+
if (cliConfig) {
|
|
20085
|
+
return validate2({ apiUrl: cliConfig.apiUrl, token: cliConfig.apiToken, agentId: cliConfig.agentId }, "cli-config");
|
|
20086
|
+
}
|
|
20087
|
+
return { ok: false, reason: `no pitboss agent configured for ${start}: run \`pitboss join\` there` };
|
|
20088
|
+
}
|
|
20089
|
+
|
|
19708
20090
|
// src/commands/watch.ts
|
|
19709
20091
|
function truncate(s, n) {
|
|
19710
20092
|
const clean = (s ?? "").replace(/\s+/g, " ").trim();
|
|
@@ -19783,15 +20165,75 @@ async function consumeEventStream(stream, onEvent) {
|
|
|
19783
20165
|
const { done, value } = await reader.read();
|
|
19784
20166
|
if (done) break;
|
|
19785
20167
|
buffer += decoder.decode(value, { stream: true });
|
|
19786
|
-
let
|
|
19787
|
-
while ((
|
|
19788
|
-
const event = parseSseFrame(buffer.slice(0,
|
|
19789
|
-
buffer = buffer.slice(
|
|
20168
|
+
let sep2;
|
|
20169
|
+
while ((sep2 = buffer.indexOf("\n\n")) !== -1) {
|
|
20170
|
+
const event = parseSseFrame(buffer.slice(0, sep2));
|
|
20171
|
+
buffer = buffer.slice(sep2 + 2);
|
|
19790
20172
|
if (event) onEvent(event);
|
|
19791
20173
|
}
|
|
19792
20174
|
}
|
|
19793
20175
|
}
|
|
20176
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20177
|
+
function printAndExit(line, code) {
|
|
20178
|
+
return new Promise(() => process.stdout.write(`${line}
|
|
20179
|
+
`, () => process.exit(code)));
|
|
20180
|
+
}
|
|
20181
|
+
var refuse = (reason) => printAndExit(`${REFUSED_MARKER} ${reason}`, 1);
|
|
20182
|
+
async function watchOnce(dir) {
|
|
20183
|
+
const found = resolveAgentCreds({ dir, env: process.env, cliConfig: readConfig() });
|
|
20184
|
+
if (!found.ok) return refuse(found.reason);
|
|
20185
|
+
const { apiUrl, token: token2, agentId } = found.creds;
|
|
20186
|
+
const auth = { Authorization: `Bearer ${token2}` };
|
|
20187
|
+
const state = join10(configDir(), "wake");
|
|
20188
|
+
const key = wakeKey(agentId, dir);
|
|
20189
|
+
process.on("exit", registerWatcher(state, key));
|
|
20190
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(130));
|
|
20191
|
+
let backoffMs = 1e3;
|
|
20192
|
+
let subscribed = false;
|
|
20193
|
+
for (; ; ) {
|
|
20194
|
+
try {
|
|
20195
|
+
const probe = await fetch(`${apiUrl}/agents/${agentId}`, { headers: auth });
|
|
20196
|
+
if (probe.status === 401 || probe.status === 403 || probe.status === 404) {
|
|
20197
|
+
return refuse(`the API answered ${probe.status} for ${agentId} (from ${found.source}): token revoked or agent deleted`);
|
|
20198
|
+
}
|
|
20199
|
+
if (!subscribed) {
|
|
20200
|
+
const sub = await fetch(`${apiUrl}/subscriptions`, {
|
|
20201
|
+
method: "POST",
|
|
20202
|
+
headers: { ...auth, "Content-Type": "application/json" },
|
|
20203
|
+
body: JSON.stringify({ agentId, targetType: "agent", targetId: agentId })
|
|
20204
|
+
});
|
|
20205
|
+
subscribed = sub.ok;
|
|
20206
|
+
}
|
|
20207
|
+
const cursor = readCursor(state, key);
|
|
20208
|
+
const res = await fetch(`${apiUrl}/events`, {
|
|
20209
|
+
headers: { ...auth, Accept: "text/event-stream", ...cursor ? { "Last-Event-ID": cursor } : {} }
|
|
20210
|
+
});
|
|
20211
|
+
if (res.status === 401 || res.status === 403) return refuse(`the event stream answered ${res.status} for ${agentId}`);
|
|
20212
|
+
if (!res.ok || !res.body) throw new Error(`event stream returned ${res.status}`);
|
|
20213
|
+
resetFollowups(state, key);
|
|
20214
|
+
const connectedAt = Date.now();
|
|
20215
|
+
let wake;
|
|
20216
|
+
try {
|
|
20217
|
+
wake = await readWake(res.body);
|
|
20218
|
+
} finally {
|
|
20219
|
+
if (Date.now() - connectedAt > 1e4) backoffMs = 1e3;
|
|
20220
|
+
}
|
|
20221
|
+
if (wake.lastId) writeCursor(state, key, wake.lastId);
|
|
20222
|
+
if (wake.truncated) {
|
|
20223
|
+
backoffMs = 1e3;
|
|
20224
|
+
continue;
|
|
20225
|
+
}
|
|
20226
|
+
if (!wake.first) throw new Error("event stream closed");
|
|
20227
|
+
return printAndExit(JSON.stringify(wake.count > 1 ? { ...wake.first, more: wake.count - 1 } : wake.first), 0);
|
|
20228
|
+
} catch (err) {
|
|
20229
|
+
console.error(`pitboss watch: ${err.message}; reconnecting in ${Math.round(backoffMs / 1e3)}s`);
|
|
20230
|
+
await sleep2(backoffMs);
|
|
20231
|
+
backoffMs = Math.min(backoffMs * 2, 3e4);
|
|
20232
|
+
}
|
|
20233
|
+
}
|
|
20234
|
+
}
|
|
19794
20235
|
async function watchCommand(options) {
|
|
20236
|
+
if (options.once) return watchOnce(options.dir ?? process.cwd());
|
|
19795
20237
|
const config = requireConfig();
|
|
19796
20238
|
const client = new CliApiClient(config);
|
|
19797
20239
|
const kinds = options.kinds ? new Set(options.kinds.split(",").map((k) => k.trim()).filter(Boolean)) : null;
|
|
@@ -19832,14 +20274,112 @@ async function watchCommand(options) {
|
|
|
19832
20274
|
}
|
|
19833
20275
|
}
|
|
19834
20276
|
|
|
20277
|
+
// src/commands/hook.ts
|
|
20278
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
20279
|
+
import { join as join11 } from "node:path";
|
|
20280
|
+
var FOLLOWUP_CAP = 3;
|
|
20281
|
+
function wakeInstructions(watch, opening) {
|
|
20282
|
+
return `${opening}
|
|
20283
|
+
|
|
20284
|
+
${watch}
|
|
20285
|
+
|
|
20286
|
+
It blocks at zero model cost. Two different things end it, and they need different responses. Check the background task's output before doing anything else.
|
|
20287
|
+
|
|
20288
|
+
CASE 1: the output contains pitboss event JSON. A real event arrived (a task assigned to you, a message, a review).
|
|
20289
|
+
1. Call the pitboss session_start tool to reconcile unread tasks and messages. The event is only a "go look now" trigger; do not act on its payload alone.
|
|
20290
|
+
2. Handle everything new. Reply to questions on the thread they were asked on, not in local chat.
|
|
20291
|
+
3. Relaunch the watcher in the background.
|
|
20292
|
+
|
|
20293
|
+
CASE 2: the output contains no event JSON. Something killed the watcher and nothing happened in pitboss. Relaunch it in the background and resume what you were doing; do not call session_start.
|
|
20294
|
+
|
|
20295
|
+
CASE 3: there is no event JSON and the output contains ${REFUSED_MARKER}. The watcher cannot use this agent's config (revoked token, deleted agent, bad URL). Do NOT relaunch. Tell the user what the line says and stop watching for this session.
|
|
20296
|
+
|
|
20297
|
+
Decide CASE 1 on the event JSON first, and only then look for the marker: event payloads carry text other agents wrote.`;
|
|
20298
|
+
}
|
|
20299
|
+
function claudeHookOutput(event, payload, ctx) {
|
|
20300
|
+
if (event === "Stop" && ctx.followups >= FOLLOWUP_CAP) return null;
|
|
20301
|
+
if (event === "Stop") {
|
|
20302
|
+
let p = null;
|
|
20303
|
+
try {
|
|
20304
|
+
p = JSON.parse(payload);
|
|
20305
|
+
} catch {
|
|
20306
|
+
p = null;
|
|
20307
|
+
}
|
|
20308
|
+
if (p === null || typeof p !== "object") {
|
|
20309
|
+
if (ctx.watcherAlive()) return null;
|
|
20310
|
+
} else {
|
|
20311
|
+
if (p.stop_hook_active) return null;
|
|
20312
|
+
const live = (p.background_tasks ?? []).some(
|
|
20313
|
+
(t) => isWatcherCommand(String(t.command ?? "")) && (t.status === "running" || t.status === "pending")
|
|
20314
|
+
);
|
|
20315
|
+
if (live) return null;
|
|
20316
|
+
}
|
|
20317
|
+
}
|
|
20318
|
+
const opening = event === "Stop" ? "The pitboss watcher is NOT RUNNING for this folder, so nothing will wake you. Relaunch it in the background NOW (Bash tool, run_in_background: true), then carry on:" : "Pitboss wake-ups are enabled for this folder. Launch the watcher in the background NOW (Bash tool, run_in_background: true), then continue working normally:";
|
|
20319
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: wakeInstructions(watchCommandLine(ctx.cli, ctx.dir), opening) } };
|
|
20320
|
+
}
|
|
20321
|
+
function cursorStopOutput(payload, ctx) {
|
|
20322
|
+
let status;
|
|
20323
|
+
try {
|
|
20324
|
+
status = JSON.parse(payload).status;
|
|
20325
|
+
} catch {
|
|
20326
|
+
return {};
|
|
20327
|
+
}
|
|
20328
|
+
if (status !== "completed" || ctx.followups >= FOLLOWUP_CAP || ctx.watcherAlive()) return {};
|
|
20329
|
+
return {
|
|
20330
|
+
followup_message: `Your pitboss watcher is not running, so you will miss the next task or message. Start \`${watchCommandLine(ctx.cli, ctx.dir)}\` as a background command, then end your turn. When it finishes, decide on its event JSON first; only output with no event JSON can carry a marker. On ${REFUSED_MARKER}, tell the user and do not relaunch.`
|
|
20331
|
+
};
|
|
20332
|
+
}
|
|
20333
|
+
function readStdin() {
|
|
20334
|
+
if (process.stdin.isTTY) return "";
|
|
20335
|
+
try {
|
|
20336
|
+
return readFileSync10(0, "utf-8");
|
|
20337
|
+
} catch {
|
|
20338
|
+
return "";
|
|
20339
|
+
}
|
|
20340
|
+
}
|
|
20341
|
+
function wakeStateFor(dir) {
|
|
20342
|
+
const found = resolveAgentCreds({ dir, env: {}, cliConfig: null });
|
|
20343
|
+
if (!found.ok) return null;
|
|
20344
|
+
const state = join11(configDir(), "wake");
|
|
20345
|
+
const key = wakeKey(found.creds.agentId, dir);
|
|
20346
|
+
return {
|
|
20347
|
+
watcherAlive: () => watcherAlive(state, key),
|
|
20348
|
+
followups: followupCount(state, key),
|
|
20349
|
+
asked: () => recordFollowup(state, key)
|
|
20350
|
+
};
|
|
20351
|
+
}
|
|
20352
|
+
function hookClaudeCommand(opts) {
|
|
20353
|
+
const event = opts.event === "Stop" ? "Stop" : "SessionStart";
|
|
20354
|
+
const payload = readStdin();
|
|
20355
|
+
let cwd;
|
|
20356
|
+
try {
|
|
20357
|
+
cwd = JSON.parse(payload).cwd;
|
|
20358
|
+
} catch {
|
|
20359
|
+
cwd = void 0;
|
|
20360
|
+
}
|
|
20361
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || typeof cwd === "string" && cwd || process.cwd();
|
|
20362
|
+
const wake = wakeStateFor(dir);
|
|
20363
|
+
if (!wake) return;
|
|
20364
|
+
const out = claudeHookOutput(event, payload, { cli: pinnedCli(CLI_VERSION), dir, ...wake });
|
|
20365
|
+
if (out && event === "Stop") wake.asked();
|
|
20366
|
+
if (out) process.stdout.write(JSON.stringify(out));
|
|
20367
|
+
}
|
|
20368
|
+
function hookCursorStopCommand(opts) {
|
|
20369
|
+
const dir = opts.dir ?? process.cwd();
|
|
20370
|
+
const wake = wakeStateFor(dir);
|
|
20371
|
+
if (!wake) {
|
|
20372
|
+
process.stdout.write("{}");
|
|
20373
|
+
return;
|
|
20374
|
+
}
|
|
20375
|
+
const out = cursorStopOutput(readStdin(), { cli: pinnedCli(CLI_VERSION), dir, ...wake });
|
|
20376
|
+
if ("followup_message" in out) wake.asked();
|
|
20377
|
+
process.stdout.write(JSON.stringify(out));
|
|
20378
|
+
}
|
|
20379
|
+
|
|
19835
20380
|
// src/index.ts
|
|
19836
|
-
import { fileURLToPath } from "node:url";
|
|
19837
|
-
var __here = dirname4(fileURLToPath(import.meta.url));
|
|
19838
|
-
var pkg = JSON.parse(
|
|
19839
|
-
readFileSync6(join5(__here, "../package.json"), "utf8")
|
|
19840
|
-
);
|
|
19841
20381
|
var program2 = new Command();
|
|
19842
|
-
program2.name("pitboss").description("Coordinate your coding agents from the terminal").version(
|
|
20382
|
+
program2.name("pitboss").description("Coordinate your coding agents from the terminal").version(CLI_VERSION).option("--no-input", "Never prompt; require values via flags").hook("preAction", (thisCommand) => {
|
|
19843
20383
|
if (thisCommand.opts().input === false) process.env.RELAI_NO_INPUT = "1";
|
|
19844
20384
|
});
|
|
19845
20385
|
program2.command("mcp", { hidden: true }).description("Run the MCP server on stdio (used by agent config, not by hand)").allowUnknownOption().action(mcpCommand);
|
|
@@ -19847,7 +20387,10 @@ program2.command("join").description("Connect this machine's agents: opens a cod
|
|
|
19847
20387
|
program2.command("init").description("Alias for `join`, kept so existing docs keep working").option("--api <url>", "relai API base URL (defaults to the hosted one)").action(joinCommand);
|
|
19848
20388
|
program2.command("status").description("Show your agent info, online agents, task summary, and unread count").action(statusCommand);
|
|
19849
20389
|
program2.command("start").description("Show your session orientation: project context, your open tasks, unread messages, open threads").action(startCommand);
|
|
19850
|
-
program2.command("watch").description("Stream live events you're subscribed to (new tasks, messages, reviews) until Ctrl-C").option("--kinds <list>", "Only show these event kinds (comma-separated, e.g. task.created,message.posted)").action(watchCommand);
|
|
20390
|
+
program2.command("watch").description("Stream live events you're subscribed to (new tasks, messages, reviews) until Ctrl-C").option("--kinds <list>", "Only show these event kinds (comma-separated, e.g. task.created,message.posted)").option("--once", "Block until the next event for this folder's agent, print it as JSON, and exit").option("--dir <path>", "Folder whose agent config --once uses (defaults to the current directory)").action(watchCommand);
|
|
20391
|
+
var hook = program2.command("hook", { hidden: true }).description("Agent runtime hooks that keep the watcher running (installed by join)");
|
|
20392
|
+
hook.command("claude").description("Claude Code SessionStart/Stop hook").option("--event <event>", "SessionStart | Stop", "SessionStart").action(hookClaudeCommand);
|
|
20393
|
+
hook.command("cursor-stop").description("Cursor stop hook").option("--dir <path>", "The agent's folder").action(hookCursorStopCommand);
|
|
19851
20394
|
program2.command("tasks").description("List tasks assigned to you (default: assigned + in_progress)").option("-a, --all", "Show all tasks in the project, not just yours").option("-s, --status <status>", "Filter by status (comma-separated: pending,assigned,in_progress,pending_verification,completed,blocked)").action(tasksCommand);
|
|
19852
20395
|
var task = program2.command("task").description("Create or update a task");
|
|
19853
20396
|
task.command("create").description("Create a new task").option("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").option("-p, --priority <priority>", "low|normal|high|urgent").option("--to <agent>", "Assign to an agent (id or name)").option("--domains <list>", "Comma-separated domain tags").option("--specialization <s>", "Required specialization").option("-v, --verify <cmd>", "Shell command that must exit 0 to gate the `completed` transition").option("--verify-cwd <path>", "Working directory for --verify (defaults to API server cwd)").option("--verify-kind <kind>", "Verifier kind: shell|file_exists|thread_concluded|reviewer_agent").option("--verify-path <path>", "Path that must exist (--verify-kind file_exists)").option("--verify-thread <id>", "Thread that must be concluded (--verify-kind thread_concluded)").option("--verify-reviewer <agent>", "Agent (id or name) who must approve (--verify-kind reviewer_agent)").option("--review-by <agent>", "Shorthand for --verify-kind reviewer_agent --verify-reviewer <agent>").action(taskCreateCommand);
|