slopwaresystems 0.4.13 → 0.4.16
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/dist/index.js +319 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26460,7 +26460,7 @@ var TaskEventRecord = external_exports.object({
|
|
|
26460
26460
|
});
|
|
26461
26461
|
|
|
26462
26462
|
// ../contract/src/work/sessions.ts
|
|
26463
|
-
var AgentHarness = external_exports.enum(["claude", "codex", "other"]);
|
|
26463
|
+
var AgentHarness = external_exports.enum(["claude", "codex", "cursor", "other"]);
|
|
26464
26464
|
var WORKERS_PER_SESSION = 8;
|
|
26465
26465
|
var WORKER_LABEL_MAX = 80;
|
|
26466
26466
|
var DOING_MAX = 120;
|
|
@@ -27144,7 +27144,7 @@ function persistRememberedConversation(cwd, conversation) {
|
|
|
27144
27144
|
const directory = `${buildConversationPath(cwd)}.d`;
|
|
27145
27145
|
persistLocalState(join3(directory, `${generateLocalStateKey(cwd, conversation.id)}.json`), JSON.stringify(conversation));
|
|
27146
27146
|
}
|
|
27147
|
-
function fetchRememberedConversation(cwd, notBefore) {
|
|
27147
|
+
function fetchRememberedConversation(cwd, notBefore, harness) {
|
|
27148
27148
|
const path2 = buildConversationPath(cwd);
|
|
27149
27149
|
const candidates = [];
|
|
27150
27150
|
let paths;
|
|
@@ -27158,12 +27158,13 @@ function fetchRememberedConversation(cwd, notBefore) {
|
|
|
27158
27158
|
try {
|
|
27159
27159
|
if (statSync(candidate).mtimeMs < notBefore) continue;
|
|
27160
27160
|
const parsed = JSON.parse(readFileSync3(candidate, "utf8"));
|
|
27161
|
-
const
|
|
27162
|
-
if (typeof parsed.id === "string" && parsed.id &&
|
|
27161
|
+
const wrote = AgentHarness.safeParse(parsed.harness ?? "other");
|
|
27162
|
+
if (typeof parsed.id === "string" && parsed.id && wrote.success) candidates.push({ id: parsed.id, harness: wrote.data });
|
|
27163
27163
|
} catch {
|
|
27164
27164
|
}
|
|
27165
27165
|
}
|
|
27166
|
-
const
|
|
27166
|
+
const mine = harness ? candidates.filter((candidate) => candidate.harness === harness) : candidates;
|
|
27167
|
+
const distinct = [...new Map((mine.length ? mine : candidates).map((candidate) => [candidate.id, candidate])).values()];
|
|
27167
27168
|
return distinct.length === 1 ? distinct[0] : null;
|
|
27168
27169
|
}
|
|
27169
27170
|
|
|
@@ -27332,6 +27333,10 @@ function fetchCodexHomes(root = homedir3(), inUse = findHomesInUse) {
|
|
|
27332
27333
|
const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
|
|
27333
27334
|
return deduplicateStrings([join5(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...findDirectoriesNamed(root, ".codex").filter((dir) => hasAnyEntry(dir, ["config.toml", "auth.json", "sessions"]))]);
|
|
27334
27335
|
}
|
|
27336
|
+
function fetchCursorHomes(root = homedir3(), _inUse = findHomesInUse) {
|
|
27337
|
+
if (process.env.SLOP_CURSOR_HOME) return [process.env.SLOP_CURSOR_HOME];
|
|
27338
|
+
return deduplicateStrings([join5(root, ".cursor"), ...findDirectoriesNamed(root, ".cursor").filter((dir) => hasAnyEntry(dir, ["cli-config.json", "hooks.json", "mcp.json", "projects"]))]);
|
|
27339
|
+
}
|
|
27335
27340
|
function resolveOwnHarnessHome(variable) {
|
|
27336
27341
|
if (process.platform === "win32") return null;
|
|
27337
27342
|
try {
|
|
@@ -27493,8 +27498,112 @@ function wakeCodexOverWs(text, threadId, endpoint) {
|
|
|
27493
27498
|
});
|
|
27494
27499
|
});
|
|
27495
27500
|
}
|
|
27501
|
+
var CURSOR_TURN_CAP_MS = 6e5;
|
|
27502
|
+
var CURSOR_LOAD_WITHIN_MS = 3e4;
|
|
27503
|
+
function wakeCursor(text, chatId, cwd, options = {}) {
|
|
27504
|
+
const binary = options.binary ?? "cursor-agent";
|
|
27505
|
+
return wakeCursorOverAcp(text, chatId, cwd, binary).then((woken) => woken || resumeCursorChat(text, chatId, cwd, binary));
|
|
27506
|
+
}
|
|
27507
|
+
function wakeCursorOverAcp(text, chatId, cwd, binary) {
|
|
27508
|
+
return new Promise((resolve6) => {
|
|
27509
|
+
let child;
|
|
27510
|
+
try {
|
|
27511
|
+
child = spawn(binary, ["acp"], { cwd, stdio: ["pipe", "pipe", "ignore"] });
|
|
27512
|
+
} catch {
|
|
27513
|
+
resolve6(false);
|
|
27514
|
+
return;
|
|
27515
|
+
}
|
|
27516
|
+
let settled = false;
|
|
27517
|
+
let nextId = 1;
|
|
27518
|
+
let buffer = "";
|
|
27519
|
+
const finishWake = (woken, keepRunning = false) => {
|
|
27520
|
+
if (settled) return;
|
|
27521
|
+
settled = true;
|
|
27522
|
+
clearTimeout(loadTimer);
|
|
27523
|
+
if (!keepRunning) child.kill();
|
|
27524
|
+
else setTimeout(() => child.kill(), CURSOR_TURN_CAP_MS).unref?.();
|
|
27525
|
+
resolve6(woken);
|
|
27526
|
+
};
|
|
27527
|
+
const loadTimer = setTimeout(() => finishWake(false), CURSOR_LOAD_WITHIN_MS);
|
|
27528
|
+
const send = (message) => {
|
|
27529
|
+
try {
|
|
27530
|
+
child.stdin?.write(`${JSON.stringify(message)}
|
|
27531
|
+
`);
|
|
27532
|
+
} catch {
|
|
27533
|
+
finishWake(false);
|
|
27534
|
+
}
|
|
27535
|
+
};
|
|
27536
|
+
const call = (method, params) => {
|
|
27537
|
+
const id = nextId++;
|
|
27538
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
27539
|
+
return id;
|
|
27540
|
+
};
|
|
27541
|
+
const initializeId = call("initialize", { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }, clientInfo: { name: "slop", version: "0.4.16" } });
|
|
27542
|
+
let loadId = 0;
|
|
27543
|
+
let promptId = 0;
|
|
27544
|
+
child.on("error", () => finishWake(false));
|
|
27545
|
+
child.on("exit", () => finishWake(false));
|
|
27546
|
+
child.stdout?.on("data", (chunk) => {
|
|
27547
|
+
buffer += chunk.toString("utf8");
|
|
27548
|
+
let index;
|
|
27549
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
27550
|
+
const line = buffer.slice(0, index);
|
|
27551
|
+
buffer = buffer.slice(index + 1);
|
|
27552
|
+
if (!line.trim()) continue;
|
|
27553
|
+
let message;
|
|
27554
|
+
try {
|
|
27555
|
+
message = JSON.parse(line);
|
|
27556
|
+
} catch {
|
|
27557
|
+
continue;
|
|
27558
|
+
}
|
|
27559
|
+
if (message.id !== void 0 && message.method === "session/request_permission") {
|
|
27560
|
+
const options = message.params?.options ?? [];
|
|
27561
|
+
const allow = options.find((option) => option.kind === "allow_once") ?? options.find((option) => /allow|accept/i.test(option.optionId ?? "")) ?? options[0];
|
|
27562
|
+
send({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "selected", optionId: allow?.optionId ?? "allow" } } });
|
|
27563
|
+
continue;
|
|
27564
|
+
}
|
|
27565
|
+
if (message.id !== void 0 && message.method) {
|
|
27566
|
+
send({ jsonrpc: "2.0", id: message.id, result: {} });
|
|
27567
|
+
continue;
|
|
27568
|
+
}
|
|
27569
|
+
if (message.id === initializeId) {
|
|
27570
|
+
if (message.error) finishWake(false);
|
|
27571
|
+
else loadId = call("session/load", { sessionId: chatId, cwd, mcpServers: [] });
|
|
27572
|
+
continue;
|
|
27573
|
+
}
|
|
27574
|
+
if (message.id === loadId && loadId !== 0) {
|
|
27575
|
+
if (message.error) finishWake(false);
|
|
27576
|
+
else promptId = call("session/prompt", { sessionId: chatId, prompt: [{ type: "text", text }] });
|
|
27577
|
+
continue;
|
|
27578
|
+
}
|
|
27579
|
+
if (promptId !== 0 && (message.id === promptId || message.method === "session/update")) finishWake(true, true);
|
|
27580
|
+
}
|
|
27581
|
+
});
|
|
27582
|
+
});
|
|
27583
|
+
}
|
|
27584
|
+
function resumeCursorChat(text, chatId, cwd, binary) {
|
|
27585
|
+
return new Promise((resolve6) => {
|
|
27586
|
+
let child;
|
|
27587
|
+
try {
|
|
27588
|
+
child = spawn(binary, ["--resume", chatId, "-p", text, "--output-format", "text"], { cwd, stdio: ["ignore", "ignore", "ignore"] });
|
|
27589
|
+
} catch {
|
|
27590
|
+
resolve6(false);
|
|
27591
|
+
return;
|
|
27592
|
+
}
|
|
27593
|
+
const timer = setTimeout(() => resolve6(true), 5e3);
|
|
27594
|
+
child.on("error", () => {
|
|
27595
|
+
clearTimeout(timer);
|
|
27596
|
+
resolve6(false);
|
|
27597
|
+
});
|
|
27598
|
+
child.on("exit", (code) => {
|
|
27599
|
+
clearTimeout(timer);
|
|
27600
|
+
resolve6(code === 0);
|
|
27601
|
+
});
|
|
27602
|
+
});
|
|
27603
|
+
}
|
|
27496
27604
|
|
|
27497
27605
|
// src/platform/harness.ts
|
|
27606
|
+
var AgentHarnessValues = AgentHarness.options;
|
|
27498
27607
|
var HEAD_BYTES = 64 * 1024;
|
|
27499
27608
|
var parent = null;
|
|
27500
27609
|
function fetchParentCommand() {
|
|
@@ -27508,9 +27617,11 @@ function fetchParentCommand() {
|
|
|
27508
27617
|
}
|
|
27509
27618
|
function parseHarnessFromCommand(command) {
|
|
27510
27619
|
const first = basename2(command.trim().split(/\s+/)[0] ?? "");
|
|
27620
|
+
if (/^cursor-agent(\b|$)/.test(first)) return "cursor";
|
|
27511
27621
|
if (/^claude(\b|$)/.test(first)) return "claude";
|
|
27512
27622
|
if (/^codex(\b|$)/.test(first)) return "codex";
|
|
27513
27623
|
if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
|
|
27624
|
+
if (/(^|\/)cursor-agent(\s|$)/.test(command)) return "cursor";
|
|
27514
27625
|
return null;
|
|
27515
27626
|
}
|
|
27516
27627
|
function isNonInteractiveCommand(command) {
|
|
@@ -27641,9 +27752,34 @@ function resolveCodexBinary() {
|
|
|
27641
27752
|
}
|
|
27642
27753
|
return "codex";
|
|
27643
27754
|
}
|
|
27755
|
+
function resolveParentHarness() {
|
|
27756
|
+
return parseHarnessFromCommand(fetchParentCommand());
|
|
27757
|
+
}
|
|
27758
|
+
var RETRY_ACP_AFTER_MS = 5e3;
|
|
27759
|
+
var acpSession = null;
|
|
27760
|
+
function fetchCursorAcpSessionByCwd(cwd, binary = "cursor-agent") {
|
|
27761
|
+
if (acpSession && (acpSession.id !== null || Date.now() - acpSession.at < RETRY_ACP_AFTER_MS)) return acpSession.id;
|
|
27762
|
+
acpSession = { id: null, at: Date.now() };
|
|
27763
|
+
const said = [
|
|
27764
|
+
{ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }, clientInfo: { name: "slop", version: "0.4.16" } } },
|
|
27765
|
+
{ jsonrpc: "2.0", id: 2, method: "session/list", params: {} }
|
|
27766
|
+
];
|
|
27767
|
+
try {
|
|
27768
|
+
const answered = execFileSync3(binary, ["acp"], { input: `${said.map((message) => JSON.stringify(message)).join("\n")}
|
|
27769
|
+
`, encoding: "utf8", timeout: 15e3, stdio: ["pipe", "pipe", "ignore"], cwd });
|
|
27770
|
+
for (const line of answered.split("\n")) {
|
|
27771
|
+
if (!line.includes('"sessions"')) continue;
|
|
27772
|
+
const listed = JSON.parse(line).result?.sessions ?? [];
|
|
27773
|
+
const here = listed.filter((session) => typeof session.sessionId === "string" && typeof session.cwd === "string" && isSamePath(session.cwd, cwd));
|
|
27774
|
+
if (here.length === 1) acpSession = { id: here[0].sessionId, at: Date.now() };
|
|
27775
|
+
}
|
|
27776
|
+
} catch {
|
|
27777
|
+
}
|
|
27778
|
+
return acpSession.id;
|
|
27779
|
+
}
|
|
27644
27780
|
function fetchHarnessConversation(cwd) {
|
|
27645
27781
|
const explicit = process.env.SLOP_CONVERSATION_ID?.trim();
|
|
27646
|
-
if (explicit) return { id: explicit, harness: process.env.SLOP_HARNESS
|
|
27782
|
+
if (explicit) return { id: explicit, harness: AgentHarnessValues.includes(process.env.SLOP_HARNESS) ? process.env.SLOP_HARNESS : "other" };
|
|
27647
27783
|
if (process.env.CODEX_THREAD_ID?.trim()) return { id: process.env.CODEX_THREAD_ID.trim(), harness: "codex" };
|
|
27648
27784
|
if (process.env.CLAUDE_CODE_SESSION_ID?.trim()) return { id: process.env.CLAUDE_CODE_SESSION_ID.trim(), harness: "claude" };
|
|
27649
27785
|
const harness = parseHarnessFromCommand(fetchParentCommand());
|
|
@@ -27664,9 +27800,13 @@ var startedAt = Date.now();
|
|
|
27664
27800
|
function fetchKnownConversation(cwd) {
|
|
27665
27801
|
const own2 = fetchHarnessConversation(cwd);
|
|
27666
27802
|
if (own2) return own2;
|
|
27667
|
-
const remembered = fetchRememberedConversation(cwd, startedAt - 12e4);
|
|
27803
|
+
const remembered = fetchRememberedConversation(cwd, startedAt - 12e4, resolveParentHarness());
|
|
27668
27804
|
if (remembered?.harness === "codex" && isMainChatThread(fetchCodexHomes().map((home) => join8(home, "sessions")), remembered.id)) return remembered;
|
|
27669
|
-
|
|
27805
|
+
const known = remembered?.harness === "codex" ? null : remembered;
|
|
27806
|
+
if (known) return known;
|
|
27807
|
+
if (resolveParentHarness() !== "cursor") return null;
|
|
27808
|
+
const acp = fetchCursorAcpSessionByCwd(cwd);
|
|
27809
|
+
return acp ? { id: acp, harness: "cursor" } : null;
|
|
27670
27810
|
}
|
|
27671
27811
|
function resolveAgentIdentity(cwd, fallback = "process") {
|
|
27672
27812
|
const conversation = fetchKnownConversation(cwd);
|
|
@@ -28800,6 +28940,7 @@ var HeldSession = class {
|
|
|
28800
28940
|
import { closeSync as closeSync2, existsSync as existsSync10, openSync as openSync2, readdirSync as readdirSync7, readFileSync as readFileSync12, readSync as readSync2, statSync as statSync4 } from "fs";
|
|
28801
28941
|
import { basename as basename4, dirname as dirname7, join as join14 } from "path";
|
|
28802
28942
|
var WORKER_FRESH_MS = 12e4;
|
|
28943
|
+
var WORKER_GONE_MS = 3e5;
|
|
28803
28944
|
var READ_CAP = 256 * 1024;
|
|
28804
28945
|
var CARRY_CAP = 64 * 1024;
|
|
28805
28946
|
var CHILD_SCAN_CAP = 40;
|
|
@@ -28869,6 +29010,67 @@ function parseToolCallFromCodexLine(line) {
|
|
|
28869
29010
|
if (payload.type === "local_shell_call") return formatToolCallLine("shell", payload.action);
|
|
28870
29011
|
return null;
|
|
28871
29012
|
}
|
|
29013
|
+
function parseCursorTaskFromLine(line) {
|
|
29014
|
+
if (!line.includes("CallDynamicTool") || !line.includes('"Task"')) return null;
|
|
29015
|
+
let entry;
|
|
29016
|
+
try {
|
|
29017
|
+
entry = JSON.parse(line);
|
|
29018
|
+
} catch {
|
|
29019
|
+
return null;
|
|
29020
|
+
}
|
|
29021
|
+
const content = entry.message?.content;
|
|
29022
|
+
if (!Array.isArray(content)) return null;
|
|
29023
|
+
for (const block of content) {
|
|
29024
|
+
if (!block || typeof block !== "object") continue;
|
|
29025
|
+
const { type, name, input: input2 } = block;
|
|
29026
|
+
if (type !== "tool_use" || name !== "CallDynamicTool" || input2?.toolName !== "Task") continue;
|
|
29027
|
+
const asked = input2.arguments ?? {};
|
|
29028
|
+
const prompt = typeof asked.prompt === "string" ? asked.prompt : null;
|
|
29029
|
+
if (!prompt) continue;
|
|
29030
|
+
const label = [asked.subagent_type, asked.description].filter((part) => typeof part === "string" && part.trim() !== "").join(" \u2014 ");
|
|
29031
|
+
return { label: truncate(label || "subagent", WORKER_LABEL_MAX), prompt };
|
|
29032
|
+
}
|
|
29033
|
+
return null;
|
|
29034
|
+
}
|
|
29035
|
+
function parseCursorUserQuery(line) {
|
|
29036
|
+
let entry;
|
|
29037
|
+
try {
|
|
29038
|
+
entry = JSON.parse(line);
|
|
29039
|
+
} catch {
|
|
29040
|
+
return null;
|
|
29041
|
+
}
|
|
29042
|
+
const content = entry.message?.content;
|
|
29043
|
+
if (entry.role !== "user" || !Array.isArray(content)) return null;
|
|
29044
|
+
for (const block of content) {
|
|
29045
|
+
const text = block?.text;
|
|
29046
|
+
if (typeof text !== "string") continue;
|
|
29047
|
+
const asked = /<user_query>\s*([\s\S]*?)\s*<\/user_query>/.exec(text);
|
|
29048
|
+
if (asked?.[1]) return asked[1];
|
|
29049
|
+
}
|
|
29050
|
+
return null;
|
|
29051
|
+
}
|
|
29052
|
+
function findCursorWorkerFiles(transcriptPath, conversation, changedAfter, asked) {
|
|
29053
|
+
if (asked.length === 0) return [];
|
|
29054
|
+
const directory = dirname7(dirname7(transcriptPath));
|
|
29055
|
+
let chats;
|
|
29056
|
+
try {
|
|
29057
|
+
chats = readdirSync7(directory);
|
|
29058
|
+
} catch {
|
|
29059
|
+
return [];
|
|
29060
|
+
}
|
|
29061
|
+
const found = [];
|
|
29062
|
+
for (const chat of chats) {
|
|
29063
|
+
if (chat === conversation) continue;
|
|
29064
|
+
const path2 = join14(directory, chat, `${chat}.jsonl`);
|
|
29065
|
+
const changedAt = fetchChangedAt(path2);
|
|
29066
|
+
if (changedAt < changedAfter) continue;
|
|
29067
|
+
const query = parseCursorUserQuery(fetchFileHead(path2, 16 * 1024).split("\n")[0] ?? "");
|
|
29068
|
+
const task = query === null ? void 0 : asked.find((candidate) => candidate.prompt === query);
|
|
29069
|
+
if (!task) continue;
|
|
29070
|
+
found.push({ ref: chat, label: task.label, path: path2, directory: null, branch: null, changedAt });
|
|
29071
|
+
}
|
|
29072
|
+
return found.sort((a, b) => b.changedAt - a.changedAt);
|
|
29073
|
+
}
|
|
28872
29074
|
var fetchFileSize = (path2) => {
|
|
28873
29075
|
try {
|
|
28874
29076
|
return statSync4(path2).size;
|
|
@@ -28937,6 +29139,21 @@ function findClaudeTranscriptPath(homes, conversation) {
|
|
|
28937
29139
|
}
|
|
28938
29140
|
return null;
|
|
28939
29141
|
}
|
|
29142
|
+
function findCursorTranscriptPath(homes, conversation) {
|
|
29143
|
+
for (const home of homes) {
|
|
29144
|
+
let projects;
|
|
29145
|
+
try {
|
|
29146
|
+
projects = readdirSync7(join14(home, "projects"));
|
|
29147
|
+
} catch {
|
|
29148
|
+
continue;
|
|
29149
|
+
}
|
|
29150
|
+
for (const project of projects) {
|
|
29151
|
+
const path2 = join14(home, "projects", project, "agent-transcripts", conversation, `${conversation}.jsonl`);
|
|
29152
|
+
if (existsSync10(path2)) return path2;
|
|
29153
|
+
}
|
|
29154
|
+
}
|
|
29155
|
+
return null;
|
|
29156
|
+
}
|
|
28940
29157
|
function parseClaudeWorkerLabel(text) {
|
|
28941
29158
|
let meta3;
|
|
28942
29159
|
try {
|
|
@@ -28983,6 +29200,12 @@ function parseWhereFromJson(line) {
|
|
|
28983
29200
|
}
|
|
28984
29201
|
}
|
|
28985
29202
|
var isCodexHelperRollout = (head) => /"other"\s*:\s*"guardian"|"thread_source"\s*:\s*"[^"]*guardian/.test(head);
|
|
29203
|
+
function parseCodexWorkerLabel(head) {
|
|
29204
|
+
const nickname = /"agent_nickname"\s*:\s*"([^"]+)"/.exec(head)?.[1];
|
|
29205
|
+
const role = /"agent_role"\s*:\s*"([^"]+)"/.exec(head)?.[1];
|
|
29206
|
+
const parts = [nickname, role].filter((part) => Boolean(part));
|
|
29207
|
+
return parts.length ? truncate(parts.join(" \u2014 "), WORKER_LABEL_MAX) : null;
|
|
29208
|
+
}
|
|
28986
29209
|
function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
|
|
28987
29210
|
const rollouts = [];
|
|
28988
29211
|
const findRolloutsInDirectory = (directory, depth) => {
|
|
@@ -29012,10 +29235,11 @@ function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
|
|
|
29012
29235
|
if (!head.includes('"session_meta"') || isCodexHelperRollout(head)) continue;
|
|
29013
29236
|
if (!new RegExp(`"parent_thread_id"\\s*:\\s*"${parentThreadId}"`).test(head)) continue;
|
|
29014
29237
|
const where = parseWhereFromJson(head);
|
|
29015
|
-
found.push({ ...rollout, label: "subagent", directory: where.directory, branch: where.branch });
|
|
29238
|
+
found.push({ ...rollout, label: parseCodexWorkerLabel(head) ?? "subagent", directory: where.directory, branch: where.branch });
|
|
29016
29239
|
}
|
|
29017
29240
|
return found;
|
|
29018
29241
|
}
|
|
29242
|
+
var ASKED_CAP = 16;
|
|
29019
29243
|
var HarnessActivity = class {
|
|
29020
29244
|
constructor(conversation, onReport) {
|
|
29021
29245
|
this.conversation = conversation;
|
|
@@ -29024,6 +29248,8 @@ var HarnessActivity = class {
|
|
|
29024
29248
|
conversation;
|
|
29025
29249
|
onReport;
|
|
29026
29250
|
workers = /* @__PURE__ */ new Map();
|
|
29251
|
+
/** What this chat has asked a subagent to do, which is how a Cursor chat's children are claimed. */
|
|
29252
|
+
asked = [];
|
|
29027
29253
|
own = null;
|
|
29028
29254
|
doing = null;
|
|
29029
29255
|
watched = false;
|
|
@@ -29041,10 +29267,7 @@ var HarnessActivity = class {
|
|
|
29041
29267
|
setWatched(on) {
|
|
29042
29268
|
if (on === this.watched) return;
|
|
29043
29269
|
this.watched = on;
|
|
29044
|
-
if (on)
|
|
29045
|
-
this.own = this.own && new FileTail(this.own.path);
|
|
29046
|
-
for (const worker of this.workers.values()) worker.tail = new FileTail(worker.path);
|
|
29047
|
-
}
|
|
29270
|
+
if (on) this.doing = null;
|
|
29048
29271
|
this.tick();
|
|
29049
29272
|
}
|
|
29050
29273
|
/** Say it again: the room forgets a session's activity when its socket drops, and only the agent can refill it. */
|
|
@@ -29060,6 +29283,10 @@ var HarnessActivity = class {
|
|
|
29060
29283
|
tick() {
|
|
29061
29284
|
if (this.stopped) return;
|
|
29062
29285
|
const now = Date.now();
|
|
29286
|
+
try {
|
|
29287
|
+
this.read();
|
|
29288
|
+
} catch {
|
|
29289
|
+
}
|
|
29063
29290
|
if (now - this.discoveredAt >= DISCOVER_EVERY_MS) {
|
|
29064
29291
|
this.discoveredAt = now;
|
|
29065
29292
|
try {
|
|
@@ -29067,12 +29294,6 @@ var HarnessActivity = class {
|
|
|
29067
29294
|
} catch {
|
|
29068
29295
|
}
|
|
29069
29296
|
}
|
|
29070
|
-
if (this.watched) {
|
|
29071
|
-
try {
|
|
29072
|
-
this.read();
|
|
29073
|
-
} catch {
|
|
29074
|
-
}
|
|
29075
|
-
}
|
|
29076
29297
|
const activity = this.build();
|
|
29077
29298
|
const said = JSON.stringify(activity);
|
|
29078
29299
|
if (said === this.reported) return;
|
|
@@ -29081,7 +29302,7 @@ var HarnessActivity = class {
|
|
|
29081
29302
|
}
|
|
29082
29303
|
/** Which files to read: this chat's own, and one per worker still working. */
|
|
29083
29304
|
discover(now) {
|
|
29084
|
-
const found = this.findWorkerFiles(now -
|
|
29305
|
+
const found = this.findWorkerFiles(now - WORKER_GONE_MS);
|
|
29085
29306
|
for (const file2 of found) {
|
|
29086
29307
|
const known = this.workers.get(file2.ref);
|
|
29087
29308
|
if (known) Object.assign(known, { label: file2.label, directory: file2.directory, branch: file2.branch, changedAt: file2.changedAt });
|
|
@@ -29096,6 +29317,11 @@ var HarnessActivity = class {
|
|
|
29096
29317
|
this.own ??= transcript ? new FileTail(transcript, "end") : null;
|
|
29097
29318
|
return transcript ? findClaudeWorkerFiles(transcript, this.conversation.id, changedAfter) : [];
|
|
29098
29319
|
}
|
|
29320
|
+
if (this.conversation.harness === "cursor") {
|
|
29321
|
+
const transcript = findCursorTranscriptPath(fetchCursorHomes(), this.conversation.id);
|
|
29322
|
+
this.own ??= transcript ? new FileTail(transcript, "end") : null;
|
|
29323
|
+
return transcript ? findCursorWorkerFiles(transcript, this.conversation.id, changedAfter, this.asked) : [];
|
|
29324
|
+
}
|
|
29099
29325
|
if (this.conversation.harness === "codex") {
|
|
29100
29326
|
const roots = fetchCodexHomes().map((home) => join14(home, "sessions"));
|
|
29101
29327
|
const rollout = fetchCodexRolloutPath(roots, this.conversation.id);
|
|
@@ -29112,11 +29338,23 @@ var HarnessActivity = class {
|
|
|
29112
29338
|
for (const line of tail.fetchNewLines()) newest = parse3(line) ?? newest;
|
|
29113
29339
|
return newest;
|
|
29114
29340
|
};
|
|
29115
|
-
if (this.own)
|
|
29341
|
+
if (this.own) {
|
|
29342
|
+
let newest = null;
|
|
29343
|
+
for (const line of this.own.fetchNewLines()) {
|
|
29344
|
+
newest = parse3(line) ?? newest;
|
|
29345
|
+
const task = this.conversation.harness === "cursor" ? parseCursorTaskFromLine(line) : null;
|
|
29346
|
+
if (task) {
|
|
29347
|
+
this.asked.push(task);
|
|
29348
|
+
while (this.asked.length > ASKED_CAP) this.asked.shift();
|
|
29349
|
+
}
|
|
29350
|
+
}
|
|
29351
|
+
this.doing = newest ?? this.doing;
|
|
29352
|
+
}
|
|
29116
29353
|
for (const worker of this.workers.values()) worker.doing = findNewestCall(worker.tail) ?? worker.doing;
|
|
29117
29354
|
}
|
|
29118
29355
|
build() {
|
|
29119
|
-
const
|
|
29356
|
+
const now = Date.now();
|
|
29357
|
+
const workers = [...this.workers.values()].sort((a, b) => b.changedAt - a.changedAt).slice(0, WORKERS_PER_SESSION).map((worker) => ({ ref: worker.ref, label: worker.label, directory: worker.directory, branch: worker.branch, doing: this.watched && now - worker.changedAt < WORKER_FRESH_MS ? worker.doing : null, since: worker.since }));
|
|
29120
29358
|
return { doing: this.watched ? this.doing : null, workers };
|
|
29121
29359
|
}
|
|
29122
29360
|
};
|
|
@@ -29673,7 +29911,7 @@ function createProjectTools(api, project, identity) {
|
|
|
29673
29911
|
if (waking || stopped) return;
|
|
29674
29912
|
waking = true;
|
|
29675
29913
|
const text = `Messages are waiting for you on Slopware Tasks in project ${project.key}. Use session check with project ${project.id} to read them, then act or reply only if it changes something. Do not acknowledge.`;
|
|
29676
|
-
const wake = identity.harness === "codex" && identity.conversation ? wakeCodex(text, identity.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : identity.harness === "claude" ? wakeHarness(text, void 0, identity.conversation) : Promise.resolve(false);
|
|
29914
|
+
const wake = identity.harness === "codex" && identity.conversation ? wakeCodex(text, identity.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : identity.harness === "claude" ? wakeHarness(text, void 0, identity.conversation) : identity.harness === "cursor" && identity.conversation ? wakeCursor(text, identity.conversation, process.cwd()) : Promise.resolve(false);
|
|
29677
29915
|
void wake.catch(() => false).finally(() => {
|
|
29678
29916
|
waking = false;
|
|
29679
29917
|
});
|
|
@@ -30161,6 +30399,42 @@ tool_timeout_sec = 21600
|
|
|
30161
30399
|
done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (Codex is woken with codex queue; no hook holds a turn open)`);
|
|
30162
30400
|
return done;
|
|
30163
30401
|
}
|
|
30402
|
+
function installIntoCursorHome(dir) {
|
|
30403
|
+
const done = [];
|
|
30404
|
+
mkdirSync9(dir, { recursive: true });
|
|
30405
|
+
const mcpPath = join16(dir, "mcp.json");
|
|
30406
|
+
const mcp = fetchJsonFile(mcpPath);
|
|
30407
|
+
const servers = mcp.mcpServers ?? {};
|
|
30408
|
+
const had = "slop" in servers;
|
|
30409
|
+
servers.slop = { command: "slop", args: ["mcp"] };
|
|
30410
|
+
mcp.mcpServers = servers;
|
|
30411
|
+
writeFileSync8(mcpPath, `${JSON.stringify(mcp, null, 2)}
|
|
30412
|
+
`);
|
|
30413
|
+
done.push(`Cursor: MCP server "slop" ${had ? "already in" : "added to"} ${mcpPath}`);
|
|
30414
|
+
const hooksPath = join16(dir, "hooks.json");
|
|
30415
|
+
const file2 = fetchJsonFile(hooksPath);
|
|
30416
|
+
file2.version = typeof file2.version === "number" ? file2.version : 1;
|
|
30417
|
+
file2.hooks = mergeCursorHooks(file2.hooks ?? {});
|
|
30418
|
+
writeFileSync8(hooksPath, `${JSON.stringify(file2, null, 2)}
|
|
30419
|
+
`);
|
|
30420
|
+
done.push(`Cursor: sessionStart hook written to ${hooksPath}; nothing wakes an idle chat, so messages wait for its next tool call`);
|
|
30421
|
+
try {
|
|
30422
|
+
execFileSync6("cursor-agent", ["mcp", "enable", "slop"], { stdio: "ignore" });
|
|
30423
|
+
done.push("Cursor: slop is on its approved MCP list");
|
|
30424
|
+
} catch {
|
|
30425
|
+
done.push("Cursor: approve the server once with `cursor-agent mcp enable slop`, or answer its prompt");
|
|
30426
|
+
}
|
|
30427
|
+
return done;
|
|
30428
|
+
}
|
|
30429
|
+
function mergeCursorHooks(hooks) {
|
|
30430
|
+
const isOwnHook = (hook) => typeof hook.command === "string" && /(^|\s)slop hook /.test(hook.command);
|
|
30431
|
+
for (const event of Object.keys(hooks)) {
|
|
30432
|
+
hooks[event] = (hooks[event] ?? []).filter((hook) => !isOwnHook(hook));
|
|
30433
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
30434
|
+
}
|
|
30435
|
+
hooks.sessionStart = [...hooks.sessionStart ?? [], { command: "slop hook start --harness cursor", timeout: 5 }];
|
|
30436
|
+
return hooks;
|
|
30437
|
+
}
|
|
30164
30438
|
async function trustCodexHooks(codexHome) {
|
|
30165
30439
|
const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
|
|
30166
30440
|
const where = codexHome === join16(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
|
|
@@ -30198,7 +30472,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
30198
30472
|
const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
30199
30473
|
`);
|
|
30200
30474
|
try {
|
|
30201
|
-
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.
|
|
30475
|
+
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.16" }, capabilities: { experimentalApi: true } });
|
|
30202
30476
|
sendNotification("initialized", {});
|
|
30203
30477
|
const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
|
|
30204
30478
|
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
@@ -30214,7 +30488,8 @@ async function trustCodexHooks(codexHome) {
|
|
|
30214
30488
|
server.kill();
|
|
30215
30489
|
}
|
|
30216
30490
|
}
|
|
30217
|
-
async function parseHookInput(
|
|
30491
|
+
async function parseHookInput(installedAs, remember = true) {
|
|
30492
|
+
const harness = resolveParentHarness() ?? installedAs;
|
|
30218
30493
|
const input2 = await new Promise((resolve6) => {
|
|
30219
30494
|
let data = "";
|
|
30220
30495
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
@@ -30225,13 +30500,13 @@ async function parseHookInput(harness, remember = true) {
|
|
|
30225
30500
|
let conversation = null;
|
|
30226
30501
|
try {
|
|
30227
30502
|
const parsed = JSON.parse(input2);
|
|
30228
|
-
cwd = parsed.cwd ?? cwd;
|
|
30503
|
+
cwd = parsed.cwd ?? parsed.workspace_roots?.[0] ?? cwd;
|
|
30229
30504
|
conversation = parsed.session_id ?? null;
|
|
30230
30505
|
} catch {
|
|
30231
30506
|
}
|
|
30232
30507
|
const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join16(home, "sessions")), conversation);
|
|
30233
30508
|
if (remember && conversation && ownThread && !isNonInteractiveHarness()) persistRememberedConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
30234
|
-
return { cwd, conversation };
|
|
30509
|
+
return { cwd, conversation, harness };
|
|
30235
30510
|
}
|
|
30236
30511
|
async function runPromptHook(harness) {
|
|
30237
30512
|
await parseHookInput(harness);
|
|
@@ -30239,12 +30514,13 @@ async function runPromptHook(harness) {
|
|
|
30239
30514
|
async function runToolHook(harness) {
|
|
30240
30515
|
await parseHookInput(harness, false);
|
|
30241
30516
|
}
|
|
30242
|
-
async function runStartHook(
|
|
30517
|
+
async function runStartHook(installedAs) {
|
|
30243
30518
|
if (isNonInteractiveHarness()) return;
|
|
30244
|
-
const { conversation } = await parseHookInput(
|
|
30519
|
+
const { conversation, harness } = await parseHookInput(installedAs);
|
|
30245
30520
|
if (!conversation) return;
|
|
30246
|
-
const
|
|
30247
|
-
|
|
30521
|
+
const carried = harness === "codex" ? " (no wait tool on Codex; a reply becomes your next turn)" : harness === "cursor" ? " (no wait tool on Cursor; a message for you arrives as a new turn in this chat)" : "";
|
|
30522
|
+
const text = `On Slopware Tasks you are ${generateAgentName(conversation)}: that is how people and other agents address you (session who lists everyone). Messages between agents are letters, read when the other side gets to them: write once, reply only when it changes something, never wait for a reply${carried}.`;
|
|
30523
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text }, additional_context: text })}
|
|
30248
30524
|
`);
|
|
30249
30525
|
}
|
|
30250
30526
|
async function runStopHook(harness) {
|
|
@@ -30377,7 +30653,7 @@ function registerMcpCommands(program3, version2) {
|
|
|
30377
30653
|
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(attachUpdateNoticeToTools(buildTools(), version2), { name: "slop", version: version2 }));
|
|
30378
30654
|
mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
|
|
30379
30655
|
const homes = fetchCodexHomes();
|
|
30380
|
-
const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home))];
|
|
30656
|
+
const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home)), ...fetchCursorHomes().flatMap((home) => installIntoCursorHome(home))];
|
|
30381
30657
|
let codexPresent = true;
|
|
30382
30658
|
try {
|
|
30383
30659
|
execFileSync6("codex", ["--version"], { stdio: "ignore" });
|
|
@@ -30388,11 +30664,15 @@ function registerMcpCommands(program3, version2) {
|
|
|
30388
30664
|
emitOutput({ done: lines }, () => lines.join("\n"));
|
|
30389
30665
|
});
|
|
30390
30666
|
const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
|
|
30391
|
-
const parseHarnessFromOption = (options) =>
|
|
30392
|
-
|
|
30393
|
-
|
|
30394
|
-
|
|
30395
|
-
|
|
30667
|
+
const parseHarnessFromOption = (options) => {
|
|
30668
|
+
if (!options.harness) return null;
|
|
30669
|
+
const known = AgentHarness.safeParse(options.harness);
|
|
30670
|
+
return known.success ? known.data : "other";
|
|
30671
|
+
};
|
|
30672
|
+
hook.command("start").description("SessionStart hook (Claude Code, Codex, Cursor): writes the conversation down so the MCP server is its session, and tells the agent its name and how messages work.").option("--harness <name>", "claude, codex or cursor").action((options) => runStartHook(parseHarnessFromOption(options)));
|
|
30673
|
+
hook.command("tool").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude, codex or cursor").action((options) => runToolHook(parseHarnessFromOption(options)));
|
|
30674
|
+
hook.command("prompt").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude, codex or cursor").action((options) => runPromptHook(parseHarnessFromOption(options)));
|
|
30675
|
+
hook.command("stop").description("No longer installed (Claude Code is woken over its socket, Codex through codex queue); kept so an older install's hook still hands over unread messages.").option("--harness <name>", "claude, codex or cursor").action((options) => runStopHook(parseHarnessFromOption(options)));
|
|
30396
30676
|
}
|
|
30397
30677
|
|
|
30398
30678
|
// src/commands/sessions.ts
|
|
@@ -30480,13 +30760,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
|
|
|
30480
30760
|
}
|
|
30481
30761
|
|
|
30482
30762
|
// src/index.ts
|
|
30483
|
-
var program2 = new Command().name("slop").description("Slopware Tasks from the terminal. Everything a person can do on the board, an agent can do here.").version("0.4.
|
|
30763
|
+
var program2 = new Command().name("slop").description("Slopware Tasks from the terminal. Everything a person can do on the board, an agent can do here.").version("0.4.16").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => updateJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
|
|
30484
30764
|
registerAuthCommands(program2);
|
|
30485
30765
|
registerProjectCommands(program2);
|
|
30486
30766
|
var taskCommand = registerTaskCommands(program2);
|
|
30487
30767
|
registerSessionCommands(program2);
|
|
30488
30768
|
registerThreadCommands(program2, taskCommand);
|
|
30489
|
-
registerMcpCommands(program2, "0.4.
|
|
30769
|
+
registerMcpCommands(program2, "0.4.16");
|
|
30490
30770
|
registerGuideCommand(program2);
|
|
30491
30771
|
registerSkillCommands(program2);
|
|
30492
30772
|
program2.parseAsync().catch(exitWithError);
|