slopwaresystems 0.4.15 → 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 +231 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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
 
@@ -27497,6 +27498,109 @@ function wakeCodexOverWs(text, threadId, endpoint) {
27497
27498
  });
27498
27499
  });
27499
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
+ }
27500
27604
 
27501
27605
  // src/platform/harness.ts
27502
27606
  var AgentHarnessValues = AgentHarness.options;
@@ -27648,6 +27752,31 @@ function resolveCodexBinary() {
27648
27752
  }
27649
27753
  return "codex";
27650
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
+ }
27651
27780
  function fetchHarnessConversation(cwd) {
27652
27781
  const explicit = process.env.SLOP_CONVERSATION_ID?.trim();
27653
27782
  if (explicit) return { id: explicit, harness: AgentHarnessValues.includes(process.env.SLOP_HARNESS) ? process.env.SLOP_HARNESS : "other" };
@@ -27671,9 +27800,13 @@ var startedAt = Date.now();
27671
27800
  function fetchKnownConversation(cwd) {
27672
27801
  const own2 = fetchHarnessConversation(cwd);
27673
27802
  if (own2) return own2;
27674
- const remembered = fetchRememberedConversation(cwd, startedAt - 12e4);
27803
+ const remembered = fetchRememberedConversation(cwd, startedAt - 12e4, resolveParentHarness());
27675
27804
  if (remembered?.harness === "codex" && isMainChatThread(fetchCodexHomes().map((home) => join8(home, "sessions")), remembered.id)) return remembered;
27676
- 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;
27677
27810
  }
27678
27811
  function resolveAgentIdentity(cwd, fallback = "process") {
27679
27812
  const conversation = fetchKnownConversation(cwd);
@@ -28877,6 +29010,67 @@ function parseToolCallFromCodexLine(line) {
28877
29010
  if (payload.type === "local_shell_call") return formatToolCallLine("shell", payload.action);
28878
29011
  return null;
28879
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
+ }
28880
29074
  var fetchFileSize = (path2) => {
28881
29075
  try {
28882
29076
  return statSync4(path2).size;
@@ -29045,6 +29239,7 @@ function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
29045
29239
  }
29046
29240
  return found;
29047
29241
  }
29242
+ var ASKED_CAP = 16;
29048
29243
  var HarnessActivity = class {
29049
29244
  constructor(conversation, onReport) {
29050
29245
  this.conversation = conversation;
@@ -29053,6 +29248,8 @@ var HarnessActivity = class {
29053
29248
  conversation;
29054
29249
  onReport;
29055
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 = [];
29056
29253
  own = null;
29057
29254
  doing = null;
29058
29255
  watched = false;
@@ -29070,10 +29267,7 @@ var HarnessActivity = class {
29070
29267
  setWatched(on) {
29071
29268
  if (on === this.watched) return;
29072
29269
  this.watched = on;
29073
- if (on) {
29074
- this.own = this.own && new FileTail(this.own.path);
29075
- for (const worker of this.workers.values()) worker.tail = new FileTail(worker.path);
29076
- }
29270
+ if (on) this.doing = null;
29077
29271
  this.tick();
29078
29272
  }
29079
29273
  /** Say it again: the room forgets a session's activity when its socket drops, and only the agent can refill it. */
@@ -29089,6 +29283,10 @@ var HarnessActivity = class {
29089
29283
  tick() {
29090
29284
  if (this.stopped) return;
29091
29285
  const now = Date.now();
29286
+ try {
29287
+ this.read();
29288
+ } catch {
29289
+ }
29092
29290
  if (now - this.discoveredAt >= DISCOVER_EVERY_MS) {
29093
29291
  this.discoveredAt = now;
29094
29292
  try {
@@ -29096,12 +29294,6 @@ var HarnessActivity = class {
29096
29294
  } catch {
29097
29295
  }
29098
29296
  }
29099
- if (this.watched) {
29100
- try {
29101
- this.read();
29102
- } catch {
29103
- }
29104
- }
29105
29297
  const activity = this.build();
29106
29298
  const said = JSON.stringify(activity);
29107
29299
  if (said === this.reported) return;
@@ -29128,7 +29320,7 @@ var HarnessActivity = class {
29128
29320
  if (this.conversation.harness === "cursor") {
29129
29321
  const transcript = findCursorTranscriptPath(fetchCursorHomes(), this.conversation.id);
29130
29322
  this.own ??= transcript ? new FileTail(transcript, "end") : null;
29131
- return [];
29323
+ return transcript ? findCursorWorkerFiles(transcript, this.conversation.id, changedAfter, this.asked) : [];
29132
29324
  }
29133
29325
  if (this.conversation.harness === "codex") {
29134
29326
  const roots = fetchCodexHomes().map((home) => join14(home, "sessions"));
@@ -29146,7 +29338,18 @@ var HarnessActivity = class {
29146
29338
  for (const line of tail.fetchNewLines()) newest = parse3(line) ?? newest;
29147
29339
  return newest;
29148
29340
  };
29149
- 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
+ }
29150
29353
  for (const worker of this.workers.values()) worker.doing = findNewestCall(worker.tail) ?? worker.doing;
29151
29354
  }
29152
29355
  build() {
@@ -29708,7 +29911,7 @@ function createProjectTools(api, project, identity) {
29708
29911
  if (waking || stopped) return;
29709
29912
  waking = true;
29710
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.`;
29711
- 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);
29712
29915
  void wake.catch(() => false).finally(() => {
29713
29916
  waking = false;
29714
29917
  });
@@ -30269,7 +30472,7 @@ async function trustCodexHooks(codexHome) {
30269
30472
  const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
30270
30473
  `);
30271
30474
  try {
30272
- await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.15" }, capabilities: { experimentalApi: true } });
30475
+ await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.16" }, capabilities: { experimentalApi: true } });
30273
30476
  sendNotification("initialized", {});
30274
30477
  const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
30275
30478
  const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
@@ -30285,7 +30488,8 @@ async function trustCodexHooks(codexHome) {
30285
30488
  server.kill();
30286
30489
  }
30287
30490
  }
30288
- async function parseHookInput(harness, remember = true) {
30491
+ async function parseHookInput(installedAs, remember = true) {
30492
+ const harness = resolveParentHarness() ?? installedAs;
30289
30493
  const input2 = await new Promise((resolve6) => {
30290
30494
  let data = "";
30291
30495
  process.stdin.on("data", (chunk) => data += chunk);
@@ -30302,7 +30506,7 @@ async function parseHookInput(harness, remember = true) {
30302
30506
  }
30303
30507
  const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join16(home, "sessions")), conversation);
30304
30508
  if (remember && conversation && ownThread && !isNonInteractiveHarness()) persistRememberedConversation(cwd, { id: conversation, harness: harness ?? "other" });
30305
- return { cwd, conversation };
30509
+ return { cwd, conversation, harness };
30306
30510
  }
30307
30511
  async function runPromptHook(harness) {
30308
30512
  await parseHookInput(harness);
@@ -30310,11 +30514,11 @@ async function runPromptHook(harness) {
30310
30514
  async function runToolHook(harness) {
30311
30515
  await parseHookInput(harness, false);
30312
30516
  }
30313
- async function runStartHook(harness) {
30517
+ async function runStartHook(installedAs) {
30314
30518
  if (isNonInteractiveHarness()) return;
30315
- const { conversation } = await parseHookInput(harness);
30519
+ const { conversation, harness } = await parseHookInput(installedAs);
30316
30520
  if (!conversation) return;
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)" : "";
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)" : "";
30318
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}.`;
30319
30523
  process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text }, additional_context: text })}
30320
30524
  `);
@@ -30556,13 +30760,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
30556
30760
  }
30557
30761
 
30558
30762
  // src/index.ts
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)");
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)");
30560
30764
  registerAuthCommands(program2);
30561
30765
  registerProjectCommands(program2);
30562
30766
  var taskCommand = registerTaskCommands(program2);
30563
30767
  registerSessionCommands(program2);
30564
30768
  registerThreadCommands(program2, taskCommand);
30565
- registerMcpCommands(program2, "0.4.15");
30769
+ registerMcpCommands(program2, "0.4.16");
30566
30770
  registerGuideCommand(program2);
30567
30771
  registerSkillCommands(program2);
30568
30772
  program2.parseAsync().catch(exitWithError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopwaresystems",
3
- "version": "0.4.15",
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",