slopwaresystems 0.4.8 → 0.4.10
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 +1726 -1165
- 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
|
|
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 ordinary answers are cut at 16k characters and say so; ask for less with `limit`, `offset` or `before`. Incoming messages are kept whole in a separate batch. Use `session check` to read any remaining messages.\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. A failed handoff is retried; a crash just after output can repeat a message, so do not repeat an action you already completed.\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,185 +24095,683 @@ 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
|
-
|
|
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);
|
|
24215
|
+
}
|
|
24216
|
+
var findDirectoriesNamed = (root, prefix) => {
|
|
24217
|
+
try {
|
|
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
|
+
});
|
|
24225
|
+
} catch {
|
|
24226
|
+
return [];
|
|
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"]))]);
|
|
24234
|
+
}
|
|
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"]))]);
|
|
24239
|
+
}
|
|
24240
|
+
function resolveOwnHarnessHome(variable) {
|
|
24241
|
+
if (process.platform === "win32") return null;
|
|
24242
|
+
try {
|
|
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
|
+
}
|
|
24253
|
+
} catch {
|
|
24254
|
+
}
|
|
24255
|
+
return null;
|
|
24256
|
+
}
|
|
24257
|
+
|
|
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
|
+
}
|
|
24268
|
+
}
|
|
24269
|
+
return null;
|
|
24270
|
+
}
|
|
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 };
|
|
24276
|
+
}
|
|
24277
|
+
const registration = fetchHarnessRegistration(pid);
|
|
24278
|
+
return registration?.sessionId ? { sessionId: registration.sessionId, socketPath: registration.messagingSocketPath ?? null } : null;
|
|
24279
|
+
}
|
|
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 }
|
|
24289
|
+
};
|
|
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));
|
|
24303
|
+
});
|
|
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;
|
|
24318
|
+
}
|
|
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);
|
|
24330
|
+
});
|
|
24331
|
+
});
|
|
24332
|
+
}
|
|
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);
|
|
24356
|
+
};
|
|
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"));
|
|
24361
|
+
return;
|
|
24362
|
+
}
|
|
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 {
|
|
24379
|
+
}
|
|
24380
|
+
});
|
|
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
|
+
})();
|
|
24398
|
+
});
|
|
24399
|
+
});
|
|
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 = "";
|
|
24410
|
+
}
|
|
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) => {
|
|
24428
|
+
try {
|
|
24429
|
+
return realpathSync3(path2);
|
|
24430
|
+
} catch {
|
|
24431
|
+
return path2;
|
|
24432
|
+
}
|
|
24433
|
+
};
|
|
24434
|
+
return resolveRealPath(a) === resolveRealPath(b);
|
|
24435
|
+
};
|
|
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;
|
|
24458
|
+
}
|
|
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 {
|
|
24464
|
+
}
|
|
24465
|
+
}
|
|
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;
|
|
24482
|
+
}
|
|
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;
|
|
24496
|
+
}
|
|
24497
|
+
return matching.size === 1 ? [...matching][0] : null;
|
|
24498
|
+
}
|
|
24499
|
+
function isMainChatThread(roots, threadId) {
|
|
24500
|
+
const path2 = fetchCodexRolloutPath(roots, threadId);
|
|
24501
|
+
if (!path2) return true;
|
|
24502
|
+
try {
|
|
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
|
+
}
|
|
24511
|
+
} catch {
|
|
24512
|
+
return true;
|
|
24513
|
+
}
|
|
24514
|
+
}
|
|
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;
|
|
24541
|
+
}
|
|
24542
|
+
function resolveCodexBinary() {
|
|
24543
|
+
try {
|
|
24544
|
+
execFileSync3(process.platform === "win32" ? "where" : "which", ["codex"], { stdio: "ignore" });
|
|
24545
|
+
return "codex";
|
|
24546
|
+
} catch {
|
|
24547
|
+
}
|
|
24548
|
+
if (parseHarnessFromCommand(fetchParentCommand()) === "codex" && process.platform !== "win32") {
|
|
24549
|
+
try {
|
|
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;
|
|
24552
|
+
} catch {
|
|
24553
|
+
}
|
|
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
|
+
}
|
|
24571
|
+
return null;
|
|
24572
|
+
}
|
|
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)
|
|
24592
|
+
};
|
|
24593
|
+
}
|
|
24594
|
+
|
|
24595
|
+
// src/platform/api.ts
|
|
24596
|
+
function buildApiClient(options) {
|
|
24597
|
+
async function sendApiRequest(method, path2, schema, body2) {
|
|
24598
|
+
try {
|
|
24599
|
+
assertApiResponseContract(method, path2, schema);
|
|
24600
|
+
} catch {
|
|
24601
|
+
throw new CliError("This request does not match the API contract", "Update slop to the latest version", "internal");
|
|
24602
|
+
}
|
|
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;
|
|
24608
|
+
try {
|
|
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");
|
|
24618
|
+
}
|
|
24619
|
+
const text = await response.text();
|
|
24620
|
+
if (response.ok) {
|
|
24621
|
+
try {
|
|
24622
|
+
return schema.parse(JSON.parse(text));
|
|
24623
|
+
} catch {
|
|
24624
|
+
throw new CliError("The server returned an incompatible response", "Update slop to the latest version", "internal");
|
|
24625
|
+
}
|
|
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;
|
|
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(/\/+$/, "");
|
|
24207
24775
|
const api = buildApiClient({ baseUrl, token: null });
|
|
24208
24776
|
const started = await api.post("/cli/auth/start", ApiResponses.StartCliAuthResult, { machine: buildMachineInfo() });
|
|
24209
24777
|
const remote = isHeadless();
|
|
@@ -24231,106 +24799,41 @@ Agent skills installed in ~/.claude/skills and ~/.agents/skills. Next: slop mcp
|
|
|
24231
24799
|
}
|
|
24232
24800
|
throw new CliError("The login request expired before it was approved", "slop login");
|
|
24233
24801
|
});
|
|
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; 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\`.
|
|
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));
|
|
24269
|
-
}
|
|
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()) {
|
|
24277
|
-
try {
|
|
24278
|
-
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
24279
|
-
} catch {
|
|
24280
|
-
return null;
|
|
24281
|
-
}
|
|
24282
|
-
}
|
|
24283
|
-
function fetchRepo() {
|
|
24284
|
-
const commonDir = runGit(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
24285
|
-
if (!commonDir) return null;
|
|
24286
|
-
const worktree = runGit(["rev-parse", "--show-toplevel"]);
|
|
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;
|
|
24298
|
-
}
|
|
24299
|
-
function fetchRepoLink(repo) {
|
|
24300
|
-
try {
|
|
24301
|
-
return JSON.parse(readFileSync5(repo.linkPath, "utf8"));
|
|
24302
|
-
} 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
|
-
}
|
|
24330
|
-
return {
|
|
24331
|
-
info: { fingerprint, name: remoteUrl?.split("/").slice(-1)[0] ?? basename(repo.root), remoteUrl, rootPath: repo.root },
|
|
24332
|
-
localRepoId
|
|
24333
|
-
};
|
|
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(() => {
|
|
24805
|
+
});
|
|
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 are kept whole; use \`session check\` for any remaining messages. A crash just after output can repeat a message, so do not repeat an action you already completed. 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));
|
|
24334
24837
|
}
|
|
24335
24838
|
|
|
24336
24839
|
// src/commands/projects.ts
|
|
@@ -24391,696 +24894,757 @@ function registerProjectCommands(program3) {
|
|
|
24391
24894
|
if (!STATUS_CATEGORIES.includes(options.category)) throw new CliError(`Unknown category "${options.category}"`, `use one of: ${STATUS_CATEGORIES.join(", ")}`);
|
|
24392
24895
|
const target = await fetchResolvedProject(api, options.project);
|
|
24393
24896
|
const statuses = await api.post(`/projects/${target.id}/statuses`, ApiResponses.ProjectStatuses, { label, category: options.category });
|
|
24394
|
-
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
24395
|
-
});
|
|
24396
|
-
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) => {
|
|
24397
|
-
const api = buildAuthedApiClient();
|
|
24398
|
-
const target = await fetchResolvedProject(api, options.project);
|
|
24399
|
-
const statuses = await api.patch(`/projects/${target.id}/statuses/${requireStatusByRef(target, ref).id}`, ApiResponses.ProjectStatuses, { label, category: options.category });
|
|
24400
|
-
emitOutput(statuses, () => formatStatusLines(statuses));
|
|
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));
|
|
24414
|
-
});
|
|
24415
|
-
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) => {
|
|
24416
|
-
const api = buildAuthedApiClient();
|
|
24417
|
-
const target = await resolveProject(api, options.project);
|
|
24418
|
-
if (options.name === void 0 && options.description === void 0) {
|
|
24419
|
-
throw new CliError("Nothing to change", "pass --name or --description");
|
|
24420
|
-
}
|
|
24421
|
-
const updated = await api.patch(`/projects/${target.id}`, ApiResponses.Project, { name: options.name, description: options.description });
|
|
24422
|
-
emitOutput(updated, () => `Updated ${updated.key} ${updated.name}`);
|
|
24423
|
-
});
|
|
24424
|
-
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) => {
|
|
24425
|
-
const api = buildAuthedApiClient();
|
|
24426
|
-
const orgs = await api.get("/orgs", ApiResponses.Orgs);
|
|
24427
|
-
const org = options.org ? orgs.find((candidate) => candidate.id === options.org || candidate.slug === options.org) : orgs.length === 1 ? orgs[0] : void 0;
|
|
24428
|
-
if (!org) throw new CliError(options.org ? `No org "${options.org}"` : "You belong to several orgs", `pass --org: ${orgs.map((candidate) => candidate.slug).join(", ")}`);
|
|
24429
|
-
const key = options.key ?? generateProjectKeyFromName(name);
|
|
24430
|
-
if (!key) throw new CliError("Could not derive a key from that name", "pass --key, e.g. --key WEB");
|
|
24431
|
-
const project2 = await api.post(`/orgs/${org.id}/projects`, ApiResponses.Project, { name, key, description: options.description ?? "" });
|
|
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(() => {
|
|
24450
|
-
});
|
|
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
|
-
});
|
|
24465
|
-
}
|
|
24466
|
-
|
|
24467
|
-
// src/commands/mcp.ts
|
|
24468
|
-
import { execFileSync as execFileSync5, spawn as spawn3 } from "child_process";
|
|
24469
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
24470
|
-
import "path";
|
|
24471
|
-
import { homedir as homedir4 } from "os";
|
|
24472
|
-
import { join as join12 } from "path";
|
|
24473
|
-
|
|
24474
|
-
// src/platform/live.ts
|
|
24475
|
-
var PING_EVERY_MS = 25e3;
|
|
24476
|
-
var PONG_WITHIN_MS = 1e4;
|
|
24477
|
-
var STANDBY_POLL_MS = 2e4;
|
|
24478
|
-
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
24479
|
-
const ticket = await api.post(`/projects/${projectId}/tasks/${encodeURIComponent(taskKey)}/session/ticket`, ApiResponses.ConnectTicket, options.claim ?? {});
|
|
24480
|
-
const socket = new WebSocket(`${api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
24481
|
-
const outcome = await new Promise((resolve5) => {
|
|
24482
|
-
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => resolve5({ kind: "timeout" }), options.timeoutMs) : null;
|
|
24483
|
-
const finishWait = (result) => {
|
|
24484
|
-
if (timer) clearTimeout(timer);
|
|
24485
|
-
resolve5(result);
|
|
24486
|
-
};
|
|
24487
|
-
socket.addEventListener("message", (event) => {
|
|
24488
|
-
if (String(event.data) === "pong") return;
|
|
24489
|
-
const push = JSON.parse(String(event.data));
|
|
24490
|
-
if (push.type === "hello") {
|
|
24491
|
-
api.sessionId = push.session.id;
|
|
24492
|
-
persistRememberedSession(process.cwd(), push.session.id);
|
|
24493
|
-
options.onHello?.(push.session);
|
|
24494
|
-
return;
|
|
24495
|
-
}
|
|
24496
|
-
if (push.type === "end") {
|
|
24497
|
-
finishWait({ kind: "ended" });
|
|
24498
|
-
return;
|
|
24499
|
-
}
|
|
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
|
-
});
|
|
24504
|
-
socket.addEventListener("close", (event) => finishWait({ kind: "dropped", reason: event.reason || `code ${event.code}` }));
|
|
24505
|
-
socket.addEventListener("error", () => finishWait({ kind: "dropped", reason: "socket error" }));
|
|
24506
|
-
});
|
|
24507
|
-
if (outcome.kind !== "message") persistRememberedSession(process.cwd(), null);
|
|
24508
|
-
if (outcome.kind === "message" || outcome.kind === "timeout") {
|
|
24509
|
-
socket.close(1e3, outcome.kind === "message" ? "handled" : "timeout");
|
|
24510
|
-
await new Promise((resolve5) => {
|
|
24511
|
-
socket.addEventListener("close", () => resolve5(), { once: true });
|
|
24512
|
-
setTimeout(resolve5, 500);
|
|
24513
|
-
});
|
|
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
|
-
});
|
|
24552
|
-
async start() {
|
|
24553
|
-
await this.connect();
|
|
24554
|
-
}
|
|
24555
|
-
async connect() {
|
|
24556
|
-
if (this.stopped || this.connecting || this.socket) return;
|
|
24557
|
-
this.connecting = true;
|
|
24558
|
-
if (this.timer) clearTimeout(this.timer);
|
|
24559
|
-
try {
|
|
24560
|
-
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.hooks.claim());
|
|
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
|
-
});
|
|
24615
|
-
} catch {
|
|
24616
|
-
this.schedule();
|
|
24617
|
-
} finally {
|
|
24618
|
-
this.connecting = false;
|
|
24619
|
-
}
|
|
24620
|
-
}
|
|
24621
|
-
/** A ping every 25s; no pong within 10s means the socket is dead however open it looks. Close it and come back. */
|
|
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
|
-
}
|
|
24670
|
-
};
|
|
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(() => {
|
|
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
|
+
);
|
|
24967
|
+
});
|
|
24968
|
+
}
|
|
24671
24969
|
|
|
24672
|
-
// src/
|
|
24673
|
-
import {
|
|
24674
|
-
import {
|
|
24970
|
+
// src/commands/mcp.ts
|
|
24971
|
+
import { execFileSync as execFileSync6, spawn as spawn3 } from "child_process";
|
|
24972
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
|
|
24973
|
+
import { homedir as homedir4 } from "os";
|
|
24974
|
+
import { join as join15 } from "path";
|
|
24675
24975
|
|
|
24676
|
-
// src/platform/
|
|
24677
|
-
import {
|
|
24678
|
-
import {
|
|
24679
|
-
import { basename as basename2, join as join8 } from "path";
|
|
24976
|
+
// src/platform/session-recovery.ts
|
|
24977
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
24978
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
24680
24979
|
|
|
24681
|
-
// src/platform/
|
|
24682
|
-
import {
|
|
24683
|
-
import {
|
|
24684
|
-
import {
|
|
24685
|
-
import { spawn as spawn2 } from "child_process";
|
|
24686
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
24980
|
+
// src/platform/inbox.ts
|
|
24981
|
+
import { createHash as createHash6, randomUUID as randomUUID7 } from "crypto";
|
|
24982
|
+
import { existsSync as existsSync9, linkSync as linkSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync10, readdirSync as readdirSync6, realpathSync as realpathSync4, renameSync as renameSync2, rmSync as rmSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
24983
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
24687
24984
|
|
|
24688
|
-
// src/platform/
|
|
24689
|
-
import { execFileSync as
|
|
24690
|
-
import {
|
|
24691
|
-
import {
|
|
24692
|
-
import { join as
|
|
24693
|
-
var
|
|
24694
|
-
function
|
|
24695
|
-
if (process.platform === "win32") return [];
|
|
24696
|
-
const seen = [];
|
|
24985
|
+
// src/platform/inbox-claims.ts
|
|
24986
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
24987
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
24988
|
+
import { existsSync as existsSync7, linkSync as linkSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
24989
|
+
import { join as join10 } from "path";
|
|
24990
|
+
var processOwner = null;
|
|
24991
|
+
function fetchProcessStart(pid) {
|
|
24697
24992
|
try {
|
|
24698
|
-
|
|
24699
|
-
|
|
24700
|
-
|
|
24701
|
-
|
|
24702
|
-
|
|
24703
|
-
|
|
24993
|
+
return execFileSync5("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", env: { ...process.env, LC_ALL: "C", TZ: "UTC" }, stdio: ["ignore", "pipe", "ignore"], timeout: 1e3 }).trim() || null;
|
|
24994
|
+
} catch {
|
|
24995
|
+
return null;
|
|
24996
|
+
}
|
|
24997
|
+
}
|
|
24998
|
+
function isClaimOwnerRunning(owner) {
|
|
24999
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) throw new Error("Invalid inbox claim owner");
|
|
25000
|
+
try {
|
|
25001
|
+
process.kill(owner.pid, 0);
|
|
25002
|
+
} catch (error62) {
|
|
25003
|
+
if (error62.code === "ESRCH") return false;
|
|
25004
|
+
return true;
|
|
25005
|
+
}
|
|
25006
|
+
if (owner.pid === process.pid) return !owner.started || !processOwner?.started || owner.started === processOwner.started;
|
|
25007
|
+
const started = owner.started && fetchProcessStart(owner.pid);
|
|
25008
|
+
return !started || started === owner.started;
|
|
25009
|
+
}
|
|
25010
|
+
function claimInboxMessage(directory) {
|
|
25011
|
+
mkdirSync5(directory, { recursive: true });
|
|
25012
|
+
processOwner ??= { pid: process.pid, started: fetchProcessStart(process.pid) };
|
|
25013
|
+
for (; ; ) {
|
|
25014
|
+
let names;
|
|
25015
|
+
try {
|
|
25016
|
+
names = readdirSync4(directory).filter((name) => /^\d+\.json$/.test(name)).sort((a, b) => Number(a.slice(0, -5)) - Number(b.slice(0, -5)));
|
|
25017
|
+
} catch (error62) {
|
|
25018
|
+
if (error62.code === "ENOENT") return null;
|
|
25019
|
+
throw error62;
|
|
25020
|
+
}
|
|
25021
|
+
const last = names.at(-1);
|
|
25022
|
+
if (last && !existsSync7(join10(directory, `${last}.released`))) {
|
|
25023
|
+
let owner;
|
|
25024
|
+
try {
|
|
25025
|
+
owner = JSON.parse(readFileSync8(join10(directory, last), "utf8"));
|
|
25026
|
+
} catch (error62) {
|
|
25027
|
+
if (error62.code === "ENOENT") return null;
|
|
25028
|
+
throw error62;
|
|
24704
25029
|
}
|
|
24705
|
-
|
|
24706
|
-
|
|
25030
|
+
if (isClaimOwnerRunning(owner)) return null;
|
|
25031
|
+
}
|
|
25032
|
+
const next = join10(directory, `${last ? Number(last.slice(0, -5)) + 1 : 0}.json`);
|
|
25033
|
+
const temporary = join10(directory, `${randomUUID6()}.writing`);
|
|
25034
|
+
try {
|
|
25035
|
+
writeFileSync6(temporary, JSON.stringify(processOwner), { flag: "wx", mode: 384 });
|
|
25036
|
+
linkSync2(temporary, next);
|
|
25037
|
+
return { release: () => {
|
|
24707
25038
|
try {
|
|
24708
|
-
|
|
24709
|
-
} catch {
|
|
25039
|
+
writeFileSync6(`${next}.released`, "", { mode: 384 });
|
|
25040
|
+
} catch (error62) {
|
|
25041
|
+
if (error62.code !== "ENOENT") throw error62;
|
|
24710
25042
|
}
|
|
24711
|
-
}
|
|
24712
|
-
|
|
24713
|
-
|
|
24714
|
-
|
|
24715
|
-
|
|
24716
|
-
|
|
24717
|
-
seen.push(runPs(["eww", "-o", "command=", "-p", String(pid)]));
|
|
24718
|
-
pid = Number(runPs(["-o", "ppid=", "-p", String(pid)]).trim());
|
|
25043
|
+
} };
|
|
25044
|
+
} catch (error62) {
|
|
25045
|
+
if (error62.code === "ENOENT") return null;
|
|
25046
|
+
if (error62.code !== "EEXIST") throw error62;
|
|
25047
|
+
} finally {
|
|
25048
|
+
rmSync5(temporary, { force: true });
|
|
24719
25049
|
}
|
|
24720
|
-
seen.push(runPs(["axeww", "-o", "command="]));
|
|
24721
|
-
} catch {
|
|
24722
25050
|
}
|
|
24723
|
-
return seen;
|
|
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);
|
|
24732
25051
|
}
|
|
24733
|
-
|
|
25052
|
+
|
|
25053
|
+
// src/platform/inbox-receipts.ts
|
|
25054
|
+
import { createHash as createHash5 } from "crypto";
|
|
25055
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync9, readdirSync as readdirSync5, rmSync as rmSync6 } from "fs";
|
|
25056
|
+
import { join as join11 } from "path";
|
|
25057
|
+
var COMPACTION_SIZE = 128;
|
|
25058
|
+
var archives = /* @__PURE__ */ new Map();
|
|
25059
|
+
function fetchArchivedReceipts(directory) {
|
|
25060
|
+
let cached2 = archives.get(directory);
|
|
25061
|
+
if (!cached2) {
|
|
25062
|
+
cached2 = { files: /* @__PURE__ */ new Set(), keys: /* @__PURE__ */ new Set() };
|
|
25063
|
+
archives.set(directory, cached2);
|
|
25064
|
+
}
|
|
25065
|
+
let names;
|
|
24734
25066
|
try {
|
|
24735
|
-
|
|
24736
|
-
|
|
24737
|
-
|
|
24738
|
-
|
|
24739
|
-
return false;
|
|
24740
|
-
}
|
|
24741
|
-
});
|
|
24742
|
-
} catch {
|
|
24743
|
-
return [];
|
|
25067
|
+
names = readdirSync5(join11(directory, "compacted"));
|
|
25068
|
+
} catch (error62) {
|
|
25069
|
+
if (error62.code === "ENOENT") return cached2.keys;
|
|
25070
|
+
throw error62;
|
|
24744
25071
|
}
|
|
24745
|
-
|
|
24746
|
-
|
|
24747
|
-
|
|
24748
|
-
|
|
24749
|
-
|
|
24750
|
-
|
|
25072
|
+
for (const name of names) {
|
|
25073
|
+
if (!name.endsWith(".json") || cached2.files.has(name)) continue;
|
|
25074
|
+
const keys = JSON.parse(readFileSync9(join11(directory, "compacted", name), "utf8"));
|
|
25075
|
+
if (!Array.isArray(keys) || !keys.every((key) => typeof key === "string" && /^[a-f0-9]{64}$/.test(key))) throw new Error("Invalid inbox receipt archive");
|
|
25076
|
+
for (const key of keys) cached2.keys.add(key);
|
|
25077
|
+
cached2.files.add(name);
|
|
25078
|
+
}
|
|
25079
|
+
return cached2.keys;
|
|
24751
25080
|
}
|
|
24752
|
-
function
|
|
24753
|
-
|
|
24754
|
-
const named = process.env.CODEX_HOME ? [process.env.CODEX_HOME] : [];
|
|
24755
|
-
return deduplicateStrings([join6(root, ".codex"), ...named, ...inUse("CODEX_HOME"), ...findDirectoriesNamed(root, ".codex").filter((dir) => hasAnyEntry(dir, ["config.toml", "auth.json", "sessions"]))]);
|
|
25081
|
+
function hasInboxReceipt(directory, key) {
|
|
25082
|
+
return existsSync8(join11(directory, key)) || fetchArchivedReceipts(directory).has(key);
|
|
24756
25083
|
}
|
|
24757
|
-
function
|
|
24758
|
-
|
|
25084
|
+
function compactInboxReceipts(directory) {
|
|
25085
|
+
let keys;
|
|
24759
25086
|
try {
|
|
24760
|
-
|
|
24761
|
-
|
|
24762
|
-
|
|
24763
|
-
|
|
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
|
-
}
|
|
24770
|
-
} catch {
|
|
25087
|
+
keys = readdirSync5(directory).filter((name2) => /^[a-f0-9]{64}$/.test(name2)).sort();
|
|
25088
|
+
} catch (error62) {
|
|
25089
|
+
if (error62.code === "ENOENT") return;
|
|
25090
|
+
throw error62;
|
|
24771
25091
|
}
|
|
24772
|
-
return
|
|
25092
|
+
if (keys.length < COMPACTION_SIZE) return;
|
|
25093
|
+
mkdirSync6(join11(directory, "compacted"), { recursive: true });
|
|
25094
|
+
const text = JSON.stringify(keys);
|
|
25095
|
+
const name = createHash5("sha256").update(text).digest("hex");
|
|
25096
|
+
persistLocalState(join11(directory, "compacted", `${name}.json`), text, false);
|
|
25097
|
+
for (const key of keys) rmSync6(join11(directory, key), { force: true });
|
|
24773
25098
|
}
|
|
24774
25099
|
|
|
24775
|
-
// src/platform/
|
|
24776
|
-
|
|
24777
|
-
|
|
24778
|
-
|
|
24779
|
-
|
|
25100
|
+
// src/platform/inbox.ts
|
|
25101
|
+
var buildInboxDir = () => join12(dirname5(buildConfigPath()), "inbox");
|
|
25102
|
+
var resolveRealCwd2 = (cwd) => {
|
|
25103
|
+
try {
|
|
25104
|
+
return realpathSync4(cwd);
|
|
25105
|
+
} catch {
|
|
25106
|
+
return cwd;
|
|
25107
|
+
}
|
|
25108
|
+
};
|
|
25109
|
+
var generateInboxKey = (cwd, owner) => owner ? generateLocalStateKey(cwd, JSON.stringify([owner.baseUrl.replace(/\/$/, ""), owner.projectId, owner.conversation])) : createHash6("sha256").update(resolveRealCwd2(cwd)).digest("hex").slice(0, 16);
|
|
25110
|
+
var buildInboxPath = (cwd) => join12(buildInboxDir(), `${generateInboxKey(cwd)}.jsonl`);
|
|
25111
|
+
var buildInboxQueuePath = (cwd, owner) => join12(buildInboxDir(), generateInboxKey(cwd, owner));
|
|
25112
|
+
var sequence = 0;
|
|
25113
|
+
function persistInboxPush(cwd, push, owner) {
|
|
25114
|
+
const directory = buildInboxQueuePath(cwd, owner);
|
|
25115
|
+
mkdirSync7(directory, { recursive: true });
|
|
25116
|
+
const name = `${Date.now()}.${process.pid}.${String(sequence++).padStart(12, "0")}.${randomUUID7()}`;
|
|
25117
|
+
const temporary = join12(directory, `${name}.writing`);
|
|
25118
|
+
writeFileSync7(temporary, JSON.stringify(push), { flag: "wx" });
|
|
25119
|
+
if (owner) {
|
|
25120
|
+
const key = createHash6("sha256").update(push.message.id).digest("hex");
|
|
25121
|
+
const published = join12(directory, `${key}.json`);
|
|
24780
25122
|
try {
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
return
|
|
25123
|
+
if (hasInboxReceipt(join12(directory, "receipts"), key)) return false;
|
|
25124
|
+
linkSync3(temporary, published);
|
|
25125
|
+
return true;
|
|
25126
|
+
} catch (error62) {
|
|
25127
|
+
if (error62.code === "EEXIST") return !hasInboxReceipt(join12(directory, "receipts"), key);
|
|
25128
|
+
throw error62;
|
|
25129
|
+
} finally {
|
|
25130
|
+
rmSync7(temporary, { force: true });
|
|
24784
25131
|
}
|
|
24785
25132
|
}
|
|
24786
|
-
|
|
24787
|
-
|
|
24788
|
-
function resolveClaudeSession(pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid) {
|
|
24789
|
-
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
24790
|
-
if (sessionId) return { sessionId, socketPath: process.env.CLAUDE_CODE_MESSAGING_SOCKET || fetchHarnessRegistration(pid)?.messagingSocketPath || null };
|
|
24791
|
-
const registration = fetchHarnessRegistration(pid);
|
|
24792
|
-
return registration?.sessionId ? { sessionId: registration.sessionId, socketPath: registration.messagingSocketPath ?? null } : null;
|
|
24793
|
-
}
|
|
24794
|
-
function wakeHarness(text, pid = Number(process.env.SLOP_HARNESS_PID) || process.ppid) {
|
|
24795
|
-
const session = resolveClaudeSession(pid);
|
|
24796
|
-
if (!session?.socketPath) return Promise.resolve(false);
|
|
24797
|
-
const frame = {
|
|
24798
|
-
uuid: randomUUID3(),
|
|
24799
|
-
session_id: session.sessionId,
|
|
24800
|
-
type: "user",
|
|
24801
|
-
parent_tool_use_id: null,
|
|
24802
|
-
message: { role: "user", content: text }
|
|
24803
|
-
};
|
|
24804
|
-
return new Promise((resolve5) => {
|
|
24805
|
-
const socket = connect({ path: session.socketPath });
|
|
24806
|
-
const finishWake = (ok) => {
|
|
24807
|
-
clearTimeout(timer);
|
|
24808
|
-
socket.destroy();
|
|
24809
|
-
resolve5(ok);
|
|
24810
|
-
};
|
|
24811
|
-
const timer = setTimeout(() => finishWake(false), 3e3);
|
|
24812
|
-
socket.on("connect", () => {
|
|
24813
|
-
socket.write(`${JSON.stringify(frame)}
|
|
24814
|
-
`, (error62) => error62 ? finishWake(false) : setTimeout(() => finishWake(true), 200));
|
|
24815
|
-
});
|
|
24816
|
-
socket.on("error", () => finishWake(false));
|
|
24817
|
-
});
|
|
25133
|
+
renameSync2(temporary, join12(directory, `${name}.json`));
|
|
25134
|
+
return true;
|
|
24818
25135
|
}
|
|
24819
|
-
function
|
|
24820
|
-
const
|
|
24821
|
-
|
|
24822
|
-
|
|
24823
|
-
|
|
25136
|
+
function deleteAcknowledgedInboxClaims(directory) {
|
|
25137
|
+
const claims = join12(directory, "claims");
|
|
25138
|
+
let names;
|
|
25139
|
+
try {
|
|
25140
|
+
names = readdirSync6(claims);
|
|
25141
|
+
} catch (error62) {
|
|
25142
|
+
if (error62.code === "ENOENT") return;
|
|
25143
|
+
throw error62;
|
|
25144
|
+
}
|
|
25145
|
+
for (const key of names) {
|
|
25146
|
+
if (!/^[a-f0-9]{64}$/.test(key) || !hasInboxReceipt(join12(directory, "receipts"), key)) continue;
|
|
24824
25147
|
try {
|
|
24825
|
-
|
|
24826
|
-
|
|
24827
|
-
|
|
24828
|
-
|
|
24829
|
-
|
|
24830
|
-
|
|
24831
|
-
|
|
25148
|
+
rmSync7(join12(claims, key), { recursive: true, force: true });
|
|
25149
|
+
} catch (error62) {
|
|
25150
|
+
if (error62.code !== "ENOTEMPTY") throw error62;
|
|
25151
|
+
}
|
|
25152
|
+
}
|
|
25153
|
+
}
|
|
25154
|
+
function claimInbox(cwd, owner, accept = () => true) {
|
|
25155
|
+
const directory = buildInboxQueuePath(cwd, owner);
|
|
25156
|
+
const receipts = join12(directory, "receipts");
|
|
25157
|
+
const claims = [];
|
|
25158
|
+
let settled = false;
|
|
25159
|
+
const release = () => {
|
|
25160
|
+
if (settled) return;
|
|
25161
|
+
for (const claim2 of claims) claim2.release();
|
|
25162
|
+
settled = true;
|
|
25163
|
+
};
|
|
25164
|
+
const delivery = {
|
|
25165
|
+
pushes: [],
|
|
25166
|
+
release,
|
|
25167
|
+
acknowledge: () => {
|
|
25168
|
+
if (settled) return;
|
|
25169
|
+
mkdirSync7(receipts, { recursive: true });
|
|
25170
|
+
for (const { key } of claims) {
|
|
25171
|
+
try {
|
|
25172
|
+
writeFileSync7(join12(receipts, key), "", { flag: "wx", mode: 384 });
|
|
25173
|
+
} catch (error62) {
|
|
25174
|
+
if (error62.code !== "EEXIST") throw error62;
|
|
25175
|
+
}
|
|
25176
|
+
}
|
|
25177
|
+
settled = true;
|
|
25178
|
+
try {
|
|
25179
|
+
for (const { key } of claims) rmSync7(join12(directory, `${key}.json`), { force: true });
|
|
25180
|
+
deleteAcknowledgedInboxClaims(directory);
|
|
25181
|
+
compactInboxReceipts(receipts);
|
|
25182
|
+
} catch {
|
|
25183
|
+
}
|
|
24832
25184
|
}
|
|
24833
|
-
|
|
24834
|
-
|
|
24835
|
-
|
|
24836
|
-
}, 15e3);
|
|
24837
|
-
child.on("error", () => {
|
|
24838
|
-
clearTimeout(timer);
|
|
24839
|
-
resolve5(false);
|
|
24840
|
-
});
|
|
24841
|
-
child.on("exit", (code) => {
|
|
24842
|
-
clearTimeout(timer);
|
|
24843
|
-
resolve5(code === 0);
|
|
24844
|
-
});
|
|
24845
|
-
});
|
|
24846
|
-
}
|
|
24847
|
-
function wakeCodexOverWs(text, threadId, endpoint) {
|
|
24848
|
-
return new Promise((resolve5) => {
|
|
24849
|
-
let socket;
|
|
25185
|
+
};
|
|
25186
|
+
try {
|
|
25187
|
+
let names;
|
|
24850
25188
|
try {
|
|
24851
|
-
|
|
24852
|
-
} catch {
|
|
24853
|
-
|
|
24854
|
-
|
|
25189
|
+
names = readdirSync6(directory).filter((file2) => /^[a-f0-9]{64}\.json$/.test(file2));
|
|
25190
|
+
} catch (error62) {
|
|
25191
|
+
if (error62.code === "ENOENT") return delivery;
|
|
25192
|
+
throw error62;
|
|
24855
25193
|
}
|
|
24856
|
-
|
|
24857
|
-
const
|
|
24858
|
-
const finishWake = (ok) => {
|
|
24859
|
-
clearTimeout(timer);
|
|
25194
|
+
const available = [];
|
|
25195
|
+
for (const name of names) {
|
|
24860
25196
|
try {
|
|
24861
|
-
|
|
24862
|
-
} catch {
|
|
25197
|
+
available.push({ key: name.slice(0, -5), push: JSON.parse(readFileSync10(join12(directory, name), "utf8")) });
|
|
25198
|
+
} catch (error62) {
|
|
25199
|
+
if (error62.code !== "ENOENT") throw error62;
|
|
24863
25200
|
}
|
|
24864
|
-
|
|
24865
|
-
|
|
24866
|
-
const
|
|
24867
|
-
|
|
24868
|
-
|
|
24869
|
-
|
|
24870
|
-
|
|
24871
|
-
|
|
24872
|
-
|
|
24873
|
-
socket.addEventListener("message", (event) => {
|
|
25201
|
+
}
|
|
25202
|
+
available.sort((a, b) => a.push.message.createdAt.localeCompare(b.push.message.createdAt) || a.push.message.id.localeCompare(b.push.message.id));
|
|
25203
|
+
for (const { key, push } of available) {
|
|
25204
|
+
if (hasInboxReceipt(receipts, key)) {
|
|
25205
|
+
rmSync7(join12(directory, `${key}.json`), { force: true });
|
|
25206
|
+
continue;
|
|
25207
|
+
}
|
|
25208
|
+
if (!accept(push)) continue;
|
|
25209
|
+
let claim2;
|
|
24874
25210
|
try {
|
|
24875
|
-
|
|
24876
|
-
|
|
24877
|
-
|
|
25211
|
+
claim2 = claimInboxMessage(join12(directory, "claims", key));
|
|
25212
|
+
} catch (error62) {
|
|
25213
|
+
if (hasInboxReceipt(receipts, key)) continue;
|
|
25214
|
+
throw error62;
|
|
24878
25215
|
}
|
|
24879
|
-
|
|
24880
|
-
|
|
24881
|
-
|
|
24882
|
-
|
|
24883
|
-
|
|
24884
|
-
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
return;
|
|
24889
|
-
}
|
|
24890
|
-
await call("turn/start", { threadId, input: [{ type: "text", text }] });
|
|
24891
|
-
finishWake(true);
|
|
24892
|
-
} catch {
|
|
24893
|
-
finishWake(false);
|
|
24894
|
-
}
|
|
24895
|
-
})();
|
|
24896
|
-
});
|
|
24897
|
-
});
|
|
25216
|
+
if (!claim2) continue;
|
|
25217
|
+
claims.push({ key, ...claim2 });
|
|
25218
|
+
if (!hasInboxReceipt(receipts, key)) delivery.pushes.push(push);
|
|
25219
|
+
}
|
|
25220
|
+
return delivery;
|
|
25221
|
+
} catch (error62) {
|
|
25222
|
+
release();
|
|
25223
|
+
throw error62;
|
|
25224
|
+
}
|
|
24898
25225
|
}
|
|
24899
|
-
|
|
24900
|
-
|
|
24901
|
-
var parent = null;
|
|
24902
|
-
function fetchParentCommand() {
|
|
24903
|
-
if (parent !== null) return parent;
|
|
25226
|
+
function hasPendingInboxMessages(cwd, owner) {
|
|
25227
|
+
const directory = buildInboxQueuePath(cwd, owner);
|
|
24904
25228
|
try {
|
|
24905
|
-
|
|
24906
|
-
} catch {
|
|
24907
|
-
|
|
25229
|
+
return readdirSync6(directory).some((name) => /^[a-f0-9]{64}\.json$/.test(name) && !hasInboxReceipt(join12(directory, "receipts"), name.slice(0, -5)));
|
|
25230
|
+
} catch (error62) {
|
|
25231
|
+
if (error62.code === "ENOENT") return false;
|
|
25232
|
+
throw error62;
|
|
24908
25233
|
}
|
|
24909
|
-
return parent;
|
|
24910
|
-
}
|
|
24911
|
-
function parseHarnessFromCommand(command) {
|
|
24912
|
-
const first = basename2(command.trim().split(/\s+/)[0] ?? "");
|
|
24913
|
-
if (/^claude(\b|$)/.test(first)) return "claude";
|
|
24914
|
-
if (/^codex(\b|$)/.test(first)) return "codex";
|
|
24915
|
-
if (/(^|\/)codex(\S*)?\s/.test(command)) return "codex";
|
|
24916
|
-
return null;
|
|
24917
|
-
}
|
|
24918
|
-
function isNonInteractiveCommand(command) {
|
|
24919
|
-
return /(^|[\s/])codex(\S*)?\s+exec(\s|$)/.test(command);
|
|
24920
25234
|
}
|
|
24921
|
-
function
|
|
24922
|
-
|
|
25235
|
+
function drainInbox(cwd) {
|
|
25236
|
+
const pushes = drainLegacyInbox(cwd);
|
|
25237
|
+
const directory = buildInboxQueuePath(cwd);
|
|
25238
|
+
let names;
|
|
25239
|
+
try {
|
|
25240
|
+
names = readdirSync6(directory).filter((file2) => file2.endsWith(".json")).sort();
|
|
25241
|
+
} catch (error62) {
|
|
25242
|
+
if (error62.code === "ENOENT") return pushes;
|
|
25243
|
+
throw error62;
|
|
25244
|
+
}
|
|
25245
|
+
for (const name of names) {
|
|
25246
|
+
const taken = join12(directory, `${name}.${randomUUID7()}.draining`);
|
|
25247
|
+
try {
|
|
25248
|
+
renameSync2(join12(directory, name), taken);
|
|
25249
|
+
} catch (error62) {
|
|
25250
|
+
if (error62.code === "ENOENT") continue;
|
|
25251
|
+
throw error62;
|
|
25252
|
+
}
|
|
25253
|
+
try {
|
|
25254
|
+
pushes.push(JSON.parse(readFileSync10(taken, "utf8")));
|
|
25255
|
+
} finally {
|
|
25256
|
+
rmSync7(taken, { force: true });
|
|
25257
|
+
}
|
|
25258
|
+
}
|
|
25259
|
+
return pushes;
|
|
24923
25260
|
}
|
|
24924
|
-
|
|
24925
|
-
const
|
|
25261
|
+
function drainLegacyInbox(cwd) {
|
|
25262
|
+
const path2 = buildInboxPath(cwd);
|
|
25263
|
+
if (!existsSync9(path2)) return [];
|
|
25264
|
+
const taken = `${path2}.${process.pid}.${Date.now()}.draining`;
|
|
25265
|
+
try {
|
|
25266
|
+
renameSync2(path2, taken);
|
|
25267
|
+
} catch {
|
|
25268
|
+
return [];
|
|
25269
|
+
}
|
|
25270
|
+
const lines = readFileSync10(taken, "utf8").split("\n").filter(Boolean);
|
|
25271
|
+
rmSync7(taken, { force: true });
|
|
25272
|
+
return lines.flatMap((line) => {
|
|
24926
25273
|
try {
|
|
24927
|
-
return
|
|
25274
|
+
return [JSON.parse(line)];
|
|
24928
25275
|
} catch {
|
|
24929
|
-
return
|
|
25276
|
+
return [];
|
|
24930
25277
|
}
|
|
24931
|
-
};
|
|
24932
|
-
|
|
25278
|
+
});
|
|
25279
|
+
}
|
|
25280
|
+
|
|
25281
|
+
// src/platform/session-recovery.ts
|
|
25282
|
+
var SessionRecovery = class {
|
|
25283
|
+
constructor(api, owner, cwd, deliverPush) {
|
|
25284
|
+
this.api = api;
|
|
25285
|
+
this.owner = owner;
|
|
25286
|
+
this.deliverPush = deliverPush;
|
|
25287
|
+
this.path = join13(dirname6(buildConfigPath()), "cursors", `${generateInboxKey(cwd, owner)}.json`);
|
|
25288
|
+
}
|
|
25289
|
+
api;
|
|
25290
|
+
owner;
|
|
25291
|
+
deliverPush;
|
|
25292
|
+
cursor = null;
|
|
25293
|
+
preparing = null;
|
|
25294
|
+
syncing = null;
|
|
25295
|
+
resync = false;
|
|
25296
|
+
retry = null;
|
|
25297
|
+
stopped = false;
|
|
25298
|
+
path;
|
|
25299
|
+
syncCursor() {
|
|
25300
|
+
return this.preparing ??= this.fetchCursor().catch((error62) => {
|
|
25301
|
+
this.preparing = null;
|
|
25302
|
+
throw error62;
|
|
25303
|
+
});
|
|
25304
|
+
}
|
|
25305
|
+
async fetchCursor() {
|
|
25306
|
+
try {
|
|
25307
|
+
const value = JSON.parse(readFileSync11(this.path, "utf8"));
|
|
25308
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid saved message cursor");
|
|
25309
|
+
this.cursor = value;
|
|
25310
|
+
} catch (error62) {
|
|
25311
|
+
if (error62.code !== "ENOENT") throw error62;
|
|
25312
|
+
const page = await this.api.get(`/projects/${this.owner.projectId}/session/messages`, ApiResponses.SessionMessages);
|
|
25313
|
+
persistLocalState(this.path, JSON.stringify(page.cursor), false);
|
|
25314
|
+
await this.fetchCursor();
|
|
25315
|
+
}
|
|
25316
|
+
}
|
|
25317
|
+
sync() {
|
|
25318
|
+
if (this.stopped) return Promise.resolve();
|
|
25319
|
+
if (this.syncing) {
|
|
25320
|
+
this.resync = true;
|
|
25321
|
+
return this.syncing;
|
|
25322
|
+
}
|
|
25323
|
+
return this.syncing ??= this.syncMessages().catch(() => {
|
|
25324
|
+
if (!this.stopped && !this.retry) this.retry = setTimeout(() => {
|
|
25325
|
+
this.retry = null;
|
|
25326
|
+
void this.sync();
|
|
25327
|
+
}, 2e4);
|
|
25328
|
+
}).finally(() => {
|
|
25329
|
+
this.syncing = null;
|
|
25330
|
+
});
|
|
25331
|
+
}
|
|
25332
|
+
async syncMessages() {
|
|
25333
|
+
await this.syncCursor();
|
|
25334
|
+
if (this.retry) clearTimeout(this.retry);
|
|
25335
|
+
this.retry = null;
|
|
25336
|
+
while (!this.stopped) {
|
|
25337
|
+
this.resync = false;
|
|
25338
|
+
const page = await this.api.get(`/projects/${this.owner.projectId}/session/messages?cursor=${this.cursor}`, ApiResponses.SessionMessages);
|
|
25339
|
+
if (this.stopped) return;
|
|
25340
|
+
if (page.cursor < this.cursor || page.hasMore && page.cursor === this.cursor) throw new Error("Message recovery did not advance");
|
|
25341
|
+
for (const push of page.pushes) this.deliverPush(push);
|
|
25342
|
+
persistLocalState(this.path, JSON.stringify(page.cursor));
|
|
25343
|
+
this.cursor = page.cursor;
|
|
25344
|
+
if (!page.hasMore && !this.resync) return;
|
|
25345
|
+
}
|
|
25346
|
+
}
|
|
25347
|
+
stop() {
|
|
25348
|
+
this.stopped = true;
|
|
25349
|
+
if (this.retry) clearTimeout(this.retry);
|
|
25350
|
+
this.retry = null;
|
|
25351
|
+
}
|
|
24933
25352
|
};
|
|
24934
|
-
|
|
24935
|
-
|
|
24936
|
-
|
|
24937
|
-
|
|
24938
|
-
|
|
24939
|
-
|
|
24940
|
-
|
|
25353
|
+
|
|
25354
|
+
// src/platform/live.ts
|
|
25355
|
+
var PING_EVERY_MS = 25e3;
|
|
25356
|
+
var PONG_WITHIN_MS = 1e4;
|
|
25357
|
+
var STANDBY_POLL_MS = 2e4;
|
|
25358
|
+
var HELLO_WITHIN_MS = 1e4;
|
|
25359
|
+
function parseSessionPush(data) {
|
|
25360
|
+
try {
|
|
25361
|
+
const parsed = SessionPush.safeParse(JSON.parse(data));
|
|
25362
|
+
return parsed.success ? parsed.data : null;
|
|
25363
|
+
} catch {
|
|
25364
|
+
return null;
|
|
25365
|
+
}
|
|
24941
25366
|
}
|
|
24942
|
-
function
|
|
24943
|
-
|
|
24944
|
-
|
|
24945
|
-
|
|
25367
|
+
async function waitForMessage(api, projectId, taskKey, options = {}) {
|
|
25368
|
+
return new Promise((resolve6, reject) => {
|
|
25369
|
+
let finished = false;
|
|
25370
|
+
const finishWait = (outcome) => {
|
|
25371
|
+
if (finished) return;
|
|
25372
|
+
finished = true;
|
|
25373
|
+
if (timer) clearTimeout(timer);
|
|
25374
|
+
held.stop(outcome.kind === "message" ? "handled" : outcome.kind === "timeout" ? "timeout" : "released");
|
|
25375
|
+
setTimeout(() => resolve6(outcome), 50);
|
|
25376
|
+
};
|
|
25377
|
+
const held = new HeldSession(api, projectId, taskKey, {
|
|
25378
|
+
claim: () => options.claim ?? { harness: null, directory: null, branch: null },
|
|
25379
|
+
reconnect: false,
|
|
25380
|
+
onHello: options.onHello,
|
|
25381
|
+
onEnd: () => finishWait({ kind: "ended" }),
|
|
25382
|
+
onDrop: (reason) => finishWait({ kind: "dropped", reason }),
|
|
25383
|
+
onMessage: (push) => {
|
|
25384
|
+
if (!options.onMessage?.(push)) finishWait({ kind: "message", push });
|
|
25385
|
+
}
|
|
25386
|
+
});
|
|
25387
|
+
const timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => finishWait({ kind: "timeout" }), options.timeoutMs) : null;
|
|
25388
|
+
void held.start().catch((error62) => {
|
|
25389
|
+
if (finished) return;
|
|
25390
|
+
finished = true;
|
|
25391
|
+
if (timer) clearTimeout(timer);
|
|
25392
|
+
held.stop();
|
|
25393
|
+
reject(error62);
|
|
25394
|
+
});
|
|
25395
|
+
});
|
|
25396
|
+
}
|
|
25397
|
+
var HeldSession = class {
|
|
25398
|
+
constructor(api, projectId, taskKey, hooks) {
|
|
25399
|
+
this.api = api;
|
|
25400
|
+
this.projectId = projectId;
|
|
25401
|
+
this.taskKey = taskKey;
|
|
25402
|
+
this.hooks = hooks;
|
|
25403
|
+
this.identity = { ...hooks.claim() };
|
|
25404
|
+
}
|
|
25405
|
+
api;
|
|
25406
|
+
projectId;
|
|
25407
|
+
taskKey;
|
|
25408
|
+
hooks;
|
|
25409
|
+
socket = null;
|
|
25410
|
+
stopped = false;
|
|
25411
|
+
connecting = false;
|
|
25412
|
+
timer = null;
|
|
25413
|
+
heartbeat = null;
|
|
25414
|
+
pongDue = null;
|
|
25415
|
+
helloDue = null;
|
|
25416
|
+
greetings = /* @__PURE__ */ new Set();
|
|
25417
|
+
attempt = 0;
|
|
25418
|
+
connected = 0;
|
|
25419
|
+
delivered = /* @__PURE__ */ new Set();
|
|
25420
|
+
identity;
|
|
25421
|
+
session = null;
|
|
25422
|
+
/**
|
|
25423
|
+
* Whoever is waiting for a message. Handed each one, and answers whether it took it (a waiter for one thread lets
|
|
25424
|
+
* the rest pass to the inbox); handed null when the agent is made unreachable under them.
|
|
25425
|
+
*/
|
|
25426
|
+
waiters = /* @__PURE__ */ new Set();
|
|
25427
|
+
/** A write needs an accepted identity. A timeout must fail rather than silently send as another agent. */
|
|
25428
|
+
requireSession() {
|
|
25429
|
+
if (this.stopped) return Promise.reject(new Error("Session ended; start a new session before writing"));
|
|
25430
|
+
if (this.session) return Promise.resolve(this.session);
|
|
25431
|
+
return new Promise((resolve6, reject) => {
|
|
25432
|
+
const finishGreeting = (session) => {
|
|
25433
|
+
clearTimeout(timer);
|
|
25434
|
+
this.greetings.delete(finishGreeting);
|
|
25435
|
+
if (session) resolve6(session);
|
|
25436
|
+
else reject(new Error("Session is not connected; retry when the connection recovers"));
|
|
25437
|
+
};
|
|
25438
|
+
const timer = setTimeout(() => finishGreeting(null), HELLO_WITHIN_MS);
|
|
25439
|
+
this.greetings.add(finishGreeting);
|
|
25440
|
+
});
|
|
25441
|
+
}
|
|
25442
|
+
get isStopped() {
|
|
25443
|
+
return this.stopped;
|
|
25444
|
+
}
|
|
25445
|
+
async start() {
|
|
25446
|
+
await this.connect();
|
|
25447
|
+
}
|
|
25448
|
+
async connect() {
|
|
25449
|
+
if (this.stopped || this.connecting || this.socket) return;
|
|
25450
|
+
this.connecting = true;
|
|
25451
|
+
if (this.timer) clearTimeout(this.timer);
|
|
24946
25452
|
try {
|
|
24947
|
-
|
|
24948
|
-
|
|
24949
|
-
|
|
25453
|
+
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);
|
|
25454
|
+
if (this.stopped) return;
|
|
25455
|
+
const socket = new WebSocket(`${this.api.baseUrl.replace(/^http/, "ws")}/api${ticket.path}`);
|
|
25456
|
+
this.socket = socket;
|
|
25457
|
+
let greeted = false;
|
|
25458
|
+
this.helloDue = setTimeout(() => this.retryConnection(socket, "no hello"), HELLO_WITHIN_MS);
|
|
25459
|
+
socket.addEventListener("open", () => {
|
|
25460
|
+
if (this.stopped || this.socket !== socket) return;
|
|
25461
|
+
this.startHeartbeat(socket);
|
|
25462
|
+
});
|
|
25463
|
+
socket.addEventListener("message", (event) => {
|
|
25464
|
+
if (this.stopped || this.socket !== socket) return;
|
|
25465
|
+
const data = String(event.data);
|
|
25466
|
+
if (data === "pong") {
|
|
25467
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
25468
|
+
this.pongDue = null;
|
|
25469
|
+
return;
|
|
25470
|
+
}
|
|
25471
|
+
const push = parseSessionPush(data);
|
|
25472
|
+
if (!push) return;
|
|
25473
|
+
if (push.type === "hello") {
|
|
25474
|
+
if (greeted) return;
|
|
25475
|
+
try {
|
|
25476
|
+
persistRememberedSession(process.cwd(), push.session.id, this.identity.conversation);
|
|
25477
|
+
} catch {
|
|
25478
|
+
this.retryConnection(socket, "cannot save session identity");
|
|
25479
|
+
return;
|
|
25480
|
+
}
|
|
25481
|
+
greeted = true;
|
|
25482
|
+
this.api.sessionId = push.session.id;
|
|
25483
|
+
this.session = push.session;
|
|
25484
|
+
this.taskKey = push.session.taskKey;
|
|
25485
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25486
|
+
this.helloDue = null;
|
|
25487
|
+
this.attempt = 0;
|
|
25488
|
+
for (const greeting of [...this.greetings]) greeting(push.session);
|
|
25489
|
+
this.hooks.onHello?.(push.session);
|
|
25490
|
+
this.connected += 1;
|
|
25491
|
+
if (this.connected > 1) void Promise.resolve().then(() => !this.stopped && this.socket === socket ? this.hooks.onReconnect?.(push.session) : void 0).catch(() => void 0);
|
|
25492
|
+
return;
|
|
25493
|
+
}
|
|
25494
|
+
if (push.type === "end") {
|
|
25495
|
+
this.hooks.onEnd?.();
|
|
25496
|
+
this.stop();
|
|
25497
|
+
return;
|
|
25498
|
+
}
|
|
25499
|
+
if (push.type === "reachable") {
|
|
25500
|
+
if (this.session) this.session = { ...this.session, reachable: push.on };
|
|
25501
|
+
if (!push.on) this.releaseWaiters();
|
|
25502
|
+
this.hooks.onReachable?.(push.on);
|
|
25503
|
+
return;
|
|
25504
|
+
}
|
|
25505
|
+
if (push.type !== "thread") return;
|
|
25506
|
+
try {
|
|
25507
|
+
this.deliverPush(push);
|
|
25508
|
+
} catch {
|
|
25509
|
+
this.retryConnection(socket, "delivery failed");
|
|
25510
|
+
}
|
|
25511
|
+
});
|
|
25512
|
+
socket.addEventListener("close", (event) => {
|
|
25513
|
+
if (this.socket !== socket) return;
|
|
25514
|
+
this.stopHeartbeat();
|
|
25515
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25516
|
+
this.helloDue = null;
|
|
25517
|
+
this.socket = null;
|
|
25518
|
+
if (this.stopped) return;
|
|
25519
|
+
if (event.reason === "ended") {
|
|
25520
|
+
this.hooks.onEnd?.();
|
|
25521
|
+
this.stop();
|
|
25522
|
+
return;
|
|
25523
|
+
}
|
|
25524
|
+
this.hooks.onDrop?.(event.reason || `code ${event.code}`);
|
|
25525
|
+
if (this.hooks.reconnect === false) {
|
|
25526
|
+
this.stop();
|
|
25527
|
+
return;
|
|
25528
|
+
}
|
|
25529
|
+
if (event.reason === "replaced") this.standBy();
|
|
25530
|
+
else this.schedule();
|
|
25531
|
+
});
|
|
25532
|
+
socket.addEventListener("error", () => {
|
|
25533
|
+
this.retryConnection(socket, "socket error");
|
|
25534
|
+
});
|
|
25535
|
+
} catch (error62) {
|
|
25536
|
+
if (this.hooks.reconnect === false) {
|
|
25537
|
+
this.stop();
|
|
25538
|
+
throw error62;
|
|
25539
|
+
}
|
|
25540
|
+
this.hooks.onDrop?.(error62 instanceof Error ? error62.message : String(error62));
|
|
25541
|
+
this.schedule();
|
|
25542
|
+
} finally {
|
|
25543
|
+
this.connecting = false;
|
|
24950
25544
|
}
|
|
24951
|
-
|
|
24952
|
-
|
|
24953
|
-
|
|
24954
|
-
|
|
24955
|
-
|
|
25545
|
+
}
|
|
25546
|
+
/** Live and recovered messages follow exactly the same local delivery path. */
|
|
25547
|
+
deliverPush(push) {
|
|
25548
|
+
if (this.stopped || this.session?.reachable === false || this.delivered.has(push.message.id)) return;
|
|
25549
|
+
if (this.hooks.onPush?.(push) === false) return;
|
|
25550
|
+
for (const waiter of [...this.waiters]) {
|
|
25551
|
+
if (waiter(push)) {
|
|
25552
|
+
this.markPushDelivered(push.message.id);
|
|
25553
|
+
return;
|
|
24956
25554
|
}
|
|
24957
|
-
|
|
24958
|
-
|
|
25555
|
+
}
|
|
25556
|
+
this.hooks.onMessage(push);
|
|
25557
|
+
this.markPushDelivered(push.message.id);
|
|
25558
|
+
}
|
|
25559
|
+
markPushDelivered(id) {
|
|
25560
|
+
this.delivered.add(id);
|
|
25561
|
+
if (this.delivered.size > 1e4) this.delivered.delete(this.delivered.values().next().value);
|
|
25562
|
+
}
|
|
25563
|
+
/** A ping every 25s; no pong within 10s means the socket is dead however open it looks. Close it and come back. */
|
|
25564
|
+
startHeartbeat(socket) {
|
|
25565
|
+
this.stopHeartbeat();
|
|
25566
|
+
this.heartbeat = setInterval(() => {
|
|
25567
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
24959
25568
|
try {
|
|
24960
|
-
|
|
25569
|
+
socket.send("ping");
|
|
24961
25570
|
} catch {
|
|
25571
|
+
this.retryConnection(socket, "ping failed");
|
|
25572
|
+
return;
|
|
24962
25573
|
}
|
|
24963
|
-
|
|
24964
|
-
|
|
24965
|
-
|
|
24966
|
-
|
|
24967
|
-
|
|
24968
|
-
let head = "";
|
|
24969
|
-
try {
|
|
24970
|
-
const fd = openSync(rollout.path, "r");
|
|
24971
|
-
try {
|
|
24972
|
-
const buffer = Buffer.alloc(64 * 1024);
|
|
24973
|
-
head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
|
|
24974
|
-
} finally {
|
|
24975
|
-
closeSync(fd);
|
|
25574
|
+
if (!this.pongDue) {
|
|
25575
|
+
this.pongDue = setTimeout(() => {
|
|
25576
|
+
this.pongDue = null;
|
|
25577
|
+
this.retryConnection(socket, "no pong");
|
|
25578
|
+
}, PONG_WITHIN_MS);
|
|
24976
25579
|
}
|
|
24977
|
-
}
|
|
24978
|
-
|
|
24979
|
-
|
|
24980
|
-
|
|
24981
|
-
if (
|
|
24982
|
-
|
|
24983
|
-
|
|
24984
|
-
if (
|
|
24985
|
-
|
|
25580
|
+
}, PING_EVERY_MS);
|
|
25581
|
+
}
|
|
25582
|
+
/** Detach first: close may arrive late or never, and an old callback must not change its replacement. */
|
|
25583
|
+
retryConnection(socket, reason) {
|
|
25584
|
+
if (this.stopped || this.socket !== socket) return;
|
|
25585
|
+
this.socket = null;
|
|
25586
|
+
this.stopHeartbeat();
|
|
25587
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25588
|
+
this.helloDue = null;
|
|
24986
25589
|
try {
|
|
24987
|
-
|
|
25590
|
+
socket.close(4e3, reason);
|
|
24988
25591
|
} catch {
|
|
24989
|
-
continue;
|
|
24990
25592
|
}
|
|
24991
|
-
|
|
25593
|
+
this.hooks.onDrop?.(reason);
|
|
25594
|
+
if (this.hooks.reconnect !== false) this.schedule();
|
|
25595
|
+
else this.stop();
|
|
24992
25596
|
}
|
|
24993
|
-
|
|
24994
|
-
|
|
24995
|
-
|
|
24996
|
-
|
|
24997
|
-
if (!path2) return true;
|
|
24998
|
-
try {
|
|
24999
|
-
const fd = openSync(path2, "r");
|
|
25000
|
-
try {
|
|
25001
|
-
const buffer = Buffer.alloc(64 * 1024);
|
|
25002
|
-
const head = buffer.subarray(0, readSync(fd, buffer, 0, buffer.length, 0)).toString("utf8");
|
|
25003
|
-
return isMainChatRollout(head.split("\n")[0] ?? "");
|
|
25004
|
-
} finally {
|
|
25005
|
-
closeSync(fd);
|
|
25006
|
-
}
|
|
25007
|
-
} catch {
|
|
25008
|
-
return true;
|
|
25597
|
+
releaseWaiters() {
|
|
25598
|
+
const waiters = [...this.waiters];
|
|
25599
|
+
this.waiters.clear();
|
|
25600
|
+
for (const waiter of waiters) waiter(null);
|
|
25009
25601
|
}
|
|
25010
|
-
|
|
25011
|
-
|
|
25012
|
-
|
|
25013
|
-
|
|
25014
|
-
|
|
25015
|
-
|
|
25016
|
-
|
|
25017
|
-
|
|
25018
|
-
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
|
|
25022
|
-
|
|
25023
|
-
|
|
25024
|
-
|
|
25025
|
-
|
|
25026
|
-
|
|
25027
|
-
if (
|
|
25602
|
+
stopHeartbeat() {
|
|
25603
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
25604
|
+
if (this.pongDue) clearTimeout(this.pongDue);
|
|
25605
|
+
this.heartbeat = null;
|
|
25606
|
+
this.pongDue = null;
|
|
25607
|
+
}
|
|
25608
|
+
schedule() {
|
|
25609
|
+
if (this.stopped) return;
|
|
25610
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25611
|
+
this.attempt += 1;
|
|
25612
|
+
this.timer = setTimeout(() => void this.connect(), Math.min(3e4, 1e3 * 2 ** Math.min(this.attempt, 5)));
|
|
25613
|
+
}
|
|
25614
|
+
/** Another process holds this session now: look now and then, and take it back once it is no longer live. */
|
|
25615
|
+
standBy() {
|
|
25616
|
+
if (this.stopped) return;
|
|
25617
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25618
|
+
this.timer = setTimeout(async () => {
|
|
25619
|
+
if (this.stopped) return;
|
|
25620
|
+
let holder = true;
|
|
25028
25621
|
try {
|
|
25029
|
-
const
|
|
25030
|
-
|
|
25622
|
+
const sessions = await this.api.get(`/projects/${this.projectId}/sessions`, ApiResponses.Sessions);
|
|
25623
|
+
holder = sessions.some((session) => session.id === this.session?.id && session.state === "live");
|
|
25031
25624
|
} catch {
|
|
25032
25625
|
}
|
|
25033
|
-
|
|
25034
|
-
|
|
25035
|
-
|
|
25036
|
-
return best ? best.path : null;
|
|
25037
|
-
}
|
|
25038
|
-
function resolveCodexBinary() {
|
|
25039
|
-
try {
|
|
25040
|
-
execFileSync4(process.platform === "win32" ? "where" : "which", ["codex"], { stdio: "ignore" });
|
|
25041
|
-
return "codex";
|
|
25042
|
-
} catch {
|
|
25626
|
+
if (holder) this.standBy();
|
|
25627
|
+
else void this.connect();
|
|
25628
|
+
}, STANDBY_POLL_MS);
|
|
25043
25629
|
}
|
|
25044
|
-
|
|
25630
|
+
stop(reason = "released") {
|
|
25631
|
+
if (this.stopped) return;
|
|
25632
|
+
this.stopped = true;
|
|
25633
|
+
this.releaseWaiters();
|
|
25634
|
+
for (const greeting of [...this.greetings]) greeting(null);
|
|
25635
|
+
if (this.helloDue) clearTimeout(this.helloDue);
|
|
25636
|
+
this.helloDue = null;
|
|
25637
|
+
if (reason !== "handled" && this.api.sessionId === this.session?.id) this.api.sessionId = null;
|
|
25638
|
+
if (this.timer) clearTimeout(this.timer);
|
|
25639
|
+
this.stopHeartbeat();
|
|
25640
|
+
const socket = this.socket;
|
|
25641
|
+
this.socket = null;
|
|
25045
25642
|
try {
|
|
25046
|
-
|
|
25047
|
-
if (exe && basename2(exe).startsWith("codex")) return exe;
|
|
25643
|
+
socket?.close(1e3, reason);
|
|
25048
25644
|
} catch {
|
|
25049
25645
|
}
|
|
25050
25646
|
}
|
|
25051
|
-
|
|
25052
|
-
}
|
|
25053
|
-
function fetchHarnessConversation(cwd) {
|
|
25054
|
-
const harness = parseHarnessFromCommand(fetchParentCommand());
|
|
25055
|
-
if (harness === "claude") {
|
|
25056
|
-
const id = resolveClaudeSession()?.sessionId;
|
|
25057
|
-
return id ? { id, harness } : null;
|
|
25058
|
-
}
|
|
25059
|
-
if (harness === "codex" && !isNonInteractiveHarness()) {
|
|
25060
|
-
const id = fetchCodexConversationByCwd(fetchCodexHomes().map((home) => join8(home, "sessions")), cwd);
|
|
25061
|
-
return id ? { id, harness } : null;
|
|
25062
|
-
}
|
|
25063
|
-
return null;
|
|
25064
|
-
}
|
|
25065
|
-
|
|
25066
|
-
// src/platform/agent-identity.ts
|
|
25067
|
-
var processConversation = randomUUID4();
|
|
25068
|
-
var startedAt = Date.now();
|
|
25069
|
-
function fetchKnownConversation(cwd) {
|
|
25070
|
-
const remembered = fetchRememberedConversation(cwd, startedAt - 12e4);
|
|
25071
|
-
if (remembered?.harness === "codex" && isMainChatThread(fetchCodexHomes().map((home) => join9(home, "sessions")), remembered.id)) return remembered;
|
|
25072
|
-
return fetchHarnessConversation(cwd) ?? remembered;
|
|
25073
|
-
}
|
|
25074
|
-
function resolveAgentIdentity(cwd, fallback = "process") {
|
|
25075
|
-
const conversation = fetchKnownConversation(cwd);
|
|
25076
|
-
const repo = fetchRepo();
|
|
25077
|
-
return {
|
|
25078
|
-
conversation: conversation?.id ?? (fallback === "process" ? processConversation : `dir:${resolve3(cwd)}`),
|
|
25079
|
-
harness: conversation?.harness ?? null,
|
|
25080
|
-
directory: repo ? basename3(repo.worktree) : basename3(cwd),
|
|
25081
|
-
branch: fetchCurrentBranch(cwd)
|
|
25082
|
-
};
|
|
25083
|
-
}
|
|
25647
|
+
};
|
|
25084
25648
|
|
|
25085
25649
|
// src/platform/bounded.ts
|
|
25086
25650
|
var RESULT_CAP = 16e3;
|
|
@@ -25093,45 +25657,99 @@ function truncateToolResult(result) {
|
|
|
25093
25657
|
|
|
25094
25658
|
// src/platform/mcp.ts
|
|
25095
25659
|
import { createInterface } from "readline";
|
|
25096
|
-
|
|
25097
|
-
|
|
25098
|
-
|
|
25660
|
+
var McpOutput = class {
|
|
25661
|
+
deliveries = [];
|
|
25662
|
+
remainingMessageChars = RESULT_CAP;
|
|
25663
|
+
registerDelivery(delivery) {
|
|
25664
|
+
this.deliveries.push(delivery);
|
|
25665
|
+
this.remainingMessageChars -= delivery.text.length;
|
|
25666
|
+
}
|
|
25667
|
+
formatResult(text) {
|
|
25668
|
+
const messages = this.deliveries.map((delivery) => delivery.text).filter(Boolean);
|
|
25669
|
+
return [truncateToolResult(text), ...messages.length ? ["--- Messages for you (read once; do not acknowledge). Use session check for any remaining messages. ---", ...messages] : []].filter(Boolean).join("\n\n");
|
|
25670
|
+
}
|
|
25671
|
+
acknowledgeDeliveries() {
|
|
25672
|
+
for (const delivery of this.deliveries) delivery.acknowledge();
|
|
25673
|
+
}
|
|
25674
|
+
releaseDeliveries() {
|
|
25675
|
+
for (const delivery of this.deliveries) delivery.release();
|
|
25676
|
+
}
|
|
25677
|
+
};
|
|
25678
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25679
|
+
function buildMcpRequestHandler(tools, info, sendJsonRpcMessage) {
|
|
25099
25680
|
const sendJsonRpcResult = (id, result) => sendJsonRpcMessage({ jsonrpc: "2.0", id, result });
|
|
25100
25681
|
const sendJsonRpcError = (id, code, message) => sendJsonRpcMessage({ jsonrpc: "2.0", id, error: { code, message } });
|
|
25101
|
-
|
|
25102
|
-
|
|
25682
|
+
return async (value) => {
|
|
25683
|
+
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") {
|
|
25684
|
+
await sendJsonRpcError(null, -32600, "Invalid request");
|
|
25685
|
+
return;
|
|
25686
|
+
}
|
|
25687
|
+
const request = value;
|
|
25688
|
+
const { id, method } = request;
|
|
25689
|
+
if (id === void 0) return;
|
|
25690
|
+
if (request.params !== void 0 && !isRecord(request.params)) {
|
|
25691
|
+
await sendJsonRpcError(id, -32602, "Params must be an object");
|
|
25692
|
+
return;
|
|
25693
|
+
}
|
|
25694
|
+
const params = request.params ?? {};
|
|
25103
25695
|
switch (method) {
|
|
25104
25696
|
case "initialize":
|
|
25105
|
-
sendJsonRpcResult(id, { protocolVersion: params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: info });
|
|
25697
|
+
await sendJsonRpcResult(id, { protocolVersion: params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: info });
|
|
25106
25698
|
return;
|
|
25107
25699
|
case "notifications/initialized":
|
|
25108
25700
|
case "notifications/cancelled":
|
|
25109
25701
|
return;
|
|
25110
25702
|
case "ping":
|
|
25111
|
-
sendJsonRpcResult(id, {});
|
|
25703
|
+
await sendJsonRpcResult(id, {});
|
|
25112
25704
|
return;
|
|
25113
25705
|
case "tools/list":
|
|
25114
|
-
sendJsonRpcResult(id, { tools: tools.map(({ name, description: description2, inputSchema }) => ({ name, description: description2, inputSchema })) });
|
|
25706
|
+
await sendJsonRpcResult(id, { tools: tools.map(({ name, description: description2, inputSchema }) => ({ name, description: description2, inputSchema })) });
|
|
25115
25707
|
return;
|
|
25116
25708
|
case "tools/call": {
|
|
25117
25709
|
const name = params.name;
|
|
25710
|
+
if (typeof name !== "string" || params.arguments !== void 0 && !isRecord(params.arguments)) {
|
|
25711
|
+
await sendJsonRpcError(id, -32602, "Tool name and arguments are invalid");
|
|
25712
|
+
return;
|
|
25713
|
+
}
|
|
25118
25714
|
const tool = tools.find((candidate) => candidate.name === name);
|
|
25119
25715
|
if (!tool) {
|
|
25120
|
-
sendJsonRpcError(id, -32602, `No tool named ${name}`);
|
|
25716
|
+
await sendJsonRpcError(id, -32602, `No tool named ${name}`);
|
|
25717
|
+
return;
|
|
25718
|
+
}
|
|
25719
|
+
const output2 = new McpOutput();
|
|
25720
|
+
let text;
|
|
25721
|
+
try {
|
|
25722
|
+
text = output2.formatResult(await tool.run(params.arguments ?? {}, output2));
|
|
25723
|
+
} catch (error62) {
|
|
25724
|
+
output2.releaseDeliveries();
|
|
25725
|
+
await sendJsonRpcResult(id, { content: [{ type: "text", text: error62 instanceof Error ? error62.message : String(error62) }], isError: true });
|
|
25121
25726
|
return;
|
|
25122
25727
|
}
|
|
25123
25728
|
try {
|
|
25124
|
-
|
|
25125
|
-
|
|
25729
|
+
await sendJsonRpcResult(id, { content: [{ type: "text", text }] });
|
|
25730
|
+
output2.acknowledgeDeliveries();
|
|
25126
25731
|
} catch (error62) {
|
|
25127
|
-
|
|
25732
|
+
output2.releaseDeliveries();
|
|
25733
|
+
throw error62;
|
|
25128
25734
|
}
|
|
25129
25735
|
return;
|
|
25130
25736
|
}
|
|
25131
25737
|
default:
|
|
25132
|
-
if (id !== void 0) sendJsonRpcError(id, -32601, `Unknown method ${method}`);
|
|
25738
|
+
if (id !== void 0) await sendJsonRpcError(id, -32601, `Unknown method ${method}`);
|
|
25133
25739
|
}
|
|
25134
25740
|
};
|
|
25741
|
+
}
|
|
25742
|
+
function serveMcp(tools, info) {
|
|
25743
|
+
const sendJsonRpcMessage = (message) => new Promise((resolve6, reject) => {
|
|
25744
|
+
process.stdout.write(`${JSON.stringify(message)}
|
|
25745
|
+
`, (error62) => error62 ? reject(error62) : resolve6());
|
|
25746
|
+
});
|
|
25747
|
+
const endFailedOutput = () => {
|
|
25748
|
+
process.exitCode = 1;
|
|
25749
|
+
process.stdin.destroy();
|
|
25750
|
+
};
|
|
25751
|
+
process.stdout.on("error", endFailedOutput);
|
|
25752
|
+
const handleJsonRpcRequest = buildMcpRequestHandler(tools, info, sendJsonRpcMessage);
|
|
25135
25753
|
const lines = createInterface({ input: process.stdin });
|
|
25136
25754
|
lines.on("line", (line) => {
|
|
25137
25755
|
if (!line.trim()) return;
|
|
@@ -25139,20 +25757,19 @@ function serveMcp(tools, info) {
|
|
|
25139
25757
|
try {
|
|
25140
25758
|
request = JSON.parse(line);
|
|
25141
25759
|
} catch {
|
|
25142
|
-
|
|
25760
|
+
void sendJsonRpcMessage({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }).catch(endFailedOutput);
|
|
25143
25761
|
return;
|
|
25144
25762
|
}
|
|
25145
|
-
void handleJsonRpcRequest(request);
|
|
25763
|
+
void handleJsonRpcRequest(request).catch(endFailedOutput);
|
|
25146
25764
|
});
|
|
25147
|
-
lines.on("close", () => process.exit(0));
|
|
25148
25765
|
}
|
|
25149
25766
|
|
|
25150
25767
|
// src/commands/tasks.ts
|
|
25151
|
-
import { readFileSync as
|
|
25768
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
25152
25769
|
|
|
25153
25770
|
// src/commands/items.ts
|
|
25154
|
-
import { readFileSync as
|
|
25155
|
-
var fetchTextArgument = (value) => value === "-" ?
|
|
25771
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
25772
|
+
var fetchTextArgument = (value) => value === "-" ? readFileSync12(0, "utf8").trim() : value;
|
|
25156
25773
|
var buildItemsPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
25157
25774
|
function formatPlanItems(items) {
|
|
25158
25775
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -25179,7 +25796,7 @@ function registerItemCommands(task, registerProjectOption) {
|
|
|
25179
25796
|
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) => {
|
|
25180
25797
|
const api = buildAuthedApiClient();
|
|
25181
25798
|
const project = await resolveProject(api, options.project);
|
|
25182
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
25799
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync12(0, "utf8") : lines.join("\n");
|
|
25183
25800
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
25184
25801
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
25185
25802
|
const items = await api.post(buildItemsPath(project.id, key), ApiResponses.TaskItems, { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -25236,7 +25853,7 @@ function parseOneOf(value, allowed, what) {
|
|
|
25236
25853
|
if (allowed.includes(normalized)) return normalized;
|
|
25237
25854
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
25238
25855
|
}
|
|
25239
|
-
var fetchTextArgument2 = (value) => value === "-" ?
|
|
25856
|
+
var fetchTextArgument2 = (value) => value === "-" ? readFileSync13(0, "utf8").trim() : value;
|
|
25240
25857
|
var buildTaskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
25241
25858
|
var fetchProject = (api, id) => api.get(`/projects/${id}`, ApiResponses.Project);
|
|
25242
25859
|
function resolveStatus(project, ref) {
|
|
@@ -25387,21 +26004,21 @@ ${buildTaskUrl(api.baseUrl, project.id, created)}`);
|
|
|
25387
26004
|
}
|
|
25388
26005
|
|
|
25389
26006
|
// src/platform/update-check.ts
|
|
25390
|
-
import { existsSync as
|
|
25391
|
-
import { dirname as
|
|
26007
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
|
|
26008
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
25392
26009
|
var CHECK_EVERY_MS = 60 * 60 * 1e3;
|
|
25393
26010
|
var REGISTRY = "https://registry.npmjs.org/slopwaresystems/latest";
|
|
25394
|
-
var buildUpdateCachePath = () =>
|
|
26011
|
+
var buildUpdateCachePath = () => join14(dirname7(buildConfigPath()), "update-check.json");
|
|
25395
26012
|
function fetchUpdateCache() {
|
|
25396
26013
|
try {
|
|
25397
|
-
return JSON.parse(
|
|
26014
|
+
return JSON.parse(readFileSync14(buildUpdateCachePath(), "utf8"));
|
|
25398
26015
|
} catch {
|
|
25399
26016
|
return { checkedAt: 0, latest: null };
|
|
25400
26017
|
}
|
|
25401
26018
|
}
|
|
25402
26019
|
function persistUpdateCache(cache) {
|
|
25403
|
-
|
|
25404
|
-
|
|
26020
|
+
mkdirSync8(dirname7(buildUpdateCachePath()), { recursive: true });
|
|
26021
|
+
writeFileSync8(buildUpdateCachePath(), JSON.stringify(cache));
|
|
25405
26022
|
}
|
|
25406
26023
|
function isNewerVersion(a, b) {
|
|
25407
26024
|
const parseVersionParts = (version2) => version2.split(".").map((part) => Number.parseInt(part, 10));
|
|
@@ -25430,11 +26047,11 @@ function buildUpdateNotice(current) {
|
|
|
25430
26047
|
if (!latest || !isNewerVersion(latest, current)) return null;
|
|
25431
26048
|
return `Update available: slopwaresystems ${latest} (this is ${current}). Ask your person to run: npm i -g slopwaresystems && slop mcp install`;
|
|
25432
26049
|
}
|
|
25433
|
-
var isUpdateCheckEnabled = () => !
|
|
26050
|
+
var isUpdateCheckEnabled = () => !existsSync10(join14(dirname7(buildConfigPath()), "no-update-check")) && process.env.SLOP_NO_UPDATE_CHECK !== "1";
|
|
25434
26051
|
|
|
25435
26052
|
// src/commands/threads.ts
|
|
25436
|
-
import { readFileSync as
|
|
25437
|
-
var fetchTextArgument3 = (value) => value === "-" ?
|
|
26053
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
26054
|
+
var fetchTextArgument3 = (value) => value === "-" ? readFileSync15(0, "utf8").trim() : value;
|
|
25438
26055
|
var formatMessageTime = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
25439
26056
|
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}` : ""})`;
|
|
25440
26057
|
function formatThreadLine(thread) {
|
|
@@ -25505,76 +26122,6 @@ function registerThreadCommands(program3, task) {
|
|
|
25505
26122
|
}
|
|
25506
26123
|
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.`;
|
|
25507
26124
|
|
|
25508
|
-
// src/platform/inbox.ts
|
|
25509
|
-
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
25510
|
-
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";
|
|
25511
|
-
import { dirname as dirname5, join as join11 } from "path";
|
|
25512
|
-
var buildInboxDir = () => join11(dirname5(buildConfigPath()), "inbox");
|
|
25513
|
-
var resolveRealCwd2 = (cwd) => {
|
|
25514
|
-
try {
|
|
25515
|
-
return realpathSync3(cwd);
|
|
25516
|
-
} catch {
|
|
25517
|
-
return cwd;
|
|
25518
|
-
}
|
|
25519
|
-
};
|
|
25520
|
-
var generateInboxKey = (cwd) => createHash4("sha256").update(resolveRealCwd2(cwd)).digest("hex").slice(0, 16);
|
|
25521
|
-
var buildInboxPath = (cwd) => join11(buildInboxDir(), `${generateInboxKey(cwd)}.jsonl`);
|
|
25522
|
-
var buildInboxQueuePath = (cwd) => join11(buildInboxDir(), generateInboxKey(cwd));
|
|
25523
|
-
var sequence = 0;
|
|
25524
|
-
function persistInboxPush(cwd, push) {
|
|
25525
|
-
const directory = buildInboxQueuePath(cwd);
|
|
25526
|
-
mkdirSync6(directory, { recursive: true });
|
|
25527
|
-
const name = `${Date.now()}.${process.pid}.${String(sequence++).padStart(12, "0")}.${randomUUID5()}`;
|
|
25528
|
-
const temporary = join11(directory, `${name}.writing`);
|
|
25529
|
-
writeFileSync7(temporary, JSON.stringify(push), { flag: "wx" });
|
|
25530
|
-
renameSync(temporary, join11(directory, `${name}.json`));
|
|
25531
|
-
}
|
|
25532
|
-
function drainInbox(cwd) {
|
|
25533
|
-
const pushes = drainLegacyInbox(cwd);
|
|
25534
|
-
const directory = buildInboxQueuePath(cwd);
|
|
25535
|
-
let names;
|
|
25536
|
-
try {
|
|
25537
|
-
names = readdirSync3(directory).filter((file2) => file2.endsWith(".json")).sort();
|
|
25538
|
-
} catch (error62) {
|
|
25539
|
-
if (error62.code === "ENOENT") return pushes;
|
|
25540
|
-
throw error62;
|
|
25541
|
-
}
|
|
25542
|
-
for (const name of names) {
|
|
25543
|
-
const taken = join11(directory, `${name}.${randomUUID5()}.draining`);
|
|
25544
|
-
try {
|
|
25545
|
-
renameSync(join11(directory, name), taken);
|
|
25546
|
-
} catch (error62) {
|
|
25547
|
-
if (error62.code === "ENOENT") continue;
|
|
25548
|
-
throw error62;
|
|
25549
|
-
}
|
|
25550
|
-
try {
|
|
25551
|
-
pushes.push(JSON.parse(readFileSync12(taken, "utf8")));
|
|
25552
|
-
} finally {
|
|
25553
|
-
rmSync4(taken, { force: true });
|
|
25554
|
-
}
|
|
25555
|
-
}
|
|
25556
|
-
return pushes;
|
|
25557
|
-
}
|
|
25558
|
-
function drainLegacyInbox(cwd) {
|
|
25559
|
-
const path2 = buildInboxPath(cwd);
|
|
25560
|
-
if (!existsSync9(path2)) return [];
|
|
25561
|
-
const taken = `${path2}.${process.pid}.${Date.now()}.draining`;
|
|
25562
|
-
try {
|
|
25563
|
-
renameSync(path2, taken);
|
|
25564
|
-
} catch {
|
|
25565
|
-
return [];
|
|
25566
|
-
}
|
|
25567
|
-
const lines = readFileSync12(taken, "utf8").split("\n").filter(Boolean);
|
|
25568
|
-
rmSync4(taken, { force: true });
|
|
25569
|
-
return lines.flatMap((line) => {
|
|
25570
|
-
try {
|
|
25571
|
-
return [JSON.parse(line)];
|
|
25572
|
-
} catch {
|
|
25573
|
-
return [];
|
|
25574
|
-
}
|
|
25575
|
-
});
|
|
25576
|
-
}
|
|
25577
|
-
|
|
25578
26125
|
// src/commands/mcp.ts
|
|
25579
26126
|
var buildStringSchema = (description2) => ({ type: "string", description: description2 });
|
|
25580
26127
|
var buildIntegerSchema = (description2) => ({ type: "integer", description: description2 });
|
|
@@ -25593,139 +26140,142 @@ var formatSessionLine = (session, mine = null) => [
|
|
|
25593
26140
|
session.reachable ? "" : "(not visible to teammates)"
|
|
25594
26141
|
].filter(Boolean).join(" ");
|
|
25595
26142
|
async function buildUnreadDigest(api, projectId, taskKey) {
|
|
25596
|
-
const [about,
|
|
26143
|
+
const [about, general] = await Promise.all([
|
|
25597
26144
|
taskKey ? api.get(`/projects/${projectId}/threads?task=${encodeURIComponent(taskKey)}&unread=1&read=1`, ApiResponses.Threads) : Promise.resolve([]),
|
|
25598
|
-
api.get(`/projects/${projectId}/threads?participant=1&unread=1&read=1`, ApiResponses.Threads),
|
|
25599
26145
|
api.get(`/projects/${projectId}/threads?scope=general&unread=1&read=1`, ApiResponses.Threads)
|
|
25600
26146
|
]);
|
|
25601
26147
|
const lines = [];
|
|
25602
26148
|
if (about.length) lines.push(`Unread about ${taskKey} (read one with the thread tool):`, ...about.map(formatThreadLine));
|
|
25603
|
-
const elsewhere = mine.filter((thread) => !about.some((candidate) => candidate.id === thread.id));
|
|
25604
|
-
if (elsewhere.length) lines.push("Unread in threads you are part of (answers to you):", ...elsewhere.map(formatThreadLine));
|
|
25605
26149
|
if (general.length) lines.push("Unread general threads (context; nobody has to answer):", ...general.map(formatThreadLine));
|
|
25606
26150
|
return lines.length ? lines.join("\n") : "Nothing unread.";
|
|
25607
26151
|
}
|
|
25608
|
-
async function
|
|
25609
|
-
const
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
for (const thread of [...about, ...mine]) {
|
|
25615
|
-
if (seen.has(thread.id) || thread.unread === 0) continue;
|
|
25616
|
-
seen.add(thread.id);
|
|
25617
|
-
const detail = await api.get(`/projects/${projectId}/threads/${thread.id}?read=0`, ApiResponses.ThreadDetail);
|
|
25618
|
-
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 });
|
|
26152
|
+
async function fetchProjectsOverview(api) {
|
|
26153
|
+
const list = await api.get("/projects", ApiResponses.Projects);
|
|
26154
|
+
let linked = null;
|
|
26155
|
+
try {
|
|
26156
|
+
linked = (await resolveProject(api)).key;
|
|
26157
|
+
} catch {
|
|
25619
26158
|
}
|
|
25620
|
-
}
|
|
25621
|
-
|
|
25622
|
-
|
|
25623
|
-
|
|
26159
|
+
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>");
|
|
26160
|
+
}
|
|
26161
|
+
function createProjectTools(api, project, identity) {
|
|
26162
|
+
api.sessionId = null;
|
|
26163
|
+
const resolveProjectFromArgs = async (_args) => project;
|
|
26164
|
+
const owner = { baseUrl: api.baseUrl, projectId: project.id, conversation: identity.conversation };
|
|
26165
|
+
const claimOwnInbox = (output2, accept = () => true) => {
|
|
26166
|
+
let remaining = output2.remainingMessageChars;
|
|
26167
|
+
const delivery = claimInbox(process.cwd(), owner, (push) => {
|
|
26168
|
+
if (!accept(push)) return false;
|
|
26169
|
+
const length = formatPushBlock(push).length + 2;
|
|
26170
|
+
if (length > remaining && remaining !== RESULT_CAP) return false;
|
|
26171
|
+
remaining -= length;
|
|
26172
|
+
return true;
|
|
26173
|
+
});
|
|
26174
|
+
output2.registerDelivery({ ...delivery, text: delivery.pushes.map(formatPushBlock).join("\n\n") });
|
|
26175
|
+
return delivery.pushes.length > 0;
|
|
26176
|
+
};
|
|
26177
|
+
const persistOwnInboxPush = (push) => persistInboxPush(process.cwd(), push, owner);
|
|
25624
26178
|
let held = null;
|
|
25625
|
-
let
|
|
25626
|
-
const
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
|
|
26179
|
+
let stopped = false;
|
|
26180
|
+
const recovery = new SessionRecovery(api, owner, process.cwd(), (push) => held?.deliverPush(push));
|
|
26181
|
+
let waking = false;
|
|
26182
|
+
const wakeOwnHarness = () => {
|
|
26183
|
+
if (waking || stopped) return;
|
|
26184
|
+
waking = true;
|
|
26185
|
+
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.`;
|
|
26186
|
+
const wake = identity.harness === "codex" && identity.conversation ? wakeCodex(text, identity.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : identity.harness === "claude" ? wakeHarness(text, void 0, identity.conversation) : Promise.resolve(false);
|
|
26187
|
+
void wake.catch(() => false).finally(() => {
|
|
26188
|
+
waking = false;
|
|
26189
|
+
});
|
|
26190
|
+
};
|
|
26191
|
+
const resolveOwnAgentIdentity = () => identity;
|
|
25630
26192
|
const holdSession = async (projectId, taskKey) => {
|
|
25631
|
-
if (
|
|
26193
|
+
if (stopped) throw new Error("The project session is stopping");
|
|
26194
|
+
if (held && !held.isStopped && held.projectId === projectId && held.taskKey === taskKey) {
|
|
26195
|
+
await held.requireSession();
|
|
26196
|
+
return;
|
|
26197
|
+
}
|
|
26198
|
+
await recovery.syncCursor();
|
|
26199
|
+
if (stopped) throw new Error("The project session is stopping");
|
|
26200
|
+
if (held && !held.isStopped && held.taskKey === taskKey) {
|
|
26201
|
+
await held.requireSession();
|
|
26202
|
+
return;
|
|
26203
|
+
}
|
|
25632
26204
|
held?.stop();
|
|
25633
26205
|
held = new HeldSession(api, projectId, taskKey, {
|
|
25634
|
-
|
|
25635
|
-
|
|
25636
|
-
// a turn at once; mid-turn, it becomes the next turn when the running one ends, which is why the skills tell
|
|
25637
|
-
// agents to treat messages as letters that may be read late. When neither can be done the message waits in
|
|
25638
|
-
// the inbox and rides the chat's next slop tool result. Nothing holds a turn open.
|
|
25639
|
-
onMessage: (push) => {
|
|
25640
|
-
const me = resolveOwnAgentIdentity();
|
|
25641
|
-
const wake = me.harness === "codex" && me.conversation ? wakeCodex(formatWakeMessage([push]), me.conversation, { binary: resolveCodexBinary(), codexHome: resolveOwnHarnessHome("CODEX_HOME") }) : wakeHarness(formatWakeMessage([push]));
|
|
25642
|
-
void wake.then((woken) => {
|
|
25643
|
-
if (!woken) persistInboxPush(process.cwd(), push);
|
|
25644
|
-
});
|
|
25645
|
-
},
|
|
26206
|
+
onPush: persistOwnInboxPush,
|
|
26207
|
+
onMessage: wakeOwnHarness,
|
|
25646
26208
|
claim: resolveOwnAgentIdentity,
|
|
25647
|
-
onReconnect: (
|
|
26209
|
+
onReconnect: () => recovery.sync()
|
|
25648
26210
|
});
|
|
25649
|
-
|
|
26211
|
+
const current = held;
|
|
26212
|
+
await current.start();
|
|
26213
|
+
await current.requireSession();
|
|
26214
|
+
await recovery.sync();
|
|
26215
|
+
if (held === current && !current.isStopped && hasPendingInboxMessages(process.cwd(), owner)) wakeOwnHarness();
|
|
25650
26216
|
};
|
|
25651
26217
|
const resolveProjectWithPresence = async (args) => {
|
|
25652
26218
|
const target = await resolveProjectFromArgs(args);
|
|
25653
|
-
if (!held) await holdSession(target.id, null);
|
|
25654
|
-
await held?.
|
|
26219
|
+
if (!held || held.isStopped) await holdSession(target.id, null);
|
|
26220
|
+
await held?.requireSession();
|
|
25655
26221
|
return target;
|
|
25656
26222
|
};
|
|
25657
26223
|
const releaseTaskSession = async () => {
|
|
25658
26224
|
if (held?.taskKey) await holdSession(held.projectId, null);
|
|
25659
26225
|
};
|
|
25660
|
-
|
|
25661
|
-
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
25665
|
-
if (!held) await holdSession(target.id, null);
|
|
25666
|
-
})().catch(() => void 0);
|
|
25667
|
-
if (!isNonInteractiveHarness()) void trustCodexHooksForOwnHome().catch(() => void 0);
|
|
25668
|
-
const endSessionAndExit = () => {
|
|
25669
|
-
held?.stop();
|
|
25670
|
-
held = null;
|
|
25671
|
-
setTimeout(() => process.exit(0), 300).unref();
|
|
25672
|
-
};
|
|
25673
|
-
process.stdin.once("end", endSessionAndExit);
|
|
25674
|
-
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, endSessionAndExit);
|
|
25675
|
-
const waitForNextMessage = async (thread, minutes) => {
|
|
25676
|
-
const queued = drainInbox(process.cwd());
|
|
25677
|
-
const wanted = queued.filter((push) => !thread || push.thread.id === thread);
|
|
25678
|
-
for (const other of queued) if (!wanted.includes(other)) persistInboxPush(process.cwd(), other);
|
|
25679
|
-
if (wanted.length) return { pushes: wanted, released: false };
|
|
25680
|
-
return new Promise((resolve5) => {
|
|
26226
|
+
const waitForNextMessage = async (output2, thread, minutes) => {
|
|
26227
|
+
if (claimOwnInbox(output2, (push) => !thread || push.thread.id === thread)) return { received: true, released: false };
|
|
26228
|
+
const waitingOn = held;
|
|
26229
|
+
if (!waitingOn || waitingOn.isStopped || !waitingOn.session?.reachable) return { received: false, released: true };
|
|
26230
|
+
return new Promise((resolve6) => {
|
|
25681
26231
|
const acceptDeliveredPush = (delivered) => {
|
|
25682
26232
|
if (delivered && thread && delivered.thread.id !== thread) return false;
|
|
25683
|
-
|
|
26233
|
+
if (delivered && !claimOwnInbox(output2, (push) => push.message.id === delivered.message.id)) return false;
|
|
26234
|
+
waitingOn.waiters.delete(acceptDeliveredPush);
|
|
25684
26235
|
clearTimeout(timer);
|
|
25685
|
-
|
|
26236
|
+
resolve6({ received: delivered !== null, released: delivered === null });
|
|
25686
26237
|
return true;
|
|
25687
26238
|
};
|
|
25688
|
-
|
|
26239
|
+
waitingOn.waiters.add(acceptDeliveredPush);
|
|
25689
26240
|
const timer = setTimeout(() => {
|
|
25690
|
-
|
|
25691
|
-
|
|
26241
|
+
waitingOn.waiters.delete(acceptDeliveredPush);
|
|
26242
|
+
resolve6({ received: false, released: false });
|
|
25692
26243
|
}, minutes * 6e4);
|
|
25693
26244
|
});
|
|
25694
26245
|
};
|
|
25695
26246
|
const formatWaitOutcome = (outcome, minutes, thread) => {
|
|
25696
|
-
if (outcome.
|
|
25697
|
-
if (outcome.released) return "
|
|
26247
|
+
if (outcome.received) return "";
|
|
26248
|
+
if (outcome.released) return "This wait ended because the session was released, replaced, or made unreachable.";
|
|
25698
26249
|
return thread ? `No reply in ${thread} in ${minutes} minutes.` : `No message in ${minutes} minutes.`;
|
|
25699
26250
|
};
|
|
25700
|
-
|
|
26251
|
+
const tools = [
|
|
25701
26252
|
{
|
|
25702
26253
|
name: "wait_for_message",
|
|
25703
26254
|
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.",
|
|
25704
26255
|
inputSchema: buildToolSchema({ task: buildStringSchema("Task key to go live on first (optional)"), thread: buildStringSchema("Only a message in this thread"), minutes: buildIntegerSchema("Give up after (default 300; 20 with thread)"), project: PROJECT }),
|
|
25705
|
-
run: async (args) => {
|
|
26256
|
+
run: async (args, output2) => {
|
|
25706
26257
|
const target = await resolveProjectWithPresence(args);
|
|
25707
26258
|
if (args.task) await holdSession(target.id, String(args.task));
|
|
25708
26259
|
const thread = args.thread ? String(args.thread) : null;
|
|
25709
26260
|
const minutes = typeof args.minutes === "number" ? args.minutes : thread ? 20 : 300;
|
|
25710
|
-
return formatWaitOutcome(await waitForNextMessage(thread, minutes), minutes, thread);
|
|
26261
|
+
return formatWaitOutcome(await waitForNextMessage(output2, thread, minutes), minutes, thread);
|
|
25711
26262
|
}
|
|
25712
26263
|
},
|
|
25713
26264
|
{
|
|
25714
26265
|
name: "check_messages",
|
|
25715
26266
|
description: "What is unread right now: anything the held session received since you last looked, threads about a task (if given), and general threads. Does not wait. Marks them read.",
|
|
25716
26267
|
inputSchema: buildToolSchema({ task: buildStringSchema("Task key"), project: PROJECT }),
|
|
25717
|
-
run: async (args) => {
|
|
26268
|
+
run: async (args, output2) => {
|
|
25718
26269
|
const target = await resolveProjectWithPresence(args);
|
|
25719
|
-
|
|
25720
|
-
|
|
25721
|
-
return [...queued, rest].join("\n\n");
|
|
26270
|
+
claimOwnInbox(output2);
|
|
26271
|
+
return buildUnreadDigest(api, target.id, args.task ?? null);
|
|
25722
26272
|
}
|
|
25723
26273
|
},
|
|
25724
26274
|
{
|
|
25725
26275
|
name: "reply",
|
|
25726
26276
|
description: "Reply in a thread. In a thread about a task, it reaches whoever is live on that task now, or waits for them.",
|
|
25727
26277
|
inputSchema: buildToolSchema({ thread: buildStringSchema("Thread id"), body: buildStringSchema("The message (Markdown)"), wait: buildIntegerSchema("Claude Code only, rarely: then wait this many minutes for a reply in the thread"), project: PROJECT }, ["thread", "body"]),
|
|
25728
|
-
run: async (args) => {
|
|
26278
|
+
run: async (args, output2) => {
|
|
25729
26279
|
const target = await resolveProjectWithPresence(args);
|
|
25730
26280
|
const thread = String(args.thread);
|
|
25731
26281
|
const result = await api.post(`/projects/${target.id}/threads/${thread}/messages`, ApiResponses.CreatedThreadMessage, { body: String(args.body) });
|
|
@@ -25733,14 +26283,14 @@ ${pushes.map(formatPushBlock).join("\n\n")}`;
|
|
|
25733
26283
|
if (typeof args.wait !== "number" || args.wait <= 0) return replied;
|
|
25734
26284
|
return `${replied}
|
|
25735
26285
|
|
|
25736
|
-
${formatWaitOutcome(await waitForNextMessage(thread, args.wait), args.wait, thread)}`;
|
|
26286
|
+
${formatWaitOutcome(await waitForNextMessage(output2, thread, args.wait), args.wait, thread)}`;
|
|
25737
26287
|
}
|
|
25738
26288
|
},
|
|
25739
26289
|
{
|
|
25740
26290
|
name: "post",
|
|
25741
26291
|
description: "Start a thread. With a task: about that task, to whoever is live on it (or whoever picks it up). Without: general, for people and for context; nobody has to answer.",
|
|
25742
26292
|
inputSchema: buildToolSchema({ subject: buildStringSchema("Subject (default: the first line of the body)"), body: buildStringSchema("First message (Markdown)"), task: buildStringSchema("Task key, to make it about that task"), wait: buildIntegerSchema("Claude Code only, rarely: then wait this many minutes for a reply in the thread"), project: PROJECT }, ["body"]),
|
|
25743
|
-
run: async (args) => {
|
|
26293
|
+
run: async (args, output2) => {
|
|
25744
26294
|
const target = await resolveProjectWithPresence(args);
|
|
25745
26295
|
const result = await api.post(`/projects/${target.id}/threads`, ApiResponses.CreatedThread, { subject: args.subject ? String(args.subject) : void 0, body: String(args.body), task: args.task, to: args.to });
|
|
25746
26296
|
const names = result.thread.to.map((recipient) => recipient.agent).join(", ");
|
|
@@ -25748,7 +26298,7 @@ ${formatWaitOutcome(await waitForNextMessage(thread, args.wait), args.wait, thre
|
|
|
25748
26298
|
if (typeof args.wait !== "number" || args.wait <= 0) return started;
|
|
25749
26299
|
return `${started}
|
|
25750
26300
|
|
|
25751
|
-
${formatWaitOutcome(await waitForNextMessage(result.thread.id, args.wait), args.wait, result.thread.id)}`;
|
|
26301
|
+
${formatWaitOutcome(await waitForNextMessage(output2, result.thread.id, args.wait), args.wait, result.thread.id)}`;
|
|
25752
26302
|
}
|
|
25753
26303
|
},
|
|
25754
26304
|
{
|
|
@@ -26005,21 +26555,6 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
26005
26555
|
return members2.map((member) => `${member.name} ${member.email} ${member.role}`).join("\n");
|
|
26006
26556
|
}
|
|
26007
26557
|
},
|
|
26008
|
-
{
|
|
26009
|
-
name: "projects",
|
|
26010
|
-
description: "Every project you can reach, and which one this repository is linked to.",
|
|
26011
|
-
inputSchema: buildToolSchema({}),
|
|
26012
|
-
run: async () => {
|
|
26013
|
-
const list = await api.get("/projects", ApiResponses.Projects);
|
|
26014
|
-
let linked = null;
|
|
26015
|
-
try {
|
|
26016
|
-
linked = (await resolveProjectFromArgs({})).key;
|
|
26017
|
-
} catch {
|
|
26018
|
-
linked = null;
|
|
26019
|
-
}
|
|
26020
|
-
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>");
|
|
26021
|
-
}
|
|
26022
|
-
},
|
|
26023
26558
|
{
|
|
26024
26559
|
name: "finish_task",
|
|
26025
26560
|
description: "Move a task to the first done column.",
|
|
@@ -26032,35 +26567,48 @@ ${task.notes}` : ""].filter(Boolean).join("\n");
|
|
|
26032
26567
|
}
|
|
26033
26568
|
}
|
|
26034
26569
|
];
|
|
26570
|
+
return {
|
|
26571
|
+
start: () => holdSession(project.id, null),
|
|
26572
|
+
stop: () => {
|
|
26573
|
+
stopped = true;
|
|
26574
|
+
recovery.stop();
|
|
26575
|
+
held?.stop();
|
|
26576
|
+
},
|
|
26577
|
+
tools: tools.map((tool) => ({ ...tool, run: async (args, output2) => {
|
|
26578
|
+
const result = await tool.run(args, output2);
|
|
26579
|
+
if (tool.name !== "check_messages" && tool.name !== "wait_for_message") claimOwnInbox(output2);
|
|
26580
|
+
return result;
|
|
26581
|
+
} }))
|
|
26582
|
+
};
|
|
26035
26583
|
}
|
|
26036
26584
|
function fetchJsonFile(path2) {
|
|
26037
26585
|
try {
|
|
26038
|
-
return JSON.parse(
|
|
26586
|
+
return JSON.parse(readFileSync16(path2, "utf8"));
|
|
26039
26587
|
} catch {
|
|
26040
26588
|
return {};
|
|
26041
26589
|
}
|
|
26042
26590
|
}
|
|
26043
26591
|
function installIntoClaudeHome(dir) {
|
|
26044
26592
|
const done = [];
|
|
26045
|
-
const primary = dir ===
|
|
26593
|
+
const primary = dir === join15(homedir4(), ".claude");
|
|
26046
26594
|
const env = { ...process.env };
|
|
26047
26595
|
if (primary) delete env.CLAUDE_CONFIG_DIR;
|
|
26048
26596
|
else env.CLAUDE_CONFIG_DIR = dir;
|
|
26049
26597
|
const where = primary ? "" : ` for ${dir}`;
|
|
26050
26598
|
try {
|
|
26051
|
-
|
|
26599
|
+
execFileSync6("claude", ["mcp", "add", "--scope", "user", "slop", "--", "slop", "mcp"], { stdio: ["ignore", "ignore", "pipe"], env });
|
|
26052
26600
|
done.push(`Claude Code: MCP server "slop" registered (user scope${where})`);
|
|
26053
26601
|
} catch (error62) {
|
|
26054
26602
|
const said = String(error62.stderr ?? "") + String(error62.stdout ?? "");
|
|
26055
26603
|
if (/already exists/i.test(said)) done.push(`Claude Code: MCP server "slop" already registered (user scope${where})`);
|
|
26056
26604
|
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"})`);
|
|
26057
26605
|
}
|
|
26058
|
-
const path2 =
|
|
26059
|
-
|
|
26606
|
+
const path2 = join15(dir, "settings.json");
|
|
26607
|
+
mkdirSync9(dir, { recursive: true });
|
|
26060
26608
|
const settings = fetchJsonFile(path2);
|
|
26061
26609
|
settings.hooks = mergeHooks(settings.hooks ?? {}, "claude");
|
|
26062
26610
|
settings.env = { ...settings.env ?? {}, MCP_TOOL_TIMEOUT: "21600000" };
|
|
26063
|
-
|
|
26611
|
+
writeFileSync9(path2, `${JSON.stringify(settings, null, 2)}
|
|
26064
26612
|
`);
|
|
26065
26613
|
done.push(`Claude Code: SessionStart hook and MCP_TOOL_TIMEOUT written to ${path2}; other slop hooks removed (its session socket wakes it)`);
|
|
26066
26614
|
return done;
|
|
@@ -26083,31 +26631,31 @@ function mergeHooks(hooks, harness) {
|
|
|
26083
26631
|
}
|
|
26084
26632
|
function installIntoCodexHome(dir) {
|
|
26085
26633
|
const done = [];
|
|
26086
|
-
|
|
26087
|
-
const configTomlPath =
|
|
26088
|
-
const current =
|
|
26634
|
+
mkdirSync9(dir, { recursive: true });
|
|
26635
|
+
const configTomlPath = join15(dir, "config.toml");
|
|
26636
|
+
const current = existsSync11(configTomlPath) ? readFileSync16(configTomlPath, "utf8") : "";
|
|
26089
26637
|
if (/^\[mcp_servers\.slop\]/m.test(current)) {
|
|
26090
|
-
if (/tool_timeout_sec = 1800\b/.test(current))
|
|
26638
|
+
if (/tool_timeout_sec = 1800\b/.test(current)) writeFileSync9(configTomlPath, current.replace(/tool_timeout_sec = 1800\b/, "tool_timeout_sec = 21600"));
|
|
26091
26639
|
done.push(`Codex: MCP server already in ${configTomlPath}`);
|
|
26092
26640
|
} else {
|
|
26093
|
-
|
|
26641
|
+
writeFileSync9(configTomlPath, `${current.trimEnd()}${current.trim() ? "\n\n" : ""}[mcp_servers.slop]
|
|
26094
26642
|
command = "slop"
|
|
26095
26643
|
args = ["mcp"]
|
|
26096
26644
|
tool_timeout_sec = 21600
|
|
26097
26645
|
`);
|
|
26098
26646
|
done.push(`Codex: MCP server "slop" added to ${configTomlPath}`);
|
|
26099
26647
|
}
|
|
26100
|
-
const hooksPath =
|
|
26648
|
+
const hooksPath = join15(dir, "hooks.json");
|
|
26101
26649
|
const file2 = fetchJsonFile(hooksPath);
|
|
26102
26650
|
file2.hooks = mergeHooks(file2.hooks ?? {}, "codex");
|
|
26103
|
-
|
|
26651
|
+
writeFileSync9(hooksPath, `${JSON.stringify(file2, null, 2)}
|
|
26104
26652
|
`);
|
|
26105
26653
|
done.push(`Codex: SessionStart hook written to ${hooksPath}; other slop hooks removed (Codex is woken with codex queue; no hook holds a turn open)`);
|
|
26106
26654
|
return done;
|
|
26107
26655
|
}
|
|
26108
26656
|
async function trustCodexHooks(codexHome) {
|
|
26109
26657
|
const server = spawn3("codex", ["app-server", "-c", "mcp_servers={}"], { stdio: ["pipe", "pipe", "ignore"], env: { ...process.env, CODEX_HOME: codexHome } });
|
|
26110
|
-
const where = codexHome ===
|
|
26658
|
+
const where = codexHome === join15(homedir4(), ".codex") ? "" : ` under ${codexHome}`;
|
|
26111
26659
|
let buffer = "";
|
|
26112
26660
|
const pending = /* @__PURE__ */ new Map();
|
|
26113
26661
|
server.stdout.on("data", (chunk) => {
|
|
@@ -26125,7 +26673,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
26125
26673
|
}
|
|
26126
26674
|
});
|
|
26127
26675
|
let nextId = 1;
|
|
26128
|
-
const callCodexMethod = (method, params) => new Promise((
|
|
26676
|
+
const callCodexMethod = (method, params) => new Promise((resolve6, reject) => {
|
|
26129
26677
|
const id = nextId++;
|
|
26130
26678
|
const timer = setTimeout(() => {
|
|
26131
26679
|
pending.delete(id);
|
|
@@ -26134,7 +26682,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
26134
26682
|
pending.set(id, (message) => {
|
|
26135
26683
|
clearTimeout(timer);
|
|
26136
26684
|
if (message.error) reject(new Error(message.error.message ?? `${method} failed`));
|
|
26137
|
-
else
|
|
26685
|
+
else resolve6(message.result);
|
|
26138
26686
|
});
|
|
26139
26687
|
server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
|
|
26140
26688
|
`);
|
|
@@ -26142,7 +26690,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
26142
26690
|
const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
26143
26691
|
`);
|
|
26144
26692
|
try {
|
|
26145
|
-
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.
|
|
26693
|
+
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.4.10" }, capabilities: { experimentalApi: true } });
|
|
26146
26694
|
sendNotification("initialized", {});
|
|
26147
26695
|
const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
|
|
26148
26696
|
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
@@ -26158,22 +26706,12 @@ async function trustCodexHooks(codexHome) {
|
|
|
26158
26706
|
server.kill();
|
|
26159
26707
|
}
|
|
26160
26708
|
}
|
|
26161
|
-
async function trustCodexHooksForOwnHome() {
|
|
26162
|
-
const home = resolveOwnHarnessHome("CODEX_HOME");
|
|
26163
|
-
if (!home) return;
|
|
26164
|
-
const hooksPath = join12(home, "hooks.json");
|
|
26165
|
-
const configTomlPath = join12(home, "config.toml");
|
|
26166
|
-
if (!existsSync10(hooksPath) || !existsSync10(configTomlPath)) return;
|
|
26167
|
-
if (!readFileSync13(hooksPath, "utf8").includes("slop hook stop")) return;
|
|
26168
|
-
if (readFileSync13(configTomlPath, "utf8").includes(`hooks.state."${hooksPath}:stop:`)) return;
|
|
26169
|
-
await trustCodexHooks(home);
|
|
26170
|
-
}
|
|
26171
26709
|
async function parseHookInput(harness, remember = true) {
|
|
26172
|
-
const input2 = await new Promise((
|
|
26710
|
+
const input2 = await new Promise((resolve6) => {
|
|
26173
26711
|
let data = "";
|
|
26174
26712
|
process.stdin.on("data", (chunk) => data += chunk);
|
|
26175
|
-
process.stdin.on("end", () =>
|
|
26176
|
-
setTimeout(() =>
|
|
26713
|
+
process.stdin.on("end", () => resolve6(data));
|
|
26714
|
+
setTimeout(() => resolve6(data), 2e3).unref();
|
|
26177
26715
|
});
|
|
26178
26716
|
let cwd = process.cwd();
|
|
26179
26717
|
let conversation = null;
|
|
@@ -26183,7 +26721,7 @@ async function parseHookInput(harness, remember = true) {
|
|
|
26183
26721
|
conversation = parsed.session_id ?? null;
|
|
26184
26722
|
} catch {
|
|
26185
26723
|
}
|
|
26186
|
-
const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) =>
|
|
26724
|
+
const ownThread = harness !== "codex" || !conversation || isMainChatThread(fetchCodexHomes().map((home) => join15(home, "sessions")), conversation);
|
|
26187
26725
|
if (remember && conversation && ownThread && !isNonInteractiveHarness()) persistRememberedConversation(cwd, { id: conversation, harness: harness ?? "other" });
|
|
26188
26726
|
return { cwd, conversation };
|
|
26189
26727
|
}
|
|
@@ -26235,20 +26773,49 @@ ${parts.join("\n")}` })}
|
|
|
26235
26773
|
}
|
|
26236
26774
|
function buildTools() {
|
|
26237
26775
|
if (isNonInteractiveHarness()) return [];
|
|
26238
|
-
const
|
|
26239
|
-
|
|
26240
|
-
|
|
26241
|
-
|
|
26242
|
-
|
|
26776
|
+
const api = buildAuthedApiClient();
|
|
26777
|
+
let identity = null;
|
|
26778
|
+
let stopped = false;
|
|
26779
|
+
const projects = /* @__PURE__ */ new Map();
|
|
26780
|
+
const requireProjectTools = (project) => {
|
|
26781
|
+
if (stopped) throw new Error("The MCP server is stopping");
|
|
26782
|
+
let context = projects.get(project.id);
|
|
26783
|
+
if (!context) {
|
|
26784
|
+
identity ??= resolveAgentIdentity(process.cwd());
|
|
26785
|
+
context = createProjectTools(buildAuthedApiClient(), project, identity);
|
|
26786
|
+
projects.set(project.id, context);
|
|
26787
|
+
}
|
|
26788
|
+
return context;
|
|
26789
|
+
};
|
|
26790
|
+
const runHandlerByName = async (name, args, output2) => {
|
|
26791
|
+
if (name === "projects") return fetchProjectsOverview(api);
|
|
26792
|
+
const project = await resolveProject(api, args.project);
|
|
26793
|
+
const handler = requireProjectTools(project).tools.find((candidate) => candidate.name === name);
|
|
26794
|
+
if (!handler) throw new Error("No such action");
|
|
26795
|
+
return handler.run(args, output2);
|
|
26796
|
+
};
|
|
26797
|
+
void (async () => {
|
|
26798
|
+
for (let waited = 0; waited < 1e4 && !fetchKnownConversation(process.cwd()) && !stopped; waited += 250) await new Promise((resolve6) => setTimeout(resolve6, 250));
|
|
26799
|
+
if (stopped) return;
|
|
26800
|
+
await requireProjectTools(await resolveProject(api)).start();
|
|
26801
|
+
})().catch(() => void 0);
|
|
26802
|
+
const endSessionAndExit = () => {
|
|
26803
|
+
if (stopped) return;
|
|
26804
|
+
stopped = true;
|
|
26805
|
+
for (const context of projects.values()) context.stop();
|
|
26806
|
+
setTimeout(() => process.exit(process.exitCode ?? 0), 300).unref();
|
|
26243
26807
|
};
|
|
26808
|
+
process.stdin.once("end", endSessionAndExit);
|
|
26809
|
+
process.stdin.once("close", endSessionAndExit);
|
|
26810
|
+
for (const signal of ["SIGTERM", "SIGHUP", "SIGINT"]) process.once(signal, endSessionAndExit);
|
|
26244
26811
|
const buildGroupedTool = (name, description2, actions, properties) => ({
|
|
26245
26812
|
name,
|
|
26246
26813
|
description: description2,
|
|
26247
26814
|
inputSchema: buildToolSchema({ action: { type: "string", enum: Object.keys(actions) }, ...properties, project: PROJECT }, ["action"]),
|
|
26248
|
-
run: (args) => {
|
|
26815
|
+
run: (args, output2) => {
|
|
26249
26816
|
const target = actions[String(args.action)];
|
|
26250
26817
|
if (!target) throw new Error(`action must be one of: ${Object.keys(actions).join(", ")}`);
|
|
26251
|
-
return runHandlerByName(target, args);
|
|
26818
|
+
return runHandlerByName(target, args, output2);
|
|
26252
26819
|
}
|
|
26253
26820
|
});
|
|
26254
26821
|
return [
|
|
@@ -26285,17 +26852,12 @@ function buildTools() {
|
|
|
26285
26852
|
)
|
|
26286
26853
|
];
|
|
26287
26854
|
}
|
|
26288
|
-
function
|
|
26855
|
+
function attachUpdateNoticeToTools(list, version2) {
|
|
26289
26856
|
return list.map((tool) => ({
|
|
26290
26857
|
...tool,
|
|
26291
|
-
run: async (args) => {
|
|
26292
|
-
const result =
|
|
26858
|
+
run: async (args, output2) => {
|
|
26859
|
+
const result = await tool.run(args, output2);
|
|
26293
26860
|
const parts = [result];
|
|
26294
|
-
if (!(tool.name === "session" && (args.action === "check" || args.action === "wait"))) {
|
|
26295
|
-
const arrived = drainInbox(process.cwd());
|
|
26296
|
-
if (arrived.length) parts.push(`--- Messages arrived while you worked (answer what is still open, once; do not acknowledge) ---
|
|
26297
|
-
${arrived.map(formatPushBlock).join("\n\n")}`);
|
|
26298
|
-
}
|
|
26299
26861
|
const notice = isUpdateCheckEnabled() ? buildUpdateNotice(version2) : null;
|
|
26300
26862
|
if (notice) parts.push(notice);
|
|
26301
26863
|
return parts.join("\n\n");
|
|
@@ -26304,13 +26866,13 @@ ${arrived.map(formatPushBlock).join("\n\n")}`);
|
|
|
26304
26866
|
}
|
|
26305
26867
|
function registerMcpCommands(program3, version2) {
|
|
26306
26868
|
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.");
|
|
26307
|
-
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(
|
|
26869
|
+
mcp.command("serve", { isDefault: true }).description("Serve MCP over stdio (what the harness runs).").action(() => serveMcp(attachUpdateNoticeToTools(buildTools(), version2), { name: "slop", version: version2 }));
|
|
26308
26870
|
mcp.command("install").description("Register the server and the two hooks with Claude Code and Codex on this machine.").action(async () => {
|
|
26309
26871
|
const homes = fetchCodexHomes();
|
|
26310
26872
|
const lines = [...fetchClaudeHomes().flatMap((home) => installIntoClaudeHome(home)), ...homes.flatMap((home) => installIntoCodexHome(home))];
|
|
26311
26873
|
let codexPresent = true;
|
|
26312
26874
|
try {
|
|
26313
|
-
|
|
26875
|
+
execFileSync6("codex", ["--version"], { stdio: "ignore" });
|
|
26314
26876
|
} catch {
|
|
26315
26877
|
codexPresent = false;
|
|
26316
26878
|
}
|
|
@@ -26326,10 +26888,9 @@ function registerMcpCommands(program3, version2) {
|
|
|
26326
26888
|
}
|
|
26327
26889
|
|
|
26328
26890
|
// src/commands/sessions.ts
|
|
26329
|
-
import { readFileSync as readFileSync14 } from "fs";
|
|
26330
26891
|
var buildTaskPath2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
26331
26892
|
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"];
|
|
26332
|
-
function registerSessionCommands(program3
|
|
26893
|
+
function registerSessionCommands(program3) {
|
|
26333
26894
|
const registerProjectOption = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
26334
26895
|
const session = program3.command("session").description("Claim a task and be reachable on it while you work.");
|
|
26335
26896
|
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) => {
|
|
@@ -26411,13 +26972,13 @@ ${written.map((path2) => ` ${path2}`).join("\n")}`);
|
|
|
26411
26972
|
}
|
|
26412
26973
|
|
|
26413
26974
|
// src/index.ts
|
|
26414
|
-
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.
|
|
26975
|
+
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.10").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)");
|
|
26415
26976
|
registerAuthCommands(program2);
|
|
26416
26977
|
registerProjectCommands(program2);
|
|
26417
26978
|
var taskCommand = registerTaskCommands(program2);
|
|
26418
|
-
registerSessionCommands(program2
|
|
26979
|
+
registerSessionCommands(program2);
|
|
26419
26980
|
registerThreadCommands(program2, taskCommand);
|
|
26420
|
-
registerMcpCommands(program2, "0.4.
|
|
26981
|
+
registerMcpCommands(program2, "0.4.10");
|
|
26421
26982
|
registerGuideCommand(program2);
|
|
26422
26983
|
registerSkillCommands(program2);
|
|
26423
26984
|
program2.parseAsync().catch(exitWithError);
|