slopwaresystems 0.4.13 → 0.4.15

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 +93 -17
  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;
@@ -27332,6 +27332,10 @@ function fetchCodexHomes(root = homedir3(), inUse = findHomesInUse) {
27332
27332
  const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
27333
27333
  return deduplicateStrings([join5(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...findDirectoriesNamed(root, ".codex").filter((dir) => hasAnyEntry(dir, ["config.toml", "auth.json", "sessions"]))]);
27334
27334
  }
27335
+ function fetchCursorHomes(root = homedir3(), _inUse = findHomesInUse) {
27336
+ if (process.env.SLOP_CURSOR_HOME) return [process.env.SLOP_CURSOR_HOME];
27337
+ return deduplicateStrings([join5(root, ".cursor"), ...findDirectoriesNamed(root, ".cursor").filter((dir) => hasAnyEntry(dir, ["cli-config.json", "hooks.json", "mcp.json", "projects"]))]);
27338
+ }
27335
27339
  function resolveOwnHarnessHome(variable) {
27336
27340
  if (process.platform === "win32") return null;
27337
27341
  try {
@@ -27495,6 +27499,7 @@ function wakeCodexOverWs(text, threadId, endpoint) {
27495
27499
  }
27496
27500
 
27497
27501
  // src/platform/harness.ts
27502
+ var AgentHarnessValues = AgentHarness.options;
27498
27503
  var HEAD_BYTES = 64 * 1024;
27499
27504
  var parent = null;
27500
27505
  function fetchParentCommand() {
@@ -27508,9 +27513,11 @@ function fetchParentCommand() {
27508
27513
  }
27509
27514
  function parseHarnessFromCommand(command) {
27510
27515
  const first = basename2(command.trim().split(/\s+/)[0] ?? "");
27516
+ if (/^cursor-agent(\b|$)/.test(first)) return "cursor";
27511
27517
  if (/^claude(\b|$)/.test(first)) return "claude";
27512
27518
  if (/^codex(\b|$)/.test(first)) return "codex";
27513
27519
  if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
27520
+ if (/(^|\/)cursor-agent(\s|$)/.test(command)) return "cursor";
27514
27521
  return null;
27515
27522
  }
27516
27523
  function isNonInteractiveCommand(command) {
@@ -27643,7 +27650,7 @@ function resolveCodexBinary() {
27643
27650
  }
27644
27651
  function fetchHarnessConversation(cwd) {
27645
27652
  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" };
27653
+ if (explicit) return { id: explicit, harness: AgentHarnessValues.includes(process.env.SLOP_HARNESS) ? process.env.SLOP_HARNESS : "other" };
27647
27654
  if (process.env.CODEX_THREAD_ID?.trim()) return { id: process.env.CODEX_THREAD_ID.trim(), harness: "codex" };
27648
27655
  if (process.env.CLAUDE_CODE_SESSION_ID?.trim()) return { id: process.env.CLAUDE_CODE_SESSION_ID.trim(), harness: "claude" };
27649
27656
  const harness = parseHarnessFromCommand(fetchParentCommand());
@@ -28800,6 +28807,7 @@ var HeldSession = class {
28800
28807
  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
28808
  import { basename as basename4, dirname as dirname7, join as join14 } from "path";
28802
28809
  var WORKER_FRESH_MS = 12e4;
28810
+ var WORKER_GONE_MS = 3e5;
28803
28811
  var READ_CAP = 256 * 1024;
28804
28812
  var CARRY_CAP = 64 * 1024;
28805
28813
  var CHILD_SCAN_CAP = 40;
@@ -28937,6 +28945,21 @@ function findClaudeTranscriptPath(homes, conversation) {
28937
28945
  }
28938
28946
  return null;
28939
28947
  }
28948
+ function findCursorTranscriptPath(homes, conversation) {
28949
+ for (const home of homes) {
28950
+ let projects;
28951
+ try {
28952
+ projects = readdirSync7(join14(home, "projects"));
28953
+ } catch {
28954
+ continue;
28955
+ }
28956
+ for (const project of projects) {
28957
+ const path2 = join14(home, "projects", project, "agent-transcripts", conversation, `${conversation}.jsonl`);
28958
+ if (existsSync10(path2)) return path2;
28959
+ }
28960
+ }
28961
+ return null;
28962
+ }
28940
28963
  function parseClaudeWorkerLabel(text) {
28941
28964
  let meta3;
28942
28965
  try {
@@ -28983,6 +29006,12 @@ function parseWhereFromJson(line) {
28983
29006
  }
28984
29007
  }
28985
29008
  var isCodexHelperRollout = (head) => /"other"\s*:\s*"guardian"|"thread_source"\s*:\s*"[^"]*guardian/.test(head);
29009
+ function parseCodexWorkerLabel(head) {
29010
+ const nickname = /"agent_nickname"\s*:\s*"([^"]+)"/.exec(head)?.[1];
29011
+ const role = /"agent_role"\s*:\s*"([^"]+)"/.exec(head)?.[1];
29012
+ const parts = [nickname, role].filter((part) => Boolean(part));
29013
+ return parts.length ? truncate(parts.join(" \u2014 "), WORKER_LABEL_MAX) : null;
29014
+ }
28986
29015
  function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
28987
29016
  const rollouts = [];
28988
29017
  const findRolloutsInDirectory = (directory, depth) => {
@@ -29012,7 +29041,7 @@ function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
29012
29041
  if (!head.includes('"session_meta"') || isCodexHelperRollout(head)) continue;
29013
29042
  if (!new RegExp(`"parent_thread_id"\\s*:\\s*"${parentThreadId}"`).test(head)) continue;
29014
29043
  const where = parseWhereFromJson(head);
29015
- found.push({ ...rollout, label: "subagent", directory: where.directory, branch: where.branch });
29044
+ found.push({ ...rollout, label: parseCodexWorkerLabel(head) ?? "subagent", directory: where.directory, branch: where.branch });
29016
29045
  }
29017
29046
  return found;
29018
29047
  }
@@ -29081,7 +29110,7 @@ var HarnessActivity = class {
29081
29110
  }
29082
29111
  /** Which files to read: this chat's own, and one per worker still working. */
29083
29112
  discover(now) {
29084
- const found = this.findWorkerFiles(now - WORKER_FRESH_MS);
29113
+ const found = this.findWorkerFiles(now - WORKER_GONE_MS);
29085
29114
  for (const file2 of found) {
29086
29115
  const known = this.workers.get(file2.ref);
29087
29116
  if (known) Object.assign(known, { label: file2.label, directory: file2.directory, branch: file2.branch, changedAt: file2.changedAt });
@@ -29096,6 +29125,11 @@ var HarnessActivity = class {
29096
29125
  this.own ??= transcript ? new FileTail(transcript, "end") : null;
29097
29126
  return transcript ? findClaudeWorkerFiles(transcript, this.conversation.id, changedAfter) : [];
29098
29127
  }
29128
+ if (this.conversation.harness === "cursor") {
29129
+ const transcript = findCursorTranscriptPath(fetchCursorHomes(), this.conversation.id);
29130
+ this.own ??= transcript ? new FileTail(transcript, "end") : null;
29131
+ return [];
29132
+ }
29099
29133
  if (this.conversation.harness === "codex") {
29100
29134
  const roots = fetchCodexHomes().map((home) => join14(home, "sessions"));
29101
29135
  const rollout = fetchCodexRolloutPath(roots, this.conversation.id);
@@ -29116,7 +29150,8 @@ var HarnessActivity = class {
29116
29150
  for (const worker of this.workers.values()) worker.doing = findNewestCall(worker.tail) ?? worker.doing;
29117
29151
  }
29118
29152
  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 }));
29153
+ const now = Date.now();
29154
+ 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
29155
  return { doing: this.watched ? this.doing : null, workers };
29121
29156
  }
