slopwaresystems 0.2.2 → 0.2.4
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 +143 -92
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3385,7 +3385,7 @@ var slop_cli_default = '---\nname: slop-cli\ndescription: How to use the slop CL
|
|
|
3385
3385
|
var slop_planning_default = '---\nname: slop-planning\ndescription: How to break long or multi-session work into a plan on its task, keep each item\'s note current, and pick up where you left off. Load when a task will take more than one sitting, or when asked to plan, continue, or report progress.\n---\n\n# Planning long work on a task\n\nA task\'s **plan** is an ordered list of items, each with a **note**: the latest true statement about that item. The plan plus the task\'s timeline is your memory between sessions. Write to it as you work, not at the end.\n\n## Starting\n\n1. Read the task: `slop task show <KEY>`. If it has a plan, read the notes; they are what the last session knew.\n2. If there is no plan, write one. Items are concrete, checkable, and ordered by what unlocks what. Eight to fifteen is typical; more means the task should be split.\n\n```bash\nslop task plan <KEY> - <<\'EOF\'\nInventory every Intercom object we rely on (conversations, users, tags)\nDecide the target schema for the support agent\nWrite and test the export script against a sample\nRun the full export; record counts\nImport into the support agent; verify counts match\nSwitch inbound routing; keep Intercom read-only for a week\nRemove the Intercom SDK and env vars\nEOF\n```\n\n\n## Working\n\n- Before an item: `slop task item note <KEY> <n> "Starting. Approach: \u2026"`\n- As facts arrive, replace the note. Notes are state, not a log: "Exported 1,204 conversations; 3 failed (attachments over 10 MB), ids in scripts/export/failed.json".\n- When an item is done: `slop task item done <KEY> <n>`. If it turned out to be two things, add the second: `slop task item add <KEY> "\u2026"`.\n- Blocked? Put the blocker in the note, then move on to an item that is not blocked. Do not mark blocked items done.\n- Decisions go in the description (edit it) so the next reader does not have to reconstruct them.\n\n## Follow-ups and later\n\n- Work discovered but out of scope becomes its own task: `slop task create "\u2026" --status backlog -d "Found while doing <KEY>: \u2026"`.\n- Something to check later on this task is an item: `slop task item add <KEY> "Confirm Intercom exports stayed empty for a week"`.\n\n## Finishing\n\n`slop task items <KEY>` should show every item done or removed. Put what changed and where at the end of the description, link the PR (`slop task link`), then `slop task done <KEY>`.\n\n## Picking up a task you did not start\n\n`slop task show`, `slop task items`, `slop task history`, in that order. The notes tell you the present; the history tells you how it got there. Then continue from the first item that is not done.\n';
|
|
3386
3386
|
|
|
3387
3387
|
// skills/slop-sessions.md
|
|
3388
|
-
var slop_sessions_default = '---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project\'s message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live
|
|
3388
|
+
var slop_sessions_default = '---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project\'s message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live. With the MCP server it exists from your first slop tool call until your harness exits: first as presence in the project (no task), then on the task you claim. Being live means messages find you. Sessions are visible to everyone (`slop sessions`, the board\'s live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, or in **a thread you have written in** (that is how answers find you, even from a general thread or another ticket), reaches you at once. Nothing else does.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop (MCP: the way that works)\n\nYour harness has the `slop` MCP server (`slop mcp install` set it up). Use its tools; nothing runs in the background and nothing has to be restarted.\n\n1. **Claim:** `claim_task(task)`. Assigns the task to you, moves it to in progress, and returns what is unread about it. Read that first.\n2. **Work.** Keep the plan\'s notes current with `note_item`, tick with `finish_item`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `claim_task` upgrades it to the task; `finish_task` or `release_task` drops back to presence. Either way, for as long as the harness runs the server A message in a thread about your task, or in any thread you have written in, reaches you on its own: right after your next tool call (any tool; a hook on Claude Code and on Codex hands it to you), and in every slop tool\'s result besides. There is nothing to poll and nothing to restart. When one arrives, act or answer with `reply(thread, body)` and carry on.\n4. **Nothing to do but wait?** `wait_for_message(task, minutes)` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `finish_task(task)` (it also releases the session), or `release_task` to step off without finishing.\n\nBoth harnesses also refuse to stop while messages are unread about your task (a Stop hook), so nothing is lost at the end of a turn.\n\n## The loop without MCP (scripts, older setups)\n\n`slop session start <KEY>`, then `slop session wait <KEY>` in the background. It prints the first message and exits; **the exit is the message**: read it, reply with `slop thread reply <id> "\u2026"`, run `slop session wait <KEY>` again at once. Exit codes: 0 message, 2 ended on purpose (stop), 3 timeout, 4 dropped (relaunch). `--follow` streams instead of exiting, for harnesses that watch output.\n\n## Talking\n\n```bash\nslop sessions # who is live on what\nslop threads [--task KEY] [--general] [--unread]\nslop thread show <id> # read it (marks it read)\nslop thread reply <id> "\u2026" # in a task thread: reaches whoever is live on it, or waits for them\nslop thread new "Subject" -m "\u2026" --task WEB-12 # a new thread about a task\nslop thread new "Subject" -m "\u2026" # a general thread: for people, and for context\nslop task ask WEB-12 "\u2026" # shorthand: a new thread about WEB-12 from one message\n```\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan\'s notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent, and one agent per working directory: run each agent in its own checkout or git worktree, so its inbox is its own.\n- Do not claim a task that has someone else\'s live session (`sessions`); write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan\'s notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n';
|
|
3389
3389
|
|
|
3390
3390
|
// skills/slop-working.md
|
|
3391
3391
|
var slop_working_default = '---\nname: slop-working\ndescription: The working loop for tasks on the board (claim, do, report, finish) and what to record where. Load at the start of any coding session in a repository linked with slop.\n---\n\n# The loop\n\n1. **What is there:** `slop task list --assignee me` (or `slop task list`). If nothing is assigned to you, take the top item from the first "to do" column: `slop task assign <KEY> me`.\n2. **Claim it:** `slop session start <KEY>` (assigns you, moves it to in progress), then run `slop session wait <KEY>` in the background so other agents can reach you (see `slop-sessions`). Read `slop task show <KEY>` fully, including the plan and its notes.\n3. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n4. **Report as you go**: the item\'s note is the present state of that piece of work; the description holds what you decided and why. Write in sentences a teammate can act on.\n5. **Finish:** link the PR, put what changed and where at the end of the description, `slop task done <KEY>`, `slop session end <KEY>`.\n\n# Where things go\n\n| This | Goes here |\n|---|---|\n| The current truth about a piece of the work | the item\'s note |\n| A decision and its reason | the description (edit it) |\n| A question for the agent on a ticket | a thread about that ticket (`slop task ask KEY "\u2026"`) |\n| A note for people, or for whoever comes next | a general thread (`slop post "\u2026"`) |\n| A discovery, a hand-off | the description, or a new task if it is work |\n| A pull request, a design, a log | a link |\n| Loose working notes nobody needs to act on | the task\'s notes (`slop task notes`) |\n| Work you found but will not do now | a new task in backlog |\n\n# Etiquette\n\n- One task in progress at a time per agent. `slop sessions` shows who is live; do not take a task with someone else\'s live session without asking (`slop task ask`).\n- Never delete a task or someone else\'s items; archive and remove are for the owner.\n- Severity is the impact if nothing is done: `critical` is data loss or money, `high` is users blocked, `medium` is friction, `low` is polish.\n';
|
|
@@ -23442,8 +23442,9 @@ var TaskEventRecord = external_exports.object({
|
|
|
23442
23442
|
// ../contract/src/work/sessions.ts
|
|
23443
23443
|
var Session = external_exports.object({
|
|
23444
23444
|
id: Id,
|
|
23445
|
-
|
|
23446
|
-
|
|
23445
|
+
/** Null: the agent is live in the project but on no task yet (presence). Replies in threads it is part of still reach it. */
|
|
23446
|
+
taskId: Id.nullable(),
|
|
23447
|
+
taskKey: external_exports.string().nullable(),
|
|
23447
23448
|
user: UserRef,
|
|
23448
23449
|
machine: external_exports.object({ id: Id, name: external_exports.string() }),
|
|
23449
23450
|
startedAt: Timestamp
|
|
@@ -23517,6 +23518,29 @@ function resolveSession() {
|
|
|
23517
23518
|
return { baseUrl, token };
|
|
23518
23519
|
}
|
|
23519
23520
|
|
|
23521
|
+
// src/platform/session-file.ts
|
|
23522
|
+
import { createHash } from "crypto";
|
|
23523
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
23524
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
23525
|
+
var sessionsDir = () => join3(dirname2(configPath()), "sessions");
|
|
23526
|
+
var realCwd = (cwd) => {
|
|
23527
|
+
try {
|
|
23528
|
+
return realpathSync(cwd);
|
|
23529
|
+
} catch {
|
|
23530
|
+
return cwd;
|
|
23531
|
+
}
|
|
23532
|
+
};
|
|
23533
|
+
var sessionFile = (cwd) => join3(sessionsDir(), `${createHash("sha256").update(realCwd(cwd)).digest("hex").slice(0, 16)}.txt`);
|
|
23534
|
+
function rememberSession(cwd, sessionId) {
|
|
23535
|
+
mkdirSync3(sessionsDir(), { recursive: true });
|
|
23536
|
+
if (sessionId) writeFileSync3(sessionFile(cwd), sessionId);
|
|
23537
|
+
else rmSync2(sessionFile(cwd), { force: true });
|
|
23538
|
+
}
|
|
23539
|
+
function rememberedSession(cwd) {
|
|
23540
|
+
const path3 = sessionFile(cwd);
|
|
23541
|
+
return existsSync3(path3) ? readFileSync3(path3, "utf8").trim() || null : null;
|
|
23542
|
+
}
|
|
23543
|
+
|
|
23520
23544
|
// src/platform/errors.ts
|
|
23521
23545
|
var CliError = class extends Error {
|
|
23522
23546
|
constructor(message, hint, code = "error", exitCode = 1) {
|
|
@@ -23536,6 +23560,7 @@ function createApi(options) {
|
|
|
23536
23560
|
const headers = { accept: "application/json" };
|
|
23537
23561
|
if (body2 !== void 0) headers["content-type"] = "application/json";
|
|
23538
23562
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
23563
|
+
if (client.sessionId) headers["x-slop-session"] = client.sessionId;
|
|
23539
23564
|
let response;
|
|
23540
23565
|
try {
|
|
23541
23566
|
response = await fetch(`${options.baseUrl}/api${path3}`, {
|
|
@@ -23561,27 +23586,31 @@ function createApi(options) {
|
|
|
23561
23586
|
}
|
|
23562
23587
|
throw new CliError(`${method} ${path3} failed with HTTP ${response.status}`, void 0, "http");
|
|
23563
23588
|
}
|
|
23564
|
-
|
|
23589
|
+
const client = {
|
|
23565
23590
|
baseUrl: options.baseUrl,
|
|
23591
|
+
sessionId: null,
|
|
23566
23592
|
get: (path3) => request("GET", path3),
|
|
23567
23593
|
post: (path3, body2) => request("POST", path3, body2 ?? {}),
|
|
23568
23594
|
patch: (path3, body2) => request("PATCH", path3, body2),
|
|
23569
23595
|
put: (path3, body2) => request("PUT", path3, body2),
|
|
23570
23596
|
delete: (path3) => request("DELETE", path3)
|
|
23571
23597
|
};
|
|
23598
|
+
return client;
|
|
23572
23599
|
}
|
|
23573
23600
|
function authedApi() {
|
|
23574
23601
|
const session = resolveSession();
|
|
23575
23602
|
if (!session.token) throw new CliError(`Not signed in to ${session.baseUrl}`, "slop login", "unauthenticated");
|
|
23576
|
-
|
|
23603
|
+
const client = createApi(session);
|
|
23604
|
+
client.sessionId = rememberedSession(process.cwd());
|
|
23605
|
+
return client;
|
|
23577
23606
|
}
|
|
23578
23607
|
|
|
23579
23608
|
// src/platform/machine.ts
|
|
23580
23609
|
import { execFileSync } from "child_process";
|
|
23581
|
-
import { createHash, randomUUID } from "crypto";
|
|
23582
|
-
import { existsSync as
|
|
23610
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
23611
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
23583
23612
|
import { hostname as hostname3, platform, userInfo } from "os";
|
|
23584
|
-
import { dirname as
|
|
23613
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
23585
23614
|
function run(command, args) {
|
|
23586
23615
|
try {
|
|
23587
23616
|
return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
@@ -23595,7 +23624,7 @@ function osMachineId() {
|
|
|
23595
23624
|
return /"IOPlatformUUID" = "([^"]+)"/.exec(run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]) ?? "")?.[1] ?? null;
|
|
23596
23625
|
case "linux":
|
|
23597
23626
|
for (const path3 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
23598
|
-
if (
|
|
23627
|
+
if (existsSync4(path3)) return readFileSync4(path3, "utf8").trim() || null;
|
|
23599
23628
|
}
|
|
23600
23629
|
return null;
|
|
23601
23630
|
case "win32":
|
|
@@ -23607,18 +23636,18 @@ function osMachineId() {
|
|
|
23607
23636
|
}
|
|
23608
23637
|
}
|
|
23609
23638
|
function persistedMachineId() {
|
|
23610
|
-
const path3 =
|
|
23611
|
-
if (
|
|
23639
|
+
const path3 = join4(dirname3(configPath()), "machine-id");
|
|
23640
|
+
if (existsSync4(path3)) return readFileSync4(path3, "utf8").trim();
|
|
23612
23641
|
const id = randomUUID();
|
|
23613
|
-
|
|
23614
|
-
|
|
23642
|
+
mkdirSync4(dirname3(path3), { recursive: true, mode: 448 });
|
|
23643
|
+
writeFileSync4(path3, `${id}
|
|
23615
23644
|
`, { mode: 384 });
|
|
23616
23645
|
return id;
|
|
23617
23646
|
}
|
|
23618
23647
|
function machineInfo() {
|
|
23619
23648
|
const raw = `${osMachineId() ?? persistedMachineId()}:${userInfo().username}`;
|
|
23620
23649
|
return {
|
|
23621
|
-
fingerprint:
|
|
23650
|
+
fingerprint: createHash2("sha256").update(raw).digest("hex"),
|
|
23622
23651
|
name: hostname3().replace(/\.local$/, ""),
|
|
23623
23652
|
platform: platform()
|
|
23624
23653
|
};
|
|
@@ -23759,9 +23788,9 @@ function registerGuideCommand(program3) {
|
|
|
23759
23788
|
|
|
23760
23789
|
// src/platform/git.ts
|
|
23761
23790
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
23762
|
-
import { createHash as
|
|
23763
|
-
import { existsSync as
|
|
23764
|
-
import { basename, join as
|
|
23791
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
23792
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
23793
|
+
import { basename, join as join5, resolve } from "path";
|
|
23765
23794
|
function git(args, cwd = process.cwd()) {
|
|
23766
23795
|
try {
|
|
23767
23796
|
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -23774,7 +23803,7 @@ function findRepo() {
|
|
|
23774
23803
|
if (!commonDir) return null;
|
|
23775
23804
|
const worktree = git(["rev-parse", "--show-toplevel"]);
|
|
23776
23805
|
const root = basename(commonDir) === ".git" ? resolve(commonDir, "..") : worktree ?? commonDir;
|
|
23777
|
-
return { root, worktree: worktree ?? root, linkPath:
|
|
23806
|
+
return { root, worktree: worktree ?? root, linkPath: join5(commonDir, "slop.json") };
|
|
23778
23807
|
}
|
|
23779
23808
|
function requireRepo() {
|
|
23780
23809
|
const repo = findRepo();
|
|
@@ -23783,17 +23812,17 @@ function requireRepo() {
|
|
|
23783
23812
|
}
|
|
23784
23813
|
function readLink(repo) {
|
|
23785
23814
|
try {
|
|
23786
|
-
return JSON.parse(
|
|
23815
|
+
return JSON.parse(readFileSync5(repo.linkPath, "utf8"));
|
|
23787
23816
|
} catch {
|
|
23788
23817
|
return null;
|
|
23789
23818
|
}
|
|
23790
23819
|
}
|
|
23791
23820
|
function writeLink(repo, link) {
|
|
23792
|
-
|
|
23821
|
+
writeFileSync5(repo.linkPath, `${JSON.stringify(link, null, 2)}
|
|
23793
23822
|
`);
|
|
23794
23823
|
}
|
|
23795
23824
|
function removeLink(repo) {
|
|
23796
|
-
if (
|
|
23825
|
+
if (existsSync5(repo.linkPath)) rmSync3(repo.linkPath);
|
|
23797
23826
|
}
|
|
23798
23827
|
function normalizeRemote(url2) {
|
|
23799
23828
|
const scp = /^[^@/]+@([^:/]+):(.+)$/.exec(url2);
|
|
@@ -23807,7 +23836,7 @@ function describeRepo(repo, existing) {
|
|
|
23807
23836
|
let fingerprint;
|
|
23808
23837
|
let localRepoId;
|
|
23809
23838
|
if (roots) {
|
|
23810
|
-
fingerprint =
|
|
23839
|
+
fingerprint = createHash3("sha256").update(roots.split("\n").sort().join("\n")).digest("hex");
|
|
23811
23840
|
} else {
|
|
23812
23841
|
localRepoId = existing?.localRepoId ?? randomUUID2();
|
|
23813
23842
|
fingerprint = `local:${localRepoId}`;
|
|
@@ -23951,10 +23980,11 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
|
23951
23980
|
|
|
23952
23981
|
// src/commands/mcp.ts
|
|
23953
23982
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
23954
|
-
import {
|
|
23955
|
-
import {
|
|
23983
|
+
import { createHash as createHash4 } from "crypto";
|
|
23984
|
+
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync9, readdirSync, realpathSync as realpathSync2, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
23985
|
+
import { dirname as dirname4 } from "path";
|
|
23956
23986
|
import { homedir as homedir3 } from "os";
|
|
23957
|
-
import { join as
|
|
23987
|
+
import { join as join6 } from "path";
|
|
23958
23988
|
|
|
23959
23989
|
// src/platform/live.ts
|
|
23960
23990
|
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
@@ -23969,6 +23999,8 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
|
23969
23999
|
socket.addEventListener("message", (event) => {
|
|
23970
24000
|
const push = JSON.parse(String(event.data));
|
|
23971
24001
|
if (push.type === "hello") {
|
|
24002
|
+
api.sessionId = push.session.id;
|
|
24003
|
+
rememberSession(process.cwd(), push.session.id);
|
|
23972
24004
|
options.onHello?.(push.session);
|
|
23973
24005
|
return;
|
|
23974
24006
|
}
|
|
@@ -23983,6 +24015,7 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
|
23983
24015
|
socket.addEventListener("close", (event) => done({ kind: "dropped", reason: event.reason || `code ${event.code}` }));
|
|
23984
24016
|
socket.addEventListener("error", () => done({ kind: "dropped", reason: "socket error" }));
|
|
23985
24017
|
});
|
|
24018
|
+
if (outcome.kind !== "message") rememberSession(process.cwd(), null);
|
|
23986
24019
|
if (outcome.kind === "message" || outcome.kind === "timeout") {
|
|
23987
24020
|
socket.close(1e3, outcome.kind === "message" ? "handled" : "timeout");
|
|
23988
24021
|
await new Promise((resolve2) => {
|
|
@@ -24014,7 +24047,7 @@ var HeldSession = class {
|
|
|
24014
24047
|
async connect() {
|
|
24015
24048
|
if (this.stopped) return;
|
|
24016
24049
|
try {
|
|
24017
|
-
const ticket = await this.api.post(`/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket`);
|
|
24050
|
+
const ticket = await this.api.post(this.taskKey ? `/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket` : `/projects/${this.projectId}/session/ticket`);
|
|
24018
24051
|
const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
24019
24052
|
this.socket = socket;
|
|
24020
24053
|
socket.addEventListener("open", () => {
|
|
@@ -24022,6 +24055,11 @@ var HeldSession = class {
|
|
|
24022
24055
|
});
|
|
24023
24056
|
socket.addEventListener("message", (event) => {
|
|
24024
24057
|
const push = JSON.parse(String(event.data));
|
|
24058
|
+
if (push.type === "hello") {
|
|
24059
|
+
this.api.sessionId = push.session.id;
|
|
24060
|
+
rememberSession(process.cwd(), push.session.id);
|
|
24061
|
+
return;
|
|
24062
|
+
}
|
|
24025
24063
|
if (push.type === "end") {
|
|
24026
24064
|
this.stopped = true;
|
|
24027
24065
|
return;
|
|
@@ -24047,6 +24085,8 @@ var HeldSession = class {
|
|
|
24047
24085
|
}
|
|
24048
24086
|
stop() {
|
|
24049
24087
|
this.stopped = true;
|
|
24088
|
+
this.api.sessionId = null;
|
|
24089
|
+
rememberSession(process.cwd(), null);
|
|
24050
24090
|
if (this.timer) clearTimeout(this.timer);
|
|
24051
24091
|
this.socket?.close(1e3, "released");
|
|
24052
24092
|
}
|
|
@@ -24109,11 +24149,11 @@ function serveMcp(tools2, info) {
|
|
|
24109
24149
|
}
|
|
24110
24150
|
|
|
24111
24151
|
// src/commands/tasks.ts
|
|
24112
|
-
import { readFileSync as
|
|
24152
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
24113
24153
|
|
|
24114
24154
|
// src/commands/items.ts
|
|
24115
|
-
import { readFileSync as
|
|
24116
|
-
var readText = (value) => value === "-" ?
|
|
24155
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
24156
|
+
var readText = (value) => value === "-" ? readFileSync6(0, "utf8").trim() : value;
|
|
24117
24157
|
var path2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
24118
24158
|
function renderItems(items) {
|
|
24119
24159
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -24140,7 +24180,7 @@ function registerItemCommands(task, withProject) {
|
|
|
24140
24180
|
withProject(task.command("plan <key> [lines...]")).description('Append items to the plan, one per argument; "-" reads one per line from stdin (list markers are fine).').action(async (key, lines, options) => {
|
|
24141
24181
|
const api = authedApi();
|
|
24142
24182
|
const project = await resolveProject(api, options.project);
|
|
24143
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
24183
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync6(0, "utf8") : lines.join("\n");
|
|
24144
24184
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
24145
24185
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
24146
24186
|
const items = await api.post(path2(project.id, key), { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -24197,7 +24237,7 @@ function oneOf(value, allowed, what) {
|
|
|
24197
24237
|
if (allowed.includes(normalized)) return normalized;
|
|
24198
24238
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
24199
24239
|
}
|
|
24200
|
-
var readDescription = (value) => value === "-" ?
|
|
24240
|
+
var readDescription = (value) => value === "-" ? readFileSync7(0, "utf8").trim() : value;
|
|
24201
24241
|
var taskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
24202
24242
|
var loadProject = (api, id) => api.get(`/projects/${id}`);
|
|
24203
24243
|
function resolveStatus(project, ref) {
|
|
@@ -24348,8 +24388,8 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24348
24388
|
}
|
|
24349
24389
|
|
|
24350
24390
|
// src/commands/threads.ts
|
|
24351
|
-
import { readFileSync as
|
|
24352
|
-
var readText2 = (value) => value === "-" ?
|
|
24391
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
24392
|
+
var readText2 = (value) => value === "-" ? readFileSync8(0, "utf8").trim() : value;
|
|
24353
24393
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
24354
24394
|
var who = (message) => `${message.author.name}${message.via === "agent" ? ` (agent${message.machine ? ` \xB7 ${message.machine}` : ""})` : ""}`;
|
|
24355
24395
|
function threadLine(thread) {
|
|
@@ -24430,24 +24470,28 @@ var int2 = (description2) => ({ type: "integer", description: description2 });
|
|
|
24430
24470
|
var schema = (properties, required2 = []) => ({ type: "object", properties, required: required2, additionalProperties: false });
|
|
24431
24471
|
var PROJECT = str("Project key. Defaults to the project this repository is linked to.");
|
|
24432
24472
|
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24433
|
-
var inboxDir = () =>
|
|
24434
|
-
var
|
|
24435
|
-
|
|
24436
|
-
|
|
24437
|
-
|
|
24473
|
+
var inboxDir = () => join6(dirname4(configPath()), "inbox");
|
|
24474
|
+
var realCwd2 = (cwd) => {
|
|
24475
|
+
try {
|
|
24476
|
+
return realpathSync2(cwd);
|
|
24477
|
+
} catch {
|
|
24478
|
+
return cwd;
|
|
24479
|
+
}
|
|
24480
|
+
};
|
|
24481
|
+
var inboxKey = (cwd) => createHash4("sha256").update(realCwd2(cwd)).digest("hex").slice(0, 16);
|
|
24482
|
+
var inboxFile = (cwd) => join6(inboxDir(), `${inboxKey(cwd)}.jsonl`);
|
|
24483
|
+
function inboxWrite(cwd, push) {
|
|
24484
|
+
mkdirSync5(inboxDir(), { recursive: true });
|
|
24485
|
+
appendFileSync(inboxFile(cwd), `${JSON.stringify(push)}
|
|
24438
24486
|
`);
|
|
24439
24487
|
}
|
|
24440
|
-
function inboxDrain(
|
|
24441
|
-
const path3 = inboxFile(
|
|
24442
|
-
if (!
|
|
24443
|
-
const lines =
|
|
24444
|
-
|
|
24488
|
+
function inboxDrain(cwd) {
|
|
24489
|
+
const path3 = inboxFile(cwd);
|
|
24490
|
+
if (!existsSync6(path3)) return [];
|
|
24491
|
+
const lines = readFileSync9(path3, "utf8").split("\n").filter(Boolean);
|
|
24492
|
+
rmSync4(path3, { force: true });
|
|
24445
24493
|
return lines.map((line) => JSON.parse(line));
|
|
24446
24494
|
}
|
|
24447
|
-
function inboxDrainAll() {
|
|
24448
|
-
if (!existsSync5(inboxDir())) return [];
|
|
24449
|
-
return readdirSync(inboxDir()).flatMap((name) => inboxDrain(name.replace(/\.jsonl$/, "")));
|
|
24450
|
-
}
|
|
24451
24495
|
async function digest(api, projectId, taskKey) {
|
|
24452
24496
|
const [about, mine, general] = await Promise.all([
|
|
24453
24497
|
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`) : Promise.resolve([]),
|
|
@@ -24463,17 +24507,21 @@ async function digest(api, projectId, taskKey) {
|
|
|
24463
24507
|
}
|
|
24464
24508
|
function tools() {
|
|
24465
24509
|
const api = authedApi();
|
|
24466
|
-
const
|
|
24510
|
+
const resolveOnly = (args) => resolveProject(api, args.project);
|
|
24467
24511
|
let held = null;
|
|
24468
24512
|
const hold = async (projectId, taskKey) => {
|
|
24469
24513
|
if (held && held.projectId === projectId && held.taskKey === taskKey) return;
|
|
24470
24514
|
held?.stop();
|
|
24471
|
-
held = new HeldSession(api, projectId, taskKey, (push) => inboxWrite(
|
|
24515
|
+
held = new HeldSession(api, projectId, taskKey, (push) => inboxWrite(process.cwd(), push));
|
|
24472
24516
|
await held.start();
|
|
24473
24517
|
};
|
|
24474
|
-
const
|
|
24475
|
-
|
|
24476
|
-
held
|
|
24518
|
+
const project = async (args) => {
|
|
24519
|
+
const target = await resolveOnly(args);
|
|
24520
|
+
if (!held) await hold(target.id, null);
|
|
24521
|
+
return target;
|
|
24522
|
+
};
|
|
24523
|
+
const release = async () => {
|
|
24524
|
+
if (held?.taskKey) await hold(held.projectId, null);
|
|
24477
24525
|
};
|
|
24478
24526
|
return [
|
|
24479
24527
|
{
|
|
@@ -24483,7 +24531,7 @@ function tools() {
|
|
|
24483
24531
|
run: async (args) => {
|
|
24484
24532
|
const target = await project(args);
|
|
24485
24533
|
await hold(target.id, String(args.task));
|
|
24486
|
-
const queued = inboxDrain(
|
|
24534
|
+
const queued = inboxDrain(process.cwd());
|
|
24487
24535
|
if (queued.length) return queued.map(pushBlock).join("\n\n");
|
|
24488
24536
|
const minutes = typeof args.minutes === "number" ? args.minutes : 10;
|
|
24489
24537
|
const push = await new Promise((resolve2) => {
|
|
@@ -24506,7 +24554,7 @@ function tools() {
|
|
|
24506
24554
|
inputSchema: schema({ task: str("Task key"), project: PROJECT }),
|
|
24507
24555
|
run: async (args) => {
|
|
24508
24556
|
const target = await project(args);
|
|
24509
|
-
const queued = inboxDrain(
|
|
24557
|
+
const queued = inboxDrain(process.cwd()).map(pushBlock);
|
|
24510
24558
|
const rest = await digest(api, target.id, args.task ?? null);
|
|
24511
24559
|
return [...queued, rest].join("\n\n");
|
|
24512
24560
|
}
|
|
@@ -24552,11 +24600,11 @@ function tools() {
|
|
|
24552
24600
|
},
|
|
24553
24601
|
{
|
|
24554
24602
|
name: "sessions",
|
|
24555
|
-
description: "Who is live on which task
|
|
24603
|
+
description: "Who is live right now: on which task, or present in the project on no task yet.",
|
|
24556
24604
|
inputSchema: schema({ project: PROJECT }),
|
|
24557
24605
|
run: async (args) => {
|
|
24558
24606
|
const list = await api.get(`/projects/${(await project(args)).id}/sessions`);
|
|
24559
|
-
return list.length ? list.map((session) => `${session.taskKey} ${session.user.name} ${session.machine.name} since ${session.startedAt}`).join("\n") : "Nobody is live.";
|
|
24607
|
+
return list.length ? list.map((session) => `${session.taskKey ?? "(no task)"} ${session.user.name} ${session.machine.name} since ${session.startedAt}`).join("\n") : "Nobody is live.";
|
|
24560
24608
|
}
|
|
24561
24609
|
},
|
|
24562
24610
|
{
|
|
@@ -24575,11 +24623,11 @@ ${await digest(api, target.id, task.key)}`;
|
|
|
24575
24623
|
},
|
|
24576
24624
|
{
|
|
24577
24625
|
name: "release_task",
|
|
24578
|
-
description: "
|
|
24626
|
+
description: "Step off the task you hold without finishing it. You stay live in the project, reachable in threads you are part of.",
|
|
24579
24627
|
inputSchema: schema({ project: PROJECT }),
|
|
24580
24628
|
run: async () => {
|
|
24581
|
-
release();
|
|
24582
|
-
return "Released.";
|
|
24629
|
+
await release();
|
|
24630
|
+
return "Released. Still live in the project.";
|
|
24583
24631
|
}
|
|
24584
24632
|
},
|
|
24585
24633
|
{
|
|
@@ -24631,15 +24679,15 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
24631
24679
|
run: async (args) => {
|
|
24632
24680
|
const target = await loadProject(api, (await project(args)).id);
|
|
24633
24681
|
const task = await api.patch(taskPath(target.id, String(args.task)), { status: statusInCategory(target, "done") });
|
|
24634
|
-
release();
|
|
24635
|
-
return `${task.key} is done.`;
|
|
24682
|
+
await release();
|
|
24683
|
+
return `${task.key} is done. You are still live in the project, reachable in threads you are part of.`;
|
|
24636
24684
|
}
|
|
24637
24685
|
}
|
|
24638
24686
|
];
|
|
24639
24687
|
}
|
|
24640
24688
|
function readJson(path3) {
|
|
24641
24689
|
try {
|
|
24642
|
-
return JSON.parse(
|
|
24690
|
+
return JSON.parse(readFileSync9(path3, "utf8"));
|
|
24643
24691
|
} catch {
|
|
24644
24692
|
return {};
|
|
24645
24693
|
}
|
|
@@ -24652,13 +24700,13 @@ function installClaude() {
|
|
|
24652
24700
|
} catch {
|
|
24653
24701
|
done.push("Claude Code: run `claude mcp add --scope user slop -- slop mcp` (the claude command was not found here)");
|
|
24654
24702
|
}
|
|
24655
|
-
const dir =
|
|
24656
|
-
const path3 =
|
|
24657
|
-
|
|
24703
|
+
const dir = join6(homedir3(), ".claude");
|
|
24704
|
+
const path3 = join6(dir, "settings.json");
|
|
24705
|
+
mkdirSync5(dir, { recursive: true });
|
|
24658
24706
|
const settings = readJson(path3);
|
|
24659
24707
|
settings.hooks = mergeHooks(settings.hooks ?? {});
|
|
24660
24708
|
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "1800000" };
|
|
24661
|
-
|
|
24709
|
+
writeFileSync6(path3, `${JSON.stringify(settings, null, 2)}
|
|
24662
24710
|
`);
|
|
24663
24711
|
done.push(`Claude Code: PostToolUse and Stop hooks and MCP_TOOL_TIMEOUT written to ${path3}`);
|
|
24664
24712
|
return done;
|
|
@@ -24676,29 +24724,42 @@ function mergeHooks(hooks) {
|
|
|
24676
24724
|
}
|
|
24677
24725
|
function installCodex() {
|
|
24678
24726
|
const done = [];
|
|
24679
|
-
const dir =
|
|
24680
|
-
|
|
24681
|
-
const configPath2 =
|
|
24682
|
-
const current =
|
|
24727
|
+
const dir = join6(homedir3(), ".codex");
|
|
24728
|
+
mkdirSync5(dir, { recursive: true });
|
|
24729
|
+
const configPath2 = join6(dir, "config.toml");
|
|
24730
|
+
const current = existsSync6(configPath2) ? readFileSync9(configPath2, "utf8") : "";
|
|
24683
24731
|
if (/^\[mcp_servers\.slop\]/m.test(current)) done.push(`Codex: MCP server already in ${configPath2}`);
|
|
24684
24732
|
else {
|
|
24685
|
-
|
|
24733
|
+
writeFileSync6(configPath2, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
|
|
24686
24734
|
command = "slop"
|
|
24687
24735
|
args = ["mcp"]
|
|
24688
24736
|
tool_timeout_sec = 1800
|
|
24689
24737
|
`);
|
|
24690
24738
|
done.push(`Codex: MCP server "slop" added to ${configPath2}`);
|
|
24691
24739
|
}
|
|
24692
|
-
const hooksPath =
|
|
24740
|
+
const hooksPath = join6(dir, "hooks.json");
|
|
24693
24741
|
const file2 = readJson(hooksPath);
|
|
24694
24742
|
file2.hooks = mergeHooks(file2.hooks ?? {});
|
|
24695
|
-
|
|
24743
|
+
writeFileSync6(hooksPath, `${JSON.stringify(file2, null, 2)}
|
|
24696
24744
|
`);
|
|
24697
24745
|
done.push(`Codex: PostToolUse and Stop hooks written to ${hooksPath}. Codex asks you to trust new hooks once (/hooks in a session).`);
|
|
24698
24746
|
return done;
|
|
24699
24747
|
}
|
|
24700
|
-
function
|
|
24701
|
-
const
|
|
24748
|
+
async function hookCwd() {
|
|
24749
|
+
const input2 = await new Promise((resolve2) => {
|
|
24750
|
+
let data = "";
|
|
24751
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
24752
|
+
process.stdin.on("end", () => resolve2(data));
|
|
24753
|
+
setTimeout(() => resolve2(data), 2e3);
|
|
24754
|
+
});
|
|
24755
|
+
try {
|
|
24756
|
+
return JSON.parse(input2).cwd ?? process.cwd();
|
|
24757
|
+
} catch {
|
|
24758
|
+
return process.cwd();
|
|
24759
|
+
}
|
|
24760
|
+
}
|
|
24761
|
+
async function tickHook() {
|
|
24762
|
+
const arrived = inboxDrain(await hookCwd());
|
|
24702
24763
|
if (arrived.length === 0) return;
|
|
24703
24764
|
const text = `Messages arrived on Slopware Tasks while you worked:
|
|
24704
24765
|
|
|
@@ -24709,19 +24770,9 @@ Act or answer with the slop MCP \`reply\` tool, then carry on.`;
|
|
|
24709
24770
|
`);
|
|
24710
24771
|
}
|
|
24711
24772
|
async function stopHook() {
|
|
24712
|
-
const
|
|
24713
|
-
let data = "";
|
|
24714
|
-
process.stdin.on("data", (chunk) => data += chunk);
|
|
24715
|
-
process.stdin.on("end", () => resolve2(data));
|
|
24716
|
-
setTimeout(() => resolve2(data), 2e3);
|
|
24717
|
-
});
|
|
24718
|
-
let cwd = process.cwd();
|
|
24719
|
-
try {
|
|
24720
|
-
cwd = JSON.parse(input2).cwd ?? cwd;
|
|
24721
|
-
} catch {
|
|
24722
|
-
}
|
|
24773
|
+
const cwd = await hookCwd();
|
|
24723
24774
|
process.chdir(cwd);
|
|
24724
|
-
const parts =
|
|
24775
|
+
const parts = inboxDrain(cwd).map(pushBlock);
|
|
24725
24776
|
const repo = findRepo();
|
|
24726
24777
|
const link = repo ? readLink(repo) : null;
|
|
24727
24778
|
if (!link) {
|
|
@@ -24753,7 +24804,7 @@ function withInbox(list) {
|
|
|
24753
24804
|
...tool,
|
|
24754
24805
|
run: async (args) => {
|
|
24755
24806
|
const result = await tool.run(args);
|
|
24756
|
-
const arrived =
|
|
24807
|
+
const arrived = inboxDrain(process.cwd());
|
|
24757
24808
|
return arrived.length ? `${result}
|
|
24758
24809
|
|
|
24759
24810
|
--- Messages arrived while you worked ---
|
|
@@ -24775,9 +24826,9 @@ function registerMcpCommands(program3, version2) {
|
|
|
24775
24826
|
}
|
|
24776
24827
|
|
|
24777
24828
|
// src/commands/sessions.ts
|
|
24778
|
-
import { readFileSync as
|
|
24829
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
24779
24830
|
var taskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24780
|
-
var sessionLine = (session) => [session.taskKey, session.user.name, session.machine.name, session.startedAt];
|
|
24831
|
+
var sessionLine = (session) => [session.taskKey ?? "(no task)", session.user.name, session.machine.name, session.startedAt];
|
|
24781
24832
|
function registerSessionCommands(program3, task) {
|
|
24782
24833
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
24783
24834
|
const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
|
|
@@ -24859,13 +24910,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
|
|
|
24859
24910
|
}
|
|
24860
24911
|
|
|
24861
24912
|
// src/index.ts
|
|
24862
|
-
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.2.
|
|
24913
|
+
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.2.4").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
|
|
24863
24914
|
registerAuthCommands(program2);
|
|
24864
24915
|
registerProjectCommands(program2);
|
|
24865
24916
|
var taskCommand = registerTaskCommands(program2);
|
|
24866
24917
|
registerSessionCommands(program2, taskCommand);
|
|
24867
24918
|
registerThreadCommands(program2, taskCommand);
|
|
24868
|
-
registerMcpCommands(program2, "0.2.
|
|
24919
|
+
registerMcpCommands(program2, "0.2.4");
|
|
24869
24920
|
registerGuideCommand(program2);
|
|
24870
24921
|
registerSkillCommands(program2);
|
|
24871
24922
|
program2.parseAsync().catch(fail);
|