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.
Files changed (2) hide show
  1. package/dist/index.js +319 -39
  2. 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 harness = AgentHarness.safeParse(parsed.harness ?? "other");
27162
- if (typeof parsed.id === "string" && parsed.id && harness.success) candidates.push({ id: parsed.id, harness: harness.data });
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 distinct = [...new Map(candidates.map((candidate) => [candidate.id, candidate])).values()];
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 === "codex" ? "codex" : process.env.SLOP_HARNESS === "claude" ? "claude" : "other" };
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
- return remembered?.harness === "codex" ? null : remembered;
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 - WORKER_FRESH_MS);
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) this.doing = findNewestCall(this.own) ?? this.doing;
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 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 ? worker.doing : null, since: worker.since }));
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.13" }, capabilities: { experimentalApi: true } });
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(harness, remember = true) {
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(harness) {
30517
+ async function runStartHook(installedAs) {
30243
30518
  if (isNonInteractiveHarness()) return;
30244
- const { conversation } = await parseHookInput(harness);
30519
+ const { conversation, harness } = await parseHookInput(installedAs);
30245
30520
  if (!conversation) return;
30246
- 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${harness === "codex" ? " (no wait tool on Codex; a reply becomes your next turn)" : ""}.`;
30247
- process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } })}
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) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
30392
- hook.command("start").description("SessionStart hook (Claude Code and Codex): 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 or codex").action((options) => runStartHook(parseHarnessFromOption(options)));
30393
- hook.command("tool").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude or codex").action((options) => runToolHook(parseHarnessFromOption(options)));
30394
- hook.command("prompt").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude or codex").action((options) => runPromptHook(parseHarnessFromOption(options)));
30395
- 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 or codex").action((options) => runStopHook(parseHarnessFromOption(options)));
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.13").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)");
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.13");
30769
+ registerMcpCommands(program2, "0.4.16");
30490
30770
  registerGuideCommand(program2);
30491
30771
  registerSkillCommands(program2);
30492
30772
  program2.parseAsync().catch(exitWithError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.13",
3
+ "version": "0.4.16",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",