29122
29157
  };
@@ -30161,6 +30196,42 @@ tool_timeout_sec = 21600
30161
30196
  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
30197
  return done;
30163
30198
  }
30199
+ function installIntoCursorHome(dir) {
30200
+ const done = [];
30201
+ mkdirSync9(dir, { recursive: true });
30202
+ const mcpPath = join16(dir, "mcp.json");
30203
+ const mcp = fetchJsonFile(mcpPath);
30204
+ const servers = mcp.mcpServers ?? {};
30205
+ const had = "slop" in servers;
30206
+ servers.slop = { command: "slop", args: ["mcp"] };
30207
+ mcp.mcpServers = servers;
30208
+ writeFileSync8(mcpPath, `${JSON.stringify(mcp, null, 2)}
30209
+ `);
30210
+ done.push(`Cursor: MCP server "slop" ${had ? "already in" : "added to"} ${mcpPath}`);
30211
+ const hooksPath = join16(dir, "hooks.json");
30212
+ const file2 = fetchJsonFile(hooksPath);
30213
+ file2.version = typeof file2.version === "number" ? file2.version : 1;
30214
+ file2.hooks = mergeCursorHooks(file2.hooks ?? {});
30215
+ writeFileSync8(hooksPath, `${JSON.stringify(file2, null, 2)}
30216
+ `);
30217
+ done.push(`Cursor: sessionStart hook written to ${hooksPath}; nothing wakes an idle chat, so messages wait for its next tool call`);
30218
+ try {
30219
+ execFileSync6("cursor-agent", ["mcp", "enable", "slop"], { stdio: "ignore" });
30220
+ done.push("Cursor: slop is on its approved MCP list");
30221
+ } catch {
30222
+ done.push("Cursor: approve the server once with `cursor-agent mcp enable slop`, or answer its prompt");
30223
+ }
30224
+ return done;
30225
+ }
30226
+ function mergeCursorHooks(hooks) {
30227
+ const isOwnHook = (hook) => typeof hook.command === "string" && /(^|\s)slop hook /.test(hook.command);
30228
+ for (const event of Object.keys(hooks)) {
30229
+ hooks[event] = (hooks[event] ?? []).filter((hook) => !isOwnHook(hook));
30230
+ if (hooks[event].length === 0) delete hooks[event];
30231
+ }
30232
+ hooks.sessionStart = [...hooks.sessionStart ?? [], { command: "slop hook start --harness cursor", timeout: 5 }];
30233
+ return hooks;
30234
+ }
30164
30235
  async function trustCodexHooks(codexHome) {
30165
30236
  const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
30166
30237
  const where = codexHome === join16(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
@@ -30198,7 +30269,7 @@ async function trustCodexHooks(codexHome) {
30198
30269
  const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
30199
30270
  `);
30200
30271
  try {
30201
- await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.13" }, capabilities: { experimentalApi: true } });
30272
+ await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.15" }, capabilities: { experimentalApi: true } });
30202
30273
  sendNotification("initialized", {});
30203
30274
  const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
30204
30275
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -30225,7 +30296,7 @@ async function parseHookInput(harness, remember = true) {
30225
30296
  let conversation = null;
30226
30297
  try {
30227
30298
  const parsed = JSON.parse(input2);
30228
- cwd = parsed.cwd ?? cwd;
30299
+ cwd = parsed.cwd ?? parsed.workspace_roots?.[0] ?? cwd;
30229
30300
  conversation = parsed.session_id ?? null;
30230
30301
  } catch {
30231
30302
  }
@@ -30243,8 +30314,9 @@ async function runStartHook(harness) {
30243
30314
  if (isNonInteractiveHarness()) return;
30244
30315
  const { conversation } = await parseHookInput(harness);
30245
30316
  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 } })}
30317
+ const carried = harness === "codex" ? " (no wait tool on Codex; a reply becomes your next turn)" : harness === "cursor" ? " (nothing wakes an idle chat here: a message reaches you with your next slop tool call)" : "";
30318
+ 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}.`;
30319
+ process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text }, additional_context: text })}
30248
30320
  `);
30249
30321
  }
30250
30322
  async function runStopHook(harness) {
@@ -30377,7 +30449,7 @@ function registerMcpCommands(program3, version2) {
30377
30449
  mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(attachUpdateNoticeToTools(buildTools(), version2), { name: "slop", version: version2 }));
30378
30450
  mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
30379
30451
  const homes = fetchCodexHomes();
30380
- const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home))];
30452
+ const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home)), ...fetchCursorHomes().flatMap((home) => installIntoCursorHome(home))];
30381
30453
  let codexPresent = true;
30382
30454
  try {
30383
30455
  execFileSync6("codex", ["--version"], { stdio: "ignore" });
@@ -30388,11 +30460,15 @@ function registerMcpCommands(program3, version2) {
30388
30460
  emitOutput({ done: lines }, () => lines.join("\n"));
30389
30461
  });
30390
30462
  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)));
30463
+ const parseHarnessFromOption = (options) => {
30464
+ if (!options.harness) return null;
30465
+ const known = AgentHarness.safeParse(options.harness);
30466
+ return known.success ? known.data : "other";
30467
+ };
30468
+ 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)));
30469
+ 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)));
30470
+ 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)));
30471
+ 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
30472
  }
30397
30473
 
30398
30474
  // src/commands/sessions.ts
@@ -30480,13 +30556,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
30480
30556
  }
30481
30557
 
30482
30558
  // 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)");
30559
+ 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.15").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
30560
  registerAuthCommands(program2);
30485
30561
  registerProjectCommands(program2);
30486
30562
  var taskCommand = registerTaskCommands(program2);
30487
30563
  registerSessionCommands(program2);
30488
30564
  registerThreadCommands(program2, taskCommand);
30489
- registerMcpCommands(program2, "0.4.13");
30565
+ registerMcpCommands(program2, "0.4.15");
30490
30566
  registerGuideCommand(program2);
30491
30567
  registerSkillCommands(program2);
30492
30568
  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.15",
4
4
  "private": false,
5
5
  "description": "slop: the Slopware Tasks board from the terminal, for people and coding agents.",
6
6
  "type": "module",