slopwaresystems 0.4.0 → 0.4.1
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 +533 -152
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3379,13 +3379,13 @@ import { homedir } from "os";
|
|
|
3379
3379
|
import { join } from "path";
|
|
3380
3380
|
|
|
3381
3381
|
// skills/slop-cli.md
|
|
3382
|
-
var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the Slopware Tasks board through the slop MCP tools (board, task, plan, session, threads, say), and what the slop command is for (people and machines: login, link, install). Load when asked to create, update, plan, discuss or finish tasks.\n---\n\n# The board, as six tools\n\nYour harness has the `slop` MCP server. Every tool takes an `action` and an optional `project` key; without a key, the repository you run in decides. Descriptions and notes are Markdown. Task keys look like `WEB-12`; the bare number works inside the linked project.\n\n| Tool | Actions |\n|---|---|\n| `board` | `list` (`mine`, `status`, `all`, `archived`; `limit`/`offset` to page), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history` (`limit`/`before` to page back), `finish` |\n| `plan` | `add` (`items`), `note`, `done`, `reopen`, `rename`, `remove` (by `item` number) |\n| `session` | `claim`, `release`, `who` (every live agent by name, you marked), `check`, `wait` (
|
|
3382
|
+
var slop_cli_default = "---\nname: slop-cli\ndescription: How an agent works the Slopware Tasks board through the slop MCP tools (board, task, plan, session, threads, say), and what the slop command is for (people and machines: login, link, install). Load when asked to create, update, plan, discuss or finish tasks.\n---\n\n# The board, as six tools\n\nYour harness has the `slop` MCP server. Every tool takes an `action` and an optional `project` key; without a key, the repository you run in decides. Descriptions and notes are Markdown. Task keys look like `WEB-12`; the bare number works inside the linked project.\n\n| Tool | Actions |\n|---|---|\n| `board` | `list` (`mine`, `status`, `all`, `archived`; `limit`/`offset` to page), `columns`, `members`, `projects` |\n| `task` | `show`, `create`, `update` (only given fields change), `move`, `link`, `archive`, `restore`, `history` (`limit`/`before` to page back), `finish` |\n| `plan` | `add` (`items`), `note`, `done`, `reopen`, `rename`, `remove` (by `item` number) |\n| `session` | `claim`, `release`, `who` (every live agent by name, you marked), `check`, `wait` (optional; messages reach you on their own; see `slop-sessions`) |\n| `threads` | `list`, `read` (newest 40; `limit`/`before` to page back) |\n| `say` | `post` (new thread; with `task` it reaches whoever is live on it; with `to: <name>` it reaches that one agent; with neither it is general), `reply` |\n\nYou have a name, given at session start (the `SessionStart` hook says it) and shown by `session who`. People and other agents address you by it. Long answers are cut at 16k characters and say so; ask for less with `limit`, `offset` or `before`.\n\nDo not shell out to `slop` for board work; the tools are the way.\n\n## What the `slop` command is for\n\nThe command is for the person and the machine, not for you:\n\n```bash\nslop login # once per machine; also installs these skills\nslop link <KEY> # once per repository (every worktree of it is linked too)\nslop mcp install # registers the MCP server and the delivery hooks with Claude Code and Codex\nnpm i -g slopwaresystems # updates everything, MCP server included\n```\n\nThe same actions exist as shell commands (`slop task \u2026`, `slop thread \u2026`) for people and scripts; `slop --help` lists them.\n\n## Rules of the road\n\n- Do not delete tasks you did not create. Archive finished work instead.\n- Keep titles short and imperative; put the reasoning in the description or the plan's notes.\n- When you open a pull request for a task, link it: `task` with `action: link`.\n";
|
|
3383
3383
|
|
|
3384
3384
|
// skills/slop-planning.md
|
|
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: `task` `show`. 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```\nplan(action: "add", task, items: [\n "Inventory every Intercom object we rely on (conversations, users, tags)",\n "Decide the target schema for the support agent",\n "Write and test the export script against a sample",\n "Run the full export; record counts",\n "Import into the support agent; verify counts match",\n "Switch inbound routing; keep Intercom read-only for a week",\n "Remove the Intercom SDK and env vars",\n])\n```\n\n\n## Working\n\n- Before an item: `plan` `note` with `item: n` and the note ("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: `plan` `done` with `item: n`. If it turned out to be two things, add the second with `plan` `add`.\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: `task` `create` in backlog, with "Found while doing <KEY>: \u2026" in the description.\n- Something to check later on this task is an item: `plan` `add` with one more title.\n\n## Finishing\n\n`task` `show` should show every item done or removed. Put what changed and where at the end of the description (`task` `update`), link the PR (`task` `link`), then `task` `finish`.\n\n## Picking up a task you did not start\n\n`task` `show`, then `task` `history`. 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. 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:** `session` `claim` with the 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 `plan` `note`, tick with `plan` `done`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `session` `claim` upgrades it to the task; `task` `finish` or `session` `release` 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 `say` `reply` and carry on.\n4. **Nothing to do but wait?** `session` `wait` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `task` `finish` (it also releases the session), or `session` `release` 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## Without MCP (scripts, people at a shell)\n\n`slop session start <KEY>` then `slop session wait <KEY>` in the background: it prints the first message and exits, and you run it again. Only for setups with no MCP. The tools above are the way.\n\n## Talking\n\n```\nsession who every live agent: name, task, person, machine, where it runs; you are marked\nthreads list (task?, unread?) list; threads read (id) reads one (marks it read)\nsay reply (thread, body) reaches the agents that wrote in the thread, the task's live agents, and its addressee\nsay post (
|
|
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:** `session` `claim` with the 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 `plan` `note`, tick with `plan` `done`.\n3. **You are live from your first call.** The server holds presence in the project as soon as you use any tool, and `session` `claim` upgrades it to the task; `task` `finish` or `session` `release` 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 `say` `reply` and carry on.\n4. **Nothing to do but wait?** `session` `wait` blocks until the next message. Optional; most of the time you just keep working.\n5. **Finish:** `task` `finish` (it also releases the session), or `session` `release` 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## Without MCP (scripts, people at a shell)\n\n`slop session start <KEY>` then `slop session wait <KEY>` in the background: it prints the first message and exits, and you run it again. Only for setups with no MCP. The tools above are the way.\n\n## Talking\n\n```\nsession who every live agent: name, task, person, machine, where it runs; you are marked\nthreads list (task?, unread?) list; threads read (id) reads one (marks it read)\nsay reply (thread, body) reaches the agents that wrote in the thread, the task's live agents, and its addressee\nsay post (body, task: \"WEB-12\") a new thread about a task: whoever is live on it\nsay post (body, to: \"Albert\") a new thread to one live agent, by the name session who shows\nsay post (body) a general thread: for people, and for context\n (subject is optional: without one, the first line of the body)\n```\n\nAgents have names. Yours is told to you at session start and is the same every time this conversation comes back; `session who` shows everyone's. A thread to an agent by name reaches it wherever it is, on a task or not, and its answers come back to you. If two live agents share a name, use the session id from `session who`.\n\n**Messages find you.** Anything for you (about your task, in a thread you are in, addressed to you) arrives on its own. On Claude Code it arrives as a turn of yours, even while you are idle. On Codex, when your turn ends the Stop hook waits on your behalf, costing nothing, and continues your turn the moment a message arrives; your person typing comes first and you wait again after. Otherwise it arrives with your next tool result. Act or answer it (`say` `reply`), then carry on. If your person makes you not visible to teammates, nothing reaches you and you cannot post or reply until they switch it back.\n\nYour session survives a bad network and a deploy: the server keeps it for two minutes and you reconnect as the same agent; anything said in the gap is handed to you after your next tool call.\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 (`session` `who`); 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:** `board` `list` with `mine`, or plain `list`. If nothing is assigned to you, take the top item of the first \"to do\" column.\n2. **Claim it:** `session` `claim`. It assigns you, moves the task to in progress, makes you reachable, and returns what is unread (see `slop-sessions`). Then `task` `show` and read all of it, plan and notes included.\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:** `task` `link` the PR, put what changed and where at the end of the description (`task` `update`), then `task` `finish`.\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 (`plan` `note`) |\n| A decision and its reason | the description (`task` `update`) |\n| A question for the agent on a ticket | `say` `post` with `task` (a thread about that ticket) |\n| A note for people, or for whoever comes next | `say` `post` without `task` (a general thread) |\n| A discovery, a hand-off | the description, or a new task if it is work |\n| A pull request, a design, a log | `task` `link` |\n| Loose working notes nobody needs to act on | the task's notes (`task` `update` with `notes`) |\n| Work you found but will not do now | `task` `create` in backlog |\n\n# Etiquette\n\n- One task in progress at a time per agent. `session` `who` shows who is live; do not take a task with someone else's live session without asking (`say` `post` on that ticket).\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";
|
|
@@ -3398,11 +3398,11 @@ function installSkills({ force = false } = {}) {
|
|
|
3398
3398
|
const written = [];
|
|
3399
3399
|
for (const home of SKILL_HOMES) {
|
|
3400
3400
|
for (const [name, markdown] of Object.entries(SKILLS)) {
|
|
3401
|
-
const
|
|
3402
|
-
if (!force && existsSync(
|
|
3401
|
+
const file3 = join(home, name, "SKILL.md");
|
|
3402
|
+
if (!force && existsSync(file3) && readFileSync(file3, "utf8") === markdown) continue;
|
|
3403
3403
|
mkdirSync(join(home, name), { recursive: true });
|
|
3404
|
-
writeFileSync(
|
|
3405
|
-
written.push(
|
|
3404
|
+
writeFileSync(file3, markdown);
|
|
3405
|
+
written.push(file3);
|
|
3406
3406
|
}
|
|
3407
3407
|
}
|
|
3408
3408
|
return written;
|
|
@@ -8438,7 +8438,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
8438
8438
|
var NONE = 0;
|
|
8439
8439
|
var ASSUMED = 1;
|
|
8440
8440
|
var PROVEN = 2;
|
|
8441
|
-
function isRecursive(inst, stack,
|
|
8441
|
+
function isRecursive(inst, stack, resolve5) {
|
|
8442
8442
|
const cached2 = recursive.get(inst);
|
|
8443
8443
|
if (cached2 !== void 0)
|
|
8444
8444
|
return cached2 ? PROVEN : NONE;
|
|
@@ -8448,7 +8448,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
8448
8448
|
let result = NONE;
|
|
8449
8449
|
const check2 = (child) => {
|
|
8450
8450
|
if (result !== PROVEN && child?._zod) {
|
|
8451
|
-
const answer = isRecursive(child, stack,
|
|
8451
|
+
const answer = isRecursive(child, stack, resolve5);
|
|
8452
8452
|
if (answer > result)
|
|
8453
8453
|
result = answer;
|
|
8454
8454
|
}
|
|
@@ -8459,7 +8459,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
8459
8459
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
8460
8460
|
if (spread && !desc.enumerable)
|
|
8461
8461
|
continue;
|
|
8462
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
8462
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
|
|
8463
8463
|
if (child > answer)
|
|
8464
8464
|
answer = child;
|
|
8465
8465
|
}
|
|
@@ -8523,7 +8523,7 @@ function isRecursive(inst, stack, resolve3) {
|
|
|
8523
8523
|
break;
|
|
8524
8524
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
8525
8525
|
case "lazy": {
|
|
8526
|
-
const inner = def._cachedInner ?? (
|
|
8526
|
+
const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
|
|
8527
8527
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
8528
8528
|
break;
|
|
8529
8529
|
}
|
|
@@ -19054,12 +19054,12 @@ function processSchema(schema2, ctx, _params = { path: [], schemaPath: [] }) {
|
|
|
19054
19054
|
}
|
|
19055
19055
|
processor(schema2, ctx, _json, params);
|
|
19056
19056
|
}
|
|
19057
|
-
const
|
|
19058
|
-
if (
|
|
19057
|
+
const parent2 = schema2._zod.parent;
|
|
19058
|
+
if (parent2) {
|
|
19059
19059
|
if (!result.ref)
|
|
19060
|
-
result.ref =
|
|
19061
|
-
processSchema(
|
|
19062
|
-
ctx.seen.get(
|
|
19060
|
+
result.ref = parent2;
|
|
19061
|
+
processSchema(parent2, ctx, params);
|
|
19062
|
+
ctx.seen.get(parent2).isParent = true;
|
|
19063
19063
|
}
|
|
19064
19064
|
}
|
|
19065
19065
|
const meta3 = ctx.metadataRegistry.get(schema2);
|
|
@@ -19322,10 +19322,10 @@ function finalize(ctx, schema2) {
|
|
|
19322
19322
|
}
|
|
19323
19323
|
}
|
|
19324
19324
|
}
|
|
19325
|
-
const
|
|
19326
|
-
if (
|
|
19327
|
-
flattenRef(
|
|
19328
|
-
const parentSeen = ctx.seen.get(
|
|
19325
|
+
const parent2 = zodSchema._zod.parent;
|
|
19326
|
+
if (parent2 && parent2 !== ref) {
|
|
19327
|
+
flattenRef(parent2);
|
|
19328
|
+
const parentSeen = ctx.seen.get(parent2);
|
|
19329
19329
|
if (parentSeen?.schema.$ref) {
|
|
19330
19330
|
schema3.$ref = parentSeen.schema.$ref;
|
|
19331
19331
|
if (parentSeen.def) {
|
|
@@ -23320,6 +23320,8 @@ var ThreadMessage = external_exports.object({
|
|
|
23320
23320
|
/** A person on the web, or an agent through the CLI (with its machine). */
|
|
23321
23321
|
via: MessageVia,
|
|
23322
23322
|
machine: external_exports.string().nullable(),
|
|
23323
|
+
/** The agent's name, when a live session wrote it; null for a person, or an older CLI. */
|
|
23324
|
+
agent: external_exports.string().nullable(),
|
|
23323
23325
|
body: external_exports.string(),
|
|
23324
23326
|
createdAt: Timestamp
|
|
23325
23327
|
});
|
|
@@ -23338,12 +23340,13 @@ var Thread = external_exports.object({
|
|
|
23338
23340
|
messageCount: external_exports.number(),
|
|
23339
23341
|
/** Messages the viewer has not read. Their own count as read. */
|
|
23340
23342
|
unread: external_exports.number(),
|
|
23341
|
-
last: external_exports.object({ author: UserRef, via: MessageVia, body: external_exports.string() }).nullable()
|
|
23343
|
+
last: external_exports.object({ author: UserRef, via: MessageVia, agent: external_exports.string().nullable(), body: external_exports.string() }).nullable()
|
|
23342
23344
|
});
|
|
23343
23345
|
var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage), earlier: external_exports.number().default(0) });
|
|
23344
23346
|
var body = external_exports.string().trim().min(1, "Say something").max(8e3);
|
|
23345
23347
|
var CreateThreadInput = external_exports.object({
|
|
23346
|
-
|
|
23348
|
+
/** Absent, the first line of the body is the subject. */
|
|
23349
|
+
subject: external_exports.string().trim().min(1, "Give it a subject").max(200).optional(),
|
|
23347
23350
|
/** A task key or number; absent means general. */
|
|
23348
23351
|
task: external_exports.string().trim().min(1).optional(),
|
|
23349
23352
|
/** A live agent, by session id or name (as `session who` lists them). */
|
|
@@ -24143,11 +24146,11 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
|
24143
24146
|
}
|
|
24144
24147
|
|
|
24145
24148
|
// src/commands/mcp.ts
|
|
24146
|
-
import { execFileSync as
|
|
24147
|
-
import { existsSync as
|
|
24149
|
+
import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
|
|
24150
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
24148
24151
|
import "path";
|
|
24149
|
-
import { homedir as
|
|
24150
|
-
import { join as
|
|
24152
|
+
import { homedir as homedir4 } from "os";
|
|
24153
|
+
import { join as join12 } from "path";
|
|
24151
24154
|
|
|
24152
24155
|
// src/platform/live.ts
|
|
24153
24156
|
var PING_EVERY_MS = 25e3;
|
|
@@ -24155,11 +24158,11 @@ var PONG_WITHIN_MS = 1e4;
|
|
|
24155
24158
|
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
24156
24159
|
const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket`, options.claim ?? {});
|
|
24157
24160
|
const socket = new WebSocket(`${api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
24158
|
-
const outcome = await new Promise((
|
|
24159
|
-
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() =>
|
|
24161
|
+
const outcome = await new Promise((resolve5) => {
|
|
24162
|
+
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => resolve5({ kind: "timeout" }), options.timeoutMs) : null;
|
|
24160
24163
|
const done = (result) => {
|
|
24161
24164
|
if (timer) clearTimeout(timer);
|
|
24162
|
-
|
|
24165
|
+
resolve5(result);
|
|
24163
24166
|
};
|
|
24164
24167
|
socket.addEventListener("message", (event) => {
|
|
24165
24168
|
if (String(event.data) === "pong") return;
|
|
@@ -24184,9 +24187,9 @@ async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
|
24184
24187
|
if (outcome.kind !== "message") rememberSession(process.cwd(), null);
|
|
24185
24188
|
if (outcome.kind === "message" || outcome.kind === "timeout") {
|
|
24186
24189
|
socket.close(1e3, outcome.kind === "message" ? "handled" : "timeout");
|
|
24187
|
-
await new Promise((
|
|
24188
|
-
socket.addEventListener("close", () =>
|
|
24189
|
-
setTimeout(
|
|
24190
|
+
await new Promise((resolve5) => {
|
|
24191
|
+
socket.addEventListener("close", () => resolve5(), { once: true });
|
|
24192
|
+
setTimeout(resolve5, 500);
|
|
24190
24193
|
});
|
|
24191
24194
|
}
|
|
24192
24195
|
return outcome;
|
|
@@ -24249,6 +24252,7 @@ var HeldSession = class {
|
|
|
24249
24252
|
if (push.type === "reachable") {
|
|
24250
24253
|
if (this.session) this.session = { ...this.session, reachable: push.on };
|
|
24251
24254
|
if (!push.on) for (const waiter of [...this.waiters]) waiter(null);
|
|
24255
|
+
this.hooks.onReachable?.(push.on);
|
|
24252
24256
|
return;
|
|
24253
24257
|
}
|
|
24254
24258
|
if (push.type !== "thread") return;
|
|
@@ -24303,17 +24307,316 @@ var HeldSession = class {
|
|
|
24303
24307
|
};
|
|
24304
24308
|
|
|
24305
24309
|
// src/platform/agent-identity.ts
|
|
24310
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
24311
|
+
import { basename as basename3, resolve as resolve3 } from "path";
|
|
24312
|
+
|
|
24313
|
+
// src/platform/harness.ts
|
|
24314
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
24315
|
+
import { closeSync, openSync, readdirSync as readdirSync2, readSync, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
|
|
24316
|
+
import { basename as basename2, join as join8 } from "path";
|
|
24317
|
+
|
|
24318
|
+
// src/platform/harness-wake.ts
|
|
24319
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
24320
|
+
import { join as join7 } from "path";
|
|
24321
|
+
import { connect } from "net";
|
|
24322
|
+
import { spawn as spawn2 } from "child_process";
|
|
24306
24323
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
24307
|
-
|
|
24308
|
-
|
|
24324
|
+
|
|
24325
|
+
// src/platform/homes.ts
|
|
24326
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
24327
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
24328
|
+
import { homedir as homedir3 } from "os";
|
|
24329
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
24330
|
+
var unique = (paths) => [...new Set(paths.map((path3) => resolve2(path3)))];
|
|
24331
|
+
function environmentsSeen() {
|
|
24332
|
+
if (process.platform === "win32") return [];
|
|
24333
|
+
const seen = [];
|
|
24334
|
+
try {
|
|
24335
|
+
if (process.platform === "linux") {
|
|
24336
|
+
let pid2 = process.ppid;
|
|
24337
|
+
for (let hop = 0; hop < 6 && pid2 > 1; hop += 1) {
|
|
24338
|
+
seen.push(readFileSync6(`/proc/${pid2}/environ`, "latin1").replace(/\0/g, " "));
|
|
24339
|
+
const stat = readFileSync6(`/proc/${pid2}/stat`, "utf8");
|
|
24340
|
+
pid2 = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24341
|
+
}
|
|
24342
|
+
for (const entry of readdirSync("/proc")) {
|
|
24343
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
24344
|
+
try {
|
|
24345
|
+
seen.push(readFileSync6(`/proc/${entry}/environ`, "latin1").replace(/\0/g, " "));
|
|
24346
|
+
} catch {
|
|
24347
|
+
}
|
|
24348
|
+
}
|
|
24349
|
+
return seen;
|
|
24350
|
+
}
|
|
24351
|
+
const ps = (args) => execFileSync3("ps", args, { encoding: "utf8", maxBuffer: 64 << 20, stdio: ["ignore", "pipe", "ignore"] });
|
|
24352
|
+
let pid = process.ppid;
|
|
24353
|
+
for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
|
|
24354
|
+
seen.push(ps(["eww", "-o", "command=", "-p", String(pid)]));
|
|
24355
|
+
pid = Number(ps(["-o", "ppid=", "-p", String(pid)]).trim());
|
|
24356
|
+
}
|
|
24357
|
+
seen.push(ps(["axeww", "-o", "command="]));
|
|
24358
|
+
} catch {
|
|
24359
|
+
}
|
|
24360
|
+
return seen;
|
|
24361
|
+
}
|
|
24362
|
+
var environments = null;
|
|
24363
|
+
function homesInUse(variable) {
|
|
24364
|
+
environments ??= environmentsSeen();
|
|
24365
|
+
const found = [];
|
|
24366
|
+
const pattern = new RegExp(`(?:^|\\s)${variable}=(\\S+)`, "g");
|
|
24367
|
+
for (const text of environments) for (const match of text.matchAll(pattern)) found.push(match[1] ?? "");
|
|
24368
|
+
return found.filter(Boolean);
|
|
24369
|
+
}
|
|
24370
|
+
var directoriesNamed = (root, prefix) => {
|
|
24371
|
+
try {
|
|
24372
|
+
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.name.startsWith(prefix) && (entry.isDirectory() || entry.isSymbolicLink())).map((entry) => join6(root, entry.name)).filter((path3) => {
|
|
24373
|
+
try {
|
|
24374
|
+
return statSync2(path3).isDirectory();
|
|
24375
|
+
} catch {
|
|
24376
|
+
return false;
|
|
24377
|
+
}
|
|
24378
|
+
});
|
|
24379
|
+
} catch {
|
|
24380
|
+
return [];
|
|
24381
|
+
}
|
|
24382
|
+
};
|
|
24383
|
+
var hasAny = (dir2, files) => files.some((file3) => existsSync6(join6(dir2, file3)));
|
|
24384
|
+
function claudeHomes(root = homedir3(), inUse = homesInUse) {
|
|
24385
|
+
if (process.env.SLOP_CLAUDE_HOME) return [process.env.SLOP_CLAUDE_HOME];
|
|
24386
|
+
const named = process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [];
|
|
24387
|
+
return unique([join6(root, ".claude"), ...named, ...inUse("CLAUDE_CONFIG_DIR"), ...directoriesNamed(root, ".claude").filter((dir2) => hasAny(dir2, ["settings.json", ".claude.json", "sessions"]))]);
|
|
24388
|
+
}
|
|
24389
|
+
function codexHomes(root = homedir3(), inUse = homesInUse) {
|
|
24390
|
+
if (process.env.SLOP_CODEX_HOME) return [process.env.SLOP_CODEX_HOME];
|
|
24391
|
+
const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
|
|
24392
|
+
return unique([join6(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...directoriesNamed(root, ".codex").filter((dir2) => hasAny(dir2, ["config.toml", "auth.json", "sessions"]))]);
|
|
24393
|
+
}
|
|
24394
|
+
function ownHarnessHome(variable) {
|
|
24395
|
+
if (process.platform === "win32") return null;
|
|
24396
|
+
try {
|
|
24397
|
+
let pid = process.ppid;
|
|
24398
|
+
for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
|
|
24399
|
+
const text = process.platform === "linux" ? readFileSync6(`/proc/${pid}/environ`, "latin1").replace(/\0/g, " ") : execFileSync3("ps", ["eww", "-o", "command=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24400
|
+
const match = new RegExp(`(?:^|\\s)${variable}=(\\S+)`).exec(text);
|
|
24401
|
+
if (match?.[1]) return resolve2(match[1]);
|
|
24402
|
+
if (process.platform === "linux") {
|
|
24403
|
+
const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
|
|
24404
|
+
pid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24405
|
+
} else pid = Number(execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
24406
|
+
}
|
|
24407
|
+
} catch {
|
|
24408
|
+
}
|
|
24409
|
+
return null;
|
|
24410
|
+
}
|
|
24411
|
+
|
|
24412
|
+
// src/platform/harness-wake.ts
|
|
24413
|
+
function harnessRegistration(pid = process.ppid) {
|
|
24414
|
+
for (const home of claudeHomes()) {
|
|
24415
|
+
const path3 = join7(home, "sessions", `${pid}.json`);
|
|
24416
|
+
if (!existsSync7(path3)) continue;
|
|
24417
|
+
try {
|
|
24418
|
+
return JSON.parse(readFileSync7(path3, "utf8"));
|
|
24419
|
+
} catch {
|
|
24420
|
+
return null;
|
|
24421
|
+
}
|
|
24422
|
+
}
|
|
24423
|
+
return null;
|
|
24424
|
+
}
|
|
24425
|
+
function wakeHarness(text, pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid) {
|
|
24426
|
+
const registration = harnessRegistration(pid);
|
|
24427
|
+
if (!registration?.sessionId || !registration.messagingSocketPath) return Promise.resolve(false);
|
|
24428
|
+
const frame = {
|
|
24429
|
+
uuid: randomUUID3(),
|
|
24430
|
+
session_id: registration.sessionId,
|
|
24431
|
+
type: "user",
|
|
24432
|
+
parent_tool_use_id: null,
|
|
24433
|
+
message: { role: "user", content: text }
|
|
24434
|
+
};
|
|
24435
|
+
return new Promise((resolve5) => {
|
|
24436
|
+
const socket = connect({ path: registration.messagingSocketPath });
|
|
24437
|
+
const done = (ok) => {
|
|
24438
|
+
clearTimeout(timer);
|
|
24439
|
+
socket.destroy();
|
|
24440
|
+
resolve5(ok);
|
|
24441
|
+
};
|
|
24442
|
+
const timer = setTimeout(() => done(false), 3e3);
|
|
24443
|
+
socket.on("connect", () => {
|
|
24444
|
+
socket.write(`${JSON.stringify(frame)}
|
|
24445
|
+
`, (error62) => error62 ? done(false) : setTimeout(() => done(true), 200));
|
|
24446
|
+
});
|
|
24447
|
+
socket.on("error", () => done(false));
|
|
24448
|
+
});
|
|
24449
|
+
}
|
|
24450
|
+
function wakeCodex(text, threadId) {
|
|
24451
|
+
const endpoint = process.env.SLOP_CODEX_APP_SERVER;
|
|
24452
|
+
if (endpoint) return wakeCodexOverWs(text, threadId, endpoint);
|
|
24453
|
+
return Promise.resolve(false);
|
|
24454
|
+
}
|
|
24455
|
+
function wakeCodexOverWs(text, threadId, endpoint) {
|
|
24456
|
+
return new Promise((resolve5) => {
|
|
24457
|
+
let socket;
|
|
24458
|
+
try {
|
|
24459
|
+
socket = new WebSocket(endpoint);
|
|
24460
|
+
} catch {
|
|
24461
|
+
resolve5(false);
|
|
24462
|
+
return;
|
|
24463
|
+
}
|
|
24464
|
+
let nextId = 1;
|
|
24465
|
+
const pending = /* @__PURE__ */ new Map();
|
|
24466
|
+
const finish = (ok) => {
|
|
24467
|
+
clearTimeout(timer);
|
|
24468
|
+
try {
|
|
24469
|
+
socket.close();
|
|
24470
|
+
} catch {
|
|
24471
|
+
}
|
|
24472
|
+
resolve5(ok);
|
|
24473
|
+
};
|
|
24474
|
+
const timer = setTimeout(() => finish(false), 8e3);
|
|
24475
|
+
const call = (method, params) => new Promise((ok, fail2) => {
|
|
24476
|
+
const id = nextId++;
|
|
24477
|
+
pending.set(id, (message) => message.error ? fail2(new Error(method)) : ok(message.result));
|
|
24478
|
+
socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
24479
|
+
});
|
|
24480
|
+
socket.addEventListener("error", () => finish(false));
|
|
24481
|
+
socket.addEventListener("message", (event) => {
|
|
24482
|
+
try {
|
|
24483
|
+
const message = JSON.parse(String(event.data));
|
|
24484
|
+
if (typeof message.id === "number") pending.get(message.id)?.(message);
|
|
24485
|
+
} catch {
|
|
24486
|
+
}
|
|
24487
|
+
});
|
|
24488
|
+
socket.addEventListener("open", () => {
|
|
24489
|
+
void (async () => {
|
|
24490
|
+
try {
|
|
24491
|
+
await call("initialize", { clientInfo: { name: "slop", version: "0" }, capabilities: { experimentalApi: true } });
|
|
24492
|
+
socket.send(JSON.stringify({ jsonrpc: "2.0", method: "initialized", params: {} }));
|
|
24493
|
+
const loaded = await call("thread/loaded/list", {});
|
|
24494
|
+
if (!loaded.data.includes(threadId)) {
|
|
24495
|
+
finish(false);
|
|
24496
|
+
return;
|
|
24497
|
+
}
|
|
24498
|
+
await call("turn/start", { threadId, input: [{ type: "text", text }] });
|
|
24499
|
+
finish(true);
|
|
24500
|
+
} catch {
|
|
24501
|
+
finish(false);
|
|
24502
|
+
}
|
|
24503
|
+
})();
|
|
24504
|
+
});
|
|
24505
|
+
});
|
|
24506
|
+
}
|
|
24507
|
+
|
|
24508
|
+
// src/platform/harness.ts
|
|
24509
|
+
var parent = null;
|
|
24510
|
+
function parentCommand() {
|
|
24511
|
+
if (parent !== null) return parent;
|
|
24512
|
+
try {
|
|
24513
|
+
parent = process.platform === "win32" ? "" : execFileSync4("ps", ["-o", "command=", "-p", String(process.ppid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24514
|
+
} catch {
|
|
24515
|
+
parent = "";
|
|
24516
|
+
}
|
|
24517
|
+
return parent;
|
|
24518
|
+
}
|
|
24519
|
+
function harnessOf(command) {
|
|
24520
|
+
const first = basename2(command.trim().split(/\s+/)[0] ?? "");
|
|
24521
|
+
if (/^claude(\b|$)/.test(first)) return "claude";
|
|
24522
|
+
if (/^codex(\b|$)/.test(first)) return "codex";
|
|
24523
|
+
if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
|
|
24524
|
+
return null;
|
|
24525
|
+
}
|
|
24526
|
+
function isNonInteractive(parentCommand2) {
|
|
24527
|
+
return /(^|[\s/])codex(\S*)?\s+exec(\s|$)/.test(parentCommand2);
|
|
24528
|
+
}
|
|
24529
|
+
function nonInteractiveHarness() {
|
|
24530
|
+
return isNonInteractive(parentCommand());
|
|
24531
|
+
}
|
|
24532
|
+
var same = (a, b) => {
|
|
24533
|
+
const real = (path3) => {
|
|
24534
|
+
try {
|
|
24535
|
+
return realpathSync2(path3);
|
|
24536
|
+
} catch {
|
|
24537
|
+
return path3;
|
|
24538
|
+
}
|
|
24539
|
+
};
|
|
24540
|
+
return real(a) === real(b);
|
|
24541
|
+
};
|
|
24542
|
+
function codexConversation(roots, cwd) {
|
|
24543
|
+
const rollouts = [];
|
|
24544
|
+
const walk = (dir2, depth) => {
|
|
24545
|
+
let entries;
|
|
24546
|
+
try {
|
|
24547
|
+
entries = readdirSync2(dir2, { withFileTypes: true });
|
|
24548
|
+
} catch {
|
|
24549
|
+
return;
|
|
24550
|
+
}
|
|
24551
|
+
for (const entry of entries) {
|
|
24552
|
+
const path3 = join8(dir2, entry.name);
|
|
24553
|
+
if (entry.isDirectory()) {
|
|
24554
|
+
if (depth < 3) walk(path3, depth + 1);
|
|
24555
|
+
continue;
|
|
24556
|
+
}
|
|
24557
|
+
const match = /^rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(entry.name);
|
|
24558
|
+
if (!match) continue;
|
|
24559
|
+
try {
|
|
24560
|
+
rollouts.push({ path: path3, id: match[1] ?? "", mtime: statSync3(path3).mtimeMs });
|
|
24561
|
+
} catch {
|
|
24562
|
+
}
|
|
24563
|
+
}
|
|
24564
|
+
};
|
|
24565
|
+
for (const root of roots) walk(root, 0);
|
|
24566
|
+
rollouts.sort((a, b) => b.mtime - a.mtime);
|
|
24567
|
+
for (const rollout of rollouts.slice(0, 300)) {
|
|
24568
|
+
let head = "";
|
|
24569
|
+
try {
|
|
24570
|
+
const fd = openSync(rollout.path, "r");
|
|
24571
|
+
try {
|
|
24572
|
+
const buffer = Buffer.alloc(8192);
|
|
24573
|
+
head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
|
|
24574
|
+
} finally {
|
|
24575
|
+
closeSync(fd);
|
|
24576
|
+
}
|
|
24577
|
+
} catch {
|
|
24578
|
+
continue;
|
|
24579
|
+
}
|
|
24580
|
+
const line = head.split("\n")[0] ?? "";
|
|
24581
|
+
if (!line.includes('"session_meta"')) continue;
|
|
24582
|
+
const found = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line);
|
|
24583
|
+
if (!found) continue;
|
|
24584
|
+
let dir2;
|
|
24585
|
+
try {
|
|
24586
|
+
dir2 = JSON.parse(`"${found[1]}"`);
|
|
24587
|
+
} catch {
|
|
24588
|
+
continue;
|
|
24589
|
+
}
|
|
24590
|
+
if (same(dir2, cwd)) return rollout.id;
|
|
24591
|
+
}
|
|
24592
|
+
return null;
|
|
24593
|
+
}
|
|
24594
|
+
function harnessConversation(cwd) {
|
|
24595
|
+
const harness = harnessOf(parentCommand());
|
|
24596
|
+
if (harness === "claude") {
|
|
24597
|
+
const id = harnessRegistration()?.sessionId;
|
|
24598
|
+
return id ? { id, harness } : null;
|
|
24599
|
+
}
|
|
24600
|
+
if (harness === "codex" && !nonInteractiveHarness()) {
|
|
24601
|
+
const id = codexConversation(codexHomes().map((home) => join8(home, "sessions")), cwd);
|
|
24602
|
+
return id ? { id, harness } : null;
|
|
24603
|
+
}
|
|
24604
|
+
return null;
|
|
24605
|
+
}
|
|
24606
|
+
|
|
24607
|
+
// src/platform/agent-identity.ts
|
|
24608
|
+
var processConversation = randomUUID4();
|
|
24309
24609
|
var startedAt = Date.now();
|
|
24610
|
+
function knownConversation(cwd) {
|
|
24611
|
+
return harnessConversation(cwd) ?? rememberedConversation(cwd, startedAt - 12e4);
|
|
24612
|
+
}
|
|
24310
24613
|
function agentIdentity(cwd, fallback = "process") {
|
|
24311
|
-
const conversation =
|
|
24614
|
+
const conversation = knownConversation(cwd);
|
|
24312
24615
|
const repo = findRepo();
|
|
24313
24616
|
return {
|
|
24314
|
-
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${
|
|
24617
|
+
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve3(cwd)}`),
|
|
24315
24618
|
harness: conversation?.harness ?? null,
|
|
24316
|
-
directory: repo ?
|
|
24619
|
+
directory: repo ? basename3(repo.worktree) : basename3(cwd),
|
|
24317
24620
|
branch: currentBranch(cwd)
|
|
24318
24621
|
};
|
|
24319
24622
|
}
|
|
@@ -24384,11 +24687,11 @@ function serveMcp(tools2, info) {
|
|
|
24384
24687
|
}
|
|
24385
24688
|
|
|
24386
24689
|
// src/commands/tasks.ts
|
|
24387
|
-
import { readFileSync as
|
|
24690
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
24388
24691
|
|
|
24389
24692
|
// src/commands/items.ts
|
|
24390
|
-
import { readFileSync as
|
|
24391
|
-
var readText = (value) => value === "-" ?
|
|
24693
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
24694
|
+
var readText = (value) => value === "-" ? readFileSync8(0, "utf8").trim() : value;
|
|
24392
24695
|
var path2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
24393
24696
|
function renderItems(items) {
|
|
24394
24697
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -24415,7 +24718,7 @@ function registerItemCommands(task, withProject) {
|
|
|
24415
24718
|
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) => {
|
|
24416
24719
|
const api = authedApi();
|
|
24417
24720
|
const project = await resolveProject(api, options.project);
|
|
24418
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
24721
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync8(0, "utf8") : lines.join("\n");
|
|
24419
24722
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
24420
24723
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
24421
24724
|
const items = await api.post(path2(project.id, key), { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -24472,7 +24775,7 @@ function oneOf(value, allowed, what) {
|
|
|
24472
24775
|
if (allowed.includes(normalized)) return normalized;
|
|
24473
24776
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
24474
24777
|
}
|
|
24475
|
-
var readDescription = (value) => value === "-" ?
|
|
24778
|
+
var readDescription = (value) => value === "-" ? readFileSync9(0, "utf8").trim() : value;
|
|
24476
24779
|
var taskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
24477
24780
|
var loadProject = (api, id) => api.get(`/projects/${id}`);
|
|
24478
24781
|
function resolveStatus(project, ref) {
|
|
@@ -24623,14 +24926,14 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24623
24926
|
}
|
|
24624
24927
|
|
|
24625
24928
|
// src/platform/update-check.ts
|
|
24626
|
-
import { existsSync as
|
|
24627
|
-
import { dirname as dirname4, join as
|
|
24929
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
24930
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
24628
24931
|
var CHECK_EVERY_MS = 60 * 60 * 1e3;
|
|
24629
24932
|
var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
|
|
24630
|
-
var cacheFile = () =>
|
|
24933
|
+
var cacheFile = () => join9(dirname4(configPath()), "update-check.json");
|
|
24631
24934
|
function readCache() {
|
|
24632
24935
|
try {
|
|
24633
|
-
return JSON.parse(
|
|
24936
|
+
return JSON.parse(readFileSync10(cacheFile(), "utf8"));
|
|
24634
24937
|
} catch {
|
|
24635
24938
|
return { checkedAt: 0, latest: null };
|
|
24636
24939
|
}
|
|
@@ -24666,13 +24969,13 @@ function updateNotice(current) {
|
|
|
24666
24969
|
if (!latest || !isNewer(latest, current)) return null;
|
|
24667
24970
|
return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
|
|
24668
24971
|
}
|
|
24669
|
-
var updateChecksEnabled = () => !
|
|
24972
|
+
var updateChecksEnabled = () => !existsSync8(join9(dirname4(configPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
|
|
24670
24973
|
|
|
24671
24974
|
// src/commands/threads.ts
|
|
24672
|
-
import { readFileSync as
|
|
24673
|
-
var readText2 = (value) => value === "-" ?
|
|
24975
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
24976
|
+
var readText2 = (value) => value === "-" ? readFileSync11(0, "utf8").trim() : value;
|
|
24674
24977
|
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
24675
|
-
var who = (message) => `${message.author.name}${message.
|
|
24978
|
+
var who = (message) => message.via !== "agent" ? message.author.name : `${message.agent ?? message.author.name} (${message.agent ? `${message.author.name}'s agent` : "agent"}${message.machine ? ` \xB7 ${message.machine}` : ""})`;
|
|
24676
24979
|
function threadLine(thread) {
|
|
24677
24980
|
const scope = [thread.taskKey ?? (thread.to ? null : "general"), thread.to ? `to ${thread.to.agent}` : null].filter(Boolean).join(", ");
|
|
24678
24981
|
const unread = thread.unread > 0 ? ` ${thread.unread} unread` : "";
|
|
@@ -24728,38 +25031,33 @@ function registerThreadCommands(program3, task) {
|
|
|
24728
25031
|
const api = authedApi();
|
|
24729
25032
|
const project = await resolveProject(api, options.project);
|
|
24730
25033
|
const body2 = readText2(text);
|
|
24731
|
-
const result = await api.post(`/projects/${project.id}/threads`, {
|
|
25034
|
+
const result = await api.post(`/projects/${project.id}/threads`, { task: options.task, body: body2 });
|
|
24732
25035
|
emit(result, () => started(result));
|
|
24733
25036
|
});
|
|
24734
25037
|
withProject(task.command("ask <key> <text>")).description('Start a thread about the task and say something to whoever is live on it. "-" reads stdin.').action(async (key, text, options) => {
|
|
24735
25038
|
const api = authedApi();
|
|
24736
25039
|
const project = await resolveProject(api, options.project);
|
|
24737
25040
|
const body2 = readText2(text);
|
|
24738
|
-
const result = await api.post(`/projects/${project.id}/threads`, {
|
|
25041
|
+
const result = await api.post(`/projects/${project.id}/threads`, { task: key, body: body2 });
|
|
24739
25042
|
emit(result, () => started(result));
|
|
24740
25043
|
});
|
|
24741
25044
|
}
|
|
24742
|
-
var subjectOf = (body2) => {
|
|
24743
|
-
const line = body2.split("\n")[0]?.trim() ?? "";
|
|
24744
|
-
if (!line) throw new CliError("Say something", "the first line becomes the subject");
|
|
24745
|
-
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
24746
|
-
};
|
|
24747
25045
|
var started = (result) => result.thread.to ? `Started ${result.thread.id} to ${result.thread.to.agent}${result.delivered ? "; it has it now." : "; it reads it when it is back."}` : result.thread.taskKey ? result.delivered ? `Started ${result.thread.id} on ${result.thread.taskKey}; reached ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : `Started ${result.thread.id} on ${result.thread.taskKey}. Nobody is live on it; they see it when they start.` : `Started ${result.thread.id} (general). People see it on the web; agents at their next session start.`;
|
|
24748
25046
|
|
|
24749
25047
|
// src/platform/inbox.ts
|
|
24750
25048
|
import { createHash as createHash4 } from "crypto";
|
|
24751
|
-
import { appendFileSync, existsSync as
|
|
24752
|
-
import { dirname as dirname5, join as
|
|
24753
|
-
var inboxDir = () =>
|
|
25049
|
+
import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync12, realpathSync as realpathSync3, renameSync, rmSync as rmSync4 } from "fs";
|
|
25050
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
25051
|
+
var inboxDir = () => join10(dirname5(configPath()), "inbox");
|
|
24754
25052
|
var realCwd2 = (cwd) => {
|
|
24755
25053
|
try {
|
|
24756
|
-
return
|
|
25054
|
+
return realpathSync3(cwd);
|
|
24757
25055
|
} catch {
|
|
24758
25056
|
return cwd;
|
|
24759
25057
|
}
|
|
24760
25058
|
};
|
|
24761
25059
|
var inboxKey = (cwd) => createHash4("sha256").update(realCwd2(cwd)).digest("hex").slice(0, 16);
|
|
24762
|
-
var inboxFile = (cwd) =>
|
|
25060
|
+
var inboxFile = (cwd) => join10(inboxDir(), `${inboxKey(cwd)}.jsonl`);
|
|
24763
25061
|
function inboxWrite(cwd, push) {
|
|
24764
25062
|
mkdirSync6(inboxDir(), { recursive: true });
|
|
24765
25063
|
appendFileSync(inboxFile(cwd), `${JSON.stringify(push)}
|
|
@@ -24767,14 +25065,14 @@ function inboxWrite(cwd, push) {
|
|
|
24767
25065
|
}
|
|
24768
25066
|
function inboxDrain(cwd) {
|
|
24769
25067
|
const path3 = inboxFile(cwd);
|
|
24770
|
-
if (!
|
|
25068
|
+
if (!existsSync9(path3)) return [];
|
|
24771
25069
|
const taken = `${path3}.${process.pid}.${Date.now()}.draining`;
|
|
24772
25070
|
try {
|
|
24773
25071
|
renameSync(path3, taken);
|
|
24774
25072
|
} catch {
|
|
24775
25073
|
return [];
|
|
24776
25074
|
}
|
|
24777
|
-
const lines =
|
|
25075
|
+
const lines = readFileSync12(taken, "utf8").split("\n").filter(Boolean);
|
|
24778
25076
|
rmSync4(taken, { force: true });
|
|
24779
25077
|
return lines.flatMap((line) => {
|
|
24780
25078
|
try {
|
|
@@ -24785,6 +25083,23 @@ function inboxDrain(cwd) {
|
|
|
24785
25083
|
});
|
|
24786
25084
|
}
|
|
24787
25085
|
|
|
25086
|
+
// src/platform/wake.ts
|
|
25087
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
25088
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
25089
|
+
var dir = (kind) => join11(dirname6(configPath()), kind);
|
|
25090
|
+
var file2 = (kind, cwd) => join11(dir(kind), inboxKey(cwd));
|
|
25091
|
+
function markWake(cwd) {
|
|
25092
|
+
mkdirSync7(dir("wake"), { recursive: true });
|
|
25093
|
+
writeFileSync7(file2("wake", cwd), String(Date.now()));
|
|
25094
|
+
}
|
|
25095
|
+
function takeWake(cwd, since) {
|
|
25096
|
+
const path3 = file2("wake", cwd);
|
|
25097
|
+
if (!existsSync10(path3)) return false;
|
|
25098
|
+
const at = statSync4(path3).mtimeMs;
|
|
25099
|
+
rmSync5(path3, { force: true });
|
|
25100
|
+
return at >= since - 1e3;
|
|
25101
|
+
}
|
|
25102
|
+
|
|
24788
25103
|
// src/commands/mcp.ts
|
|
24789
25104
|
var str = (description2) => ({ type: "string", description: description2 });
|
|
24790
25105
|
var int2 = (description2) => ({ type: "integer", description: description2 });
|
|
@@ -24832,13 +25147,30 @@ function handlers() {
|
|
|
24832
25147
|
const api = authedApi();
|
|
24833
25148
|
const resolveOnly = (args) => resolveProject(api, args.project);
|
|
24834
25149
|
let held = null;
|
|
25150
|
+
let identity = null;
|
|
25151
|
+
const whoAmI = () => identity ??= agentIdentity(process.cwd());
|
|
24835
25152
|
const hold = async (projectId, taskKey) => {
|
|
24836
25153
|
if (held && held.projectId === projectId && held.taskKey === taskKey) return;
|
|
24837
25154
|
held?.stop();
|
|
24838
25155
|
held = new HeldSession(api, projectId, taskKey, {
|
|
24839
|
-
|
|
24840
|
-
|
|
24841
|
-
|
|
25156
|
+
// A message for the agent wakes its harness when the harness can be woken (Claude Code's session socket): it
|
|
25157
|
+
// arrives as a turn, now, whether the agent is idle or mid-turn. Otherwise the inbox and the hooks carry it.
|
|
25158
|
+
onMessage: (push) => {
|
|
25159
|
+
const text = `A message for you on Slopware Tasks. Act or answer it (say reply), then carry on.
|
|
25160
|
+
|
|
25161
|
+
${pushBlock(push)}`;
|
|
25162
|
+
const me = whoAmI();
|
|
25163
|
+
const wake = me.harness === "codex" && me.conversation ? wakeCodex(text, me.conversation) : wakeHarness(text);
|
|
25164
|
+
void wake.then((woken) => {
|
|
25165
|
+
if (!woken) inboxWrite(process.cwd(), push);
|
|
25166
|
+
});
|
|
25167
|
+
},
|
|
25168
|
+
claim: whoAmI,
|
|
25169
|
+
onReconnect: (session) => catchUp(api, projectId, session.taskKey, process.cwd()).catch(() => void 0),
|
|
25170
|
+
// Made unreachable: a Codex Stop hook holding a turn open has nothing to wait for; the marker lets it go.
|
|
25171
|
+
onReachable: (on) => {
|
|
25172
|
+
if (!on) markWake(process.cwd());
|
|
25173
|
+
}
|
|
24842
25174
|
});
|
|
24843
25175
|
await held.start();
|
|
24844
25176
|
};
|
|
@@ -24850,10 +25182,25 @@ function handlers() {
|
|
|
24850
25182
|
const release = async () => {
|
|
24851
25183
|
if (held?.taskKey) await hold(held.projectId, null);
|
|
24852
25184
|
};
|
|
25185
|
+
if (!nonInteractiveHarness())
|
|
25186
|
+
void (async () => {
|
|
25187
|
+
for (let waited = 0; waited < 1e4 && !knownConversation(process.cwd()); waited += 250) await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
25188
|
+
whoAmI();
|
|
25189
|
+
const target = await resolveOnly({});
|
|
25190
|
+
if (!held) await hold(target.id, null);
|
|
25191
|
+
})().catch(() => void 0);
|
|
25192
|
+
if (!nonInteractiveHarness()) void healCodexTrust().catch(() => void 0);
|
|
25193
|
+
const goodbye = () => {
|
|
25194
|
+
held?.stop();
|
|
25195
|
+
held = null;
|
|
25196
|
+
setTimeout(() => process.exit(0), 300).unref();
|
|
25197
|
+
};
|
|
25198
|
+
process.stdin.once("end", goodbye);
|
|
25199
|
+
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, goodbye);
|
|
24853
25200
|
return [
|
|
24854
25201
|
{
|
|
24855
25202
|
name: "wait_for_message",
|
|
24856
|
-
description: "Block until the next message for you (on your task, in a thread you are in, or addressed to you), or until `minutes` pass.
|
|
25203
|
+
description: "Block until the next message for you (on your task, in a thread you are in, or addressed to you), or until `minutes` pass. Optional: messages reach you on their own; use this only when you have nothing else to do and nobody is at your keyboard.",
|
|
24857
25204
|
inputSchema: schema({ task: str("Task key to go live on first (optional)"), minutes: int2("Give up after (default 300)"), project: PROJECT }),
|
|
24858
25205
|
run: async (args) => {
|
|
24859
25206
|
const target = await project(args);
|
|
@@ -24861,20 +25208,21 @@ function handlers() {
|
|
|
24861
25208
|
const queued = inboxDrain(process.cwd());
|
|
24862
25209
|
if (queued.length) return queued.map(pushBlock).join("\n\n");
|
|
24863
25210
|
const minutes = typeof args.minutes === "number" ? args.minutes : 300;
|
|
24864
|
-
const outcome = await new Promise((
|
|
25211
|
+
const outcome = await new Promise((resolve5) => {
|
|
24865
25212
|
const waiter = (delivered) => {
|
|
24866
25213
|
held?.waiters.delete(waiter);
|
|
24867
|
-
|
|
25214
|
+
clearTimeout(timer);
|
|
25215
|
+
resolve5({ push: delivered, released: delivered === null });
|
|
24868
25216
|
};
|
|
24869
25217
|
held?.waiters.add(waiter);
|
|
24870
|
-
setTimeout(() => {
|
|
25218
|
+
const timer = setTimeout(() => {
|
|
24871
25219
|
held?.waiters.delete(waiter);
|
|
24872
|
-
|
|
25220
|
+
resolve5({ push: null, released: false });
|
|
24873
25221
|
}, minutes * 6e4);
|
|
24874
25222
|
});
|
|
24875
25223
|
if (outcome.push) return pushBlock(outcome.push);
|
|
24876
25224
|
if (outcome.released) return "Your person made you not visible to teammates: nothing can reach you now. You may stop.";
|
|
24877
|
-
return `No message in ${minutes} minutes
|
|
25225
|
+
return `No message in ${minutes} minutes.`;
|
|
24878
25226
|
}
|
|
24879
25227
|
},
|
|
24880
25228
|
{
|
|
@@ -24901,10 +25249,10 @@ function handlers() {
|
|
|
24901
25249
|
{
|
|
24902
25250
|
name: "post",
|
|
24903
25251
|
description: "Start a thread. With a task: about that task, to whoever is live on it (or whoever picks it up). Without: general, for people and for context; nobody has to answer.",
|
|
24904
|
-
inputSchema: schema({ subject: str("Subject"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), project: PROJECT }, ["
|
|
25252
|
+
inputSchema: schema({ subject: str("Subject (default: the first line of the body)"), body: str("First message (Markdown)"), task: str("Task key, to make it about that task"), project: PROJECT }, ["body"]),
|
|
24905
25253
|
run: async (args) => {
|
|
24906
25254
|
const target = await project(args);
|
|
24907
|
-
const result = await api.post(`/projects/${target.id}/threads`, { subject: String(args.subject), body: String(args.body), task: args.task, to: args.to });
|
|
25255
|
+
const result = await api.post(`/projects/${target.id}/threads`, { subject: args.subject ? String(args.subject) : void 0, body: String(args.body), task: args.task, to: args.to });
|
|
24908
25256
|
return `Started ${result.thread.id}${result.thread.to ? ` to ${result.thread.to.agent}` : ""}${result.thread.taskKey ? ` on ${result.thread.taskKey}` : result.thread.to ? "" : " (general)"}${result.delivered ? `; reached ${result.delivered} live session(s)` : ""}.`;
|
|
24909
25257
|
}
|
|
24910
25258
|
},
|
|
@@ -25192,71 +25540,83 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
25192
25540
|
}
|
|
25193
25541
|
function readJson(path3) {
|
|
25194
25542
|
try {
|
|
25195
|
-
return JSON.parse(
|
|
25543
|
+
return JSON.parse(readFileSync13(path3, "utf8"));
|
|
25196
25544
|
} catch {
|
|
25197
25545
|
return {};
|
|
25198
25546
|
}
|
|
25199
25547
|
}
|
|
25200
|
-
function installClaude() {
|
|
25548
|
+
function installClaude(dir2) {
|
|
25201
25549
|
const done = [];
|
|
25550
|
+
const primary = dir2 === join12(homedir4(), ".claude");
|
|
25551
|
+
const env = { ...process.env };
|
|
25552
|
+
if (primary) delete env.CLAUDE_CONFIG_DIR;
|
|
25553
|
+
else env.CLAUDE_CONFIG_DIR = dir2;
|
|
25554
|
+
const where = primary ? "" : ` for ${dir2}`;
|
|
25202
25555
|
try {
|
|
25203
|
-
|
|
25204
|
-
done.push(
|
|
25205
|
-
} catch {
|
|
25206
|
-
|
|
25556
|
+
execFileSync5("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: ["ignore", "ignore", "pipe"], env });
|
|
25557
|
+
done.push(`Claude Code: MCP server "slop" registered (user scope${where})`);
|
|
25558
|
+
} catch (error62) {
|
|
25559
|
+
const said = String(error62.stderr ?? "") + String(error62.stdout ?? "");
|
|
25560
|
+
if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
|
|
25561
|
+
else done.push(`Claude Code: run \`${primary ? "" : `CLAUDE_CONFIG_DIR=${dir2} `}claude mcp add --scope user slop -- slop mcp\` (${said.trim() || "the claude command was not found here"})`);
|
|
25207
25562
|
}
|
|
25208
|
-
const
|
|
25209
|
-
|
|
25210
|
-
mkdirSync7(dir, { recursive: true });
|
|
25563
|
+
const path3 = join12(dir2, "settings.json");
|
|
25564
|
+
mkdirSync8(dir2, { recursive: true });
|
|
25211
25565
|
const settings = readJson(path3);
|
|
25212
25566
|
settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
|
|
25213
25567
|
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "21600000" };
|
|
25214
|
-
|
|
25568
|
+
writeFileSync8(path3, `${JSON.stringify(settings, null, 2)}
|
|
25215
25569
|
`);
|
|
25216
|
-
done.push(`Claude Code: SessionStart
|
|
25570
|
+
done.push(`Claude Code: SessionStart hook and MCP_TOOL_TIMEOUT written to ${path3}; other slop hooks removed (its session socket wakes it)`);
|
|
25217
25571
|
return done;
|
|
25218
25572
|
}
|
|
25219
25573
|
function mergeHooks(hooks, harness) {
|
|
25220
|
-
const
|
|
25221
|
-
|
|
25574
|
+
const ours = (hook) => typeof hook.command === "string" && /^slop hook /.test(hook.command);
|
|
25575
|
+
for (const event of Object.keys(hooks)) {
|
|
25576
|
+
const entries = hooks[event] ?? [];
|
|
25577
|
+
for (const entry of entries) entry.hooks = (entry.hooks ?? []).filter((hook) => !ours(hook));
|
|
25578
|
+
hooks[event] = entries.filter((entry) => (entry.hooks ?? []).length > 0);
|
|
25579
|
+
if (hooks[event].length === 0) delete hooks[event];
|
|
25580
|
+
}
|
|
25581
|
+
const add = (event, verb, timeout, statusMessage) => {
|
|
25222
25582
|
const entries = hooks[event] ?? [];
|
|
25223
|
-
|
|
25224
|
-
if (ours) Object.assign(ours, { command, timeout, statusMessage });
|
|
25225
|
-
else entries.push({ hooks: [{ type: "command", command, timeout, statusMessage }] });
|
|
25583
|
+
entries.push({ hooks: [{ type: "command", command: `slop hook ${verb} --harness ${harness}`, timeout, statusMessage }] });
|
|
25226
25584
|
hooks[event] = entries;
|
|
25227
25585
|
};
|
|
25228
|
-
|
|
25229
|
-
|
|
25230
|
-
|
|
25586
|
+
add("SessionStart", "start", 5, "Slopware Tasks");
|
|
25587
|
+
if (harness === "codex") {
|
|
25588
|
+
add("UserPromptSubmit", "prompt", 5, "Slopware Tasks");
|
|
25589
|
+
add("Stop", "stop", 21600, "Listening on Slopware Tasks");
|
|
25590
|
+
}
|
|
25231
25591
|
return hooks;
|
|
25232
25592
|
}
|
|
25233
|
-
function installCodex() {
|
|
25593
|
+
function installCodex(dir2) {
|
|
25234
25594
|
const done = [];
|
|
25235
|
-
|
|
25236
|
-
|
|
25237
|
-
const
|
|
25238
|
-
const current = existsSync8(configPath3) ? readFileSync11(configPath3, "utf8") : "";
|
|
25595
|
+
mkdirSync8(dir2, { recursive: true });
|
|
25596
|
+
const configPath3 = join12(dir2, "config.toml");
|
|
25597
|
+
const current = existsSync11(configPath3) ? readFileSync13(configPath3, "utf8") : "";
|
|
25239
25598
|
if (/^\[mcp_servers\.slop\]/m.test(current)) {
|
|
25240
|
-
if (/tool_timeout_sec = 1800\b/.test(current))
|
|
25599
|
+
if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync8(configPath3, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
|
|
25241
25600
|
done.push(`Codex: MCP server already in ${configPath3}`);
|
|
25242
25601
|
} else {
|
|
25243
|
-
|
|
25602
|
+
writeFileSync8(configPath3, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
|
|
25244
25603
|
command = "slop"
|
|
25245
25604
|
args = ["mcp"]
|
|
25246
25605
|
tool_timeout_sec = 21600
|
|
25247
25606
|
`);
|
|
25248
25607
|
done.push(`Codex: MCP server "slop" added to ${configPath3}`);
|
|
25249
25608
|
}
|
|
25250
|
-
const hooksPath =
|
|
25251
|
-
const
|
|
25252
|
-
|
|
25253
|
-
|
|
25609
|
+
const hooksPath = join12(dir2, "hooks.json");
|
|
25610
|
+
const file3 = readJson(hooksPath);
|
|
25611
|
+
file3.hooks = mergeHooks(file3.hooks ?? {}, "codex");
|
|
25612
|
+
writeFileSync8(hooksPath, `${JSON.stringify(file3, null, 2)}
|
|
25254
25613
|
`);
|
|
25255
|
-
done.push(`Codex: SessionStart,
|
|
25614
|
+
done.push(`Codex: SessionStart, UserPromptSubmit and Stop hooks written to ${hooksPath}`);
|
|
25256
25615
|
return done;
|
|
25257
25616
|
}
|
|
25258
|
-
async function trustCodexHooks() {
|
|
25259
|
-
const server =
|
|
25617
|
+
async function trustCodexHooks(codexHome) {
|
|
25618
|
+
const server = spawn3("codex", ["app-server"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
|
|
25619
|
+
const where = codexHome === join12(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
|
|
25260
25620
|
let buffer = "";
|
|
25261
25621
|
const pending = /* @__PURE__ */ new Map();
|
|
25262
25622
|
server.stdout.on("data", (chunk) => {
|
|
@@ -25274,7 +25634,7 @@ async function trustCodexHooks() {
|
|
|
25274
25634
|
}
|
|
25275
25635
|
});
|
|
25276
25636
|
let nextId = 1;
|
|
25277
|
-
const call = (method, params) => new Promise((
|
|
25637
|
+
const call = (method, params) => new Promise((resolve5, reject) => {
|
|
25278
25638
|
const id = nextId++;
|
|
25279
25639
|
const timer = setTimeout(() => {
|
|
25280
25640
|
pending.delete(id);
|
|
@@ -25283,7 +25643,7 @@ async function trustCodexHooks() {
|
|
|
25283
25643
|
pending.set(id, (message) => {
|
|
25284
25644
|
clearTimeout(timer);
|
|
25285
25645
|
if (message.error) reject(new Error(message.error.message ?? `${method} failed`));
|
|
25286
|
-
else
|
|
25646
|
+
else resolve5(message.result);
|
|
25287
25647
|
});
|
|
25288
25648
|
server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
25289
25649
|
`);
|
|
@@ -25291,28 +25651,38 @@ async function trustCodexHooks() {
|
|
|
25291
25651
|
const notify = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
25292
25652
|
`);
|
|
25293
25653
|
try {
|
|
25294
|
-
await call("initialize", { clientInfo: { name: "slop", version: "0.4.
|
|
25654
|
+
await call("initialize", { clientInfo: { name: "slop", version: "0.4.1" }, capabilities: { experimentalApi: true } });
|
|
25295
25655
|
notify("initialized", {});
|
|
25296
25656
|
const listed = await call("hooks/list", { cwds: [process.cwd()] });
|
|
25297
25657
|
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
25298
|
-
if (ours.length === 0) return
|
|
25658
|
+
if (ours.length === 0) return `Codex: could not find the slop hooks in its list${where}; trust them once with /hooks in a Codex session`;
|
|
25299
25659
|
const untrusted = ours.filter((hook) => hook.trustStatus !== "trusted");
|
|
25300
|
-
if (untrusted.length === 0) return
|
|
25660
|
+
if (untrusted.length === 0) return `Codex: the slop hooks are already trusted${where}`;
|
|
25301
25661
|
const value = Object.fromEntries(untrusted.map((hook) => [hook.key, { trusted_hash: hook.currentHash }]));
|
|
25302
25662
|
await call("config/batchWrite", { edits: [{ keyPath: "hooks.state", value, mergeStrategy: "upsert" }], reloadUserConfig: true });
|
|
25303
|
-
return `Codex: trusted ${untrusted.length} slop hook(s), so it will not ask`;
|
|
25663
|
+
return `Codex: trusted ${untrusted.length} slop hook(s)${where}, so it will not ask`;
|
|
25304
25664
|
} catch (error62) {
|
|
25305
|
-
return `Codex: could not trust the hooks automatically (${error62 instanceof Error ? error62.message : String(error62)}); trust them once with /hooks in a Codex session`;
|
|
25665
|
+
return `Codex: could not trust the hooks automatically${where} (${error62 instanceof Error ? error62.message : String(error62)}); trust them once with /hooks in a Codex session`;
|
|
25306
25666
|
} finally {
|
|
25307
25667
|
server.kill();
|
|
25308
25668
|
}
|
|
25309
25669
|
}
|
|
25670
|
+
async function healCodexTrust() {
|
|
25671
|
+
const home = ownHarnessHome("CODEX_HOME");
|
|
25672
|
+
if (!home) return;
|
|
25673
|
+
const hooksPath = join12(home, "hooks.json");
|
|
25674
|
+
const configPath3 = join12(home, "config.toml");
|
|
25675
|
+
if (!existsSync11(hooksPath) || !existsSync11(configPath3)) return;
|
|
25676
|
+
if (!readFileSync13(hooksPath, "utf8").includes("slop hook stop")) return;
|
|
25677
|
+
if (readFileSync13(configPath3, "utf8").includes(`hooks.state."${hooksPath}:stop:`)) return;
|
|
25678
|
+
await trustCodexHooks(home);
|
|
25679
|
+
}
|
|
25310
25680
|
async function hookInput(harness) {
|
|
25311
|
-
const input2 = await new Promise((
|
|
25681
|
+
const input2 = await new Promise((resolve5) => {
|
|
25312
25682
|
let data = "";
|
|
25313
25683
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
25314
|
-
process.stdin.on("end", () =>
|
|
25315
|
-
setTimeout(() =>
|
|
25684
|
+
process.stdin.on("end", () => resolve5(data));
|
|
25685
|
+
setTimeout(() => resolve5(data), 2e3);
|
|
25316
25686
|
});
|
|
25317
25687
|
let cwd = process.cwd();
|
|
25318
25688
|
let conversation = null;
|
|
@@ -25322,9 +25692,13 @@ async function hookInput(harness) {
|
|
|
25322
25692
|
conversation = parsed.session_id ?? null;
|
|
25323
25693
|
} catch {
|
|
25324
25694
|
}
|
|
25325
|
-
if (conversation) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
25695
|
+
if (conversation && !nonInteractiveHarness()) rememberConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
25326
25696
|
return { cwd, conversation };
|
|
25327
25697
|
}
|
|
25698
|
+
async function promptHook(harness) {
|
|
25699
|
+
const { cwd } = await hookInput(harness);
|
|
25700
|
+
markWake(cwd);
|
|
25701
|
+
}
|
|
25328
25702
|
async function startHook(harness) {
|
|
25329
25703
|
const { conversation } = await hookInput(harness);
|
|
25330
25704
|
if (!conversation) return;
|
|
@@ -25332,18 +25706,8 @@ async function startHook(harness) {
|
|
|
25332
25706
|
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: text } })}
|
|
25333
25707
|
`);
|
|
25334
25708
|
}
|
|
25335
|
-
async function tickHook(harness) {
|
|
25336
|
-
const arrived = inboxDrain((await hookInput(harness)).cwd);
|
|
25337
|
-
if (arrived.length === 0) return;
|
|
25338
|
-
const text = `Messages arrived on Slopware Tasks while you worked:
|
|
25339
|
-
|
|
25340
|
-
${arrived.map(pushBlock).join("\n\n")}
|
|
25341
|
-
|
|
25342
|
-
Act or answer with the slop MCP \`reply\` tool, then carry on.`;
|
|
25343
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } })}
|
|
25344
|
-
`);
|
|
25345
|
-
}
|
|
25346
25709
|
async function stopHook(harness) {
|
|
25710
|
+
if (nonInteractiveHarness()) return;
|
|
25347
25711
|
const { cwd } = await hookInput(harness);
|
|
25348
25712
|
process.chdir(cwd);
|
|
25349
25713
|
const parts = inboxDrain(cwd).map(pushBlock);
|
|
@@ -25366,24 +25730,40 @@ ${parts.join("\n\n")}` })}
|
|
|
25366
25730
|
}
|
|
25367
25731
|
const general = await api.get(`/projects/${project.id}/threads?scope=general&unread=1&read=1`);
|
|
25368
25732
|
if (general.length) parts.push("Unread general threads (context):", ...general.map(threadLine));
|
|
25369
|
-
|
|
25370
|
-
|
|
25371
|
-
if (own2?.reachable) {
|
|
25372
|
-
const unread = parts.length ? `First, these arrived while you worked:
|
|
25373
|
-
|
|
25374
|
-
${parts.join("\n")}
|
|
25733
|
+
if (parts.length) {
|
|
25734
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: `Messages arrived on Slopware Tasks while you worked. Read them (threads read), act or reply (say reply), then continue or stop.
|
|
25375
25735
|
|
|
25376
|
-
`
|
|
25377
|
-
process.stdout.write(`${JSON.stringify({ decision: "block", reason: `${unread}You are reachable on Slopware Tasks, so stay listening: call the slop session tool with action wait (no other arguments). When it returns a message, act or answer it (say reply), then stop again; if it returns with nothing, call wait again. It tells you if you may stop.` })}
|
|
25736
|
+
${parts.join("\n")}` })}
|
|
25378
25737
|
`);
|
|
25379
25738
|
return;
|
|
25380
25739
|
}
|
|
25381
|
-
|
|
25382
|
-
|
|
25740
|
+
const sessionId = rememberedSession(cwd);
|
|
25741
|
+
if (!sessionId || harness !== "codex") return;
|
|
25742
|
+
const reachable = async () => (await api.get(`/projects/${project.id}/sessions`)).some((session) => session.id === sessionId && session.reachable);
|
|
25743
|
+
if (!await reachable()) return;
|
|
25744
|
+
const began = Date.now();
|
|
25745
|
+
takeWake(cwd, began);
|
|
25746
|
+
let lastCheck = began;
|
|
25747
|
+
while (Date.now() - began < STANDBY_HOLD_MS) {
|
|
25748
|
+
await new Promise((resolve5) => setTimeout(resolve5, 500));
|
|
25749
|
+
const arrived = inboxDrain(cwd);
|
|
25750
|
+
if (arrived.length) {
|
|
25751
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: `A message for you on Slopware Tasks. Act or answer it (say reply), then stop.
|
|
25383
25752
|
|
|
25384
|
-
${
|
|
25753
|
+
${arrived.map(pushBlock).join("\n\n")}` })}
|
|
25754
|
+
`);
|
|
25755
|
+
return;
|
|
25756
|
+
}
|
|
25757
|
+
if (takeWake(cwd, began)) return;
|
|
25758
|
+
if (Date.now() - lastCheck > 6e4) {
|
|
25759
|
+
lastCheck = Date.now();
|
|
25760
|
+
if (!await reachable().catch(() => true)) return;
|
|
25761
|
+
}
|
|
25762
|
+
}
|
|
25763
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: "Still reachable on Slopware Tasks, nothing arrived. Say so in a word and stop; you will wait again." })}
|
|
25385
25764
|
`);
|
|
25386
25765
|
}
|
|
25766
|
+
var STANDBY_HOLD_MS = 5 * 60 * 60 * 1e3;
|
|
25387
25767
|
function tools() {
|
|
25388
25768
|
const fine = handlers();
|
|
25389
25769
|
const run2 = (name, args) => {
|
|
@@ -25422,7 +25802,7 @@ function tools() {
|
|
|
25422
25802
|
),
|
|
25423
25803
|
group(
|
|
25424
25804
|
"session",
|
|
25425
|
-
"Being live. claim: take a task, go live on it, get what is unread (messages then reach you
|
|
25805
|
+
"Being live. claim: take a task, go live on it, get what is unread (messages then reach you on their own). release; finish is on task. who: live agents by name (you included). check: unread now. wait: block for the next message (optional).",
|
|
25426
25806
|
{ claim: "claim_task", release: "release_task", who: "sessions", check: "check_messages", wait: "wait_for_message" },
|
|
25427
25807
|
{ task: str("Task key"), minutes: int2("wait: give up after (default 300)") }
|
|
25428
25808
|
),
|
|
@@ -25431,7 +25811,7 @@ function tools() {
|
|
|
25431
25811
|
"say",
|
|
25432
25812
|
"post: start a thread (with task: reaches whoever is live on it; with to: one live agent by name; without either: general, for people). reply: in a thread; reaches its task's live sessions, its participants and its addressee.",
|
|
25433
25813
|
{ post: "post", reply: "reply" },
|
|
25434
|
-
{ subject: str("post"), body: str("Markdown"), task: str("post: about this task"), to: str("post: an agent's name (session who)"), thread: str("reply: thread id") }
|
|
25814
|
+
{ subject: str("post: optional, else the first line"), body: str("Markdown"), task: str("post: about this task"), to: str("post: an agent's name (session who)"), thread: str("reply: thread id") }
|
|
25435
25815
|
)
|
|
25436
25816
|
];
|
|
25437
25817
|
}
|
|
@@ -25456,25 +25836,26 @@ function registerMcpCommands(program3, version2) {
|
|
|
25456
25836
|
const mcp = program3.command("mcp").description("The MCP server: the board and its threads as tools for Claude Code, Codex and any MCP client. Holds your live session while it runs.");
|
|
25457
25837
|
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(withInbox(tools(), version2), { name: "slop", version: version2 }));
|
|
25458
25838
|
mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
|
|
25459
|
-
const
|
|
25839
|
+
const homes = codexHomes();
|
|
25840
|
+
const lines = [...claudeHomes().flatMap((home) => installClaude(home)), ...homes.flatMap((home) => installCodex(home))];
|
|
25460
25841
|
let codexPresent = true;
|
|
25461
25842
|
try {
|
|
25462
|
-
|
|
25843
|
+
execFileSync5("codex", ["--version"], { stdio: "ignore" });
|
|
25463
25844
|
} catch {
|
|
25464
25845
|
codexPresent = false;
|
|
25465
25846
|
}
|
|
25466
|
-
if (codexPresent) lines.push(await trustCodexHooks());
|
|
25847
|
+
if (codexPresent) for (const home of homes) lines.push(await trustCodexHooks(home));
|
|
25467
25848
|
emit({ done: lines }, () => lines.join("\n"));
|
|
25468
25849
|
});
|
|
25469
25850
|
const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
|
|
25470
|
-
const
|
|
25471
|
-
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.").option("--harness <name>", "claude or codex").action((options) => startHook(
|
|
25472
|
-
hook.command("
|
|
25473
|
-
hook.command("
|
|
25851
|
+
const harnessOf2 = (options) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
|
|
25852
|
+
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.").option("--harness <name>", "claude or codex").action((options) => startHook(harnessOf2(options)));
|
|
25853
|
+
hook.command("prompt").description("UserPromptSubmit hook (Codex): the person typed; a waiting agent returns to handle it.").option("--harness <name>", "claude or codex").action((options) => promptHook(harnessOf2(options)));
|
|
25854
|
+
hook.command("stop").description("Stop hook (Codex): the wait. Holds the turn open at no cost until a message arrives, the person types, or the agent is made unreachable.").option("--harness <name>", "claude or codex").action((options) => stopHook(harnessOf2(options)));
|
|
25474
25855
|
}
|
|
25475
25856
|
|
|
25476
25857
|
// src/commands/sessions.ts
|
|
25477
|
-
import { readFileSync as
|
|
25858
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
25478
25859
|
var taskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
25479
25860
|
var sessionLine2 = (session) => [session.agent?.name ?? "agent", session.taskKey ?? "(no task)", session.user.name, session.machine.name, session.where?.directory ? `${session.where.directory}${session.where.branch ? `@${session.where.branch}` : ""}` : "-", session.startedAt, session.state === "reconnecting" ? "reconnecting" : "live"];
|
|
25480
25861
|
function registerSessionCommands(program3, task) {
|
|
@@ -25559,13 +25940,13 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
|
|
|
25559
25940
|
}
|
|
25560
25941
|
|
|
25561
25942
|
// src/index.ts
|
|
25562
|
-
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.
|
|
25943
|
+
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.1").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)");
|
|
25563
25944
|
registerAuthCommands(program2);
|
|
25564
25945
|
registerProjectCommands(program2);
|
|
25565
25946
|
var taskCommand = registerTaskCommands(program2);
|
|
25566
25947
|
registerSessionCommands(program2, taskCommand);
|
|
25567
25948
|
registerThreadCommands(program2, taskCommand);
|
|
25568
|
-
registerMcpCommands(program2, "0.4.
|
|
25949
|
+
registerMcpCommands(program2, "0.4.1");
|
|
25569
25950
|
registerGuideCommand(program2);
|
|
25570
25951
|
registerSkillCommands(program2);
|
|
25571
25952
|
program2.parseAsync().catch(fail);
|