slopwaresystems 0.4.7 → 0.4.9
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 +1506 -1251
- 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` (Claude Code only, and rarely; messages reach you on their own; never on Codex; 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`. Messages are letters: read when the other agent gets to them, not at once; write once, reply only when it changes something (see `slop-sessions`) |\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";
|
|
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` (Claude Code only, and rarely; messages reach you on their own; never on Codex; 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`. Messages are letters: read when the other agent gets to them, not at once; write once, reply only when it changes something (see `slop-sessions`) |\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\nWhen a wake says messages are waiting, use `session` `check` with the project named in the wake. Each conversation and project has a separate inbox, including when chats share a checkout.\n\nLaunchers can set `SLOP_CONVERSATION_ID` to the host's stable chat ID and `SLOP_HARNESS` to `claude`, `codex`, or `other`. Host-provided conversation IDs are detected automatically when available. An ambiguous directory is not enough to identify a chat; supply its ID when running several chats there without host-provided identity.\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 message arrives, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is
|
|
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 message arrives, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is your conversation\u2019s registered presence in a project, maintained by a live connection. 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## Messages are letters, not chat\n\nThe other agent is an interactive session. Your message does not interrupt it: idle, it starts a turn; busy, it queues behind the turn that is running and behind whatever the person asked, and on Codex it is not even seen until that turn ends. It is answered when the agent gets to it, which can be seconds or an hour. So:\n\n- **Nothing is read in order.** A reply you get may have been written before your latest message reached them. Read it that way: check the thread (`threads` `read`) before you act on an answer that looks stale, and do not correct someone for missing something they had not seen yet.\n- **One letter, complete.** Say everything the other side needs in one message: what you did, what you need, what you will do if you hear nothing. Do not send a message, then a follow-up, then a correction; edit your plan notes instead and write once.\n- **Several arrived at once? Answer once.** When messages pile up (they are handed to you one after another, or as a batch), read the whole thread first, then write one reply for what is still open. Never answer each one in turn.\n- **Reply only when it changes something.** Answer a question, report a result that was asked for, or say what you are going to do differently. Do not acknowledge, thank, confirm receipt, restate what the other side said, or say "no reply needed": that message is a turn on the other side and often gets one back, and that is how two agents fill a thread with eighty messages of nothing. Silence means "read, nothing to add".\n- **Stale is fine.** A message that arrives after the matter is settled needs no answer. If it asks for something already done, one line pointing at where it was done, at most.\n- **Do not wait for a reply** (Codex: never; Claude Code: only when you truly cannot proceed). End your turn; the answer arrives as a turn of yours when it comes, and the person can talk to you in the meantime.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on**, in **a thread you have written in**, or **addressed to you** reaches you at once. Nothing else does.\n- **Everything else waits.** `session` `claim` and `session` `check` print the unread threads about that task and the unread general ones (read those for context). `threads` `list` lists them any time; `threads` `read` reads one.\n- Your own messages never come back to you.\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. For as long as the harness runs the server, a message for you reaches you on its own. On Claude Code it arrives at once: idle, as a turn of yours over the session socket; mid-turn, with your next tool result. On Codex it is queued to your thread with `codex queue`: idle, that starts a turn at once; mid-turn, it becomes your next turn when the running one ends, so a message can sit for as long as the turn you are on takes, and the sender\'s next one may be waiting behind it. Every slop tool\'s result carries what arrived in the meantime besides. There is nothing to poll and nothing to restart. When one arrives, act, or answer with `say` `reply` if an answer is needed, and carry on.\n4. **Finish:** `task` `finish` (it also releases the session), or `session` `release` to step off without finishing.\n\nA wake may say that messages are waiting rather than include their bodies. Use `session` `check` with the project named in that wake. The content remains queued until a slop tool result hands it over. A failed wake leaves the content for your next tool call.\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 everyone it was sent to\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, to: "Albert, Ida") a group chat: every one of them gets it, and every reply reaches them all\nsay post (body, to: "all") everyone live in the project, in one thread (never one thread per agent)\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**Claude Code only:** when you cannot go on without the answer, `say` `reply` (or `post`) with `wait: 20` returns the reply in the same call, or `session` `wait` with `thread` waits for the next message there (20 minutes at most). This is rare; the other agent may not read your letter for a long time. **On Codex, do not use `wait`:** end your turn, and the reply comes as your next turn.\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. One thread per matter: reply in the thread that exists rather than opening a new one to the same agent.\n\n## Etiquette\n\n- Each conversation has its own session and inbox in each project. Identified conversations can share a checkout; separate worktrees are still useful for keeping code edits independent.\n- Do not claim a task that has someone else\'s live session (`session` `who`); write in a thread about it first.\n- Answer questions about your task promptly: the other agent may be blocked on you. Answer them once.\n- Report progress in the plan\'s notes, not in threads. Threads are for asking and deciding, not for status.\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";
|
|
@@ -3409,7 +3409,7 @@ function installSkills({ force = false } = {}) {
|
|
|
3409
3409
|
}
|
|
3410
3410
|
|
|
3411
3411
|
// src/commands/auth.ts
|
|
3412
|
-
import { spawn } from "child_process";
|
|
3412
|
+
import { spawn as spawn2 } from "child_process";
|
|
3413
3413
|
import { platform as platform2 } from "os";
|
|
3414
3414
|
|
|
3415
3415
|
// ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/classic/external.js
|
|
@@ -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, resolve6) {
|
|
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, resolve5) {
|
|
|
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, resolve6);
|
|
8452
8452
|
if (answer > result)
|
|
8453
8453
|
result = answer;
|
|
8454
8454
|
}
|
|
@@ -8459,7 +8459,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
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, resolve6) : NONE;
|
|
8463
8463
|
if (child > answer)
|
|
8464
8464
|
answer = child;
|
|
8465
8465
|
}
|
|
@@ -8523,7 +8523,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
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 ?? (resolve6 ? inst._zod.innerType : void 0);
|
|
8527
8527
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
8528
8528
|
break;
|
|
8529
8529
|
}
|
|
@@ -23486,11 +23486,13 @@ var SessionClaimInput = external_exports.object({
|
|
|
23486
23486
|
});
|
|
23487
23487
|
var ConnectTicket = external_exports.object({ ticket: external_exports.string(), path: external_exports.string() });
|
|
23488
23488
|
var AskInput = external_exports.object({ body: external_exports.string().trim().min(1, "Say something").max(4e3) });
|
|
23489
|
+
var ThreadPush = external_exports.object({ thread: external_exports.object({ id: Id, subject: external_exports.string(), taskKey: external_exports.string().nullable(), to: external_exports.string().nullable().optional() }), message: ThreadMessage });
|
|
23490
|
+
var ThreadSessionPush = ThreadPush.extend({ type: external_exports.literal("thread") });
|
|
23489
23491
|
var SessionPush = external_exports.discriminatedUnion("type", [
|
|
23490
23492
|
external_exports.object({ type: external_exports.literal("hello"), session: Session }),
|
|
23491
23493
|
external_exports.object({ type: external_exports.literal("event"), record: TaskEventRecord }),
|
|
23492
23494
|
/** A message in a thread about the task this session is on. */
|
|
23493
|
-
|
|
23495
|
+
ThreadSessionPush,
|
|
23494
23496
|
/** The room is closing this session (someone ran `slop session end`). */
|
|
23495
23497
|
external_exports.object({ type: external_exports.literal("end") }),
|
|
23496
23498
|
/** Reachability was switched from the web. Off releases a waiting agent: nothing can reach it any more. */
|
|
@@ -23498,6 +23500,12 @@ var SessionPush = external_exports.discriminatedUnion("type", [
|
|
|
23498
23500
|
/** To board watchers: something in the project changed; refetch. */
|
|
23499
23501
|
external_exports.object({ type: external_exports.literal("changed"), scope: external_exports.string() })
|
|
23500
23502
|
]);
|
|
23503
|
+
var SessionMessagesInput = external_exports.object({ cursor: external_exports.coerce.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional() });
|
|
23504
|
+
var SessionMessages = external_exports.object({
|
|
23505
|
+
cursor: external_exports.number().int().nonnegative(),
|
|
23506
|
+
hasMore: external_exports.boolean(),
|
|
23507
|
+
pushes: external_exports.array(ThreadSessionPush)
|
|
23508
|
+
});
|
|
23501
23509
|
|
|
23502
23510
|
// ../contract/src/work/agent-names.ts
|
|
23503
23511
|
var AGENT_NAMES = [
|
|
@@ -23862,6 +23870,7 @@ var ApiResponses = {
|
|
|
23862
23870
|
Bindings: external_exports.array(Binding),
|
|
23863
23871
|
ConnectTicket,
|
|
23864
23872
|
Sessions: external_exports.array(Session),
|
|
23873
|
+
SessionMessages,
|
|
23865
23874
|
EndedSessions: external_exports.object({ ended: external_exports.number().int().nonnegative() }),
|
|
23866
23875
|
ReachableSession: external_exports.object({ ok: external_exports.literal(true), reachable: external_exports.boolean() }),
|
|
23867
23876
|
Threads: external_exports.array(Thread),
|
|
@@ -23924,6 +23933,7 @@ registerResponseContract("POST", ["/projects/:projectId/bindings"], ApiResponses
|
|
|
23924
23933
|
registerResponseContract("POST", ["/projects/:projectId/watch/ticket", "/projects/:projectId/session/ticket", "/projects/:projectId/tasks/:ref/session/ticket"], ApiResponses.ConnectTicket);
|
|
23925
23934
|
registerResponseContract("POST", ["/projects/:projectId/session/end", "/projects/:projectId/tasks/:ref/session/end"], ApiResponses.EndedSessions);
|
|
23926
23935
|
registerResponseContract("GET", ["/projects/:projectId/sessions"], ApiResponses.Sessions);
|
|
23936
|
+
registerResponseContract("GET", ["/projects/:projectId/session/messages"], ApiResponses.SessionMessages);
|
|
23927
23937
|
registerResponseContract("POST", ["/projects/:projectId/sessions/:id/reachable"], ApiResponses.ReachableSession);
|
|
23928
23938
|
registerResponseContract("GET", ["/orgs/:orgId/threads"], ApiResponses.ProjectThreads);
|
|
23929
23939
|
registerResponseContract("GET", ["/projects/:projectId/threads"], ApiResponses.Threads);
|
|
@@ -23941,6 +23951,9 @@ function assertApiResponseContract(method, path2, schema) {
|
|
|
23941
23951
|
if (requireApiResponseSchema(method, path2) !== schema) throw new Error("The response contract does not match this API endpoint");
|
|
23942
23952
|
}
|
|
23943
23953
|
|
|
23954
|
+
// src/platform/api.ts
|
|
23955
|
+
import { resolve as resolve5 } from "path";
|
|
23956
|
+
|
|
23944
23957
|
// src/platform/config.ts
|
|
23945
23958
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
23946
23959
|
import { homedir as homedir2 } from "os";
|
|
@@ -23975,43 +23988,100 @@ function resolveSession() {
|
|
|
23975
23988
|
}
|
|
23976
23989
|
|
|
23977
23990
|
// src/platform/session-file.ts
|
|
23978
|
-
import { createHash } from "crypto";
|
|
23979
|
-
import {
|
|
23980
|
-
import { dirname as
|
|
23981
|
-
|
|
23991
|
+
import { createHash as createHash2 } from "crypto";
|
|
23992
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2, rmSync as rmSync3, statSync, readdirSync } from "fs";
|
|
23993
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
23994
|
+
|
|
23995
|
+
// src/platform/local-state.ts
|
|
23996
|
+
import { createHash, randomUUID } from "crypto";
|
|
23997
|
+
import { linkSync, mkdirSync as mkdirSync3, realpathSync, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
23998
|
+
import { dirname as dirname2, resolve } from "path";
|
|
23999
|
+
function generateLocalStateKey(cwd, owner) {
|
|
24000
|
+
let directory = resolve(cwd);
|
|
24001
|
+
try {
|
|
24002
|
+
directory = realpathSync(directory);
|
|
24003
|
+
} catch {
|
|
24004
|
+
}
|
|
24005
|
+
return createHash("sha256").update(owner === void 0 ? directory : JSON.stringify([directory, owner])).digest("hex").slice(0, 32);
|
|
24006
|
+
}
|
|
24007
|
+
function persistLocalState(path2, text, overwrite = true) {
|
|
24008
|
+
mkdirSync3(dirname2(path2), { recursive: true });
|
|
24009
|
+
const temporary = `${path2}.${randomUUID()}.writing`;
|
|
24010
|
+
try {
|
|
24011
|
+
writeFileSync3(temporary, text, { flag: "wx", mode: 384 });
|
|
24012
|
+
if (overwrite) renameSync(temporary, path2);
|
|
24013
|
+
else {
|
|
24014
|
+
try {
|
|
24015
|
+
linkSync(temporary, path2);
|
|
24016
|
+
} catch (error62) {
|
|
24017
|
+
if (error62.code !== "EEXIST") throw error62;
|
|
24018
|
+
}
|
|
24019
|
+
}
|
|
24020
|
+
} finally {
|
|
24021
|
+
rmSync2(temporary, { force: true });
|
|
24022
|
+
}
|
|
24023
|
+
}
|
|
24024
|
+
|
|
24025
|
+
// src/platform/session-file.ts
|
|
24026
|
+
var buildSessionsDir = () => join3(dirname3(buildConfigPath()), "sessions");
|
|
23982
24027
|
var resolveRealCwd = (cwd) => {
|
|
23983
24028
|
try {
|
|
23984
|
-
return
|
|
24029
|
+
return realpathSync2(cwd);
|
|
23985
24030
|
} catch {
|
|
23986
24031
|
return cwd;
|
|
23987
24032
|
}
|
|
23988
24033
|
};
|
|
23989
|
-
var buildSessionPath = (cwd) => join3(buildSessionsDir(), `${
|
|
23990
|
-
function persistRememberedSession(cwd, sessionId) {
|
|
23991
|
-
|
|
23992
|
-
|
|
23993
|
-
else rmSync2(buildSessionPath(cwd), { force: true });
|
|
24034
|
+
var buildSessionPath = (cwd, conversation) => conversation ? join3(buildSessionsDir(), `${generateLocalStateKey(cwd, conversation)}.txt`) : join3(buildSessionsDir(), `${createHash2("sha256").update(resolveRealCwd(cwd)).digest("hex").slice(0, 16)}.txt`);
|
|
24035
|
+
function persistRememberedSession(cwd, sessionId, conversation) {
|
|
24036
|
+
if (sessionId) persistLocalState(buildSessionPath(cwd, conversation), sessionId);
|
|
24037
|
+
else rmSync3(buildSessionPath(cwd, conversation), { force: true });
|
|
23994
24038
|
}
|
|
23995
|
-
function fetchRememberedSession(cwd) {
|
|
23996
|
-
|
|
23997
|
-
|
|
24039
|
+
function fetchRememberedSession(cwd, conversation) {
|
|
24040
|
+
try {
|
|
24041
|
+
return readFileSync3(buildSessionPath(cwd, conversation), "utf8").trim() || null;
|
|
24042
|
+
} catch (error62) {
|
|
24043
|
+
if (error62.code === "ENOENT") return null;
|
|
24044
|
+
throw error62;
|
|
24045
|
+
}
|
|
23998
24046
|
}
|
|
23999
|
-
var buildConversationPath = (cwd) => join3(buildSessionsDir(), `${
|
|
24047
|
+
var buildConversationPath = (cwd) => join3(buildSessionsDir(), `${createHash2("sha256").update(resolveRealCwd(cwd)).digest("hex").slice(0, 16)}.conversation.json`);
|
|
24000
24048
|
function persistRememberedConversation(cwd, conversation) {
|
|
24001
|
-
|
|
24002
|
-
|
|
24049
|
+
const directory = `${buildConversationPath(cwd)}.d`;
|
|
24050
|
+
persistLocalState(join3(directory, `${generateLocalStateKey(cwd, conversation.id)}.json`), JSON.stringify(conversation));
|
|
24003
24051
|
}
|
|
24004
24052
|
function fetchRememberedConversation(cwd, notBefore) {
|
|
24005
24053
|
const path2 = buildConversationPath(cwd);
|
|
24054
|
+
const candidates = [];
|
|
24055
|
+
let paths;
|
|
24006
24056
|
try {
|
|
24007
|
-
|
|
24008
|
-
const parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
24009
|
-
return typeof parsed.id === "string" && parsed.id ? { id: parsed.id, harness: parsed.harness ?? "other" } : null;
|
|
24057
|
+
paths = readdirSync(`${path2}.d`).map((name) => join3(`${path2}.d`, name));
|
|
24010
24058
|
} catch {
|
|
24011
|
-
|
|
24059
|
+
paths = [path2];
|
|
24060
|
+
}
|
|
24061
|
+
for (const candidate of paths) {
|
|
24062
|
+
if (candidate.endsWith(".writing")) continue;
|
|
24063
|
+
try {
|
|
24064
|
+
if (statSync(candidate).mtimeMs < notBefore) continue;
|
|
24065
|
+
const parsed = JSON.parse(readFileSync3(candidate, "utf8"));
|
|
24066
|
+
const harness = AgentHarness.safeParse(parsed.harness ?? "other");
|
|
24067
|
+
if (typeof parsed.id === "string" && parsed.id && harness.success) candidates.push({ id: parsed.id, harness: harness.data });
|
|
24068
|
+
} catch {
|
|
24069
|
+
}
|
|
24012
24070
|
}
|
|
24071
|
+
const distinct = [...new Map(candidates.map((candidate) => [candidate.id, candidate])).values()];
|
|
24072
|
+
return distinct.length === 1 ? distinct[0] : null;
|
|
24013
24073
|
}
|
|
24014
24074
|
|
|
24075
|
+
// src/platform/agent-identity.ts
|
|
24076
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
24077
|
+
import { basename as basename3, join as join8, resolve as resolve4 } from "path";
|
|
24078
|
+
|
|
24079
|
+
// src/platform/git.ts
|
|
24080
|
+
import { execFileSync } from "child_process";
|
|
24081
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
24082
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
24083
|
+
import { basename, join as join4, resolve as resolve2 } from "path";
|
|
24084
|
+
|
|
24015
24085
|
// src/platform/errors.ts
|
|
24016
24086
|
var CliError = class extends Error {
|
|
24017
24087
|
constructor(message, hint, code = "error", exitCode = 1) {
|
|
@@ -24025,1086 +24095,1370 @@ var CliError = class extends Error {
|
|
|
24025
24095
|
exitCode;
|
|
24026
24096
|
};
|
|
24027
24097
|
|
|
24028
|
-
// src/platform/
|
|
24029
|
-
function
|
|
24030
|
-
async function sendApiRequest(method, path2, schema, body2) {
|
|
24031
|
-
try {
|
|
24032
|
-
assertApiResponseContract(method, path2, schema);
|
|
24033
|
-
} catch {
|
|
24034
|
-
throw new CliError("This request does not match the API contract", "Update slop to the latest version", "internal");
|
|
24035
|
-
}
|
|
24036
|
-
const headers = { accept: "application/json" };
|
|
24037
|
-
if (body2 !== void 0) headers["content-type"] = "application/json";
|
|
24038
|
-
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
24039
|
-
if (client.sessionId) headers["x-slop-session"] = client.sessionId;
|
|
24040
|
-
let response;
|
|
24041
|
-
try {
|
|
24042
|
-
response = await fetch(`${options.baseUrl}/api${path2}`, {
|
|
24043
|
-
method,
|
|
24044
|
-
headers,
|
|
24045
|
-
body: body2 === void 0 ? void 0 : JSON.stringify(body2),
|
|
24046
|
-
signal: AbortSignal.timeout(2e4)
|
|
24047
|
-
});
|
|
24048
|
-
} catch (error62) {
|
|
24049
|
-
const reason = error62 instanceof Error && error62.name === "TimeoutError" ? "timed out" : "is unreachable";
|
|
24050
|
-
throw new CliError(`${options.baseUrl} ${reason}`, void 0, "network");
|
|
24051
|
-
}
|
|
24052
|
-
const text = await response.text();
|
|
24053
|
-
if (response.ok) {
|
|
24054
|
-
try {
|
|
24055
|
-
return schema.parse(JSON.parse(text));
|
|
24056
|
-
} catch {
|
|
24057
|
-
throw new CliError("The server returned an incompatible response", "Update slop to the latest version", "internal");
|
|
24058
|
-
}
|
|
24059
|
-
}
|
|
24060
|
-
let parsed = null;
|
|
24061
|
-
try {
|
|
24062
|
-
parsed = ApiErrorBody.safeParse(JSON.parse(text));
|
|
24063
|
-
} catch {
|
|
24064
|
-
}
|
|
24065
|
-
if (parsed?.success) {
|
|
24066
|
-
const { code, message } = parsed.data.error;
|
|
24067
|
-
throw new CliError(message, code === "unauthenticated" ? "slop login" : void 0, code);
|
|
24068
|
-
}
|
|
24069
|
-
throw new CliError(`${method} ${path2} failed with HTTP ${response.status}`, void 0, "http");
|
|
24070
|
-
}
|
|
24071
|
-
const client = {
|
|
24072
|
-
baseUrl: options.baseUrl,
|
|
24073
|
-
sessionId: null,
|
|
24074
|
-
get: (path2, schema) => sendApiRequest("GET", path2, schema),
|
|
24075
|
-
post: (path2, schema, body2) => sendApiRequest("POST", path2, schema, body2 ?? {}),
|
|
24076
|
-
patch: (path2, schema, body2) => sendApiRequest("PATCH", path2, schema, body2),
|
|
24077
|
-
put: (path2, schema, body2) => sendApiRequest("PUT", path2, schema, body2),
|
|
24078
|
-
delete: (path2, schema) => sendApiRequest("DELETE", path2, schema)
|
|
24079
|
-
};
|
|
24080
|
-
return client;
|
|
24081
|
-
}
|
|
24082
|
-
function buildAuthedApiClient() {
|
|
24083
|
-
const session = resolveSession();
|
|
24084
|
-
if (!session.token) throw new CliError(`Not signed in to ${session.baseUrl}`, "slop login", "unauthenticated");
|
|
24085
|
-
const client = buildApiClient(session);
|
|
24086
|
-
client.sessionId = fetchRememberedSession(process.cwd());
|
|
24087
|
-
return client;
|
|
24088
|
-
}
|
|
24089
|
-
|
|
24090
|
-
// src/platform/machine.ts
|
|
24091
|
-
import { execFileSync } from "child_process";
|
|
24092
|
-
import { createHash as createHash2, randomUUID } from "crypto";
|
|
24093
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
24094
|
-
import { hostname as hostname3, platform, userInfo } from "os";
|
|
24095
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
24096
|
-
function runCommand(command, args) {
|
|
24098
|
+
// src/platform/git.ts
|
|
24099
|
+
function runGit(args, cwd = process.cwd()) {
|
|
24097
24100
|
try {
|
|
24098
|
-
return execFileSync(
|
|
24101
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
24099
24102
|
} catch {
|
|
24100
24103
|
return null;
|
|
24101
24104
|
}
|
|
24102
24105
|
}
|
|
24103
|
-
function
|
|
24104
|
-
|
|
24105
|
-
|
|
24106
|
-
|
|
24107
|
-
|
|
24108
|
-
|
|
24109
|
-
if (existsSync4(path2)) return readFileSync4(path2, "utf8").trim() || null;
|
|
24110
|
-
}
|
|
24111
|
-
return null;
|
|
24112
|
-
case "win32":
|
|
24113
|
-
return /MachineGuid\s+REG_SZ\s+(\S+)/.exec(
|
|
24114
|
-
runCommand("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]) ?? ""
|
|
24115
|
-
)?.[1] ?? null;
|
|
24116
|
-
default:
|
|
24117
|
-
return null;
|
|
24118
|
-
}
|
|
24106
|
+
function fetchRepo() {
|
|
24107
|
+
const commonDir = runGit(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
24108
|
+
if (!commonDir) return null;
|
|
24109
|
+
const worktree = runGit(["rev-parse", "--show-toplevel"]);
|
|
24110
|
+
const root = basename(commonDir) === ".git" ? resolve2(commonDir, "..") : worktree ?? commonDir;
|
|
24111
|
+
return { root, worktree: worktree ?? root, linkPath: join4(commonDir, "slop.json") };
|
|
24119
24112
|
}
|
|
24120
|
-
function
|
|
24121
|
-
const
|
|
24122
|
-
|
|
24123
|
-
const id = randomUUID();
|
|
24124
|
-
mkdirSync4(dirname3(path2), { recursive: true, mode: 448 });
|
|
24125
|
-
writeFileSync4(path2, `${id}
|
|
24126
|
-
`, { mode: 384 });
|
|
24127
|
-
return id;
|
|
24113
|
+
function fetchCurrentBranch(cwd = process.cwd()) {
|
|
24114
|
+
const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
24115
|
+
return branch && branch !== "HEAD" ? branch : null;
|
|
24128
24116
|
}
|
|
24129
|
-
function
|
|
24130
|
-
const
|
|
24131
|
-
|
|
24132
|
-
|
|
24133
|
-
name: hostname3().replace(/\.local$/, ""),
|
|
24134
|
-
platform: platform()
|
|
24135
|
-
};
|
|
24117
|
+
function requireRepo() {
|
|
24118
|
+
const repo = fetchRepo();
|
|
24119
|
+
if (!repo) throw new CliError("Not inside a git repository", "cd into your repository, or pass --project KEY", "no_repo");
|
|
24120
|
+
return repo;
|
|
24136
24121
|
}
|
|
24137
|
-
|
|
24138
|
-
|
|
24139
|
-
|
|
24140
|
-
|
|
24141
|
-
|
|
24142
|
-
}
|
|
24143
|
-
var isJsonMode = () => jsonMode;
|
|
24144
|
-
function emitOutput(value, human) {
|
|
24145
|
-
process.stdout.write(jsonMode ? `${JSON.stringify(value, null, 2)}
|
|
24146
|
-
` : `${human()}
|
|
24147
|
-
`);
|
|
24122
|
+
function fetchRepoLink(repo) {
|
|
24123
|
+
try {
|
|
24124
|
+
return JSON.parse(readFileSync4(repo.linkPath, "utf8"));
|
|
24125
|
+
} catch {
|
|
24126
|
+
return null;
|
|
24127
|
+
}
|
|
24148
24128
|
}
|
|
24149
|
-
function
|
|
24150
|
-
|
|
24151
|
-
const message = error62 instanceof Error ? error62.message : String(error62);
|
|
24152
|
-
if (jsonMode) {
|
|
24153
|
-
process.stderr.write(`${JSON.stringify({ error: { code: cli?.code ?? "error", message, hint: cli?.hint } })}
|
|
24154
|
-
`);
|
|
24155
|
-
} else {
|
|
24156
|
-
process.stderr.write(`error: ${message}
|
|
24157
|
-
`);
|
|
24158
|
-
if (cli?.hint) process.stderr.write(` try: ${cli.hint}
|
|
24129
|
+
function persistRepoLink(repo, link) {
|
|
24130
|
+
writeFileSync4(repo.linkPath, `${JSON.stringify(link, null, 2)}
|
|
24159
24131
|
`);
|
|
24160
|
-
}
|
|
24161
|
-
process.exit(cli?.exitCode ?? 1);
|
|
24162
24132
|
}
|
|
24163
|
-
function
|
|
24164
|
-
|
|
24165
|
-
for (const row of rows) row.forEach((cell, index) => widths[index] = Math.max(widths[index] ?? 0, cell.length));
|
|
24166
|
-
return rows.map((row) => row.map((cell, index) => index === row.length - 1 ? cell : cell.padEnd(widths[index])).join(" ")).join("\n");
|
|
24133
|
+
function deleteTaskLink(repo) {
|
|
24134
|
+
if (existsSync3(repo.linkPath)) rmSync4(repo.linkPath);
|
|
24167
24135
|
}
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24175
|
-
];
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
|
|
24179
|
-
|
|
24180
|
-
|
|
24181
|
-
|
|
24182
|
-
|
|
24183
|
-
|
|
24184
|
-
if (task.items.length > 0) {
|
|
24185
|
-
lines.push("", `plan ${task.progress.done}/${task.progress.total} done`);
|
|
24186
|
-
task.items.forEach((item, index) => lines.push(` ${index + 1}. [${item.done ? "x" : " "}] ${item.title}${item.note ? ` (${item.note.split("\n")[0]})` : ""}`));
|
|
24136
|
+
function normalizeRemoteUrl(url2) {
|
|
24137
|
+
const scp = /^[^@/]+@([^:/]+):(.+)$/.exec(url2);
|
|
24138
|
+
const bare = scp ? `${scp[1]}/${scp[2]}` : url2.replace(/^[a-z+]+:\/\//i, "").replace(/^[^@/]+@/, "");
|
|
24139
|
+
return bare.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
24140
|
+
}
|
|
24141
|
+
function buildRepoDescription(repo, existing) {
|
|
24142
|
+
const roots = runGit(["rev-list", "--max-parents=0", "HEAD"], repo.root);
|
|
24143
|
+
const remote = runGit(["remote", "get-url", "origin"], repo.root);
|
|
24144
|
+
const remoteUrl = remote ? normalizeRemoteUrl(remote) : null;
|
|
24145
|
+
let fingerprint;
|
|
24146
|
+
let localRepoId;
|
|
24147
|
+
if (roots) {
|
|
24148
|
+
fingerprint = createHash3("sha256").update(roots.split("\n").sort().join("\n")).digest("hex");
|
|
24149
|
+
} else {
|
|
24150
|
+
localRepoId = existing?.localRepoId ?? randomUUID2();
|
|
24151
|
+
fingerprint = `local:${localRepoId}`;
|
|
24187
24152
|
}
|
|
24188
|
-
|
|
24189
|
-
|
|
24190
|
-
|
|
24153
|
+
return {
|
|
24154
|
+
info: { fingerprint, name: remoteUrl?.split("/").slice(-1)[0] ?? basename(repo.root), remoteUrl, rootPath: repo.root },
|
|
24155
|
+
localRepoId
|
|
24156
|
+
};
|
|
24191
24157
|
}
|
|
24192
24158
|
|
|
24193
|
-
// src/
|
|
24194
|
-
|
|
24195
|
-
|
|
24159
|
+
// src/platform/harness.ts
|
|
24160
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
24161
|
+
import { closeSync, openSync, readdirSync as readdirSync3, readSync, readlinkSync, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
|
|
24162
|
+
import { basename as basename2, join as join7 } from "path";
|
|
24163
|
+
|
|
24164
|
+
// src/platform/harness-wake.ts
|
|
24165
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
24166
|
+
import { join as join6 } from "path";
|
|
24167
|
+
import { connect } from "net";
|
|
24168
|
+
import { spawn } from "child_process";
|
|
24169
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
24170
|
+
|
|
24171
|
+
// src/platform/homes.ts
|
|
24172
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
24173
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
24174
|
+
import { homedir as homedir3 } from "os";
|
|
24175
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
24176
|
+
var deduplicateStrings = (paths) => [...new Set(paths.map((path2) => resolve3(path2)))];
|
|
24177
|
+
function fetchAncestorEnvironments() {
|
|
24178
|
+
if (process.platform === "win32") return [];
|
|
24179
|
+
const seen = [];
|
|
24196
24180
|
try {
|
|
24197
|
-
|
|
24198
|
-
|
|
24181
|
+
if (process.platform === "linux") {
|
|
24182
|
+
let pid2 = process.ppid;
|
|
24183
|
+
for (let hop = 0; hop < 6 && pid2 > 1; hop += 1) {
|
|
24184
|
+
seen.push(readFileSync5(`/proc/${pid2}/environ`, "latin1").replace(/\0/g, " "));
|
|
24185
|
+
const stat = readFileSync5(`/proc/${pid2}/stat`, "utf8");
|
|
24186
|
+
pid2 = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24187
|
+
}
|
|
24188
|
+
for (const entry of readdirSync2("/proc")) {
|
|
24189
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
24190
|
+
try {
|
|
24191
|
+
seen.push(readFileSync5(`/proc/${entry}/environ`, "latin1").replace(/\0/g, " "));
|
|
24192
|
+
} catch {
|
|
24193
|
+
}
|
|
24194
|
+
}
|
|
24195
|
+
return seen;
|
|
24196
|
+
}
|
|
24197
|
+
const runPs = (args) => execFileSync2("ps", args, { encoding: "utf8", maxBuffer: 64 << 20, stdio: ["ignore", "pipe", "ignore"] });
|
|
24198
|
+
let pid = process.ppid;
|
|
24199
|
+
for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
|
|
24200
|
+
seen.push(runPs(["eww", "-o", "command=", "-p", String(pid)]));
|
|
24201
|
+
pid = Number(runPs(["-o", "ppid=", "-p", String(pid)]).trim());
|
|
24202
|
+
}
|
|
24203
|
+
seen.push(runPs(["axeww", "-o", "command="]));
|
|
24199
24204
|
} catch {
|
|
24200
24205
|
}
|
|
24206
|
+
return seen;
|
|
24201
24207
|
}
|
|
24202
|
-
var
|
|
24203
|
-
|
|
24204
|
-
|
|
24205
|
-
|
|
24206
|
-
|
|
24207
|
-
|
|
24208
|
-
|
|
24209
|
-
const remote = isHeadless();
|
|
24210
|
-
process.stderr.write(`
|
|
24211
|
-
Approve this login in a browser${remote ? " on any device (this machine has no browser; your laptop or phone is fine)" : ""}:
|
|
24212
|
-
${started.verifyUrl}
|
|
24213
|
-
|
|
24214
|
-
Confirm the code matches: ${started.userCode}
|
|
24215
|
-
|
|
24216
|
-
`);
|
|
24217
|
-
if (options.browser && !remote) openBrowser(started.verifyUrl);
|
|
24218
|
-
const deadline = Date.now() + started.expiresIn * 1e3;
|
|
24219
|
-
while (Date.now() < deadline) {
|
|
24220
|
-
await waitForMilliseconds(started.interval * 1e3);
|
|
24221
|
-
const poll = await api.post("/cli/auth/poll", ApiResponses.PollCliAuthResult, { deviceCode: started.deviceCode });
|
|
24222
|
-
if (poll.status === "pending") continue;
|
|
24223
|
-
if (poll.status === "denied") throw new CliError("The login was denied in the web app");
|
|
24224
|
-
if (poll.status === "expired") break;
|
|
24225
|
-
const me = await buildApiClient({ baseUrl, token: poll.token }).get("/me", ApiResponses.Me);
|
|
24226
|
-
persistConfig({ baseUrl, token: poll.token, email: me.user.email });
|
|
24227
|
-
const skills = installSkills();
|
|
24228
|
-
emitOutput({ ok: true, user: me.user, machine: me.machine, baseUrl, skills }, () => `Signed in as ${me.user.email} on ${me.machine?.name ?? "this machine"}.${skills.length ? `
|
|
24229
|
-
Agent skills installed in ~/.claude/skills and ~/.agents/skills. Next: slop mcp install (the board as tools for Claude Code and Codex).` : ""}`);
|
|
24230
|
-
return;
|
|
24231
|
-
}
|
|
24232
|
-
throw new CliError("The login request expired before it was approved", "slop login");
|
|
24233
|
-
});
|
|
24234
|
-
program3.command("logout").description("Sign this machine out and revoke its token.").action(async () => {
|
|
24235
|
-
const session = resolveSession();
|
|
24236
|
-
if (session.token) await buildApiClient(session).post("/cli/auth/logout", ApiResponses.Ok).catch(() => {
|
|
24237
|
-
});
|
|
24238
|
-
deleteConfig();
|
|
24239
|
-
emitOutput({ ok: true }, () => "Signed out.");
|
|
24240
|
-
});
|
|
24241
|
-
program3.command("whoami").description("Show the signed-in user, machine and instance.").action(async () => {
|
|
24242
|
-
const api = buildAuthedApiClient();
|
|
24243
|
-
const me = await api.get("/me", ApiResponses.Me);
|
|
24244
|
-
emitOutput({ ...me, baseUrl: api.baseUrl }, () => `${me.user.name} <${me.user.email}>
|
|
24245
|
-
machine: ${me.machine?.name ?? "-"}
|
|
24246
|
-
instance: ${api.baseUrl}`);
|
|
24247
|
-
});
|
|
24248
|
-
}
|
|
24249
|
-
|
|
24250
|
-
// src/commands/guide.ts
|
|
24251
|
-
var AGENT_GUIDE = `## Task tracking
|
|
24252
|
-
|
|
24253
|
-
This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to keep it current.
|
|
24254
|
-
|
|
24255
|
-
- When you find work that should be tracked (a bug, a follow-up, a TODO you are not doing now), create a task:
|
|
24256
|
-
\`slop task create "Short imperative title" -d "What, where, why" -s <low|medium|high|critical>\`
|
|
24257
|
-
- Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
|
|
24258
|
-
- When it is finished and verified: \`slop task done <KEY>\`.
|
|
24259
|
-
- Descriptions are Markdown and are shown in full on the board. Write real ones: context, file paths, what done means. Pipe long ones in with \`-d -\`.
|
|
24260
|
-
- Agents use the six slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, each with an \`action\`. You have a name (session start tells you; \`session who\` lists everyone's); \`say post\` with \`to\` reaches one agent by name. Messages for you reach you on their own: idle, as a turn of yours; mid-turn, with your next tool result. Messages are letters, not chat: the other agent reads yours when it gets to it, its reply may predate your latest, so write once and completely, answer a pile of messages with one reply, and reply only when it changes something (never to acknowledge). Do not wait for replies; end your turn. The \`slop\` command is for people and scripts. Talk through threads: \`slop task ask <KEY> "\u2026"\` reaches whoever is live on that task, \`slop thread reply <id> "\u2026"\` answers, \`slop threads\` lists them; general threads are for people and for context. Method: \`slop skill slop-sessions\`.
|
|
24261
|
-
- Work that outlasts one session gets a plan on its task: \`slop task plan <KEY> -\` (one item per line), then \`slop task item note|done <KEY> <n>\` as you go; \`slop task items <KEY>\` shows where you are. \`slop skill slop-planning\` is the method; \`slop skills install\` puts the skills in this repository.
|
|
24262
|
-
- Everything is editable: \`slop task update <KEY> --title \u2026 --severity \u2026 --status \u2026 --assignee \u2026\`. Assign with \`slop task assign <KEY> <email|me|none>\`; see who exists with \`slop members\`. \`slop task list --mine\` shows your user's tasks.
|
|
24263
|
-
- Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
|
|
24264
|
-
- When you open a pull request for a task, link it: \`slop task link <KEY> <pr-url>\`.
|
|
24265
|
-
- Columns are the project's own: \`slop statuses\` lists them; \`slop task move <KEY> "<column>"\` takes a key or label. \`slop task start\` / \`slop task done\` go to the first "in progress" / "done" column. Finished work is archived from the board (\`slop task archive <KEY>\`), not deleted. Do not delete tasks you did not create.
|
|
24266
|
-
`;
|
|
24267
|
-
function registerGuideCommand(program3) {
|
|
24268
|
-
program3.command("guide").description("Print a snippet for AGENTS.md / CLAUDE.md that teaches an agent to use this CLI.").action(() => emitOutput({ markdown: AGENT_GUIDE }, () => AGENT_GUIDE));
|
|
24208
|
+
var environments = null;
|
|
24209
|
+
function findHomesInUse(variable) {
|
|
24210
|
+
environments ??= fetchAncestorEnvironments();
|
|
24211
|
+
const found = [];
|
|
24212
|
+
const pattern = new RegExp(`(?:^|\\s)${variable}=(\\S+)`, "g");
|
|
24213
|
+
for (const text of environments) for (const match of text.matchAll(pattern)) found.push(match[1] ?? "");
|
|
24214
|
+
return found.filter(Boolean);
|
|
24269
24215
|
}
|
|
24270
|
-
|
|
24271
|
-
// src/platform/git.ts
|
|
24272
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
24273
|
-
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
24274
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
24275
|
-
import { basename, join as join5, resolve } from "path";
|
|
24276
|
-
function runGit(args, cwd = process.cwd()) {
|
|
24216
|
+
var findDirectoriesNamed = (root, prefix) => {
|
|
24277
24217
|
try {
|
|
24278
|
-
return
|
|
24218
|
+
return readdirSync2(root, { withFileTypes: true }).filter((entry) => entry.name.startsWith(prefix) && (entry.isDirectory() || entry.isSymbolicLink())).map((entry) => join5(root, entry.name)).filter((path2) => {
|
|
24219
|
+
try {
|
|
24220
|
+
return statSync2(path2).isDirectory();
|
|
24221
|
+
} catch {
|
|
24222
|
+
return false;
|
|
24223
|
+
}
|
|
24224
|
+
});
|
|
24279
24225
|
} catch {
|
|
24280
|
-
return
|
|
24226
|
+
return [];
|
|
24281
24227
|
}
|
|
24228
|
+
};
|
|
24229
|
+
var hasAnyEntry = (dir, files) => files.some((file2) => existsSync4(join5(dir, file2)));
|
|
24230
|
+
function fetchClaudeHomes(root = homedir3(), inUse = findHomesInUse) {
|
|
24231
|
+
if (process.env.SLOP_CLAUDE_HOME) return [process.env.SLOP_CLAUDE_HOME];
|
|
24232
|
+
const named = process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [];
|
|
24233
|
+
return deduplicateStrings([join5(root, ".claude"), ...named, ...inUse("CLAUDE_CONFIG_DIR"), ...findDirectoriesNamed(root, ".claude").filter((dir) => hasAnyEntry(dir, ["settings.json", ".claude.json", "sessions"]))]);
|
|
24282
24234
|
}
|
|
24283
|
-
function
|
|
24284
|
-
|
|
24285
|
-
|
|
24286
|
-
|
|
24287
|
-
const root = basename(commonDir) === ".git" ? resolve(commonDir, "..") : worktree ?? commonDir;
|
|
24288
|
-
return { root, worktree: worktree ?? root, linkPath: join5(commonDir, "slop.json") };
|
|
24289
|
-
}
|
|
24290
|
-
function fetchCurrentBranch(cwd = process.cwd()) {
|
|
24291
|
-
const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
24292
|
-
return branch && branch !== "HEAD" ? branch : null;
|
|
24293
|
-
}
|
|
24294
|
-
function requireRepo() {
|
|
24295
|
-
const repo = fetchRepo();
|
|
24296
|
-
if (!repo) throw new CliError("Not inside a git repository", "cd into your repository, or pass --project KEY", "no_repo");
|
|
24297
|
-
return repo;
|
|
24235
|
+
function fetchCodexHomes(root = homedir3(), inUse = findHomesInUse) {
|
|
24236
|
+
if (process.env.SLOP_CODEX_HOME) return [process.env.SLOP_CODEX_HOME];
|
|
24237
|
+
const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
|
|
24238
|
+
return deduplicateStrings([join5(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...findDirectoriesNamed(root, ".codex").filter((dir) => hasAnyEntry(dir, ["config.toml", "auth.json", "sessions"]))]);
|
|
24298
24239
|
}
|
|
24299
|
-
function
|
|
24240
|
+
function resolveOwnHarnessHome(variable) {
|
|
24241
|
+
if (process.platform === "win32") return null;
|
|
24300
24242
|
try {
|
|
24301
|
-
|
|
24243
|
+
let pid = process.ppid;
|
|
24244
|
+
for (let hop = 0; hop < 6 && pid > 1; hop += 1) {
|
|
24245
|
+
const text = process.platform === "linux" ? readFileSync5(`/proc/${pid}/environ`, "latin1").replace(/\0/g, " ") : execFileSync2("ps", ["eww", "-o", "command=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24246
|
+
const match = new RegExp(`(?:^|\\s)${variable}=(\\S+)`).exec(text);
|
|
24247
|
+
if (match?.[1]) return resolve3(match[1]);
|
|
24248
|
+
if (process.platform === "linux") {
|
|
24249
|
+
const stat = readFileSync5(`/proc/${pid}/stat`, "utf8");
|
|
24250
|
+
pid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24251
|
+
} else pid = Number(execFileSync2("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
24252
|
+
}
|
|
24302
24253
|
} catch {
|
|
24303
|
-
return null;
|
|
24304
|
-
}
|
|
24305
|
-
}
|
|
24306
|
-
function persistRepoLink(repo, link) {
|
|
24307
|
-
writeFileSync5(repo.linkPath, `${JSON.stringify(link, null, 2)}
|
|
24308
|
-
`);
|
|
24309
|
-
}
|
|
24310
|
-
function deleteTaskLink(repo) {
|
|
24311
|
-
if (existsSync5(repo.linkPath)) rmSync3(repo.linkPath);
|
|
24312
|
-
}
|
|
24313
|
-
function normalizeRemoteUrl(url2) {
|
|
24314
|
-
const scp = /^[^@/]+@([^:/]+):(.+)$/.exec(url2);
|
|
24315
|
-
const bare = scp ? `${scp[1]}/${scp[2]}` : url2.replace(/^[a-z+]+:\/\//i, "").replace(/^[^@/]+@/, "");
|
|
24316
|
-
return bare.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
24317
|
-
}
|
|
24318
|
-
function buildRepoDescription(repo, existing) {
|
|
24319
|
-
const roots = runGit(["rev-list", "--max-parents=0", "HEAD"], repo.root);
|
|
24320
|
-
const remote = runGit(["remote", "get-url", "origin"], repo.root);
|
|
24321
|
-
const remoteUrl = remote ? normalizeRemoteUrl(remote) : null;
|
|
24322
|
-
let fingerprint;
|
|
24323
|
-
let localRepoId;
|
|
24324
|
-
if (roots) {
|
|
24325
|
-
fingerprint = createHash3("sha256").update(roots.split("\n").sort().join("\n")).digest("hex");
|
|
24326
|
-
} else {
|
|
24327
|
-
localRepoId = existing?.localRepoId ?? randomUUID2();
|
|
24328
|
-
fingerprint = `local:${localRepoId}`;
|
|
24329
24254
|
}
|
|
24330
|
-
return
|
|
24331
|
-
info: { fingerprint, name: remoteUrl?.split("/").slice(-1)[0] ?? basename(repo.root), remoteUrl, rootPath: repo.root },
|
|
24332
|
-
localRepoId
|
|
24333
|
-
};
|
|
24255
|
+
return null;
|
|
24334
24256
|
}
|
|
24335
24257
|
|
|
24336
|
-
// src/
|
|
24337
|
-
|
|
24338
|
-
const
|
|
24339
|
-
|
|
24340
|
-
|
|
24341
|
-
|
|
24342
|
-
|
|
24343
|
-
|
|
24258
|
+
// src/platform/harness-wake.ts
|
|
24259
|
+
function fetchHarnessRegistration(pid = process.ppid) {
|
|
24260
|
+
for (const home of fetchClaudeHomes()) {
|
|
24261
|
+
const path2 = join6(home, "sessions", `${pid}.json`);
|
|
24262
|
+
if (!existsSync5(path2)) continue;
|
|
24263
|
+
try {
|
|
24264
|
+
return JSON.parse(readFileSync6(path2, "utf8"));
|
|
24265
|
+
} catch {
|
|
24266
|
+
return null;
|
|
24267
|
+
}
|
|
24344
24268
|
}
|
|
24345
|
-
|
|
24269
|
+
return null;
|
|
24346
24270
|
}
|
|
24347
|
-
|
|
24348
|
-
|
|
24349
|
-
|
|
24350
|
-
|
|
24351
|
-
|
|
24352
|
-
if (link.baseUrl !== api.baseUrl) {
|
|
24353
|
-
throw new CliError(`This repository is linked to ${link.baseUrl}, but you are signed in to ${api.baseUrl}`, `slop login --url ${link.baseUrl}`);
|
|
24271
|
+
function resolveClaudeSession(pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid) {
|
|
24272
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
24273
|
+
if (sessionId) {
|
|
24274
|
+
const registration2 = fetchHarnessRegistration(pid);
|
|
24275
|
+
return { sessionId, socketPath: process.env.CLAUDE_CODE_MESSAGING_SOCKET || (registration2?.sessionId === sessionId ? registration2.messagingSocketPath : null) || null };
|
|
24354
24276
|
}
|
|
24355
|
-
|
|
24356
|
-
}
|
|
24357
|
-
async function listMembers(api, projectId) {
|
|
24358
|
-
const project = await api.get(`/projects/${projectId}`, ApiResponses.Project);
|
|
24359
|
-
return (await api.get(`/orgs/${project.orgId}/members`, ApiResponses.MembersResult)).members;
|
|
24277
|
+
const registration = fetchHarnessRegistration(pid);
|
|
24278
|
+
return registration?.sessionId ? { sessionId: registration.sessionId, socketPath: registration.messagingSocketPath ?? null } : null;
|
|
24360
24279
|
}
|
|
24361
|
-
function
|
|
24362
|
-
|
|
24363
|
-
|
|
24364
|
-
|
|
24365
|
-
|
|
24366
|
-
|
|
24367
|
-
|
|
24368
|
-
|
|
24369
|
-
|
|
24370
|
-
const api = buildAuthedApiClient();
|
|
24371
|
-
const members2 = await listMembers(api, (await resolveProject(api, options.project)).id);
|
|
24372
|
-
emitOutput(members2, () => formatTable([["NAME", "EMAIL", "ROLE"], ...members2.map((member) => [member.name, member.email, member.role])]));
|
|
24373
|
-
});
|
|
24374
|
-
program3.command("statuses").alias("columns").description("The linked project's columns, in board order.").option("-p, --project <key>", "project key (default: the linked project)").action(async (options) => {
|
|
24375
|
-
const api = buildAuthedApiClient();
|
|
24376
|
-
const target = await resolveProject(api, options.project);
|
|
24377
|
-
const statuses = (await api.get(`/projects/${target.id}`, ApiResponses.Project)).statuses;
|
|
24378
|
-
emitOutput(statuses, () => formatTable([["KEY", "LABEL", "CATEGORY"], ...statuses.map((status2) => [status2.key, status2.label, status2.category])]));
|
|
24379
|
-
});
|
|
24380
|
-
const project = program3.command("project").description("Manage projects.");
|
|
24381
|
-
const status = project.command("status").alias("column").description("Add, rename, reorder or remove the project's columns.");
|
|
24382
|
-
const fetchResolvedProject = async (api, key) => api.get(`/projects/${(await resolveProject(api, key)).id}`, ApiResponses.Project);
|
|
24383
|
-
const requireStatusByRef = (target, ref) => {
|
|
24384
|
-
const found = findStatusByKeyOrLabel(target.statuses, ref);
|
|
24385
|
-
if (!found) throw new CliError(`"${ref}" is not a column of ${target.key}`, "slop statuses");
|
|
24386
|
-
return found;
|
|
24280
|
+
function wakeHarness(text, pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid, expectedSessionId) {
|
|
24281
|
+
const session = resolveClaudeSession(pid);
|
|
24282
|
+
if (!session?.socketPath || expectedSessionId && session.sessionId !== expectedSessionId) return Promise.resolve(false);
|
|
24283
|
+
const frame = {
|
|
24284
|
+
uuid: randomUUID3(),
|
|
24285
|
+
session_id: session.sessionId,
|
|
24286
|
+
type: "user",
|
|
24287
|
+
parent_tool_use_id: null,
|
|
24288
|
+
message: { role: "user", content: text }
|
|
24387
24289
|
};
|
|
24388
|
-
|
|
24389
|
-
|
|
24390
|
-
const
|
|
24391
|
-
|
|
24392
|
-
|
|
24393
|
-
|
|
24394
|
-
|
|
24395
|
-
|
|
24396
|
-
|
|
24397
|
-
|
|
24398
|
-
|
|
24399
|
-
|
|
24400
|
-
|
|
24401
|
-
});
|
|
24402
|
-
status.command("remove <column>").description("Remove a column; its tasks move to --to.").option("-p, --project <key>").option("--to <column>", "where the column's tasks go").action(async (ref, options) => {
|
|
24403
|
-
const api = buildAuthedApiClient();
|
|
24404
|
-
const target = await fetchResolvedProject(api, options.project);
|
|
24405
|
-
const moveTo = options.to ? requireStatusByRef(target, options.to).key : null;
|
|
24406
|
-
const statuses = await api.delete(`/projects/${target.id}/statuses/${requireStatusByRef(target, ref).id}${moveTo ? `?moveTo=${encodeURIComponent(moveTo)}` : ""}`, ApiResponses.ProjectStatuses);
|
|
24407
|
-
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24408
|
-
});
|
|
24409
|
-
status.command("order <columns...>").description("Set the board order: every column, by key or label.").option("-p, --project <key>").action(async (refs, options) => {
|
|
24410
|
-
const api = buildAuthedApiClient();
|
|
24411
|
-
const target = await fetchResolvedProject(api, options.project);
|
|
24412
|
-
const statuses = await api.put(`/projects/${target.id}/statuses/order`, ApiResponses.ProjectStatuses, { ids: refs.map((ref) => requireStatusByRef(target, ref).id) });
|
|
24413
|
-
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24290
|
+
return new Promise((resolve6) => {
|
|
24291
|
+
const socket = connect({ path: session.socketPath });
|
|
24292
|
+
const finishWake = (ok) => {
|
|
24293
|
+
clearTimeout(timer);
|
|
24294
|
+
socket.destroy();
|
|
24295
|
+
resolve6(ok);
|
|
24296
|
+
};
|
|
24297
|
+
const timer = setTimeout(() => finishWake(false), 3e3);
|
|
24298
|
+
socket.on("connect", () => {
|
|
24299
|
+
socket.write(`${JSON.stringify(frame)}
|
|
24300
|
+
`, (error62) => error62 ? finishWake(false) : setTimeout(() => finishWake(true), 200));
|
|
24301
|
+
});
|
|
24302
|
+
socket.on("error", () => finishWake(false));
|
|
24414
24303
|
});
|
|
24415
|
-
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
24419
|
-
|
|
24304
|
+
}
|
|
24305
|
+
function wakeCodex(text, threadId, options = {}) {
|
|
24306
|
+
const endpoint = process.env.SLOP_CODEX_APP_SERVER;
|
|
24307
|
+
if (endpoint) return wakeCodexOverWs(text, threadId, endpoint);
|
|
24308
|
+
return new Promise((resolve6) => {
|
|
24309
|
+
let child;
|
|
24310
|
+
try {
|
|
24311
|
+
child = spawn(options.binary ?? "codex", ["queue", "--thread", threadId, "--message", text], {
|
|
24312
|
+
env: options.codexHome ? { ...process.env, CODEX_HOME: options.codexHome } : process.env,
|
|
24313
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
24314
|
+
});
|
|
24315
|
+
} catch {
|
|
24316
|
+
resolve6(false);
|
|
24317
|
+
return;
|
|
24420
24318
|
}
|
|
24421
|
-
const
|
|
24422
|
-
|
|
24423
|
-
|
|
24424
|
-
|
|
24425
|
-
|
|
24426
|
-
|
|
24427
|
-
|
|
24428
|
-
|
|
24429
|
-
|
|
24430
|
-
|
|
24431
|
-
|
|
24432
|
-
emitOutput(project2, () => `Created ${project2.key} ${project2.name}
|
|
24433
|
-
Link this repository to it: slop link ${project2.key}`);
|
|
24434
|
-
});
|
|
24435
|
-
program3.command("link <project>").description("Bind this git repository (on this machine) to a project, by key.").action(async (projectKey) => {
|
|
24436
|
-
const api = buildAuthedApiClient();
|
|
24437
|
-
const repo = requireRepo();
|
|
24438
|
-
const project2 = await fetchProjectByKey(api, projectKey);
|
|
24439
|
-
const { info, localRepoId } = buildRepoDescription(repo, fetchRepoLink(repo));
|
|
24440
|
-
const binding = await api.post(`/projects/${project2.id}/bindings`, ApiResponses.Binding, { repo: info });
|
|
24441
|
-
persistRepoLink(repo, { baseUrl: api.baseUrl, projectId: project2.id, projectKey: project2.key, projectName: project2.name, bindingId: binding.id, localRepoId });
|
|
24442
|
-
emitOutput({ project: project2, binding }, () => `Linked ${info.name} on ${binding.machine.name} to ${project2.key} ${project2.name}
|
|
24443
|
-
Try: slop task create "My first task"`);
|
|
24444
|
-
});
|
|
24445
|
-
program3.command("unlink").description("Remove this repository's binding.").action(async () => {
|
|
24446
|
-
const repo = requireRepo();
|
|
24447
|
-
const link = fetchRepoLink(repo);
|
|
24448
|
-
if (!link) throw new CliError("This repository is not linked", void 0, "not_linked");
|
|
24449
|
-
await buildAuthedApiClient().delete(`/projects/${link.projectId}/bindings/${link.bindingId}`, ApiResponses.Ok).catch(() => {
|
|
24319
|
+
const timer = setTimeout(() => {
|
|
24320
|
+
child.kill();
|
|
24321
|
+
resolve6(false);
|
|
24322
|
+
}, 15e3);
|
|
24323
|
+
child.on("error", () => {
|
|
24324
|
+
clearTimeout(timer);
|
|
24325
|
+
resolve6(false);
|
|
24326
|
+
});
|
|
24327
|
+
child.on("exit", (code) => {
|
|
24328
|
+
clearTimeout(timer);
|
|
24329
|
+
resolve6(code === 0);
|
|
24450
24330
|
});
|
|
24451
|
-
deleteTaskLink(repo);
|
|
24452
|
-
emitOutput({ ok: true }, () => `Unlinked from ${link.projectKey}.`);
|
|
24453
|
-
});
|
|
24454
|
-
program3.command("status").description("Show what this repository is linked to.").action(async () => {
|
|
24455
|
-
const repo = fetchRepo();
|
|
24456
|
-
const link = repo ? fetchRepoLink(repo) : null;
|
|
24457
|
-
const where = !repo ? "" : repo.worktree === repo.root ? repo.root : `${repo.worktree} (worktree of ${repo.root})`;
|
|
24458
|
-
emitOutput(
|
|
24459
|
-
{ repo: repo?.root ?? null, worktree: repo?.worktree ?? null, link },
|
|
24460
|
-
() => !repo ? "Not inside a git repository." : !link ? `${where}
|
|
24461
|
-
not linked. Run: slop link <PROJECT_KEY>` : `${where}
|
|
24462
|
-
linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
24463
|
-
);
|
|
24464
24331
|
});
|
|
24465
24332
|
}
|
|
24466
|
-
|
|
24467
|
-
|
|
24468
|
-
|
|
24469
|
-
|
|
24470
|
-
|
|
24471
|
-
|
|
24472
|
-
|
|
24473
|
-
|
|
24474
|
-
|
|
24475
|
-
|
|
24476
|
-
|
|
24477
|
-
|
|
24478
|
-
|
|
24479
|
-
|
|
24480
|
-
|
|
24481
|
-
|
|
24482
|
-
|
|
24483
|
-
|
|
24484
|
-
|
|
24485
|
-
|
|
24333
|
+
function wakeCodexOverWs(text, threadId, endpoint) {
|
|
24334
|
+
return new Promise((resolve6) => {
|
|
24335
|
+
let socket;
|
|
24336
|
+
try {
|
|
24337
|
+
socket = new WebSocket(endpoint);
|
|
24338
|
+
} catch {
|
|
24339
|
+
resolve6(false);
|
|
24340
|
+
return;
|
|
24341
|
+
}
|
|
24342
|
+
let finished = false;
|
|
24343
|
+
let nextId = 1;
|
|
24344
|
+
const pending = /* @__PURE__ */ new Map();
|
|
24345
|
+
const finishWake = (ok) => {
|
|
24346
|
+
if (finished) return;
|
|
24347
|
+
finished = true;
|
|
24348
|
+
clearTimeout(timer);
|
|
24349
|
+
for (const reply of pending.values()) reply({ error: "connection ended" });
|
|
24350
|
+
pending.clear();
|
|
24351
|
+
try {
|
|
24352
|
+
socket.close();
|
|
24353
|
+
} catch {
|
|
24354
|
+
}
|
|
24355
|
+
resolve6(ok);
|
|
24486
24356
|
};
|
|
24487
|
-
|
|
24488
|
-
|
|
24489
|
-
|
|
24490
|
-
|
|
24491
|
-
api.sessionId = push.session.id;
|
|
24492
|
-
persistRememberedSession(process.cwd(), push.session.id);
|
|
24493
|
-
options.onHello?.(push.session);
|
|
24357
|
+
const timer = setTimeout(() => finishWake(false), 8e3);
|
|
24358
|
+
const callCodexMethod = (method, params) => new Promise((ok, fail) => {
|
|
24359
|
+
if (finished) {
|
|
24360
|
+
fail(new Error("connection ended"));
|
|
24494
24361
|
return;
|
|
24495
24362
|
}
|
|
24496
|
-
|
|
24497
|
-
|
|
24498
|
-
|
|
24363
|
+
const id = nextId++;
|
|
24364
|
+
pending.set(id, (message) => message.error ? fail(new Error(method)) : ok(message.result));
|
|
24365
|
+
socket.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
24366
|
+
});
|
|
24367
|
+
socket.addEventListener("error", () => finishWake(false));
|
|
24368
|
+
socket.addEventListener("close", () => finishWake(false));
|
|
24369
|
+
socket.addEventListener("message", (event) => {
|
|
24370
|
+
if (finished) return;
|
|
24371
|
+
try {
|
|
24372
|
+
const message = JSON.parse(String(event.data));
|
|
24373
|
+
if (typeof message.id === "number") {
|
|
24374
|
+
const reply = pending.get(message.id);
|
|
24375
|
+
pending.delete(message.id);
|
|
24376
|
+
reply?.(message);
|
|
24377
|
+
}
|
|
24378
|
+
} catch {
|
|
24499
24379
|
}
|
|
24500
|
-
if (push.type !== "thread") return;
|
|
24501
|
-
if (options.onMessage?.({ thread: push.thread, message: push.message })) return;
|
|
24502
|
-
finishWait({ kind: "message", push: { thread: push.thread, message: push.message } });
|
|
24503
24380
|
});
|
|
24504
|
-
socket.addEventListener("
|
|
24505
|
-
|
|
24506
|
-
|
|
24507
|
-
|
|
24508
|
-
|
|
24509
|
-
|
|
24510
|
-
|
|
24511
|
-
|
|
24512
|
-
|
|
24381
|
+
socket.addEventListener("open", () => {
|
|
24382
|
+
if (finished) return;
|
|
24383
|
+
void (async () => {
|
|
24384
|
+
try {
|
|
24385
|
+
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0" }, capabilities: { experimentalApi: true } });
|
|
24386
|
+
socket.send(JSON.stringify({ jsonrpc: "2.0", method: "initialized", params: {} }));
|
|
24387
|
+
const loaded = await callCodexMethod("thread/loaded/list", {});
|
|
24388
|
+
if (!loaded.data.includes(threadId)) {
|
|
24389
|
+
finishWake(false);
|
|
24390
|
+
return;
|
|
24391
|
+
}
|
|
24392
|
+
await callCodexMethod("turn/start", { threadId, input: [{ type: "text", text }] });
|
|
24393
|
+
finishWake(true);
|
|
24394
|
+
} catch {
|
|
24395
|
+
finishWake(false);
|
|
24396
|
+
}
|
|
24397
|
+
})();
|
|
24513
24398
|
});
|
|
24514
|
-
}
|
|
24515
|
-
return outcome;
|
|
24516
|
-
}
|
|
24517
|
-
var HeldSession = class {
|
|
24518
|
-
constructor(api, projectId, taskKey, hooks) {
|
|
24519
|
-
this.api = api;
|
|
24520
|
-
this.projectId = projectId;
|
|
24521
|
-
this.taskKey = taskKey;
|
|
24522
|
-
this.hooks = hooks;
|
|
24523
|
-
}
|
|
24524
|
-
api;
|
|
24525
|
-
projectId;
|
|
24526
|
-
taskKey;
|
|
24527
|
-
hooks;
|
|
24528
|
-
socket = null;
|
|
24529
|
-
stopped = false;
|
|
24530
|
-
connecting = false;
|
|
24531
|
-
timer = null;
|
|
24532
|
-
heartbeat = null;
|
|
24533
|
-
pongDue = null;
|
|
24534
|
-
attempt = 0;
|
|
24535
|
-
connected = 0;
|
|
24536
|
-
session = null;
|
|
24537
|
-
/**
|
|
24538
|
-
* Whoever is waiting for a message. Handed each one, and answers whether it took it (a waiter for one thread lets
|
|
24539
|
-
* the rest pass to the inbox); handed null when the agent is made unreachable under them.
|
|
24540
|
-
*/
|
|
24541
|
-
waiters = /* @__PURE__ */ new Set();
|
|
24542
|
-
greeted = null;
|
|
24543
|
-
/**
|
|
24544
|
-
* Settled once the room has said hello (the session id is known, so this process's next write names its session),
|
|
24545
|
-
* or after a few seconds if the room is slow. A write before hello names no session, and a reply to it reaches
|
|
24546
|
-
* nobody, so callers wait for this before their first write.
|
|
24547
|
-
*/
|
|
24548
|
-
ready = new Promise((resolve5) => {
|
|
24549
|
-
this.greeted = resolve5;
|
|
24550
|
-
setTimeout(resolve5, 5e3).unref();
|
|
24551
24399
|
});
|
|
24552
|
-
|
|
24553
|
-
|
|
24400
|
+
}
|
|
24401
|
+
|
|
24402
|
+
// src/platform/harness.ts
|
|
24403
|
+
var parent = null;
|
|
24404
|
+
function fetchParentCommand() {
|
|
24405
|
+
if (parent !== null) return parent;
|
|
24406
|
+
try {
|
|
24407
|
+
parent = process.platform === "win32" ? "" : execFileSync3("ps", ["-o", "command=", "-p", String(process.ppid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24408
|
+
} catch {
|
|
24409
|
+
parent = "";
|
|
24554
24410
|
}
|
|
24555
|
-
|
|
24556
|
-
|
|
24557
|
-
|
|
24558
|
-
|
|
24411
|
+
return parent;
|
|
24412
|
+
}
|
|
24413
|
+
function parseHarnessFromCommand(command) {
|
|
24414
|
+
const first = basename2(command.trim().split(/\s+/)[0] ?? "");
|
|
24415
|
+
if (/^claude(\b|$)/.test(first)) return "claude";
|
|
24416
|
+
if (/^codex(\b|$)/.test(first)) return "codex";
|
|
24417
|
+
if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
|
|
24418
|
+
return null;
|
|
24419
|
+
}
|
|
24420
|
+
function isNonInteractiveCommand(command) {
|
|
24421
|
+
return /(^|[\s/])codex(\S*)?\s+exec(\s|$)/.test(command);
|
|
24422
|
+
}
|
|
24423
|
+
function isNonInteractiveHarness() {
|
|
24424
|
+
return isNonInteractiveCommand(fetchParentCommand());
|
|
24425
|
+
}
|
|
24426
|
+
var isSamePath = (a, b) => {
|
|
24427
|
+
const resolveRealPath = (path2) => {
|
|
24559
24428
|
try {
|
|
24560
|
-
|
|
24561
|
-
if (this.stopped) return;
|
|
24562
|
-
const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
24563
|
-
this.socket = socket;
|
|
24564
|
-
socket.addEventListener("open", () => {
|
|
24565
|
-
if (this.stopped || this.socket !== socket) return;
|
|
24566
|
-
this.attempt = 0;
|
|
24567
|
-
this.startHeartbeat(socket);
|
|
24568
|
-
});
|
|
24569
|
-
socket.addEventListener("message", (event) => {
|
|
24570
|
-
if (this.stopped || this.socket !== socket) return;
|
|
24571
|
-
const data = String(event.data);
|
|
24572
|
-
if (data === "pong") {
|
|
24573
|
-
if (this.pongDue) clearTimeout(this.pongDue);
|
|
24574
|
-
this.pongDue = null;
|
|
24575
|
-
return;
|
|
24576
|
-
}
|
|
24577
|
-
const push = JSON.parse(data);
|
|
24578
|
-
if (push.type === "hello") {
|
|
24579
|
-
this.api.sessionId = push.session.id;
|
|
24580
|
-
this.session = push.session;
|
|
24581
|
-
this.taskKey = push.session.taskKey;
|
|
24582
|
-
persistRememberedSession(process.cwd(), push.session.id);
|
|
24583
|
-
this.greeted?.();
|
|
24584
|
-
this.greeted = null;
|
|
24585
|
-
this.connected += 1;
|
|
24586
|
-
if (this.connected > 1) void this.hooks.onReconnect?.(push.session);
|
|
24587
|
-
return;
|
|
24588
|
-
}
|
|
24589
|
-
if (push.type === "end") {
|
|
24590
|
-
this.stopped = true;
|
|
24591
|
-
return;
|
|
24592
|
-
}
|
|
24593
|
-
if (push.type === "reachable") {
|
|
24594
|
-
if (this.session) this.session = { ...this.session, reachable: push.on };
|
|
24595
|
-
if (!push.on) for (const waiter of [...this.waiters]) waiter(null);
|
|
24596
|
-
this.hooks.onReachable?.(push.on);
|
|
24597
|
-
return;
|
|
24598
|
-
}
|
|
24599
|
-
if (push.type !== "thread") return;
|
|
24600
|
-
const delivered = { thread: push.thread, message: push.message };
|
|
24601
|
-
let taken = false;
|
|
24602
|
-
for (const waiter of [...this.waiters]) if (waiter(delivered)) taken = true;
|
|
24603
|
-
if (!taken) this.hooks.onMessage(delivered);
|
|
24604
|
-
});
|
|
24605
|
-
socket.addEventListener("close", (event) => {
|
|
24606
|
-
if (this.socket !== socket) return;
|
|
24607
|
-
this.stopHeartbeat();
|
|
24608
|
-
if (this.socket === socket) this.socket = null;
|
|
24609
|
-
if (this.stopped) return;
|
|
24610
|
-
if (event.reason === "replaced") this.standBy();
|
|
24611
|
-
else this.schedule();
|
|
24612
|
-
});
|
|
24613
|
-
socket.addEventListener("error", () => {
|
|
24614
|
-
});
|
|
24429
|
+
return realpathSync3(path2);
|
|
24615
24430
|
} catch {
|
|
24616
|
-
|
|
24617
|
-
} finally {
|
|
24618
|
-
this.connecting = false;
|
|
24431
|
+
return path2;
|
|
24619
24432
|
}
|
|
24620
|
-
}
|
|
24621
|
-
|
|
24622
|
-
startHeartbeat(socket) {
|
|
24623
|
-
this.stopHeartbeat();
|
|
24624
|
-
this.heartbeat = setInterval(() => {
|
|
24625
|
-
if (socket.readyState !== WebSocket.OPEN) return;
|
|
24626
|
-
socket.send("ping");
|
|
24627
|
-
if (!this.pongDue) {
|
|
24628
|
-
this.pongDue = setTimeout(() => {
|
|
24629
|
-
this.pongDue = null;
|
|
24630
|
-
socket.close(4e3, "no pong");
|
|
24631
|
-
}, PONG_WITHIN_MS);
|
|
24632
|
-
}
|
|
24633
|
-
}, PING_EVERY_MS);
|
|
24634
|
-
}
|
|
24635
|
-
stopHeartbeat() {
|
|
24636
|
-
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
24637
|
-
if (this.pongDue) clearTimeout(this.pongDue);
|
|
24638
|
-
this.heartbeat = null;
|
|
24639
|
-
this.pongDue = null;
|
|
24640
|
-
}
|
|
24641
|
-
schedule() {
|
|
24642
|
-
if (this.stopped) return;
|
|
24643
|
-
if (this.timer) clearTimeout(this.timer);
|
|
24644
|
-
this.attempt += 1;
|
|
24645
|
-
this.timer = setTimeout(() => void this.connect(), Math.min(3e4, 1e3 * 2 ** Math.min(this.attempt, 5)));
|
|
24646
|
-
}
|
|
24647
|
-
/** Another process holds this session now: look now and then, and take it back once it is no longer live. */
|
|
24648
|
-
standBy() {
|
|
24649
|
-
if (this.stopped) return;
|
|
24650
|
-
if (this.timer) clearTimeout(this.timer);
|
|
24651
|
-
this.timer = setTimeout(async () => {
|
|
24652
|
-
if (this.stopped) return;
|
|
24653
|
-
let holder = true;
|
|
24654
|
-
try {
|
|
24655
|
-
const sessions = await this.api.get(`/projects/${this.projectId}/sessions`, ApiResponses.Sessions);
|
|
24656
|
-
holder = sessions.some((session) => session.id === this.session?.id && session.state === "live");
|
|
24657
|
-
} catch {
|
|
24658
|
-
}
|
|
24659
|
-
if (holder) this.standBy();
|
|
24660
|
-
else void this.connect();
|
|
24661
|
-
}, STANDBY_POLL_MS);
|
|
24662
|
-
}
|
|
24663
|
-
stop() {
|
|
24664
|
-
this.stopped = true;
|
|
24665
|
-
if (this.api.sessionId === this.session?.id) this.api.sessionId = null;
|
|
24666
|
-
if (this.timer) clearTimeout(this.timer);
|
|
24667
|
-
this.stopHeartbeat();
|
|
24668
|
-
this.socket?.close(1e3, "released");
|
|
24669
|
-
}
|
|
24433
|
+
};
|
|
24434
|
+
return resolveRealPath(a) === resolveRealPath(b);
|
|
24670
24435
|
};
|
|
24671
|
-
|
|
24672
|
-
|
|
24673
|
-
|
|
24674
|
-
|
|
24675
|
-
|
|
24676
|
-
|
|
24677
|
-
|
|
24678
|
-
|
|
24679
|
-
|
|
24680
|
-
|
|
24681
|
-
|
|
24682
|
-
|
|
24683
|
-
|
|
24684
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
24687
|
-
|
|
24688
|
-
|
|
24689
|
-
|
|
24690
|
-
|
|
24691
|
-
|
|
24692
|
-
|
|
24693
|
-
var deduplicateStrings = (paths) => [...new Set(paths.map((path2) => resolve2(path2)))];
|
|
24694
|
-
function fetchAncestorEnvironments() {
|
|
24695
|
-
if (process.platform === "win32") return [];
|
|
24696
|
-
const seen = [];
|
|
24697
|
-
try {
|
|
24698
|
-
if (process.platform === "linux") {
|
|
24699
|
-
let pid2 = process.ppid;
|
|
24700
|
-
for (let hop = 0; hop < 6 && pid2 > 1; hop += 1) {
|
|
24701
|
-
seen.push(readFileSync6(`/proc/${pid2}/environ`, "latin1").replace(/\0/g, " "));
|
|
24702
|
-
const stat = readFileSync6(`/proc/${pid2}/stat`, "utf8");
|
|
24703
|
-
pid2 = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24436
|
+
function isMainChatRollout(sessionMetaLine) {
|
|
24437
|
+
if (!sessionMetaLine.includes('"session_meta"')) return false;
|
|
24438
|
+
if (/"parent_thread_id"\s*:\s*"/.test(sessionMetaLine)) return false;
|
|
24439
|
+
const source = /"source"\s*:\s*("([^"]*)"|\{)/.exec(sessionMetaLine);
|
|
24440
|
+
if (!source) return true;
|
|
24441
|
+
if (source[1] === "{") return false;
|
|
24442
|
+
return source[2] !== "exec";
|
|
24443
|
+
}
|
|
24444
|
+
function fetchCodexConversationByCwd(roots, cwd) {
|
|
24445
|
+
const rollouts = [];
|
|
24446
|
+
const findRolloutsInDirectory = (dir, depth) => {
|
|
24447
|
+
let entries;
|
|
24448
|
+
try {
|
|
24449
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
24450
|
+
} catch {
|
|
24451
|
+
return;
|
|
24452
|
+
}
|
|
24453
|
+
for (const entry of entries) {
|
|
24454
|
+
const path2 = join7(dir, entry.name);
|
|
24455
|
+
if (entry.isDirectory()) {
|
|
24456
|
+
if (depth < 3) findRolloutsInDirectory(path2, depth + 1);
|
|
24457
|
+
continue;
|
|
24704
24458
|
}
|
|
24705
|
-
|
|
24706
|
-
|
|
24707
|
-
|
|
24708
|
-
|
|
24709
|
-
|
|
24710
|
-
}
|
|
24459
|
+
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);
|
|
24460
|
+
if (!match) continue;
|
|
24461
|
+
try {
|
|
24462
|
+
rollouts.push({ path: path2, id: match[1] ?? "", mtime: statSync3(path2).mtimeMs });
|
|
24463
|
+
} catch {
|
|
24711
24464
|
}
|
|
24712
|
-
return seen;
|
|
24713
24465
|
}
|
|
24714
|
-
|
|
24715
|
-
|
|
24716
|
-
|
|
24717
|
-
|
|
24718
|
-
|
|
24466
|
+
};
|
|
24467
|
+
for (const root of roots) findRolloutsInDirectory(root, 0);
|
|
24468
|
+
rollouts.sort((a, b) => b.mtime - a.mtime);
|
|
24469
|
+
const matching = /* @__PURE__ */ new Set();
|
|
24470
|
+
for (const rollout of rollouts) {
|
|
24471
|
+
let head = "";
|
|
24472
|
+
try {
|
|
24473
|
+
const fd = openSync(rollout.path, "r");
|
|
24474
|
+
try {
|
|
24475
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
24476
|
+
head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
|
|
24477
|
+
} finally {
|
|
24478
|
+
closeSync(fd);
|
|
24479
|
+
}
|
|
24480
|
+
} catch {
|
|
24481
|
+
continue;
|
|
24719
24482
|
}
|
|
24720
|
-
|
|
24721
|
-
|
|
24483
|
+
const line = head.split("\n")[0] ?? "";
|
|
24484
|
+
if (!line.includes('"session_meta"')) continue;
|
|
24485
|
+
if (!isMainChatRollout(line)) continue;
|
|
24486
|
+
const found = /"cwd":"((?:[^"\\]|\\.)*)"/.exec(line);
|
|
24487
|
+
if (!found) continue;
|
|
24488
|
+
let dir;
|
|
24489
|
+
try {
|
|
24490
|
+
dir = JSON.parse(`"${found[1]}"`);
|
|
24491
|
+
} catch {
|
|
24492
|
+
continue;
|
|
24493
|
+
}
|
|
24494
|
+
if (isSamePath(dir, cwd)) matching.add(rollout.id);
|
|
24495
|
+
if (matching.size > 1) return null;
|
|
24722
24496
|
}
|
|
24723
|
-
return
|
|
24724
|
-
}
|
|
24725
|
-
var environments = null;
|
|
24726
|
-
function findHomesInUse(variable) {
|
|
24727
|
-
environments ??= fetchAncestorEnvironments();
|
|
24728
|
-
const found = [];
|
|
24729
|
-
const pattern = new RegExp(`(?:^|\\s)${variable}=(\\S+)`, "g");
|
|
24730
|
-
for (const text of environments) for (const match of text.matchAll(pattern)) found.push(match[1] ?? "");
|
|
24731
|
-
return found.filter(Boolean);
|
|
24497
|
+
return matching.size === 1 ? [...matching][0] : null;
|
|
24732
24498
|
}
|
|
24733
|
-
|
|
24499
|
+
function isMainChatThread(roots, threadId) {
|
|
24500
|
+
const path2 = fetchCodexRolloutPath(roots, threadId);
|
|
24501
|
+
if (!path2) return true;
|
|
24734
24502
|
try {
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
|
|
24739
|
-
|
|
24740
|
-
|
|
24741
|
-
|
|
24503
|
+
const fd = openSync(path2, "r");
|
|
24504
|
+
try {
|
|
24505
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
24506
|
+
const head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
|
|
24507
|
+
return isMainChatRollout(head.split("\n")[0] ?? "");
|
|
24508
|
+
} finally {
|
|
24509
|
+
closeSync(fd);
|
|
24510
|
+
}
|
|
24742
24511
|
} catch {
|
|
24743
|
-
return
|
|
24512
|
+
return true;
|
|
24744
24513
|
}
|
|
24745
|
-
};
|
|
24746
|
-
var hasAnyEntry = (dir, files) => files.some((file2) => existsSync6(join6(dir, file2)));
|
|
24747
|
-
function fetchClaudeHomes(root = homedir3(), inUse = findHomesInUse) {
|
|
24748
|
-
if (process.env.SLOP_CLAUDE_HOME) return [process.env.SLOP_CLAUDE_HOME];
|
|
24749
|
-
const named = process.env.CLAUDE_CONFIG_DIR ? [process.env.CLAUDE_CONFIG_DIR] : [];
|
|
24750
|
-
return deduplicateStrings([join6(root, ".claude"), ...named, ...inUse("CLAUDE_CONFIG_DIR"), ...findDirectoriesNamed(root, ".claude").filter((dir) => hasAnyEntry(dir, ["settings.json", ".claude.json", "sessions"]))]);
|
|
24751
24514
|
}
|
|
24752
|
-
function
|
|
24753
|
-
|
|
24754
|
-
|
|
24755
|
-
|
|
24515
|
+
function fetchCodexRolloutPath(roots, threadId) {
|
|
24516
|
+
const suffix = `-${threadId}.jsonl`;
|
|
24517
|
+
let best = null;
|
|
24518
|
+
const findRolloutInDirectory = (dir, depth) => {
|
|
24519
|
+
let entries;
|
|
24520
|
+
try {
|
|
24521
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
24522
|
+
} catch {
|
|
24523
|
+
return;
|
|
24524
|
+
}
|
|
24525
|
+
for (const entry of entries) {
|
|
24526
|
+
const path2 = join7(dir, entry.name);
|
|
24527
|
+
if (entry.isDirectory()) {
|
|
24528
|
+
if (depth < 3) findRolloutInDirectory(path2, depth + 1);
|
|
24529
|
+
continue;
|
|
24530
|
+
}
|
|
24531
|
+
if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(suffix)) continue;
|
|
24532
|
+
try {
|
|
24533
|
+
const mtime = statSync3(path2).mtimeMs;
|
|
24534
|
+
if (!best || mtime > best.mtime) best = { path: path2, mtime };
|
|
24535
|
+
} catch {
|
|
24536
|
+
}
|
|
24537
|
+
}
|
|
24538
|
+
};
|
|
24539
|
+
for (const root of roots) findRolloutInDirectory(root, 0);
|
|
24540
|
+
return best ? best.path : null;
|
|
24756
24541
|
}
|
|
24757
|
-
function
|
|
24758
|
-
if (process.platform === "win32") return null;
|
|
24542
|
+
function resolveCodexBinary() {
|
|
24759
24543
|
try {
|
|
24760
|
-
|
|
24761
|
-
|
|
24762
|
-
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"] });
|
|
24763
|
-
const match = new RegExp(`(?:^|\\s)${variable}=(\\S+)`).exec(text);
|
|
24764
|
-
if (match?.[1]) return resolve2(match[1]);
|
|
24765
|
-
if (process.platform === "linux") {
|
|
24766
|
-
const stat = readFileSync6(`/proc/${pid}/stat`, "utf8");
|
|
24767
|
-
pid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
24768
|
-
} else pid = Number(execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim());
|
|
24769
|
-
}
|
|
24544
|
+
execFileSync3(process.platform === "win32" ? "where" : "which", ["codex"], { stdio: "ignore" });
|
|
24545
|
+
return "codex";
|
|
24770
24546
|
} catch {
|
|
24771
24547
|
}
|
|
24772
|
-
|
|
24773
|
-
}
|
|
24774
|
-
|
|
24775
|
-
// src/platform/harness-wake.ts
|
|
24776
|
-
function fetchHarnessRegistration(pid = process.ppid) {
|
|
24777
|
-
for (const home of fetchClaudeHomes()) {
|
|
24778
|
-
const path2 = join7(home, "sessions", `${pid}.json`);
|
|
24779
|
-
if (!existsSync7(path2)) continue;
|
|
24548
|
+
if (parseHarnessFromCommand(fetchParentCommand()) === "codex" && process.platform !== "win32") {
|
|
24780
24549
|
try {
|
|
24781
|
-
|
|
24550
|
+
const exe = process.platform === "linux" ? readlinkSync(`/proc/${process.ppid}/exe`) : execFileSync3("ps", ["-o", "comm=", "-p", String(process.ppid)], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
24551
|
+
if (exe && basename2(exe).startsWith("codex")) return exe;
|
|
24782
24552
|
} catch {
|
|
24783
|
-
return null;
|
|
24784
24553
|
}
|
|
24785
24554
|
}
|
|
24555
|
+
return "codex";
|
|
24556
|
+
}
|
|
24557
|
+
function fetchHarnessConversation(cwd) {
|
|
24558
|
+
const explicit = process.env.SLOP_CONVERSATION_ID?.trim();
|
|
24559
|
+
if (explicit) return { id: explicit, harness: process.env.SLOP_HARNESS === "codex" ? "codex" : process.env.SLOP_HARNESS === "claude" ? "claude" : "other" };
|
|
24560
|
+
if (process.env.CODEX_THREAD_ID?.trim()) return { id: process.env.CODEX_THREAD_ID.trim(), harness: "codex" };
|
|
24561
|
+
if (process.env.CLAUDE_CODE_SESSION_ID?.trim()) return { id: process.env.CLAUDE_CODE_SESSION_ID.trim(), harness: "claude" };
|
|
24562
|
+
const harness = parseHarnessFromCommand(fetchParentCommand());
|
|
24563
|
+
if (harness === "claude") {
|
|
24564
|
+
const id = resolveClaudeSession()?.sessionId;
|
|
24565
|
+
return id ? { id, harness } : null;
|
|
24566
|
+
}
|
|
24567
|
+
if (harness === "codex" && !isNonInteractiveHarness()) {
|
|
24568
|
+
const id = fetchCodexConversationByCwd(fetchCodexHomes().map((home) => join7(home, "sessions")), cwd);
|
|
24569
|
+
return id ? { id, harness } : null;
|
|
24570
|
+
}
|
|
24786
24571
|
return null;
|
|
24787
24572
|
}
|
|
24788
|
-
|
|
24789
|
-
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24793
|
-
|
|
24794
|
-
|
|
24795
|
-
|
|
24796
|
-
|
|
24573
|
+
|
|
24574
|
+
// src/platform/agent-identity.ts
|
|
24575
|
+
var processConversation = randomUUID4();
|
|
24576
|
+
var startedAt = Date.now();
|
|
24577
|
+
function fetchKnownConversation(cwd) {
|
|
24578
|
+
const own2 = fetchHarnessConversation(cwd);
|
|
24579
|
+
if (own2) return own2;
|
|
24580
|
+
const remembered = fetchRememberedConversation(cwd, startedAt - 12e4);
|
|
24581
|
+
if (remembered?.harness === "codex" && isMainChatThread(fetchCodexHomes().map((home) => join8(home, "sessions")), remembered.id)) return remembered;
|
|
24582
|
+
return remembered?.harness === "codex" ? null : remembered;
|
|
24583
|
+
}
|
|
24584
|
+
function resolveAgentIdentity(cwd, fallback = "process") {
|
|
24585
|
+
const conversation = fetchKnownConversation(cwd);
|
|
24586
|
+
const repo = fetchRepo();
|
|
24587
|
+
return {
|
|
24588
|
+
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve4(cwd)}`),
|
|
24589
|
+
harness: conversation?.harness ?? null,
|
|
24590
|
+
directory: repo ? basename3(repo.worktree) : basename3(cwd),
|
|
24591
|
+
branch: fetchCurrentBranch(cwd)
|
|
24797
24592
|
};
|
|
24798
|
-
return new Promise((resolve5) => {
|
|
24799
|
-
const socket = connect({ path: registration.messagingSocketPath });
|
|
24800
|
-
const finishWake = (ok) => {
|
|
24801
|
-
clearTimeout(timer);
|
|
24802
|
-
socket.destroy();
|
|
24803
|
-
resolve5(ok);
|
|
24804
|
-
};
|
|
24805
|
-
const timer = setTimeout(() => finishWake(false), 3e3);
|
|
24806
|
-
socket.on("connect", () => {
|
|
24807
|
-
socket.write(`${JSON.stringify(frame)}
|
|
24808
|
-
`, (error62) => error62 ? finishWake(false) : setTimeout(() => finishWake(true), 200));
|
|
24809
|
-
});
|
|
24810
|
-
socket.on("error", () => finishWake(false));
|
|
24811
|
-
});
|
|
24812
24593
|
}
|
|
24813
|
-
|
|
24814
|
-
|
|
24815
|
-
|
|
24816
|
-
|
|
24817
|
-
let child;
|
|
24594
|
+
|
|
24595
|
+
// src/platform/api.ts
|
|
24596
|
+
function buildApiClient(options) {
|
|
24597
|
+
async function sendApiRequest(method, path2, schema, body2) {
|
|
24818
24598
|
try {
|
|
24819
|
-
|
|
24820
|
-
env: options.codexHome ? { ...process.env, CODEX_HOME: options.codexHome } : process.env,
|
|
24821
|
-
stdio: ["ignore", "ignore", "ignore"]
|
|
24822
|
-
});
|
|
24599
|
+
assertApiResponseContract(method, path2, schema);
|
|
24823
24600
|
} catch {
|
|
24824
|
-
|
|
24825
|
-
return;
|
|
24601
|
+
throw new CliError("This request does not match the API contract", "Update slop to the latest version", "internal");
|
|
24826
24602
|
}
|
|
24827
|
-
const
|
|
24828
|
-
|
|
24829
|
-
|
|
24830
|
-
|
|
24831
|
-
|
|
24832
|
-
clearTimeout(timer);
|
|
24833
|
-
resolve5(false);
|
|
24834
|
-
});
|
|
24835
|
-
child.on("exit", (code) => {
|
|
24836
|
-
clearTimeout(timer);
|
|
24837
|
-
resolve5(code === 0);
|
|
24838
|
-
});
|
|
24839
|
-
});
|
|
24840
|
-
}
|
|
24841
|
-
function wakeCodexOverWs(text, threadId, endpoint) {
|
|
24842
|
-
return new Promise((resolve5) => {
|
|
24843
|
-
let socket;
|
|
24603
|
+
const headers = { accept: "application/json" };
|
|
24604
|
+
if (body2 !== void 0) headers["content-type"] = "application/json";
|
|
24605
|
+
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
24606
|
+
if (client.sessionId) headers["x-slop-session"] = client.sessionId;
|
|
24607
|
+
let response;
|
|
24844
24608
|
try {
|
|
24845
|
-
|
|
24846
|
-
|
|
24847
|
-
|
|
24848
|
-
|
|
24609
|
+
response = await fetch(`${options.baseUrl}/api${path2}`, {
|
|
24610
|
+
method,
|
|
24611
|
+
headers,
|
|
24612
|
+
body: body2 === void 0 ? void 0 : JSON.stringify(body2),
|
|
24613
|
+
signal: AbortSignal.timeout(2e4)
|
|
24614
|
+
});
|
|
24615
|
+
} catch (error62) {
|
|
24616
|
+
const reason = error62 instanceof Error && error62.name === "TimeoutError" ? "timed out" : "is unreachable";
|
|
24617
|
+
throw new CliError(`${options.baseUrl} ${reason}`, void 0, "network");
|
|
24849
24618
|
}
|
|
24850
|
-
|
|
24851
|
-
|
|
24852
|
-
const finishWake = (ok) => {
|
|
24853
|
-
clearTimeout(timer);
|
|
24619
|
+
const text = await response.text();
|
|
24620
|
+
if (response.ok) {
|
|
24854
24621
|
try {
|
|
24855
|
-
|
|
24622
|
+
return schema.parse(JSON.parse(text));
|
|
24856
24623
|
} catch {
|
|
24624
|
+
throw new CliError("The server returned an incompatible response", "Update slop to the latest version", "internal");
|
|
24857
24625
|
}
|
|
24858
|
-
|
|
24859
|
-
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24626
|
+
}
|
|
24627
|
+
let parsed = null;
|
|
24628
|
+
try {
|
|
24629
|
+
parsed = ApiErrorBody.safeParse(JSON.parse(text));
|
|
24630
|
+
} catch {
|
|
24631
|
+
}
|
|
24632
|
+
if (parsed?.success) {
|
|
24633
|
+
const { code, message } = parsed.data.error;
|
|
24634
|
+
throw new CliError(message, code === "unauthenticated" ? "slop login" : void 0, code);
|
|
24635
|
+
}
|
|
24636
|
+
throw new CliError(`${method} ${path2} failed with HTTP ${response.status}`, void 0, "http");
|
|
24637
|
+
}
|
|
24638
|
+
const client = {
|
|
24639
|
+
baseUrl: options.baseUrl,
|
|
24640
|
+
sessionId: null,
|
|
24641
|
+
get: (path2, schema) => sendApiRequest("GET", path2, schema),
|
|
24642
|
+
post: (path2, schema, body2) => sendApiRequest("POST", path2, schema, body2 ?? {}),
|
|
24643
|
+
patch: (path2, schema, body2) => sendApiRequest("PATCH", path2, schema, body2),
|
|
24644
|
+
put: (path2, schema, body2) => sendApiRequest("PUT", path2, schema, body2),
|
|
24645
|
+
delete: (path2, schema) => sendApiRequest("DELETE", path2, schema)
|
|
24646
|
+
};
|
|
24647
|
+
return client;
|
|
24648
|
+
}
|
|
24649
|
+
function buildAuthedApiClient() {
|
|
24650
|
+
const session = resolveSession();
|
|
24651
|
+
if (!session.token) throw new CliError(`Not signed in to ${session.baseUrl}`, "slop login", "unauthenticated");
|
|
24652
|
+
const client = buildApiClient(session);
|
|
24653
|
+
const conversation = fetchKnownConversation(process.cwd());
|
|
24654
|
+
client.sessionId = fetchRememberedSession(process.cwd(), conversation?.id ?? `dir:${resolve5(process.cwd())}`);
|
|
24655
|
+
return client;
|
|
24656
|
+
}
|
|
24657
|
+
|
|
24658
|
+
// src/platform/machine.ts
|
|
24659
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
24660
|
+
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
24661
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
24662
|
+
import { hostname as hostname3, platform, userInfo } from "os";
|
|
24663
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
24664
|
+
function runCommand(command, args) {
|
|
24665
|
+
try {
|
|
24666
|
+
return execFileSync4(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
24667
|
+
} catch {
|
|
24668
|
+
return null;
|
|
24669
|
+
}
|
|
24670
|
+
}
|
|
24671
|
+
function fetchOsMachineId() {
|
|
24672
|
+
switch (platform()) {
|
|
24673
|
+
case "darwin":
|
|
24674
|
+
return /"IOPlatformUUID" = "([^"]+)"/.exec(runCommand("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]) ?? "")?.[1] ?? null;
|
|
24675
|
+
case "linux":
|
|
24676
|
+
for (const path2 of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
24677
|
+
if (existsSync6(path2)) return readFileSync7(path2, "utf8").trim() || null;
|
|
24872
24678
|
}
|
|
24679
|
+
return null;
|
|
24680
|
+
case "win32":
|
|
24681
|
+
return /MachineGuid\s+REG_SZ\s+(\S+)/.exec(
|
|
24682
|
+
runCommand("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]) ?? ""
|
|
24683
|
+
)?.[1] ?? null;
|
|
24684
|
+
default:
|
|
24685
|
+
return null;
|
|
24686
|
+
}
|
|
24687
|
+
}
|
|
24688
|
+
function resolvePersistedMachineId() {
|
|
24689
|
+
const path2 = join9(dirname4(buildConfigPath()), "machine-id");
|
|
24690
|
+
if (existsSync6(path2)) return readFileSync7(path2, "utf8").trim();
|
|
24691
|
+
const id = randomUUID5();
|
|
24692
|
+
mkdirSync4(dirname4(path2), { recursive: true, mode: 448 });
|
|
24693
|
+
writeFileSync5(path2, `${id}
|
|
24694
|
+
`, { mode: 384 });
|
|
24695
|
+
return id;
|
|
24696
|
+
}
|
|
24697
|
+
function buildMachineInfo() {
|
|
24698
|
+
const raw = `${fetchOsMachineId() ?? resolvePersistedMachineId()}:${userInfo().username}`;
|
|
24699
|
+
return {
|
|
24700
|
+
fingerprint: createHash4("sha256").update(raw).digest("hex"),
|
|
24701
|
+
name: hostname3().replace(/\.local$/, ""),
|
|
24702
|
+
platform: platform()
|
|
24703
|
+
};
|
|
24704
|
+
}
|
|
24705
|
+
|
|
24706
|
+
// src/platform/output.ts
|
|
24707
|
+
var jsonMode = false;
|
|
24708
|
+
var updateJsonMode = (on) => {
|
|
24709
|
+
jsonMode = on;
|
|
24710
|
+
};
|
|
24711
|
+
var isJsonMode = () => jsonMode;
|
|
24712
|
+
function emitOutput(value, human) {
|
|
24713
|
+
process.stdout.write(jsonMode ? `${JSON.stringify(value, null, 2)}
|
|
24714
|
+
` : `${human()}
|
|
24715
|
+
`);
|
|
24716
|
+
}
|
|
24717
|
+
function exitWithError(error62) {
|
|
24718
|
+
const cli = error62 instanceof CliError ? error62 : null;
|
|
24719
|
+
const message = error62 instanceof Error ? error62.message : String(error62);
|
|
24720
|
+
if (jsonMode) {
|
|
24721
|
+
process.stderr.write(`${JSON.stringify({ error: { code: cli?.code ?? "error", message, hint: cli?.hint } })}
|
|
24722
|
+
`);
|
|
24723
|
+
} else {
|
|
24724
|
+
process.stderr.write(`error: ${message}
|
|
24725
|
+
`);
|
|
24726
|
+
if (cli?.hint) process.stderr.write(` try: ${cli.hint}
|
|
24727
|
+
`);
|
|
24728
|
+
}
|
|
24729
|
+
process.exit(cli?.exitCode ?? 1);
|
|
24730
|
+
}
|
|
24731
|
+
function formatTable(rows) {
|
|
24732
|
+
const widths = [];
|
|
24733
|
+
for (const row of rows) row.forEach((cell, index) => widths[index] = Math.max(widths[index] ?? 0, cell.length));
|
|
24734
|
+
return rows.map((row) => row.map((cell, index) => index === row.length - 1 ? cell : cell.padEnd(widths[index])).join(" ")).join("\n");
|
|
24735
|
+
}
|
|
24736
|
+
var formatStatusLabel = (statuses, key) => statuses.find((status) => status.key === key)?.label ?? key;
|
|
24737
|
+
var formatTaskLine = (statuses) => (task) => [
|
|
24738
|
+
task.key,
|
|
24739
|
+
formatStatusLabel(statuses, task.status),
|
|
24740
|
+
TASK_SEVERITY_LABELS[task.severity],
|
|
24741
|
+
task.assignee?.name ?? "-",
|
|
24742
|
+
task.title
|
|
24743
|
+
];
|
|
24744
|
+
function formatTaskDetail(task, url2, statuses) {
|
|
24745
|
+
const lines = [
|
|
24746
|
+
`${task.key} ${task.title}`,
|
|
24747
|
+
`status: ${formatStatusLabel(statuses, task.status)} severity: ${TASK_SEVERITY_LABELS[task.severity]} assignee: ${task.assignee?.name ?? "nobody"} by: ${task.createdBy.name}`,
|
|
24748
|
+
url2
|
|
24749
|
+
];
|
|
24750
|
+
if (task.links.length > 0) lines.push(`links: ${task.links.map((link) => `${link.title} <${link.url}>`).join(", ")}`);
|
|
24751
|
+
if (task.archivedAt) lines.push("archived");
|
|
24752
|
+
if (task.items.length > 0) {
|
|
24753
|
+
lines.push("", `plan ${task.progress.done}/${task.progress.total} done`);
|
|
24754
|
+
task.items.forEach((item, index) => lines.push(` ${index + 1}. [${item.done ? "x" : " "}] ${item.title}${item.note ? ` (${item.note.split("\n")[0]})` : ""}`));
|
|
24755
|
+
}
|
|
24756
|
+
if (task.description) lines.push("", task.description);
|
|
24757
|
+
if (task.notes) lines.push("", "notes:", task.notes);
|
|
24758
|
+
return lines.join("\n");
|
|
24759
|
+
}
|
|
24760
|
+
|
|
24761
|
+
// src/commands/auth.ts
|
|
24762
|
+
function openBrowser(url2) {
|
|
24763
|
+
const [command, args] = platform2() === "darwin" ? ["open", [url2]] : platform2() === "win32" ? ["cmd", ["/c", "start", "", url2]] : ["xdg-open", [url2]];
|
|
24764
|
+
try {
|
|
24765
|
+
spawn2(command, args, { stdio: "ignore", detached: true }).on("error", () => {
|
|
24766
|
+
}).unref();
|
|
24767
|
+
} catch {
|
|
24768
|
+
}
|
|
24769
|
+
}
|
|
24770
|
+
var waitForMilliseconds = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
24771
|
+
var isHeadless = () => Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY) || platform2() === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
24772
|
+
function registerAuthCommands(program3) {
|
|
24773
|
+
program3.command("login").description("Sign this machine in. Opens the web app, where you approve the login. On a VM or over SSH, open the printed link on any device.").option("--url <url>", "the Slopware Tasks instance to sign in to").option("--no-browser", "print the link instead of opening it").action(async (options) => {
|
|
24774
|
+
const baseUrl = (options.url || process.env.SLOP_URL || fetchConfig()?.baseUrl || DEFAULT_URL).replace(/\/+$/, "");
|
|
24775
|
+
const api = buildApiClient({ baseUrl, token: null });
|
|
24776
|
+
const started = await api.post("/cli/auth/start", ApiResponses.StartCliAuthResult, { machine: buildMachineInfo() });
|
|
24777
|
+
const remote = isHeadless();
|
|
24778
|
+
process.stderr.write(`
|
|
24779
|
+
Approve this login in a browser${remote ? " on any device (this machine has no browser; your laptop or phone is fine)" : ""}:
|
|
24780
|
+
${started.verifyUrl}
|
|
24781
|
+
|
|
24782
|
+
Confirm the code matches: ${started.userCode}
|
|
24783
|
+
|
|
24784
|
+
`);
|
|
24785
|
+
if (options.browser && !remote) openBrowser(started.verifyUrl);
|
|
24786
|
+
const deadline = Date.now() + started.expiresIn * 1e3;
|
|
24787
|
+
while (Date.now() < deadline) {
|
|
24788
|
+
await waitForMilliseconds(started.interval * 1e3);
|
|
24789
|
+
const poll = await api.post("/cli/auth/poll", ApiResponses.PollCliAuthResult, { deviceCode: started.deviceCode });
|
|
24790
|
+
if (poll.status === "pending") continue;
|
|
24791
|
+
if (poll.status === "denied") throw new CliError("The login was denied in the web app");
|
|
24792
|
+
if (poll.status === "expired") break;
|
|
24793
|
+
const me = await buildApiClient({ baseUrl, token: poll.token }).get("/me", ApiResponses.Me);
|
|
24794
|
+
persistConfig({ baseUrl, token: poll.token, email: me.user.email });
|
|
24795
|
+
const skills = installSkills();
|
|
24796
|
+
emitOutput({ ok: true, user: me.user, machine: me.machine, baseUrl, skills }, () => `Signed in as ${me.user.email} on ${me.machine?.name ?? "this machine"}.${skills.length ? `
|
|
24797
|
+
Agent skills installed in ~/.claude/skills and ~/.agents/skills. Next: slop mcp install (the board as tools for Claude Code and Codex).` : ""}`);
|
|
24798
|
+
return;
|
|
24799
|
+
}
|
|
24800
|
+
throw new CliError("The login request expired before it was approved", "slop login");
|
|
24801
|
+
});
|
|
24802
|
+
program3.command("logout").description("Sign this machine out and revoke its token.").action(async () => {
|
|
24803
|
+
const session = resolveSession();
|
|
24804
|
+
if (session.token) await buildApiClient(session).post("/cli/auth/logout", ApiResponses.Ok).catch(() => {
|
|
24873
24805
|
});
|
|
24874
|
-
|
|
24875
|
-
|
|
24876
|
-
|
|
24877
|
-
|
|
24878
|
-
|
|
24879
|
-
|
|
24880
|
-
|
|
24881
|
-
|
|
24882
|
-
|
|
24883
|
-
|
|
24884
|
-
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
|
|
24889
|
-
|
|
24806
|
+
deleteConfig();
|
|
24807
|
+
emitOutput({ ok: true }, () => "Signed out.");
|
|
24808
|
+
});
|
|
24809
|
+
program3.command("whoami").description("Show the signed-in user, machine and instance.").action(async () => {
|
|
24810
|
+
const api = buildAuthedApiClient();
|
|
24811
|
+
const me = await api.get("/me", ApiResponses.Me);
|
|
24812
|
+
emitOutput({ ...me, baseUrl: api.baseUrl }, () => `${me.user.name} <${me.user.email}>
|
|
24813
|
+
machine: ${me.machine?.name ?? "-"}
|
|
24814
|
+
instance: ${api.baseUrl}`);
|
|
24815
|
+
});
|
|
24816
|
+
}
|
|
24817
|
+
|
|
24818
|
+
// src/commands/guide.ts
|
|
24819
|
+
var AGENT_GUIDE = `## Task tracking
|
|
24820
|
+
|
|
24821
|
+
This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to keep it current.
|
|
24822
|
+
|
|
24823
|
+
- When you find work that should be tracked (a bug, a follow-up, a TODO you are not doing now), create a task:
|
|
24824
|
+
\`slop task create "Short imperative title" -d "What, where, why" -s <low|medium|high|critical>\`
|
|
24825
|
+
- Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
|
|
24826
|
+
- When it is finished and verified: \`slop task done <KEY>\`.
|
|
24827
|
+
- Descriptions are Markdown and are shown in full on the board. Write real ones: context, file paths, what done means. Pipe long ones in with \`-d -\`.
|
|
24828
|
+
- Agents use the six slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, each with an \`action\`. You have a name (session start tells you; \`session who\` lists everyone's); \`say post\` with \`to\` reaches one agent by name. If a wake says mail is waiting, use \`session check\` with the project it names. Each conversation and project has its own inbox. Messages for you reach you on their own: idle, as a turn of yours; while you work, as your next turn (Codex) or with your next tool result (Claude Code). Messages are letters, not chat: the other agent reads yours when it gets to it, its reply may predate your latest, so write once and completely, answer a pile of messages with one reply, and reply only when it changes something (never to acknowledge). Do not wait for replies; end your turn. The \`slop\` command is for people and scripts. Talk through threads: \`slop task ask <KEY> "\u2026"\` reaches whoever is live on that task, \`slop thread reply <id> "\u2026"\` answers, \`slop threads\` lists them; general threads are for people and for context. Method: \`slop skill slop-sessions\`.
|
|
24829
|
+
- Work that outlasts one session gets a plan on its task: \`slop task plan <KEY> -\` (one item per line), then \`slop task item note|done <KEY> <n>\` as you go; \`slop task items <KEY>\` shows where you are. \`slop skill slop-planning\` is the method; \`slop skills install\` puts the skills in this repository.
|
|
24830
|
+
- Everything is editable: \`slop task update <KEY> --title \u2026 --severity \u2026 --status \u2026 --assignee \u2026\`. Assign with \`slop task assign <KEY> <email|me|none>\`; see who exists with \`slop members\`. \`slop task list --mine\` shows your user's tasks.
|
|
24831
|
+
- Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
|
|
24832
|
+
- When you open a pull request for a task, link it: \`slop task link <KEY> <pr-url>\`.
|
|
24833
|
+
- Columns are the project's own: \`slop statuses\` lists them; \`slop task move <KEY> "<column>"\` takes a key or label. \`slop task start\` / \`slop task done\` go to the first "in progress" / "done" column. Finished work is archived from the board (\`slop task archive <KEY>\`), not deleted. Do not delete tasks you did not create.
|
|
24834
|
+
`;
|
|
24835
|
+
function registerGuideCommand(program3) {
|
|
24836
|
+
program3.command("guide").description("Print a snippet for AGENTS.md / CLAUDE.md that teaches an agent to use this CLI.").action(() => emitOutput({ markdown: AGENT_GUIDE }, () => AGENT_GUIDE));
|
|
24837
|
+
}
|
|
24838
|
+
|
|
24839
|
+
// src/commands/projects.ts
|
|
24840
|
+
async function fetchProjectByKey(api, key) {
|
|
24841
|
+
const projects = await api.get("/projects", ApiResponses.Projects);
|
|
24842
|
+
const matches = projects.filter((project) => project.key === key.toUpperCase() || project.id === key);
|
|
24843
|
+
if (matches.length === 1) return matches[0];
|
|
24844
|
+
if (matches.length > 1) {
|
|
24845
|
+
throw new CliError(`More than one of your organizations has a project with the key ${key.toUpperCase()}:
|
|
24846
|
+
${matches.map((project) => ` ${project.id} ${project.name}`).join("\n")}`, "slop link <project id>");
|
|
24847
|
+
}
|
|
24848
|
+
throw new CliError(`No project with the key ${key.toUpperCase()}`, "slop projects");
|
|
24849
|
+
}
|
|
24850
|
+
async function resolveProject(api, explicit) {
|
|
24851
|
+
if (explicit) return fetchProjectByKey(api, explicit);
|
|
24852
|
+
const repo = fetchRepo();
|
|
24853
|
+
const link = repo ? fetchRepoLink(repo) : null;
|
|
24854
|
+
if (!link) throw new CliError("This repository is not linked to a project", "slop link <PROJECT_KEY> (or pass --project KEY)", "not_linked");
|
|
24855
|
+
if (link.baseUrl !== api.baseUrl) {
|
|
24856
|
+
throw new CliError(`This repository is linked to ${link.baseUrl}, but you are signed in to ${api.baseUrl}`, `slop login --url ${link.baseUrl}`);
|
|
24857
|
+
}
|
|
24858
|
+
return { id: link.projectId, key: link.projectKey };
|
|
24859
|
+
}
|
|
24860
|
+
async function listMembers(api, projectId) {
|
|
24861
|
+
const project = await api.get(`/projects/${projectId}`, ApiResponses.Project);
|
|
24862
|
+
return (await api.get(`/orgs/${project.orgId}/members`, ApiResponses.MembersResult)).members;
|
|
24863
|
+
}
|
|
24864
|
+
function registerProjectCommands(program3) {
|
|
24865
|
+
program3.command("projects").description("List the projects you can reach.").action(async () => {
|
|
24866
|
+
const projects = await buildAuthedApiClient().get("/projects", ApiResponses.Projects);
|
|
24867
|
+
emitOutput(
|
|
24868
|
+
projects,
|
|
24869
|
+
() => projects.length === 0 ? 'No projects yet. Create one: slop project create "My project"' : formatTable([["KEY", "NAME", "OPEN"], ...projects.map((project2) => [project2.key, project2.name, String(project2.openTaskCount)])])
|
|
24870
|
+
);
|
|
24871
|
+
});
|
|
24872
|
+
program3.command("members").description("List the people tasks in this project can be assigned to.").option("-p, --project <key>", "project key (default: the linked project)").action(async (options) => {
|
|
24873
|
+
const api = buildAuthedApiClient();
|
|
24874
|
+
const members2 = await listMembers(api, (await resolveProject(api, options.project)).id);
|
|
24875
|
+
emitOutput(members2, () => formatTable([["NAME", "EMAIL", "ROLE"], ...members2.map((member) => [member.name, member.email, member.role])]));
|
|
24876
|
+
});
|
|
24877
|
+
program3.command("statuses").alias("columns").description("The linked project's columns, in board order.").option("-p, --project <key>", "project key (default: the linked project)").action(async (options) => {
|
|
24878
|
+
const api = buildAuthedApiClient();
|
|
24879
|
+
const target = await resolveProject(api, options.project);
|
|
24880
|
+
const statuses = (await api.get(`/projects/${target.id}`, ApiResponses.Project)).statuses;
|
|
24881
|
+
emitOutput(statuses, () => formatTable([["KEY", "LABEL", "CATEGORY"], ...statuses.map((status2) => [status2.key, status2.label, status2.category])]));
|
|
24882
|
+
});
|
|
24883
|
+
const project = program3.command("project").description("Manage projects.");
|
|
24884
|
+
const status = project.command("status").alias("column").description("Add, rename, reorder or remove the project's columns.");
|
|
24885
|
+
const fetchResolvedProject = async (api, key) => api.get(`/projects/${(await resolveProject(api, key)).id}`, ApiResponses.Project);
|
|
24886
|
+
const requireStatusByRef = (target, ref) => {
|
|
24887
|
+
const found = findStatusByKeyOrLabel(target.statuses, ref);
|
|
24888
|
+
if (!found) throw new CliError(`"${ref}" is not a column of ${target.key}`, "slop statuses");
|
|
24889
|
+
return found;
|
|
24890
|
+
};
|
|
24891
|
+
const formatStatusLines = (statuses) => statuses.map((candidate) => `${candidate.key} (${candidate.label}, ${candidate.category})`).join("\n");
|
|
24892
|
+
status.command("add <label>").description("Add a column at the end.").option("-p, --project <key>").option("-c, --category <category>", STATUS_CATEGORIES.join(" | "), "todo").action(async (label, options) => {
|
|
24893
|
+
const api = buildAuthedApiClient();
|
|
24894
|
+
if (!STATUS_CATEGORIES.includes(options.category)) throw new CliError(`Unknown category "${options.category}"`, `use one of: ${STATUS_CATEGORIES.join(", ")}`);
|
|
24895
|
+
const target = await fetchResolvedProject(api, options.project);
|
|
24896
|
+
const statuses = await api.post(`/projects/${target.id}/statuses`, ApiResponses.ProjectStatuses, { label, category: options.category });
|
|
24897
|
+
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24898
|
+
});
|
|
24899
|
+
status.command("rename <column> <label>").description("Rename a column (its key stays).").option("-p, --project <key>").option("-c, --category <category>", STATUS_CATEGORIES.join(" | ")).action(async (ref, label, options) => {
|
|
24900
|
+
const api = buildAuthedApiClient();
|
|
24901
|
+
const target = await fetchResolvedProject(api, options.project);
|
|
24902
|
+
const statuses = await api.patch(`/projects/${target.id}/statuses/${requireStatusByRef(target, ref).id}`, ApiResponses.ProjectStatuses, { label, category: options.category });
|
|
24903
|
+
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24904
|
+
});
|
|
24905
|
+
status.command("remove <column>").description("Remove a column; its tasks move to --to.").option("-p, --project <key>").option("--to <column>", "where the column's tasks go").action(async (ref, options) => {
|
|
24906
|
+
const api = buildAuthedApiClient();
|
|
24907
|
+
const target = await fetchResolvedProject(api, options.project);
|
|
24908
|
+
const moveTo = options.to ? requireStatusByRef(target, options.to).key : null;
|
|
24909
|
+
const statuses = await api.delete(`/projects/${target.id}/statuses/${requireStatusByRef(target, ref).id}${moveTo ? `?moveTo=${encodeURIComponent(moveTo)}` : ""}`, ApiResponses.ProjectStatuses);
|
|
24910
|
+
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24911
|
+
});
|
|
24912
|
+
status.command("order <columns...>").description("Set the board order: every column, by key or label.").option("-p, --project <key>").action(async (refs, options) => {
|
|
24913
|
+
const api = buildAuthedApiClient();
|
|
24914
|
+
const target = await fetchResolvedProject(api, options.project);
|
|
24915
|
+
const statuses = await api.put(`/projects/${target.id}/statuses/order`, ApiResponses.ProjectStatuses, { ids: refs.map((ref) => requireStatusByRef(target, ref).id) });
|
|
24916
|
+
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24917
|
+
});
|
|
24918
|
+
project.command("update").description("Change the linked project's name or description.").option("-p, --project <key>", "project key (default: the linked project)").option("-n, --name <name>").option("-d, --description <text>").action(async (options) => {
|
|
24919
|
+
const api = buildAuthedApiClient();
|
|
24920
|
+
const target = await resolveProject(api, options.project);
|
|
24921
|
+
if (options.name === void 0 && options.description === void 0) {
|
|
24922
|
+
throw new CliError("Nothing to change", "pass --name or --description");
|
|
24923
|
+
}
|
|
24924
|
+
const updated = await api.patch(`/projects/${target.id}`, ApiResponses.Project, { name: options.name, description: options.description });
|
|
24925
|
+
emitOutput(updated, () => `Updated ${updated.key} ${updated.name}`);
|
|
24926
|
+
});
|
|
24927
|
+
project.command("create <name>").description("Create a project.").option("-k, --key <key>", "task key prefix, e.g. WEB (default: derived from the name)").option("-d, --description <text>", "what the project is").option("--org <slug-or-id>", "which org, if you belong to several").action(async (name, options) => {
|
|
24928
|
+
const api = buildAuthedApiClient();
|
|
24929
|
+
const orgs = await api.get("/orgs", ApiResponses.Orgs);
|
|
24930
|
+
const org = options.org ? orgs.find((candidate) => candidate.id === options.org || candidate.slug === options.org) : orgs.length === 1 ? orgs[0] : void 0;
|
|
24931
|
+
if (!org) throw new CliError(options.org ? `No org "${options.org}"` : "You belong to several orgs", `pass --org: ${orgs.map((candidate) => candidate.slug).join(", ")}`);
|
|
24932
|
+
const key = options.key ?? generateProjectKeyFromName(name);
|
|
24933
|
+
if (!key) throw new CliError("Could not derive a key from that name", "pass --key, e.g. --key WEB");
|
|
24934
|
+
const project2 = await api.post(`/orgs/${org.id}/projects`, ApiResponses.Project, { name, key, description: options.description ?? "" });
|
|
24935
|
+
emitOutput(project2, () => `Created ${project2.key} ${project2.name}
|
|
24936
|
+
Link this repository to it: slop link ${project2.key}`);
|
|
24937
|
+
});
|
|
24938
|
+
program3.command("link <project>").description("Bind this git repository (on this machine) to a project, by key.").action(async (projectKey) => {
|
|
24939
|
+
const api = buildAuthedApiClient();
|
|
24940
|
+
const repo = requireRepo();
|
|
24941
|
+
const project2 = await fetchProjectByKey(api, projectKey);
|
|
24942
|
+
const { info, localRepoId } = buildRepoDescription(repo, fetchRepoLink(repo));
|
|
24943
|
+
const binding = await api.post(`/projects/${project2.id}/bindings`, ApiResponses.Binding, { repo: info });
|
|
24944
|
+
persistRepoLink(repo, { baseUrl: api.baseUrl, projectId: project2.id, projectKey: project2.key, projectName: project2.name, bindingId: binding.id, localRepoId });
|
|
24945
|
+
emitOutput({ project: project2, binding }, () => `Linked ${info.name} on ${binding.machine.name} to ${project2.key} ${project2.name}
|
|
24946
|
+
Try: slop task create "My first task"`);
|
|
24947
|
+
});
|
|
24948
|
+
program3.command("unlink").description("Remove this repository's binding.").action(async () => {
|
|
24949
|
+
const repo = requireRepo();
|
|
24950
|
+
const link = fetchRepoLink(repo);
|
|
24951
|
+
if (!link) throw new CliError("This repository is not linked", void 0, "not_linked");
|
|
24952
|
+
await buildAuthedApiClient().delete(`/projects/${link.projectId}/bindings/${link.bindingId}`, ApiResponses.Ok).catch(() => {
|
|
24890
24953
|
});
|
|
24954
|
+
deleteTaskLink(repo);
|
|
24955
|
+
emitOutput({ ok: true }, () => `Unlinked from ${link.projectKey}.`);
|
|
24956
|
+
});
|
|
24957
|
+
program3.command("status").description("Show what this repository is linked to.").action(async () => {
|
|
24958
|
+
const repo = fetchRepo();
|
|
24959
|
+
const link = repo ? fetchRepoLink(repo) : null;
|
|
24960
|
+
const where = !repo ? "" : repo.worktree === repo.root ? repo.root : `${repo.worktree} (worktree of ${repo.root})`;
|
|
24961
|
+
emitOutput(
|
|
24962
|
+
{ repo: repo?.root ?? null, worktree: repo?.worktree ?? null, link },
|
|
24963
|
+
() => !repo ? "Not inside a git repository." : !link ? `${where}
|
|
24964
|
+
not linked. Run: slop link <PROJECT_KEY>` : `${where}
|
|
24965
|
+
linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
24966
|
+
);
|
|
24891
24967
|
});
|
|
24892
24968
|
}
|
|
24893
24969
|
|
|
24894
|
-
// src/
|
|
24895
|
-
|
|
24896
|
-
|
|
24897
|
-
|
|
24970
|
+
// src/commands/mcp.ts
|
|
24971
|
+
import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
|
|
24972
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
|
|
24973
|
+
import { homedir as homedir4 } from "os";
|
|
24974
|
+
import { join as join13 } from "path";
|
|
24975
|
+
|
|
24976
|
+
// src/platform/session-recovery.ts
|
|
24977
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
24978
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
24979
|
+
|
|
24980
|
+
// src/platform/inbox.ts
|
|
24981
|
+
import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
|
|
24982
|
+
import { existsSync as existsSync7, linkSync as linkSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, realpathSync as realpathSync4, renameSync as renameSync2, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
24983
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
24984
|
+
var buildInboxDir = () => join10(dirname5(buildConfigPath()), "inbox");
|
|
24985
|
+
var resolveRealCwd2 = (cwd) => {
|
|
24898
24986
|
try {
|
|
24899
|
-
|
|
24987
|
+
return realpathSync4(cwd);
|
|
24900
24988
|
} catch {
|
|
24901
|
-
|
|
24989
|
+
return cwd;
|
|
24902
24990
|
}
|
|
24903
|
-
return parent;
|
|
24904
|
-
}
|
|
24905
|
-
function parseHarnessFromCommand(command) {
|
|
24906
|
-
const first = basename2(command.trim().split(/\s+/)[0] ?? "");
|
|
24907
|
-
if (/^claude(\b|$)/.test(first)) return "claude";
|
|
24908
|
-
if (/^codex(\b|$)/.test(first)) return "codex";
|
|
24909
|
-
if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
|
|
24910
|
-
return null;
|
|
24911
|
-
}
|
|
24912
|
-
function isNonInteractiveCommand(command) {
|
|
24913
|
-
return /(^|[\s/])codex(\S*)?\s+exec(\s|$)/.test(command);
|
|
24914
|
-
}
|
|
24915
|
-
function isNonInteractiveHarness() {
|
|
24916
|
-
return isNonInteractiveCommand(fetchParentCommand());
|
|
24917
|
-
}
|
|
24918
|
-
var isSamePath = (a, b) => {
|
|
24919
|
-
const resolveRealPath = (path2) => {
|
|
24920
|
-
try {
|
|
24921
|
-
return realpathSync2(path2);
|
|
24922
|
-
} catch {
|
|
24923
|
-
return path2;
|
|
24924
|
-
}
|
|
24925
|
-
};
|
|
24926
|
-
return resolveRealPath(a) === resolveRealPath(b);
|
|
24927
24991
|
};
|
|
24928
|
-
|
|
24929
|
-
|
|
24930
|
-
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
24934
|
-
|
|
24935
|
-
}
|
|
24936
|
-
|
|
24937
|
-
|
|
24938
|
-
|
|
24939
|
-
|
|
24992
|
+
var generateInboxKey = (cwd, owner) => owner ? generateLocalStateKey(cwd, JSON.stringify([owner.baseUrl.replace(/\/$/, ""), owner.projectId, owner.conversation])) : createHash5("sha256").update(resolveRealCwd2(cwd)).digest("hex").slice(0, 16);
|
|
24993
|
+
var buildInboxPath = (cwd) => join10(buildInboxDir(), `${generateInboxKey(cwd)}.jsonl`);
|
|
24994
|
+
var buildInboxQueuePath = (cwd, owner) => join10(buildInboxDir(), generateInboxKey(cwd, owner));
|
|
24995
|
+
var sequence = 0;
|
|
24996
|
+
function persistInboxPush(cwd, push, owner) {
|
|
24997
|
+
const directory = buildInboxQueuePath(cwd, owner);
|
|
24998
|
+
mkdirSync5(directory, { recursive: true });
|
|
24999
|
+
const name = `${Date.now()}.${process.pid}.${String(sequence++).padStart(12, "0")}.${randomUUID6()}`;
|
|
25000
|
+
const temporary = join10(directory, `${name}.writing`);
|
|
25001
|
+
writeFileSync6(temporary, JSON.stringify(push), { flag: "wx" });
|
|
25002
|
+
if (owner) {
|
|
25003
|
+
const key = createHash5("sha256").update(push.message.id).digest("hex");
|
|
25004
|
+
const published = join10(directory, `${key}.json`);
|
|
24940
25005
|
try {
|
|
24941
|
-
|
|
24942
|
-
|
|
24943
|
-
return;
|
|
25006
|
+
if (existsSync7(join10(directory, "receipts", key))) return false;
|
|
25007
|
+
linkSync2(temporary, published);
|
|
25008
|
+
return true;
|
|
25009
|
+
} catch (error62) {
|
|
25010
|
+
if (error62.code === "EEXIST") return !existsSync7(join10(directory, "receipts", key));
|
|
25011
|
+
throw error62;
|
|
25012
|
+
} finally {
|
|
25013
|
+
rmSync5(temporary, { force: true });
|
|
24944
25014
|
}
|
|
24945
|
-
|
|
24946
|
-
|
|
24947
|
-
|
|
24948
|
-
|
|
24949
|
-
|
|
24950
|
-
|
|
24951
|
-
|
|
24952
|
-
|
|
25015
|
+
}
|
|
25016
|
+
renameSync2(temporary, join10(directory, `${name}.json`));
|
|
25017
|
+
return true;
|
|
25018
|
+
}
|
|
25019
|
+
function drainInbox(cwd, owner, accept = () => true) {
|
|
25020
|
+
const pushes = owner ? [] : drainLegacyInbox(cwd);
|
|
25021
|
+
const directory = buildInboxQueuePath(cwd, owner);
|
|
25022
|
+
let names;
|
|
25023
|
+
try {
|
|
25024
|
+
names = readdirSync4(directory).filter((file2) => file2.endsWith(".json")).sort();
|
|
25025
|
+
} catch (error62) {
|
|
25026
|
+
if (error62.code === "ENOENT") return pushes;
|
|
25027
|
+
throw error62;
|
|
25028
|
+
}
|
|
25029
|
+
if (owner) {
|
|
25030
|
+
const receipts = join10(directory, "receipts");
|
|
25031
|
+
mkdirSync5(receipts, { recursive: true });
|
|
25032
|
+
const available = [];
|
|
25033
|
+
for (const name of names) {
|
|
24953
25034
|
try {
|
|
24954
|
-
|
|
24955
|
-
} catch {
|
|
25035
|
+
available.push({ name, push: JSON.parse(readFileSync8(join10(directory, name), "utf8")) });
|
|
25036
|
+
} catch (error62) {
|
|
25037
|
+
if (error62.code !== "ENOENT") throw error62;
|
|
24956
25038
|
}
|
|
24957
25039
|
}
|
|
24958
|
-
|
|
24959
|
-
|
|
24960
|
-
|
|
24961
|
-
|
|
24962
|
-
|
|
24963
|
-
try {
|
|
24964
|
-
const fd = openSync(rollout.path, "r");
|
|
25040
|
+
available.sort((a, b) => a.push.message.createdAt.localeCompare(b.push.message.createdAt) || a.push.message.id.localeCompare(b.push.message.id));
|
|
25041
|
+
for (const { name, push } of available) {
|
|
25042
|
+
if (!accept(push)) continue;
|
|
25043
|
+
const path2 = join10(directory, name);
|
|
25044
|
+
const receipt = join10(receipts, name.slice(0, -5));
|
|
24965
25045
|
try {
|
|
24966
|
-
|
|
24967
|
-
|
|
24968
|
-
|
|
24969
|
-
|
|
25046
|
+
writeFileSync6(receipt, "", { flag: "wx", mode: 384 });
|
|
25047
|
+
} catch (error62) {
|
|
25048
|
+
if (error62.code === "EEXIST") {
|
|
25049
|
+
rmSync5(path2, { force: true });
|
|
25050
|
+
continue;
|
|
25051
|
+
}
|
|
25052
|
+
throw error62;
|
|
24970
25053
|
}
|
|
24971
|
-
|
|
24972
|
-
|
|
25054
|
+
pushes.push(push);
|
|
25055
|
+
rmSync5(path2, { force: true });
|
|
24973
25056
|
}
|
|
24974
|
-
|
|
24975
|
-
|
|
24976
|
-
|
|
24977
|
-
const
|
|
24978
|
-
if (!found) continue;
|
|
24979
|
-
let dir;
|
|
25057
|
+
return pushes;
|
|
25058
|
+
}
|
|
25059
|
+
for (const name of names) {
|
|
25060
|
+
const taken = join10(directory, `${name}.${randomUUID6()}.draining`);
|
|
24980
25061
|
try {
|
|
24981
|
-
|
|
24982
|
-
} catch {
|
|
24983
|
-
continue;
|
|
25062
|
+
renameSync2(join10(directory, name), taken);
|
|
25063
|
+
} catch (error62) {
|
|
25064
|
+
if (error62.code === "ENOENT") continue;
|
|
25065
|
+
throw error62;
|
|
25066
|
+
}
|
|
25067
|
+
try {
|
|
25068
|
+
pushes.push(JSON.parse(readFileSync8(taken, "utf8")));
|
|
25069
|
+
} finally {
|
|
25070
|
+
rmSync5(taken, { force: true });
|
|
24984
25071
|
}
|
|
24985
|
-
if (isSamePath(dir, cwd)) return rollout.id;
|
|
24986
25072
|
}
|
|
24987
|
-
return
|
|
25073
|
+
return pushes;
|
|
24988
25074
|
}
|
|
24989
|
-
function
|
|
24990
|
-
const path2 =
|
|
24991
|
-
if (!path2) return
|
|
25075
|
+
function drainLegacyInbox(cwd) {
|
|
25076
|
+
const path2 = buildInboxPath(cwd);
|
|
25077
|
+
if (!existsSync7(path2)) return [];
|
|
25078
|
+
const taken = `${path2}.${process.pid}.${Date.now()}.draining`;
|
|
24992
25079
|
try {
|
|
24993
|
-
|
|
25080
|
+
renameSync2(path2, taken);
|
|
25081
|
+
} catch {
|
|
25082
|
+
return [];
|
|
25083
|
+
}
|
|
25084
|
+
const lines = readFileSync8(taken, "utf8").split("\n").filter(Boolean);
|
|
25085
|
+
rmSync5(taken, { force: true });
|
|
25086
|
+
return lines.flatMap((line) => {
|
|
24994
25087
|
try {
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
return
|
|
25088
|
+
return [JSON.parse(line)];
|
|
25089
|
+
} catch {
|
|
25090
|
+
return [];
|
|
25091
|
+
}
|
|
25092
|
+
});
|
|
25093
|
+
}
|
|
25094
|
+
|
|
25095
|
+
// src/platform/session-recovery.ts
|
|
25096
|
+
var SessionRecovery = class {
|
|
25097
|
+
constructor(api, owner, cwd, deliverPush) {
|
|
25098
|
+
this.api = api;
|
|
25099
|
+
this.owner = owner;
|
|
25100
|
+
this.deliverPush = deliverPush;
|
|
25101
|
+
this.path = join11(dirname6(buildConfigPath()), "cursors", `${generateInboxKey(cwd, owner)}.json`);
|
|
25102
|
+
}
|
|
25103
|
+
api;
|
|
25104
|
+
owner;
|
|
25105
|
+
deliverPush;
|
|
25106
|
+
cursor = null;
|
|
25107
|
+
preparing = null;
|
|
25108
|
+
syncing = null;
|
|
25109
|
+
resync = false;
|
|
25110
|
+
retry = null;
|
|
25111
|
+
stopped = false;
|
|
25112
|
+
path;
|
|
25113
|
+
syncCursor() {
|
|
25114
|
+
return this.preparing ??= this.fetchCursor().catch((error62) => {
|
|
25115
|
+
this.preparing = null;
|
|
25116
|
+
throw error62;
|
|
25117
|
+
});
|
|
25118
|
+
}
|
|
25119
|
+
async fetchCursor() {
|
|
25120
|
+
try {
|
|
25121
|
+
const value = JSON.parse(readFileSync9(this.path, "utf8"));
|
|
25122
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid saved message cursor");
|
|
25123
|
+
this.cursor = value;
|
|
25124
|
+
} catch (error62) {
|
|
25125
|
+
if (error62.code !== "ENOENT") throw error62;
|
|
25126
|
+
const page = await this.api.get(`/projects/${this.owner.projectId}/session/messages`, ApiResponses.SessionMessages);
|
|
25127
|
+
persistLocalState(this.path, JSON.stringify(page.cursor), false);
|
|
25128
|
+
await this.fetchCursor();
|
|
25129
|
+
}
|
|
25130
|
+
}
|
|
25131
|
+
sync() {
|
|
25132
|
+
if (this.stopped) return Promise.resolve();
|
|
25133
|
+
if (this.syncing) {
|
|
25134
|
+
this.resync = true;
|
|
25135
|
+
return this.syncing;
|
|
25136
|
+
}
|
|
25137
|
+
return this.syncing ??= this.syncMessages().catch(() => {
|
|
25138
|
+
if (!this.stopped && !this.retry) this.retry = setTimeout(() => {
|
|
25139
|
+
this.retry = null;
|
|
25140
|
+
void this.sync();
|
|
25141
|
+
}, 2e4);
|
|
25142
|
+
}).finally(() => {
|
|
25143
|
+
this.syncing = null;
|
|
25144
|
+
});
|
|
25145
|
+
}
|
|
25146
|
+
async syncMessages() {
|
|
25147
|
+
await this.syncCursor();
|
|
25148
|
+
if (this.retry) clearTimeout(this.retry);
|
|
25149
|
+
this.retry = null;
|
|
25150
|
+
while (!this.stopped) {
|
|
25151
|
+
this.resync = false;
|
|
25152
|
+
const page = await this.api.get(`/projects/${this.owner.projectId}/session/messages?cursor=${this.cursor}`, ApiResponses.SessionMessages);
|
|
25153
|
+
if (this.stopped) return;
|
|
25154
|
+
if (page.cursor < this.cursor || page.hasMore && page.cursor === this.cursor) throw new Error("Message recovery did not advance");
|
|
25155
|
+
for (const push of page.pushes) this.deliverPush(push);
|
|
25156
|
+
persistLocalState(this.path, JSON.stringify(page.cursor));
|
|
25157
|
+
this.cursor = page.cursor;
|
|
25158
|
+
if (!page.hasMore && !this.resync) return;
|
|
25159
|
+
}
|
|
25160
|
+
}
|
|
25161
|
+
stop() {
|
|
25162
|
+
this.stopped = true;
|
|
25163
|
+
if (this.retry) clearTimeout(this.retry);
|
|
25164
|
+
this.retry = null;
|
|
25165
|
+
}
|
|
25166
|
+
};
|
|
25167
|
+
|
|
25168
|
+
// src/platform/live.ts
|
|
25169
|
+
var PING_EVERY_MS = 25e3;
|
|
25170
|
+
var PONG_WITHIN_MS = 1e4;
|
|
25171
|
+
var STANDBY_POLL_MS = 2e4;
|
|
25172
|
+
var HELLO_WITHIN_MS = 1e4;
|
|
25173
|
+
function parseSessionPush(data) {
|
|
25174
|
+
try {
|
|
25175
|
+
const parsed = SessionPush.safeParse(JSON.parse(data));
|
|
25176
|
+
return parsed.success ? parsed.data : null;
|
|
25177
|
+
} catch {
|
|
25178
|
+
return null;
|
|
25179
|
+
}
|
|
25180
|
+
}
|
|
25181
|
+
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
25182
|
+
return new Promise((resolve6, reject) => {
|
|
25183
|
+
let finished = false;
|
|
25184
|
+
const finishWait = (outcome) => {
|
|
25185
|
+
if (finished) return;
|
|
25186
|
+
finished = true;
|
|
25187
|
+
if (timer) clearTimeout(timer);
|
|
25188
|
+
held.stop(outcome.kind === "message" ? "handled" : outcome.kind === "timeout" ? "timeout" : "released");
|
|
25189
|
+
setTimeout(() => resolve6(outcome), 50);
|
|
25190
|
+
};
|
|
25191
|
+
const held = new HeldSession(api, projectId, taskKey, {
|
|
25192
|
+
claim: () => options.claim ?? { harness: null, directory: null, branch: null },
|
|
25193
|
+
reconnect: false,
|
|
25194
|
+
onHello: options.onHello,
|
|
25195
|
+
onEnd: () => finishWait({ kind: "ended" }),
|
|
25196
|
+
onDrop: (reason) => finishWait({ kind: "dropped", reason }),
|
|
25197
|
+
onMessage: (push) => {
|
|
25198
|
+
if (!options.onMessage?.(push)) finishWait({ kind: "message", push });
|
|
25199
|
+
}
|
|
25200
|
+
});
|
|
25201
|
+
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => finishWait({ kind: "timeout" }), options.timeoutMs) : null;
|
|
25202
|
+
void held.start().catch((error62) => {
|
|
25203
|
+
if (finished) return;
|
|
25204
|
+
finished = true;
|
|
25205
|
+
if (timer) clearTimeout(timer);
|
|
25206
|
+
held.stop();
|
|
25207
|
+
reject(error62);
|
|
25208
|
+
});
|
|
25209
|
+
});
|
|
25210
|
+
}
|
|
25211
|
+
var HeldSession = class {
|
|
25212
|
+
constructor(api, projectId, taskKey, hooks) {
|
|
25213
|
+
this.api = api;
|
|
25214
|
+
this.projectId = projectId;
|
|
25215
|
+
this.taskKey = taskKey;
|
|
25216
|
+
this.hooks = hooks;
|
|
25217
|
+
this.identity = { ...hooks.claim() };
|
|
25218
|
+
}
|
|
25219
|
+
api;
|
|
25220
|
+
projectId;
|
|
25221
|
+
taskKey;
|
|
25222
|
+
hooks;
|
|
25223
|
+
socket = null;
|
|
25224
|
+
stopped = false;
|
|
25225
|
+
connecting = false;
|
|
25226
|
+
timer = null;
|
|
25227
|
+
heartbeat = null;
|
|
25228
|
+
pongDue = null;
|
|
25229
|
+
helloDue = null;
|
|
25230
|
+
greetings = /* @__PURE__ */ new Set();
|
|
25231
|
+
attempt = 0;
|
|
25232
|
+
connected = 0;
|
|
25233
|
+
delivered = /* @__PURE__ */ new Set();
|
|
25234
|
+
identity;
|
|
25235
|
+
session = null;
|
|
25236
|
+
/**
|
|
25237
|
+
* Whoever is waiting for a message. Handed each one, and answers whether it took it (a waiter for one thread lets
|
|
25238
|
+
* the rest pass to the inbox); handed null when the agent is made unreachable under them.
|
|
25239
|
+
*/
|
|
25240
|
+
waiters = /* @__PURE__ */ new Set();
|
|
25241
|
+
/** A write needs an accepted identity. A timeout must fail rather than silently send as another agent. */
|
|
25242
|
+
requireSession() {
|
|
25243
|
+
if (this.stopped) return Promise.reject(new Error("Session ended; start a new session before writing"));
|
|
25244
|
+
if (this.session) return Promise.resolve(this.session);
|
|
25245
|
+
return new Promise((resolve6, reject) => {
|
|
25246
|
+
const finishGreeting = (session) => {
|
|
25247
|
+
clearTimeout(timer);
|
|
25248
|
+
this.greetings.delete(finishGreeting);
|
|
25249
|
+
if (session) resolve6(session);
|
|
25250
|
+
else reject(new Error("Session is not connected; retry when the connection recovers"));
|
|
25251
|
+
};
|
|
25252
|
+
const timer = setTimeout(() => finishGreeting(null), HELLO_WITHIN_MS);
|
|
25253
|
+
this.greetings.add(finishGreeting);
|
|
25254
|
+
});
|
|
25255
|
+
}
|
|
25256
|
+
get isStopped() {
|
|
25257
|
+
return this.stopped;
|
|
25258
|
+
}
|
|
25259
|
+
async start() {
|
|
25260
|
+
await this.connect();
|
|
25261
|
+
}
|
|
25262
|
+
async connect() {
|
|
25263
|
+
if (this.stopped || this.connecting || this.socket) return;
|
|
25264
|
+
this.connecting = true;
|
|
25265
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25266
|
+
try {
|
|
25267
|
+
const ticket = await this.api.post(this.taskKey ? `/projects/${this.projectId}/tasks/${encodeURIComponent(this.taskKey)}/session/ticket` : `/projects/${this.projectId}/session/ticket`, ApiResponses.ConnectTicket, this.identity);
|
|
25268
|
+
if (this.stopped) return;
|
|
25269
|
+
const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
25270
|
+
this.socket = socket;
|
|
25271
|
+
let greeted = false;
|
|
25272
|
+
this.helloDue = setTimeout(() => this.retryConnection(socket, "no hello"), HELLO_WITHIN_MS);
|
|
25273
|
+
socket.addEventListener("open", () => {
|
|
25274
|
+
if (this.stopped || this.socket !== socket) return;
|
|
25275
|
+
this.startHeartbeat(socket);
|
|
25276
|
+
});
|
|
25277
|
+
socket.addEventListener("message", (event) => {
|
|
25278
|
+
if (this.stopped || this.socket !== socket) return;
|
|
25279
|
+
const data = String(event.data);
|
|
25280
|
+
if (data === "pong") {
|
|
25281
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
25282
|
+
this.pongDue = null;
|
|
25283
|
+
return;
|
|
25284
|
+
}
|
|
25285
|
+
const push = parseSessionPush(data);
|
|
25286
|
+
if (!push) return;
|
|
25287
|
+
if (push.type === "hello") {
|
|
25288
|
+
if (greeted) return;
|
|
25289
|
+
try {
|
|
25290
|
+
persistRememberedSession(process.cwd(), push.session.id, this.identity.conversation);
|
|
25291
|
+
} catch {
|
|
25292
|
+
this.retryConnection(socket, "cannot save session identity");
|
|
25293
|
+
return;
|
|
25294
|
+
}
|
|
25295
|
+
greeted = true;
|
|
25296
|
+
this.api.sessionId = push.session.id;
|
|
25297
|
+
this.session = push.session;
|
|
25298
|
+
this.taskKey = push.session.taskKey;
|
|
25299
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25300
|
+
this.helloDue = null;
|
|
25301
|
+
this.attempt = 0;
|
|
25302
|
+
for (const greeting of [...this.greetings]) greeting(push.session);
|
|
25303
|
+
this.hooks.onHello?.(push.session);
|
|
25304
|
+
this.connected += 1;
|
|
25305
|
+
if (this.connected > 1) void Promise.resolve().then(() => !this.stopped && this.socket === socket ? this.hooks.onReconnect?.(push.session) : void 0).catch(() => void 0);
|
|
25306
|
+
return;
|
|
25307
|
+
}
|
|
25308
|
+
if (push.type === "end") {
|
|
25309
|
+
this.hooks.onEnd?.();
|
|
25310
|
+
this.stop();
|
|
25311
|
+
return;
|
|
25312
|
+
}
|
|
25313
|
+
if (push.type === "reachable") {
|
|
25314
|
+
if (this.session) this.session = { ...this.session, reachable: push.on };
|
|
25315
|
+
if (!push.on) this.releaseWaiters();
|
|
25316
|
+
this.hooks.onReachable?.(push.on);
|
|
25317
|
+
return;
|
|
25318
|
+
}
|
|
25319
|
+
if (push.type !== "thread") return;
|
|
25320
|
+
try {
|
|
25321
|
+
this.deliverPush(push);
|
|
25322
|
+
} catch {
|
|
25323
|
+
this.retryConnection(socket, "delivery failed");
|
|
25324
|
+
}
|
|
25325
|
+
});
|
|
25326
|
+
socket.addEventListener("close", (event) => {
|
|
25327
|
+
if (this.socket !== socket) return;
|
|
25328
|
+
this.stopHeartbeat();
|
|
25329
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25330
|
+
this.helloDue = null;
|
|
25331
|
+
this.socket = null;
|
|
25332
|
+
if (this.stopped) return;
|
|
25333
|
+
if (event.reason === "ended") {
|
|
25334
|
+
this.hooks.onEnd?.();
|
|
25335
|
+
this.stop();
|
|
25336
|
+
return;
|
|
25337
|
+
}
|
|
25338
|
+
this.hooks.onDrop?.(event.reason || `code ${event.code}`);
|
|
25339
|
+
if (this.hooks.reconnect === false) {
|
|
25340
|
+
this.stop();
|
|
25341
|
+
return;
|
|
25342
|
+
}
|
|
25343
|
+
if (event.reason === "replaced") this.standBy();
|
|
25344
|
+
else this.schedule();
|
|
25345
|
+
});
|
|
25346
|
+
socket.addEventListener("error", () => {
|
|
25347
|
+
this.retryConnection(socket, "socket error");
|
|
25348
|
+
});
|
|
25349
|
+
} catch (error62) {
|
|
25350
|
+
if (this.hooks.reconnect === false) {
|
|
25351
|
+
this.stop();
|
|
25352
|
+
throw error62;
|
|
25353
|
+
}
|
|
25354
|
+
this.hooks.onDrop?.(error62 instanceof Error ? error62.message : String(error62));
|
|
25355
|
+
this.schedule();
|
|
24998
25356
|
} finally {
|
|
24999
|
-
|
|
25357
|
+
this.connecting = false;
|
|
25000
25358
|
}
|
|
25001
|
-
} catch {
|
|
25002
|
-
return true;
|
|
25003
25359
|
}
|
|
25004
|
-
|
|
25005
|
-
|
|
25006
|
-
|
|
25007
|
-
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
|
|
25011
|
-
|
|
25012
|
-
} catch {
|
|
25013
|
-
return;
|
|
25014
|
-
}
|
|
25015
|
-
for (const entry of entries) {
|
|
25016
|
-
const path2 = join8(dir, entry.name);
|
|
25017
|
-
if (entry.isDirectory()) {
|
|
25018
|
-
if (depth < 3) findRolloutInDirectory(path2, depth + 1);
|
|
25019
|
-
continue;
|
|
25360
|
+
/** Live and recovered messages follow exactly the same local delivery path. */
|
|
25361
|
+
deliverPush(push) {
|
|
25362
|
+
if (this.stopped || this.session?.reachable === false || this.delivered.has(push.message.id)) return;
|
|
25363
|
+
if (this.hooks.onPush?.(push) === false) return;
|
|
25364
|
+
for (const waiter of [...this.waiters]) {
|
|
25365
|
+
if (waiter(push)) {
|
|
25366
|
+
this.markPushDelivered(push.message.id);
|
|
25367
|
+
return;
|
|
25020
25368
|
}
|
|
25021
|
-
|
|
25369
|
+
}
|
|
25370
|
+
this.hooks.onMessage(push);
|
|
25371
|
+
this.markPushDelivered(push.message.id);
|
|
25372
|
+
}
|
|
25373
|
+
markPushDelivered(id) {
|
|
25374
|
+
this.delivered.add(id);
|
|
25375
|
+
if (this.delivered.size > 1e4) this.delivered.delete(this.delivered.values().next().value);
|
|
25376
|
+
}
|
|
25377
|
+
/** A ping every 25s; no pong within 10s means the socket is dead however open it looks. Close it and come back. */
|
|
25378
|
+
startHeartbeat(socket) {
|
|
25379
|
+
this.stopHeartbeat();
|
|
25380
|
+
this.heartbeat = setInterval(() => {
|
|
25381
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
25022
25382
|
try {
|
|
25023
|
-
|
|
25024
|
-
if (!best || mtime > best.mtime) best = { path: path2, mtime };
|
|
25383
|
+
socket.send("ping");
|
|
25025
25384
|
} catch {
|
|
25385
|
+
this.retryConnection(socket, "ping failed");
|
|
25386
|
+
return;
|
|
25026
25387
|
}
|
|
25027
|
-
|
|
25028
|
-
|
|
25029
|
-
|
|
25030
|
-
|
|
25031
|
-
}
|
|
25032
|
-
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
|
|
25036
|
-
|
|
25037
|
-
|
|
25038
|
-
|
|
25039
|
-
|
|
25040
|
-
|
|
25041
|
-
|
|
25042
|
-
const fd = openSync(path2, "r");
|
|
25388
|
+
if (!this.pongDue) {
|
|
25389
|
+
this.pongDue = setTimeout(() => {
|
|
25390
|
+
this.pongDue = null;
|
|
25391
|
+
this.retryConnection(socket, "no pong");
|
|
25392
|
+
}, PONG_WITHIN_MS);
|
|
25393
|
+
}
|
|
25394
|
+
}, PING_EVERY_MS);
|
|
25395
|
+
}
|
|
25396
|
+
/** Detach first: close may arrive late or never, and an old callback must not change its replacement. */
|
|
25397
|
+
retryConnection(socket, reason) {
|
|
25398
|
+
if (this.stopped || this.socket !== socket) return;
|
|
25399
|
+
this.socket = null;
|
|
25400
|
+
this.stopHeartbeat();
|
|
25401
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25402
|
+
this.helloDue = null;
|
|
25043
25403
|
try {
|
|
25044
|
-
|
|
25045
|
-
|
|
25046
|
-
} finally {
|
|
25047
|
-
closeSync(fd);
|
|
25404
|
+
socket.close(4e3, reason);
|
|
25405
|
+
} catch {
|
|
25048
25406
|
}
|
|
25049
|
-
|
|
25050
|
-
|
|
25407
|
+
this.hooks.onDrop?.(reason);
|
|
25408
|
+
if (this.hooks.reconnect !== false) this.schedule();
|
|
25409
|
+
else this.stop();
|
|
25051
25410
|
}
|
|
25052
|
-
|
|
25053
|
-
|
|
25054
|
-
|
|
25055
|
-
|
|
25056
|
-
else if (line.includes('"task_complete"') || line.includes('"turn_aborted"')) state = "idle";
|
|
25411
|
+
releaseWaiters() {
|
|
25412
|
+
const waiters = [...this.waiters];
|
|
25413
|
+
this.waiters.clear();
|
|
25414
|
+
for (const waiter of waiters) waiter(null);
|
|
25057
25415
|
}
|
|
25058
|
-
|
|
25059
|
-
|
|
25060
|
-
|
|
25061
|
-
|
|
25062
|
-
|
|
25063
|
-
try {
|
|
25064
|
-
execFileSync4(process.platform === "win32" ? "where" : "which", ["codex"], { stdio: "ignore" });
|
|
25065
|
-
return "codex";
|
|
25066
|
-
} catch {
|
|
25416
|
+
stopHeartbeat() {
|
|
25417
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
25418
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
25419
|
+
this.heartbeat = null;
|
|
25420
|
+
this.pongDue = null;
|
|
25067
25421
|
}
|
|
25068
|
-
|
|
25422
|
+
schedule() {
|
|
25423
|
+
if (this.stopped) return;
|
|
25424
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25425
|
+
this.attempt += 1;
|
|
25426
|
+
this.timer = setTimeout(() => void this.connect(), Math.min(3e4, 1e3 * 2 ** Math.min(this.attempt, 5)));
|
|
25427
|
+
}
|
|
25428
|
+
/** Another process holds this session now: look now and then, and take it back once it is no longer live. */
|
|
25429
|
+
standBy() {
|
|
25430
|
+
if (this.stopped) return;
|
|
25431
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25432
|
+
this.timer = setTimeout(async () => {
|
|
25433
|
+
if (this.stopped) return;
|
|
25434
|
+
let holder = true;
|
|
25435
|
+
try {
|
|
25436
|
+
const sessions = await this.api.get(`/projects/${this.projectId}/sessions`, ApiResponses.Sessions);
|
|
25437
|
+
holder = sessions.some((session) => session.id === this.session?.id && session.state === "live");
|
|
25438
|
+
} catch {
|
|
25439
|
+
}
|
|
25440
|
+
if (holder) this.standBy();
|
|
25441
|
+
else void this.connect();
|
|
25442
|
+
}, STANDBY_POLL_MS);
|
|
25443
|
+
}
|
|
25444
|
+
stop(reason = "released") {
|
|
25445
|
+
if (this.stopped) return;
|
|
25446
|
+
this.stopped = true;
|
|
25447
|
+
this.releaseWaiters();
|
|
25448
|
+
for (const greeting of [...this.greetings]) greeting(null);
|
|
25449
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25450
|
+
this.helloDue = null;
|
|
25451
|
+
if (reason !== "handled" && this.api.sessionId === this.session?.id) this.api.sessionId = null;
|
|
25452
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25453
|
+
this.stopHeartbeat();
|
|
25454
|
+
const socket = this.socket;
|
|
25455
|
+
this.socket = null;
|
|
25069
25456
|
try {
|
|
25070
|
-
|
|
25071
|
-
if (exe && basename2(exe).startsWith("codex")) return exe;
|
|
25457
|
+
socket?.close(1e3, reason);
|
|
25072
25458
|
} catch {
|
|
25073
25459
|
}
|
|
25074
25460
|
}
|
|
25075
|
-
|
|
25076
|
-
}
|
|
25077
|
-
function fetchHarnessConversation(cwd) {
|
|
25078
|
-
const harness = parseHarnessFromCommand(fetchParentCommand());
|
|
25079
|
-
if (harness === "claude") {
|
|
25080
|
-
const id = fetchHarnessRegistration()?.sessionId;
|
|
25081
|
-
return id ? { id, harness } : null;
|
|
25082
|
-
}
|
|
25083
|
-
if (harness === "codex" && !isNonInteractiveHarness()) {
|
|
25084
|
-
const id = fetchCodexConversationByCwd(fetchCodexHomes().map((home) => join8(home, "sessions")), cwd);
|
|
25085
|
-
return id ? { id, harness } : null;
|
|
25086
|
-
}
|
|
25087
|
-
return null;
|
|
25088
|
-
}
|
|
25089
|
-
|
|
25090
|
-
// src/platform/agent-identity.ts
|
|
25091
|
-
var processConversation = randomUUID4();
|
|
25092
|
-
var startedAt = Date.now();
|
|
25093
|
-
function fetchKnownConversation(cwd) {
|
|
25094
|
-
const remembered = fetchRememberedConversation(cwd, startedAt - 12e4);
|
|
25095
|
-
if (remembered?.harness === "codex" && isMainChatThread(fetchCodexHomes().map((home) => join9(home, "sessions")), remembered.id)) return remembered;
|
|
25096
|
-
return fetchHarnessConversation(cwd) ?? remembered;
|
|
25097
|
-
}
|
|
25098
|
-
function resolveAgentIdentity(cwd, fallback = "process") {
|
|
25099
|
-
const conversation = fetchKnownConversation(cwd);
|
|
25100
|
-
const repo = fetchRepo();
|
|
25101
|
-
return {
|
|
25102
|
-
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve3(cwd)}`),
|
|
25103
|
-
harness: conversation?.harness ?? null,
|
|
25104
|
-
directory: repo ? basename3(repo.worktree) : basename3(cwd),
|
|
25105
|
-
branch: fetchCurrentBranch(cwd)
|
|
25106
|
-
};
|
|
25107
|
-
}
|
|
25461
|
+
};
|
|
25108
25462
|
|
|
25109
25463
|
// src/platform/bounded.ts
|
|
25110
25464
|
var RESULT_CAP = 16e3;
|
|
@@ -25117,13 +25471,23 @@ function truncateToolResult(result) {
|
|
|
25117
25471
|
|
|
25118
25472
|
// src/platform/mcp.ts
|
|
25119
25473
|
import { createInterface } from "readline";
|
|
25120
|
-
|
|
25121
|
-
|
|
25122
|
-
`);
|
|
25474
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25475
|
+
function buildMcpRequestHandler(tools, info, sendJsonRpcMessage) {
|
|
25123
25476
|
const sendJsonRpcResult = (id, result) => sendJsonRpcMessage({ jsonrpc: "2.0", id, result });
|
|
25124
25477
|
const sendJsonRpcError = (id, code, message) => sendJsonRpcMessage({ jsonrpc: "2.0", id, error: { code, message } });
|
|
25125
|
-
|
|
25126
|
-
|
|
25478
|
+
return async (value) => {
|
|
25479
|
+
if (!isRecord(value) || value.jsonrpc !== "2.0" || typeof value.method !== "string" || value.id !== void 0 && value.id !== null && typeof value.id !== "string" && typeof value.id !== "number") {
|
|
25480
|
+
sendJsonRpcError(null, -32600, "Invalid request");
|
|
25481
|
+
return;
|
|
25482
|
+
}
|
|
25483
|
+
const request = value;
|
|
25484
|
+
const { id, method } = request;
|
|
25485
|
+
if (id === void 0) return;
|
|
25486
|
+
if (request.params !== void 0 && !isRecord(request.params)) {
|
|
25487
|
+
sendJsonRpcError(id, -32602, "Params must be an object");
|
|
25488
|
+
return;
|
|
25489
|
+
}
|
|
25490
|
+
const params = request.params ?? {};
|
|
25127
25491
|
switch (method) {
|
|
25128
25492
|
case "initialize":
|
|
25129
25493
|
sendJsonRpcResult(id, { protocolVersion: params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: info });
|
|
@@ -25139,6 +25503,10 @@ function serveMcp(tools, info) {
|
|
|
25139
25503
|
return;
|
|
25140
25504
|
case "tools/call": {
|
|
25141
25505
|
const name = params.name;
|
|
25506
|
+
if (typeof name !== "string" || params.arguments !== void 0 && !isRecord(params.arguments)) {
|
|
25507
|
+
sendJsonRpcError(id, -32602, "Tool name and arguments are invalid");
|
|
25508
|
+
return;
|
|
25509
|
+
}
|
|
25142
25510
|
const tool = tools.find((candidate) => candidate.name === name);
|
|
25143
25511
|
if (!tool) {
|
|
25144
25512
|
sendJsonRpcError(id, -32602, `No tool named ${name}`);
|
|
@@ -25156,6 +25524,11 @@ function serveMcp(tools, info) {
|
|
|
25156
25524
|
if (id !== void 0) sendJsonRpcError(id, -32601, `Unknown method ${method}`);
|
|
25157
25525
|
}
|
|
25158
25526
|
};
|
|
25527
|
+
}
|
|
25528
|
+
function serveMcp(tools, info) {
|
|
25529
|
+
const sendJsonRpcMessage = (message) => process.stdout.write(`${JSON.stringify(message)}
|
|
25530
|
+
`);
|
|
25531
|
+
const handleJsonRpcRequest = buildMcpRequestHandler(tools, info, sendJsonRpcMessage);
|
|
25159
25532
|
const lines = createInterface({ input: process.stdin });
|
|
25160
25533
|
lines.on("line", (line) => {
|
|
25161
25534
|
if (!line.trim()) return;
|
|
@@ -25163,20 +25536,19 @@ function serveMcp(tools, info) {
|
|
|
25163
25536
|
try {
|
|
25164
25537
|
request = JSON.parse(line);
|
|
25165
25538
|
} catch {
|
|
25166
|
-
|
|
25539
|
+
sendJsonRpcMessage({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
25167
25540
|
return;
|
|
25168
25541
|
}
|
|
25169
25542
|
void handleJsonRpcRequest(request);
|
|
25170
25543
|
});
|
|
25171
|
-
lines.on("close", () => process.exit(0));
|
|
25172
25544
|
}
|
|
25173
25545
|
|
|
25174
25546
|
// src/commands/tasks.ts
|
|
25175
|
-
import { readFileSync as
|
|
25547
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
25176
25548
|
|
|
25177
25549
|
// src/commands/items.ts
|
|
25178
|
-
import { readFileSync as
|
|
25179
|
-
var fetchTextArgument = (value) => value === "-" ?
|
|
25550
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
25551
|
+
var fetchTextArgument = (value) => value === "-" ? readFileSync10(0, "utf8").trim() : value;
|
|
25180
25552
|
var buildItemsPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
25181
25553
|
function formatPlanItems(items) {
|
|
25182
25554
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -25203,7 +25575,7 @@ function registerItemCommands(task, registerProjectOption) {
|
|
|
25203
25575
|
registerProjectOption(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) => {
|
|
25204
25576
|
const api = buildAuthedApiClient();
|
|
25205
25577
|
const project = await resolveProject(api, options.project);
|
|
25206
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
25578
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync10(0, "utf8") : lines.join("\n");
|
|
25207
25579
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
25208
25580
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
25209
25581
|
const items = await api.post(buildItemsPath(project.id, key), ApiResponses.TaskItems, { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -25260,7 +25632,7 @@ function parseOneOf(value, allowed, what) {
|
|
|
25260
25632
|
if (allowed.includes(normalized)) return normalized;
|
|
25261
25633
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
25262
25634
|
}
|
|
25263
|
-
var fetchTextArgument2 = (value) => value === "-" ?
|
|
25635
|
+
var fetchTextArgument2 = (value) => value === "-" ? readFileSync11(0, "utf8").trim() : value;
|
|
25264
25636
|
var buildTaskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
25265
25637
|
var fetchProject = (api, id) => api.get(`/projects/${id}`, ApiResponses.Project);
|
|
25266
25638
|
function resolveStatus(project, ref) {
|
|
@@ -25411,21 +25783,21 @@ ${buildTaskUrl(api.baseUrl, project.id, created)}`);
|
|
|
25411
25783
|
}
|
|
25412
25784
|
|
|
25413
25785
|
// src/platform/update-check.ts
|
|
25414
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
25415
|
-
import { dirname as
|
|
25786
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
25787
|
+
import { dirname as dirname7, join as join12 } from "path";
|
|
25416
25788
|
var CHECK_EVERY_MS = 60 * 60 * 1e3;
|
|
25417
25789
|
var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
|
|
25418
|
-
var buildUpdateCachePath = () =>
|
|
25790
|
+
var buildUpdateCachePath = () => join12(dirname7(buildConfigPath()), "update-check.json");
|
|
25419
25791
|
function fetchUpdateCache() {
|
|
25420
25792
|
try {
|
|
25421
|
-
return JSON.parse(
|
|
25793
|
+
return JSON.parse(readFileSync12(buildUpdateCachePath(), "utf8"));
|
|
25422
25794
|
} catch {
|
|
25423
25795
|
return { checkedAt: 0, latest: null };
|
|
25424
25796
|
}
|
|
25425
25797
|
}
|
|
25426
25798
|
function persistUpdateCache(cache) {
|
|
25427
|
-
|
|
25428
|
-
|
|
25799
|
+
mkdirSync6(dirname7(buildUpdateCachePath()), { recursive: true });
|
|
25800
|
+
writeFileSync7(buildUpdateCachePath(), JSON.stringify(cache));
|
|
25429
25801
|
}
|
|
25430
25802
|
function isNewerVersion(a, b) {
|
|
25431
25803
|
const parseVersionParts = (version2) => version2.split(".").map((part) => Number.parseInt(part, 10));
|
|
@@ -25454,11 +25826,11 @@ function buildUpdateNotice(current) {
|
|
|
25454
25826
|
if (!latest || !isNewerVersion(latest, current)) return null;
|
|
25455
25827
|
return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
|
|
25456
25828
|
}
|
|
25457
|
-
var isUpdateCheckEnabled = () => !existsSync8(
|
|
25829
|
+
var isUpdateCheckEnabled = () => !existsSync8(join12(dirname7(buildConfigPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
|
|
25458
25830
|
|
|
25459
25831
|
// src/commands/threads.ts
|
|
25460
|
-
import { readFileSync as
|
|
25461
|
-
var fetchTextArgument3 = (value) => value === "-" ?
|
|
25832
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
25833
|
+
var fetchTextArgument3 = (value) => value === "-" ? readFileSync13(0, "utf8").trim() : value;
|
|
25462
25834
|
var formatMessageTime = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
25463
25835
|
var formatAuthor = (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}` : ""})`;
|
|
25464
25836
|
function formatThreadLine(thread) {
|
|
@@ -25529,85 +25901,6 @@ function registerThreadCommands(program3, task) {
|
|
|
25529
25901
|
}
|
|
25530
25902
|
var formatStartedLine = (result) => result.thread.to.length ? `Started ${result.thread.id} to ${result.thread.to.map((recipient) => recipient.agent).join(", ")}${result.delivered ? `; ${result.delivered} of them ha${result.delivered === 1 ? "s" : "ve"} it now.` : "; they read it when they are 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.`;
|
|
25531
25903
|
|
|
25532
|
-
// src/platform/inbox.ts
|
|
25533
|
-
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
25534
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync12, readdirSync as readdirSync3, realpathSync as realpathSync3, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
25535
|
-
import { dirname as dirname5, join as join11 } from "path";
|
|
25536
|
-
var buildInboxDir = () => join11(dirname5(buildConfigPath()), "inbox");
|
|
25537
|
-
var resolveRealCwd2 = (cwd) => {
|
|
25538
|
-
try {
|
|
25539
|
-
return realpathSync3(cwd);
|
|
25540
|
-
} catch {
|
|
25541
|
-
return cwd;
|
|
25542
|
-
}
|
|
25543
|
-
};
|
|
25544
|
-
var generateInboxKey = (cwd) => createHash4("sha256").update(resolveRealCwd2(cwd)).digest("hex").slice(0, 16);
|
|
25545
|
-
var buildInboxPath = (cwd) => join11(buildInboxDir(), `${generateInboxKey(cwd)}.jsonl`);
|
|
25546
|
-
var buildInboxQueuePath = (cwd) => join11(buildInboxDir(), generateInboxKey(cwd));
|
|
25547
|
-
var sequence = 0;
|
|
25548
|
-
function persistInboxPush(cwd, push) {
|
|
25549
|
-
const directory = buildInboxQueuePath(cwd);
|
|
25550
|
-
mkdirSync6(directory, { recursive: true });
|
|
25551
|
-
const name = `${Date.now()}.${process.pid}.${String(sequence++).padStart(12, "0")}.${randomUUID5()}`;
|
|
25552
|
-
const temporary = join11(directory, `${name}.writing`);
|
|
25553
|
-
writeFileSync7(temporary, JSON.stringify(push), { flag: "wx" });
|
|
25554
|
-
renameSync(temporary, join11(directory, `${name}.json`));
|
|
25555
|
-
}
|
|
25556
|
-
function hasInboxPushes(cwd) {
|
|
25557
|
-
if (existsSync9(buildInboxPath(cwd))) return true;
|
|
25558
|
-
try {
|
|
25559
|
-
return readdirSync3(buildInboxQueuePath(cwd)).some((file2) => file2.endsWith(".json"));
|
|
25560
|
-
} catch (error62) {
|
|
25561
|
-
if (error62.code === "ENOENT") return false;
|
|
25562
|
-
throw error62;
|
|
25563
|
-
}
|
|
25564
|
-
}
|
|
25565
|
-
function drainInbox(cwd) {
|
|
25566
|
-
const pushes = drainLegacyInbox(cwd);
|
|
25567
|
-
const directory = buildInboxQueuePath(cwd);
|
|
25568
|
-
let names;
|
|
25569
|
-
try {
|
|
25570
|
-
names = readdirSync3(directory).filter((file2) => file2.endsWith(".json")).sort();
|
|
25571
|
-
} catch (error62) {
|
|
25572
|
-
if (error62.code === "ENOENT") return pushes;
|
|
25573
|
-
throw error62;
|
|
25574
|
-
}
|
|
25575
|
-
for (const name of names) {
|
|
25576
|
-
const taken = join11(directory, `${name}.${randomUUID5()}.draining`);
|
|
25577
|
-
try {
|
|
25578
|
-
renameSync(join11(directory, name), taken);
|
|
25579
|
-
} catch (error62) {
|
|
25580
|
-
if (error62.code === "ENOENT") continue;
|
|
25581
|
-
throw error62;
|
|
25582
|
-
}
|
|
25583
|
-
try {
|
|
25584
|
-
pushes.push(JSON.parse(readFileSync12(taken, "utf8")));
|
|
25585
|
-
} finally {
|
|
25586
|
-
rmSync4(taken, { force: true });
|
|
25587
|
-
}
|
|
25588
|
-
}
|
|
25589
|
-
return pushes;
|
|
25590
|
-
}
|
|
25591
|
-
function drainLegacyInbox(cwd) {
|
|
25592
|
-
const path2 = buildInboxPath(cwd);
|
|
25593
|
-
if (!existsSync9(path2)) return [];
|
|
25594
|
-
const taken = `${path2}.${process.pid}.${Date.now()}.draining`;
|
|
25595
|
-
try {
|
|
25596
|
-
renameSync(path2, taken);
|
|
25597
|
-
} catch {
|
|
25598
|
-
return [];
|
|
25599
|
-
}
|
|
25600
|
-
const lines = readFileSync12(taken, "utf8").split("\n").filter(Boolean);
|
|
25601
|
-
rmSync4(taken, { force: true });
|
|
25602
|
-
return lines.flatMap((line) => {
|
|
25603
|
-
try {
|
|
25604
|
-
return [JSON.parse(line)];
|
|
25605
|
-
} catch {
|
|
25606
|
-
return [];
|
|
25607
|
-
}
|
|
25608
|
-
});
|
|
25609
|
-
}
|
|
25610
|
-
|
|
25611
25904
|
// src/commands/mcp.ts
|
|
25612
25905
|
var buildStringSchema = (description2) => ({ type: "string", description: description2 });
|
|
25613
25906
|
var buildIntegerSchema = (description2) => ({ type: "integer", description: description2 });
|
|
@@ -25626,143 +25919,104 @@ var formatSessionLine = (session, mine = null) => [
|
|
|
25626
25919
|
session.reachable ? "" : "(not visible to teammates)"
|
|
25627
25920
|
].filter(Boolean).join(" ");
|
|
25628
25921
|
async function buildUnreadDigest(api, projectId, taskKey) {
|
|
25629
|
-
const [about,
|
|
25922
|
+
const [about, general] = await Promise.all([
|
|
25630
25923
|
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`, ApiResponses.Threads) : Promise.resolve([]),
|
|
25631
|
-
api.get(`/projects/${projectId}/threads?participant=1&unread=1&read=1`, ApiResponses.Threads),
|
|
25632
25924
|
api.get(`/projects/${projectId}/threads?scope=general&unread=1&read=1`, ApiResponses.Threads)
|
|
25633
25925
|
]);
|
|
25634
25926
|
const lines = [];
|
|
25635
25927
|
if (about.length) lines.push(`Unread about ${taskKey} (read one with the thread tool):`, ...about.map(formatThreadLine));
|
|
25636
|
-
const elsewhere = mine.filter((thread) => !about.some((candidate) => candidate.id === thread.id));
|
|
25637
|
-
if (elsewhere.length) lines.push("Unread in threads you are part of (answers to you):", ...elsewhere.map(formatThreadLine));
|
|
25638
25928
|
if (general.length) lines.push("Unread general threads (context; nobody has to answer):", ...general.map(formatThreadLine));
|
|
25639
25929
|
return lines.length ? lines.join("\n") : "Nothing unread.";
|
|
25640
25930
|
}
|
|
25641
|
-
async function
|
|
25642
|
-
const
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25647
|
-
for (const thread of [...about, ...mine]) {
|
|
25648
|
-
if (seen.has(thread.id) || thread.unread === 0) continue;
|
|
25649
|
-
seen.add(thread.id);
|
|
25650
|
-
const detail = await api.get(`/projects/${projectId}/threads/${thread.id}?read=0`, ApiResponses.ThreadDetail);
|
|
25651
|
-
for (const message of detail.messages.slice(-thread.unread)) persistInboxPush(cwd, { thread: { id: detail.id, subject: detail.subject, taskKey: detail.taskKey, to: detail.to.length ? detail.to.map((recipient) => recipient.agent).join(", ") : null }, message });
|
|
25931
|
+
async function fetchProjectsOverview(api) {
|
|
25932
|
+
const list = await api.get("/projects", ApiResponses.Projects);
|
|
25933
|
+
let linked = null;
|
|
25934
|
+
try {
|
|
25935
|
+
linked = (await resolveProject(api)).key;
|
|
25936
|
+
} catch {
|
|
25652
25937
|
}
|
|
25938
|
+
return list.map((project) => `${project.key === linked ? "* " : " "}${project.key} ${project.name} (${project.openTaskCount} open)`).join("\n") + (linked ? "" : "\n\nThis repository is not linked: slop link <KEY>");
|
|
25653
25939
|
}
|
|
25654
|
-
function
|
|
25655
|
-
|
|
25656
|
-
const resolveProjectFromArgs = (
|
|
25940
|
+
function createProjectTools(api, project, identity) {
|
|
25941
|
+
api.sessionId = null;
|
|
25942
|
+
const resolveProjectFromArgs = async (_args) => project;
|
|
25943
|
+
const owner = { baseUrl: api.baseUrl, projectId: project.id, conversation: identity.conversation };
|
|
25944
|
+
const drainOwnInbox = (thread = null) => drainInbox(process.cwd(), owner, (push) => !thread || push.thread.id === thread);
|
|
25945
|
+
const persistOwnInboxPush = (push) => persistInboxPush(process.cwd(), push, owner);
|
|
25657
25946
|
let held = null;
|
|
25658
|
-
let
|
|
25659
|
-
const
|
|
25660
|
-
|
|
25661
|
-
|
|
25662
|
-
${pushes.map(formatPushBlock).join("\n\n")}`;
|
|
25663
|
-
const resolveCodexSessionRoots = () => {
|
|
25664
|
-
const own2 = resolveOwnHarnessHome("CODEX_HOME");
|
|
25665
|
-
return (own2 ? [own2, ...fetchCodexHomes()] : fetchCodexHomes()).map((home) => join12(home, "sessions"));
|
|
25666
|
-
};
|
|
25667
|
-
let flushing = false;
|
|
25668
|
-
const wakeCodexWithInboxWhenIdle = (conversation) => {
|
|
25669
|
-
if (flushing) return;
|
|
25670
|
-
flushing = true;
|
|
25671
|
-
const wakeWhenTurnIdle = () => {
|
|
25672
|
-
if (!hasInboxPushes(process.cwd())) {
|
|
25673
|
-
flushing = false;
|
|
25674
|
-
return;
|
|
25675
|
-
}
|
|
25676
|
-
if (fetchCodexTurnState(resolveCodexSessionRoots(), conversation) === "busy") {
|
|
25677
|
-
setTimeout(wakeWhenTurnIdle, 2e3).unref();
|
|
25678
|
-
return;
|
|
25679
|
-
}
|
|
25680
|
-
flushing = false;
|
|
25681
|
-
const pushes = drainInbox(process.cwd());
|
|
25682
|
-
if (pushes.length === 0) return;
|
|
25683
|
-
void wakeCodex(formatWakeMessage(pushes), conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }).then((woken) => {
|
|
25684
|
-
if (!woken) for (const push of pushes) persistInboxPush(process.cwd(), push);
|
|
25685
|
-
});
|
|
25686
|
-
};
|
|
25687
|
-
setTimeout(wakeWhenTurnIdle, 2e3).unref();
|
|
25688
|
-
};
|
|
25947
|
+
let stopped = false;
|
|
25948
|
+
const recovery = new SessionRecovery(api, owner, process.cwd(), (push) => held?.deliverPush(push));
|
|
25949
|
+
let waking = false;
|
|
25950
|
+
const resolveOwnAgentIdentity = () => identity;
|
|
25689
25951
|
const holdSession = async (projectId, taskKey) => {
|
|
25690
|
-
if (
|
|
25952
|
+
if (stopped) throw new Error("The project session is stopping");
|
|
25953
|
+
if (held && !held.isStopped && held.projectId === projectId && held.taskKey === taskKey) {
|
|
25954
|
+
await held.requireSession();
|
|
25955
|
+
return;
|
|
25956
|
+
}
|
|
25957
|
+
await recovery.syncCursor();
|
|
25958
|
+
if (stopped) throw new Error("The project session is stopping");
|
|
25959
|
+
if (held && !held.isStopped && held.taskKey === taskKey) {
|
|
25960
|
+
await held.requireSession();
|
|
25961
|
+
return;
|
|
25962
|
+
}
|
|
25691
25963
|
held?.stop();
|
|
25692
25964
|
held = new HeldSession(api, projectId, taskKey, {
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
// result. Nothing holds a turn open.
|
|
25699
|
-
onMessage: (push) => {
|
|
25965
|
+
onPush: persistOwnInboxPush,
|
|
25966
|
+
onMessage: () => {
|
|
25967
|
+
if (waking) return;
|
|
25968
|
+
waking = true;
|
|
25969
|
+
const text = `Messages are waiting for you on Slopware Tasks in project ${project.key}. Use session check with project ${project.id} to read them, then act or reply only if it changes something. Do not acknowledge.`;
|
|
25700
25970
|
const me = resolveOwnAgentIdentity();
|
|
25701
|
-
|
|
25702
|
-
|
|
25703
|
-
|
|
25704
|
-
return;
|
|
25705
|
-
}
|
|
25706
|
-
const wake = me.harness === "codex" && me.conversation ? wakeCodex(formatWakeMessage([push]), me.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : wakeHarness(formatWakeMessage([push]));
|
|
25707
|
-
void wake.then((woken) => {
|
|
25708
|
-
if (!woken) persistInboxPush(process.cwd(), push);
|
|
25971
|
+
const wake = me.harness === "codex" && me.conversation ? wakeCodex(text, me.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : me.harness === "claude" ? wakeHarness(text, void 0, me.conversation) : Promise.resolve(false);
|
|
25972
|
+
void wake.catch(() => false).finally(() => {
|
|
25973
|
+
waking = false;
|
|
25709
25974
|
});
|
|
25710
25975
|
},
|
|
25711
25976
|
claim: resolveOwnAgentIdentity,
|
|
25712
|
-
onReconnect: (
|
|
25977
|
+
onReconnect: () => recovery.sync()
|
|
25713
25978
|
});
|
|
25714
|
-
|
|
25979
|
+
const current = held;
|
|
25980
|
+
await current.start();
|
|
25981
|
+
await current.requireSession();
|
|
25982
|
+
await recovery.sync();
|
|
25715
25983
|
};
|
|
25716
25984
|
const resolveProjectWithPresence = async (args) => {
|
|
25717
25985
|
const target = await resolveProjectFromArgs(args);
|
|
25718
|
-
if (!held) await holdSession(target.id, null);
|
|
25719
|
-
await held?.
|
|
25986
|
+
if (!held || held.isStopped) await holdSession(target.id, null);
|
|
25987
|
+
await held?.requireSession();
|
|
25720
25988
|
return target;
|
|
25721
25989
|
};
|
|
25722
25990
|
const releaseTaskSession = async () => {
|
|
25723
25991
|
if (held?.taskKey) await holdSession(held.projectId, null);
|
|
25724
25992
|
};
|
|
25725
|
-
if (!isNonInteractiveHarness())
|
|
25726
|
-
void (async () => {
|
|
25727
|
-
for (let waited = 0; waited < 1e4 && !fetchKnownConversation(process.cwd()); waited += 250) await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
25728
|
-
resolveOwnAgentIdentity();
|
|
25729
|
-
const target = await resolveProjectFromArgs({});
|
|
25730
|
-
if (!held) await holdSession(target.id, null);
|
|
25731
|
-
})().catch(() => void 0);
|
|
25732
|
-
if (!isNonInteractiveHarness()) void trustCodexHooksForOwnHome().catch(() => void 0);
|
|
25733
|
-
const endSessionAndExit = () => {
|
|
25734
|
-
held?.stop();
|
|
25735
|
-
held = null;
|
|
25736
|
-
setTimeout(() => process.exit(0), 300).unref();
|
|
25737
|
-
};
|
|
25738
|
-
process.stdin.once("end", endSessionAndExit);
|
|
25739
|
-
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, endSessionAndExit);
|
|
25740
25993
|
const waitForNextMessage = async (thread, minutes) => {
|
|
25741
|
-
const
|
|
25742
|
-
const wanted = queued.filter((push) => !thread || push.thread.id === thread);
|
|
25743
|
-
for (const other of queued) if (!wanted.includes(other)) persistInboxPush(process.cwd(), other);
|
|
25994
|
+
const wanted = drainOwnInbox(thread);
|
|
25744
25995
|
if (wanted.length) return { pushes: wanted, released: false };
|
|
25745
|
-
|
|
25996
|
+
const waitingOn = held;
|
|
25997
|
+
if (!waitingOn || waitingOn.isStopped || !waitingOn.session?.reachable) return { pushes: [], released: true };
|
|
25998
|
+
return new Promise((resolve6) => {
|
|
25746
25999
|
const acceptDeliveredPush = (delivered) => {
|
|
25747
26000
|
if (delivered && thread && delivered.thread.id !== thread) return false;
|
|
25748
|
-
|
|
26001
|
+
if (delivered && drainInbox(process.cwd(), owner, (push) => push.message.id === delivered.message.id).length === 0) return false;
|
|
26002
|
+
waitingOn.waiters.delete(acceptDeliveredPush);
|
|
25749
26003
|
clearTimeout(timer);
|
|
25750
|
-
|
|
26004
|
+
resolve6({ pushes: delivered ? [delivered] : [], released: delivered === null });
|
|
25751
26005
|
return true;
|
|
25752
26006
|
};
|
|
25753
|
-
|
|
26007
|
+
waitingOn.waiters.add(acceptDeliveredPush);
|
|
25754
26008
|
const timer = setTimeout(() => {
|
|
25755
|
-
|
|
25756
|
-
|
|
26009
|
+
waitingOn.waiters.delete(acceptDeliveredPush);
|
|
26010
|
+
resolve6({ pushes: [], released: false });
|
|
25757
26011
|
}, minutes * 6e4);
|
|
25758
26012
|
});
|
|
25759
26013
|
};
|
|
25760
26014
|
const formatWaitOutcome = (outcome, minutes, thread) => {
|
|
25761
26015
|
if (outcome.pushes.length) return outcome.pushes.map(formatPushBlock).join("\n\n");
|
|
25762
|
-
if (outcome.released) return "
|
|
26016
|
+
if (outcome.released) return "This wait ended because the session was released, replaced, or made unreachable.";
|
|
25763
26017
|
return thread ? `No reply in ${thread} in ${minutes} minutes.` : `No message in ${minutes} minutes.`;
|
|
25764
26018
|
};
|
|
25765
|
-
|
|
26019
|
+
const tools = [
|
|
25766
26020
|
{
|
|
25767
26021
|
name: "wait_for_message",
|
|
25768
26022
|
description: "Claude Code only, and rarely: block until the next message for you (on your task, in a thread you are in, or addressed to you), or, with `thread`, the next one in that thread; or until `minutes` pass. Messages reach you on their own; use this only when you cannot go on without the answer. Never on Codex: end your turn and the reply is your next turn.",
|
|
@@ -25781,7 +26035,7 @@ ${pushes.map(formatPushBlock).join("\n\n")}`;
|
|
|
25781
26035
|
inputSchema: buildToolSchema({ task: buildStringSchema("Task key"), project: PROJECT }),
|
|
25782
26036
|
run: async (args) => {
|
|
25783
26037
|
const target = await resolveProjectWithPresence(args);
|
|
25784
|
-
const queued =
|
|
26038
|
+
const queued = drainOwnInbox().map(formatPushBlock);
|
|
25785
26039
|
const rest = await buildUnreadDigest(api, target.id, args.task ?? null);
|
|
25786
26040
|
return [...queued, rest].join("\n\n");
|
|
25787
26041
|
}
|
|
@@ -26070,21 +26324,6 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
26070
26324
|
return members2.map((member) => `${member.name} ${member.email} ${member.role}`).join("\n");
|
|
26071
26325
|
}
|
|
26072
26326
|
},
|
|
26073
|
-
{
|
|
26074
|
-
name: "projects",
|
|
26075
|
-
description: "Every project you can reach, and which one this repository is linked to.",
|
|
26076
|
-
inputSchema: buildToolSchema({}),
|
|
26077
|
-
run: async () => {
|
|
26078
|
-
const list = await api.get("/projects", ApiResponses.Projects);
|
|
26079
|
-
let linked = null;
|
|
26080
|
-
try {
|
|
26081
|
-
linked = (await resolveProjectFromArgs({})).key;
|
|
26082
|
-
} catch {
|
|
26083
|
-
linked = null;
|
|
26084
|
-
}
|
|
26085
|
-
return list.map((candidate) => `${candidate.key === linked ? "* " : " "}${candidate.key} ${candidate.name} (${candidate.openTaskCount} open)`).join("\n") + (linked ? "" : "\n\nThis repository is not linked: slop link <KEY>");
|
|
26086
|
-
}
|
|
26087
|
-
},
|
|
26088
26327
|
{
|
|
26089
26328
|
name: "finish_task",
|
|
26090
26329
|
description: "Move a task to the first done column.",
|
|
@@ -26097,17 +26336,33 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
26097
26336
|
}
|
|
26098
26337
|
}
|
|
26099
26338
|
];
|
|
26339
|
+
return {
|
|
26340
|
+
start: () => holdSession(project.id, null),
|
|
26341
|
+
stop: () => {
|
|
26342
|
+
stopped = true;
|
|
26343
|
+
recovery.stop();
|
|
26344
|
+
held?.stop();
|
|
26345
|
+
},
|
|
26346
|
+
tools: tools.map((tool) => ({ ...tool, run: async (args) => {
|
|
26347
|
+
const result = truncateToolResult(await tool.run(args));
|
|
26348
|
+
const arrived = tool.name === "check_messages" || tool.name === "wait_for_message" ? [] : drainOwnInbox();
|
|
26349
|
+
return arrived.length ? `${result}
|
|
26350
|
+
|
|
26351
|
+
--- Messages arrived while you worked (answer what is still open, once; do not acknowledge) ---
|
|
26352
|
+
${arrived.map(formatPushBlock).join("\n\n")}` : result;
|
|
26353
|
+
} }))
|
|
26354
|
+
};
|
|
26100
26355
|
}
|
|
26101
26356
|
function fetchJsonFile(path2) {
|
|
26102
26357
|
try {
|
|
26103
|
-
return JSON.parse(
|
|
26358
|
+
return JSON.parse(readFileSync14(path2, "utf8"));
|
|
26104
26359
|
} catch {
|
|
26105
26360
|
return {};
|
|
26106
26361
|
}
|
|
26107
26362
|
}
|
|
26108
26363
|
function installIntoClaudeHome(dir) {
|
|
26109
26364
|
const done = [];
|
|
26110
|
-
const primary = dir ===
|
|
26365
|
+
const primary = dir === join13(homedir4(), ".claude");
|
|
26111
26366
|
const env = { ...process.env };
|
|
26112
26367
|
if (primary) delete env.CLAUDE_CONFIG_DIR;
|
|
26113
26368
|
else env.CLAUDE_CONFIG_DIR = dir;
|
|
@@ -26120,7 +26375,7 @@ function installIntoClaudeHome(dir) {
|
|
|
26120
26375
|
if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
|
|
26121
26376
|
else done.push(`Claude Code: run \`${primary ? "" : `CLAUDE_CONFIG_DIR=${dir} `}claude mcp add --scope user slop -- slop mcp\` (${said.trim() || "the claude command was not found here"})`);
|
|
26122
26377
|
}
|
|
26123
|
-
const path2 =
|
|
26378
|
+
const path2 = join13(dir, "settings.json");
|
|
26124
26379
|
mkdirSync7(dir, { recursive: true });
|
|
26125
26380
|
const settings = fetchJsonFile(path2);
|
|
26126
26381
|
settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
|
|
@@ -26144,14 +26399,13 @@ function mergeHooks(hooks, harness) {
|
|
|
26144
26399
|
hooks[event] = entries;
|
|
26145
26400
|
};
|
|
26146
26401
|
installHookForEvent("SessionStart", "start", 5, "Slopware Tasks");
|
|
26147
|
-
if (harness === "codex") installHookForEvent("PostToolUse", "tool", 10, "Slopware Tasks");
|
|
26148
26402
|
return hooks;
|
|
26149
26403
|
}
|
|
26150
26404
|
function installIntoCodexHome(dir) {
|
|
26151
26405
|
const done = [];
|
|
26152
26406
|
mkdirSync7(dir, { recursive: true });
|
|
26153
|
-
const configTomlPath =
|
|
26154
|
-
const current =
|
|
26407
|
+
const configTomlPath = join13(dir, "config.toml");
|
|
26408
|
+
const current = existsSync9(configTomlPath) ? readFileSync14(configTomlPath, "utf8") : "";
|
|
26155
26409
|
if (/^\[mcp_servers\.slop\]/m.test(current)) {
|
|
26156
26410
|
if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync8(configTomlPath, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
|
|
26157
26411
|
done.push(`Codex: MCP server already in ${configTomlPath}`);
|
|
@@ -26163,17 +26417,17 @@ tool_timeout_sec = 21600
|
|
|
26163
26417
|
`);
|
|
26164
26418
|
done.push(`Codex: MCP server "slop" added to ${configTomlPath}`);
|
|
26165
26419
|
}
|
|
26166
|
-
const hooksPath =
|
|
26420
|
+
const hooksPath = join13(dir, "hooks.json");
|
|
26167
26421
|
const file2 = fetchJsonFile(hooksPath);
|
|
26168
26422
|
file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
|
|
26169
26423
|
writeFileSync8(hooksPath, `${JSON.stringify(file2, null, 2)}
|
|
26170
26424
|
`);
|
|
26171
|
-
done.push(`Codex: SessionStart
|
|
26425
|
+
done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (Codex is woken with codex queue; no hook holds a turn open)`);
|
|
26172
26426
|
return done;
|
|
26173
26427
|
}
|
|
26174
26428
|
async function trustCodexHooks(codexHome) {
|
|
26175
26429
|
const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
|
|
26176
|
-
const where = codexHome ===
|
|
26430
|
+
const where = codexHome === join13(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
|
|
26177
26431
|
let buffer = "";
|
|
26178
26432
|
const pending = /* @__PURE__ */ new Map();
|
|
26179
26433
|
server.stdout.on("data", (chunk) => {
|
|
@@ -26191,7 +26445,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
26191
26445
|
}
|
|
26192
26446
|
});
|
|
26193
26447
|
let nextId = 1;
|
|
26194
|
-
const callCodexMethod = (method, params) => new Promise((
|
|
26448
|
+
const callCodexMethod = (method, params) => new Promise((resolve6, reject) => {
|
|
26195
26449
|
const id = nextId++;
|
|
26196
26450
|
const timer = setTimeout(() => {
|
|
26197
26451
|
pending.delete(id);
|
|
@@ -26200,7 +26454,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
26200
26454
|
pending.set(id, (message) => {
|
|
26201
26455
|
clearTimeout(timer);
|
|
26202
26456
|
if (message.error) reject(new Error(message.error.message ?? `${method} failed`));
|
|
26203
|
-
else
|
|
26457
|
+
else resolve6(message.result);
|
|
26204
26458
|
});
|
|
26205
26459
|
server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
26206
26460
|
`);
|
|
@@ -26208,7 +26462,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
26208
26462
|
const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
26209
26463
|
`);
|
|
26210
26464
|
try {
|
|
26211
|
-
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.
|
|
26465
|
+
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.9" }, capabilities: { experimentalApi: true } });
|
|
26212
26466
|
sendNotification("initialized", {});
|
|
26213
26467
|
const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
|
|
26214
26468
|
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
@@ -26224,22 +26478,12 @@ async function trustCodexHooks(codexHome) {
|
|
|
26224
26478
|
server.kill();
|
|
26225
26479
|
}
|
|
26226
26480
|
}
|
|
26227
|
-
async function trustCodexHooksForOwnHome() {
|
|
26228
|
-
const home = resolveOwnHarnessHome("CODEX_HOME");
|
|
26229
|
-
if (!home) return;
|
|
26230
|
-
const hooksPath = join12(home, "hooks.json");
|
|
26231
|
-
const configTomlPath = join12(home, "config.toml");
|
|
26232
|
-
if (!existsSync10(hooksPath) || !existsSync10(configTomlPath)) return;
|
|
26233
|
-
if (!readFileSync13(hooksPath, "utf8").includes("slop hook stop")) return;
|
|
26234
|
-
if (readFileSync13(configTomlPath, "utf8").includes(`hooks.state."${hooksPath}:stop:`)) return;
|
|
26235
|
-
await trustCodexHooks(home);
|
|
26236
|
-
}
|
|
26237
26481
|
async function parseHookInput(harness, remember = true) {
|
|
26238
|
-
const input2 = await new Promise((
|
|
26482
|
+
const input2 = await new Promise((resolve6) => {
|
|
26239
26483
|
let data = "";
|
|
26240
26484
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
26241
|
-
process.stdin.on("end", () =>
|
|
26242
|
-
setTimeout(() =>
|
|
26485
|
+
process.stdin.on("end", () => resolve6(data));
|
|
26486
|
+
setTimeout(() => resolve6(data), 2e3).unref();
|
|
26243
26487
|
});
|
|
26244
26488
|
let cwd = process.cwd();
|
|
26245
26489
|
let conversation = null;
|
|
@@ -26249,7 +26493,7 @@ async function parseHookInput(harness, remember = true) {
|
|
|
26249
26493
|
conversation = parsed.session_id ?? null;
|
|
26250
26494
|
} catch {
|
|
26251
26495
|
}
|
|
26252
|
-
const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) =>
|
|
26496
|
+
const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join13(home, "sessions")), conversation);
|
|
26253
26497
|
if (remember && conversation && ownThread && !isNonInteractiveHarness()) persistRememberedConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
26254
26498
|
return { cwd, conversation };
|
|
26255
26499
|
}
|
|
@@ -26257,17 +26501,7 @@ async function runPromptHook(harness) {
|
|
|
26257
26501
|
await parseHookInput(harness);
|
|
26258
26502
|
}
|
|
26259
26503
|
async function runToolHook(harness) {
|
|
26260
|
-
|
|
26261
|
-
const { cwd, conversation } = await parseHookInput(harness, false);
|
|
26262
|
-
const known = fetchKnownConversation(cwd);
|
|
26263
|
-
if (known && conversation && known.id !== conversation) return;
|
|
26264
|
-
const arrived = drainInbox(cwd);
|
|
26265
|
-
if (arrived.length === 0) return;
|
|
26266
|
-
const text = `Messages arrived on Slopware Tasks while you worked (answer what is still open, once, with say reply; do not acknowledge):
|
|
26267
|
-
|
|
26268
|
-
${arrived.map(formatPushBlock).join("\n\n")}`;
|
|
26269
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: text } })}
|
|
26270
|
-
`);
|
|
26504
|
+
await parseHookInput(harness, false);
|
|
26271
26505
|
}
|
|
26272
26506
|
async function runStartHook(harness) {
|
|
26273
26507
|
if (isNonInteractiveHarness()) return;
|
|
@@ -26311,12 +26545,39 @@ ${parts.join("\n")}` })}
|
|
|
26311
26545
|
}
|
|
26312
26546
|
function buildTools() {
|
|
26313
26547
|
if (isNonInteractiveHarness()) return [];
|
|
26314
|
-
const
|
|
26315
|
-
|
|
26316
|
-
|
|
26317
|
-
|
|
26548
|
+
const api = buildAuthedApiClient();
|
|
26549
|
+
let identity = null;
|
|
26550
|
+
let stopped = false;
|
|
26551
|
+
const projects = /* @__PURE__ */ new Map();
|
|
26552
|
+
const requireProjectTools = (project) => {
|
|
26553
|
+
if (stopped) throw new Error("The MCP server is stopping");
|
|
26554
|
+
let context = projects.get(project.id);
|
|
26555
|
+
if (!context) {
|
|
26556
|
+
identity ??= resolveAgentIdentity(process.cwd());
|
|
26557
|
+
context = createProjectTools(buildAuthedApiClient(), project, identity);
|
|
26558
|
+
projects.set(project.id, context);
|
|
26559
|
+
}
|
|
26560
|
+
return context;
|
|
26561
|
+
};
|
|
26562
|
+
const runHandlerByName = async (name, args) => {
|
|
26563
|
+
if (name === "projects") return fetchProjectsOverview(api);
|
|
26564
|
+
const project = await resolveProject(api, args.project);
|
|
26565
|
+
const handler = requireProjectTools(project).tools.find((candidate) => candidate.name === name);
|
|
26566
|
+
if (!handler) throw new Error("No such action");
|
|
26318
26567
|
return handler.run(args);
|
|
26319
26568
|
};
|
|
26569
|
+
void (async () => {
|
|
26570
|
+
for (let waited = 0; waited < 1e4 && !fetchKnownConversation(process.cwd()) && !stopped; waited += 250) await new Promise((resolve6) => setTimeout(resolve6, 250));
|
|
26571
|
+
if (stopped) return;
|
|
26572
|
+
await requireProjectTools(await resolveProject(api)).start();
|
|
26573
|
+
})().catch(() => void 0);
|
|
26574
|
+
const endSessionAndExit = () => {
|
|
26575
|
+
stopped = true;
|
|
26576
|
+
for (const context of projects.values()) context.stop();
|
|
26577
|
+
setTimeout(() => process.exit(0), 300).unref();
|
|
26578
|
+
};
|
|
26579
|
+
process.stdin.once("end", endSessionAndExit);
|
|
26580
|
+
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, endSessionAndExit);
|
|
26320
26581
|
const buildGroupedTool = (name, description2, actions, properties) => ({
|
|
26321
26582
|
name,
|
|
26322
26583
|
description: description2,
|
|
@@ -26361,17 +26622,12 @@ function buildTools() {
|
|
|
26361
26622
|
)
|
|
26362
26623
|
];
|
|
26363
26624
|
}
|
|
26364
|
-
function
|
|
26625
|
+
function attachUpdateNoticeToTools(list, version2) {
|
|
26365
26626
|
return list.map((tool) => ({
|
|
26366
26627
|
...tool,
|
|
26367
26628
|
run: async (args) => {
|
|
26368
26629
|
const result = truncateToolResult(await tool.run(args));
|
|
26369
26630
|
const parts = [result];
|
|
26370
|
-
if (!(tool.name === "session" && (args.action === "check" || args.action === "wait"))) {
|
|
26371
|
-
const arrived = drainInbox(process.cwd());
|
|
26372
|
-
if (arrived.length) parts.push(`--- Messages arrived while you worked (answer what is still open, once; do not acknowledge) ---
|
|
26373
|
-
${arrived.map(formatPushBlock).join("\n\n")}`);
|
|
26374
|
-
}
|
|
26375
26631
|
const notice = isUpdateCheckEnabled() ? buildUpdateNotice(version2) : null;
|
|
26376
26632
|
if (notice) parts.push(notice);
|
|
26377
26633
|
return parts.join("\n\n");
|
|
@@ -26380,7 +26636,7 @@ ${arrived.map(formatPushBlock).join("\n\n")}`);
|
|
|
26380
26636
|
}
|
|
26381
26637
|
function registerMcpCommands(program3, version2) {
|
|
26382
26638
|
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.");
|
|
26383
|
-
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(
|
|
26639
|
+
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(attachUpdateNoticeToTools(buildTools(), version2), { name: "slop", version: version2 }));
|
|
26384
26640
|
mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
|
|
26385
26641
|
const homes = fetchCodexHomes();
|
|
26386
26642
|
const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home))];
|
|
@@ -26396,16 +26652,15 @@ function registerMcpCommands(program3, version2) {
|
|
|
26396
26652
|
const hook = program3.command("hook").description("Harness hooks (installed by slop mcp install).");
|
|
26397
26653
|
const parseHarnessFromOption = (options) => options.harness === "claude" || options.harness === "codex" ? options.harness : options.harness ? "other" : null;
|
|
26398
26654
|
hook.command("start").description("SessionStart hook (Claude Code and Codex): writes the conversation down so the MCP server is its session, and tells the agent its name and how messages work.").option("--harness <name>", "claude or codex").action((options) => runStartHook(parseHarnessFromOption(options)));
|
|
26399
|
-
hook.command("tool").description("
|
|
26655
|
+
hook.command("tool").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude or codex").action((options) => runToolHook(parseHarnessFromOption(options)));
|
|
26400
26656
|
hook.command("prompt").description("No longer installed; kept so an older install's hook exits cleanly.").option("--harness <name>", "claude or codex").action((options) => runPromptHook(parseHarnessFromOption(options)));
|
|
26401
26657
|
hook.command("stop").description("No longer installed (Claude Code is woken over its socket, Codex through codex queue); kept so an older install's hook still hands over unread messages.").option("--harness <name>", "claude or codex").action((options) => runStopHook(parseHarnessFromOption(options)));
|
|
26402
26658
|
}
|
|
26403
26659
|
|
|
26404
26660
|
// src/commands/sessions.ts
|
|
26405
|
-
import { readFileSync as readFileSync14 } from "fs";
|
|
26406
26661
|
var buildTaskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
26407
26662
|
var formatSessionLine2 = (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"];
|
|
26408
|
-
function registerSessionCommands(program3
|
|
26663
|
+
function registerSessionCommands(program3) {
|
|
26409
26664
|
const registerProjectOption = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
26410
26665
|
const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
|
|
26411
26666
|
registerProjectOption(session.command("start <key>")).description("Claim a task: assign it to you, move it to the first in-progress column, then tell you how to go live.").action(async (key, options) => {
|
|
@@ -26487,13 +26742,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
|
|
|
26487
26742
|
}
|
|
26488
26743
|
|
|
26489
26744
|
// src/index.ts
|
|
26490
|
-
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.
|
|
26745
|
+
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.9").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => updateJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
|
|
26491
26746
|
registerAuthCommands(program2);
|
|
26492
26747
|
registerProjectCommands(program2);
|
|
26493
26748
|
var taskCommand = registerTaskCommands(program2);
|
|
26494
|
-
registerSessionCommands(program2
|
|
26749
|
+
registerSessionCommands(program2);
|
|
26495
26750
|
registerThreadCommands(program2, taskCommand);
|
|
26496
|
-
registerMcpCommands(program2, "0.4.
|
|
26751
|
+
registerMcpCommands(program2, "0.4.9");
|
|
26497
26752
|
registerGuideCommand(program2);
|
|
26498
26753
|
registerSkillCommands(program2);
|
|
26499
26754
|
program2.parseAsync().catch(exitWithError);
|