slopwaresystems 0.9.5 → 0.10.0
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 +1 -1
- package/dist/main.js +193 -54
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -146,7 +146,7 @@ function fetchLaunchCandidate(launcher2) {
|
|
|
146
146
|
var path = fileURLToPath(import.meta.url);
|
|
147
147
|
var launcher = {
|
|
148
148
|
path,
|
|
149
|
-
bundledVersion: "0.
|
|
149
|
+
bundledVersion: "0.10.0",
|
|
150
150
|
bundledMain: fileURLToPath(new URL("./main.js", import.meta.url)),
|
|
151
151
|
installed: isInstalledLauncherPath(path)
|
|
152
152
|
};
|
package/dist/main.js
CHANGED
|
@@ -3392,7 +3392,7 @@ var slop_planning_default = '---\nname: slop-planning\ndescription: How to break
|
|
|
3392
3392
|
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.** 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, before you read any code. Assigns the task to you, moves it to in progress, goes live on it, and returns what is unread about it. Read that first. Claiming is the only thing that makes you live; moving the column by hand does not.\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. **Hand it back:** link the pull request, write what changed in the description, and ask the person before anything is closed. `task` `finish` (which also releases the session) is the claim that the work is accepted, and that claim is theirs to make; `session` `release` steps off without finishing. See `slop-working`.\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, its model; 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\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## Operations: available and orchestrator\n\nAn operation (see `slop-operations`) is run by one agent session, its **orchestrator**, which proposes who does each ready ticket; a person approves every proposal. Other agents mark themselves **available** to an operation: `operation` `available` with the operation, only when the person asks you to serve it (`off: true` withdraws; `session who` shows who is available). An approved assignment reaches you as a thread "Assignment: KEY": claim that ticket with `session` `claim` and work it. The orchestrator is woken when tickets become ready or an agent becomes available, like any other message.\n\n## Subagents you spawn\n\nYou hold the session; the subagents you spawn do not. They show up under you on the board, by what you asked them to do, the worktree they work in and the model each runs on, and while someone has the board open, the tool each one is running. Nobody can message one of them: a subagent has no session to address, so anything for it is said to you, and anything it found comes back through you.\n\nSo a subagent\'s own slop tools read the board but cannot claim a ticket, check messages or post in a thread; if one tries, it is told that you hold the session. Do not send one off to claim a ticket or answer a thread. Send it to find something out, and act on what it tells you.\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';
|
|
3393
3393
|
|
|
3394
3394
|
// skills/slop-splat.md
|
|
3395
|
-
var slop_splat_default = '---\nname: slop-splat\ndescription: How an agent uses Splat, the project\'s app rendered on the team\'s shared canvas: every route at every viewport (mobile, tablet, desktop), captured on a machine running the app, seen by everyone. Load when asked to show, capture, compare or check the UI; after changing UI; or when asked to point Splat at a dev server or port.\n---\n\n# Splat: the app on the team\'s canvas\n\nA Splat canvas belongs to a project (the web app\'s Splat tab). A **route** is a path in the app (`/settings/billing`). Each route has one **frame** per viewport (by default mobile 375, tablet 768, desktop 1280). A **capture** is one render of a frame. It is taken on a machine that runs the app, stored with the branch and commit it came from, and shown to the whole team. The canvas is seen through a branch: a frame shows that branch\'s newest capture, else the default branch\'s.\n\nEverything goes through the `splat` MCP tool. Each call is an `action` and its fields in `args`, for example `{ "action": "target", "args": { "port": 3100 } }` or `{ "action": "capture", "args": { "paths": ["/settings"], "full": true } }`. `project` defaults to the linked one.\n\n## Point it at the app\n\nSplat renders a dev server on this machine. It finds servers running inside the repository\'s worktrees by itself. Anything else (a tunnel, a container, a port you were told about) is pinned:\n\n- `splat target` `port: 3100` (optional `branch`, `label`). The pin is kept on this machine for this project, and every renderer here picks it up. `remove: true` unpins it.\n- `splat canvas` lists the routes, what each frame shows, who can render now, and this machine\'s dev servers.\n\nWhile this chat is open and the project uses Splat, the chat is also a **renderer**. It answers the web\'s
|
|
3395
|
+
var slop_splat_default = '---\nname: slop-splat\ndescription: How an agent uses Splat, the project\'s app rendered on the team\'s shared canvas: every route at every viewport (mobile, tablet, desktop), captured on a machine running the app, seen by everyone. Load when asked to show, capture, compare or check the UI; after changing UI; or when asked to point Splat at a dev server or port.\n---\n\n# Splat: the app on the team\'s canvas\n\nA Splat canvas belongs to a project (the web app\'s Splat tab). A **route** is a path in the app (`/settings/billing`). Each route has one **frame** per viewport (by default mobile 375, tablet 768, desktop 1280). A **capture** is one render of a frame. It is taken on a machine that runs the app, stored with the branch and commit it came from, and shown to the whole team. The canvas is seen through a branch: a frame shows that branch\'s newest capture, else the default branch\'s.\n\nEverything goes through the `splat` MCP tool. Each call is an `action` and its fields in `args`, for example `{ "action": "target", "args": { "port": 3100 } }` or `{ "action": "capture", "args": { "paths": ["/settings"], "full": true } }`. `project` defaults to the linked one.\n\n## Point it at the app\n\nSplat renders a dev server on this machine. It finds servers running inside the repository\'s worktrees by itself. Anything else (a tunnel, a container, a port you were told about) is pinned:\n\n- `splat target` `port: 3100` (optional `branch`, `label`). The pin is kept on this machine for this project, and every renderer here picks it up. `remove: true` unpins it.\n- `splat canvas` lists the routes, what each frame shows, who can render now, and this machine\'s dev servers.\n\nWhile this chat is open and the project uses Splat, the chat is also a **renderer**. It serves live frames through a proxy on `http://<port>.localhost:7421` and answers the web\'s Capture button. You do nothing for this.\n\n## Live and screenshots: two ways to see a frame\n\n- **Live** is the running page itself, in the viewer\'s own browser. It needs no Chrome, no setup and no screenshot, and the canvas opens frames live by default. The shared source goes live through this chat\'s proxy when the person browses on this machine, and through Slopware (the chat\'s own relay) when they browse from another computer, so a chat on a VM works from a laptop. Only the person this chat belongs to sees its dev servers live; to show a running app to the team, publish it.\n- **Screenshots** (`capture`) are snapshots: stored with the branch and commit, shown as thumbnails and history, and returned to you as images. Only these need a Chrome (or Chromium, Edge, Brave) on the machine taking them.\n\nWithout a Chrome that starts here (none installed, or a sandbox stops it), `capture` says so and gives the live link instead of failing. The person this chat belongs to can still use the page live; teammates cannot (they see screenshots, or a published app). Look at it with a browser tool of your own if you have one; if the team needs to see the change, tell the person that screenshots need Chrome on this machine.\n\n## The loop after a UI change\n\n1. `splat add` `paths: ["/settings", "/settings/billing"]` puts routes on the canvas (a frame per viewport). A path already there is reused. For a file-based router, `splat discover` lists the routes and `add: true` adds the static ones. For a router defined in code (a routes array, a `<Route>` tree), read it and add the paths yourself. Dynamic segments need an example value: `/projects/abc123`.\n2. `splat capture` `paths: [...]` screenshots them here, uploads them, and returns the images: the top of each page by default, `full: true` for the whole page in tiles, `limit` for more frames. Look at them, critique them against what was asked, and fix what is off.\n3. Capture again after fixing. Every capture is on the canvas for the team at once, stamped with the branch, the commit, and "uncommitted" when the working tree was dirty.\n\nIf a render landed on a sign-in page, the result says so. Ask the person to run `slop splat login --port <port>` once: it opens Chrome on Splat\'s own profile to sign in, and every capture after that is signed in (the cookies stay in that profile). On a machine with no screen (a VM), `slop splat login --port <port> --cookie name=value` sets the app\'s session cookie instead.\n\n## Workspaces and published apps\n\nA project can have several **workspaces**, each its own canvas (routes, layout, notes, pins, viewports). Every action takes `workspace` (a name or id); without it, the project\'s default. `splat workspaces` lists them; `name: "Checkout redesign"` makes one.\n\nA workspace holds **sources**: its shared source (captured by whichever renderer has the branch, as above) and the apps people **publish** into it. A published app runs on its publisher\'s machine (a laptop, a VM) and the team uses it **live** from their browsers through Slopware, with no tunnel, open port or local server of their own. Publishing is a person\'s (or a VM\'s) long-running command, not a chat\'s:\n\n slop splat serve --workspace "Checkout redesign" --name "Checkout experiment" --port 3100\n\nThe same name in the same workspace is the same source across restarts and port or branch changes. Several `serve --name` on one machine are several sources. A new source has no frames: `splat sources` lists the workspace\'s sources, and `frames: "Checkout experiment"` puts that source\'s frames on the canvas (every route at every viewport). `splat add` with the workspace\'s sources shown adds frames for them too. Its captures (`refresh` with `sources: ["Checkout experiment"]`) are taken by its own publisher.\n\nFrames say which source they show, and two sources\' frames of one route sit side by side after `slop splat tidy`, so a change is compared against main at a glance. Which sources a person sees is their own choice in the web\'s Sources menu; nobody else\'s view changes.\n\n## Keeping it current\n\n- `slop splat schedule 1h` (or `15m`, `6h`, `1d`, `off`) recaptures every frame on the default branch on a schedule. A `slop splat serve` with a dev server on that branch does the rounds (a VM that always runs the app); the server gives each round to one renderer. An agent\'s chat never takes rounds.\n- `slop splat tidy` lays the canvas out again: sections in order, each route\'s viewports side by side, routes as a grid. Captures, notes and pins stay.\n\n## Other actions\n\n- `view` `route` (optional `viewport`, `branch`): the team\'s latest render of a route, as images. Use it to see what the others see without running the app.\n- `refresh` (optional `paths`, `viewports`, `branch`, `renderer`, `sources`): ask a renderer on the team to capture, for a branch you do not run; a published source\'s frames are captured by its publisher.\n- `renderers`: who can render now, with their dev servers and branches.\n- `section` `title`, `paths`: a labelled group (a user journey, a comparison). `remove: <section id>` removes it with its frames.\n- `note` `markdown` or `file`: a spec or plan beside the frames. `image` `file`: a reference or mock.\n- `pin` `route`, `viewport`, `x` (0-1 across), `y` (px down the page), plus `task` (an existing key) or `title` (a new task), optional `source`: a task on a spot of the page. The task links back to the pin.\n- `viewport` `key`, `width`, `height` (or `remove: true`): the project\'s viewports. A new one gets a frame for every route.\n- `update` `route`, `path`: change a path. `remove` `route`, `confirm: true`: take it off, captures and all.\n\nScreenshots are Chrome\'s. Use them to check layout and design. They are not pixel-perfect cross-browser QA.\n';
|
|
3396
3396
|
|
|
3397
3397
|
// skills/slop-working.md
|
|
3398
3398
|
var slop_working_default = "---\nname: slop-working\ndescription: The working loop for a ticket on the board: going live on it, keeping its column true, and handing it back to the person before it is closed. Load when told to work a ticket, given a ticket key or number, asked to pick up the next thing, or at the start of any coding session in a repository linked with slop.\n---\n\n# Told a ticket? Go live on it first\n\n\"Work LB-59\", \"take a look at ST-12\", \"pick up the next one\": before you read any code, claim it.\n\n`session` `claim` with the key. That one call assigns the ticket to you, moves it to the first in-progress column, and hands you what is unread about it (read that before you start). From then on you are **live on that ticket**: the board shows you working it, `session` `who` lists you on it, and messages about it reach you on their own. See `slop-sessions`.\n\nClaiming is the only thing that makes you live. `slop task start` at a shell moves the column and nothing else, because the CLI is one short-lived process and cannot hold a session; moving the column by hand leaves the board saying someone is on the ticket with nobody there. Through the MCP tools, `session` `claim` is the way in.\n\n- Given a key that does not exist, or an ambiguous one: say so and stop. Do not guess which ticket was meant.\n- Someone else already live on it (`session` `who`): do not claim it. Ask in a thread about that ticket first.\n- Told to work something that is not on the board yet: `task` `create` it, then claim it. Work an agent is doing should be a ticket someone can see.\n- A ticket in an operation that is still drafting: do not claim it. Say so and stop; the person approves the operation first.\n\n# The loop\n\n1. **Claim it:** `session` `claim`. Then `task` `show` and read all of it, plan and notes included.\n2. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n3. **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.\n4. **Keep the column true.** The board is read by people who are not watching you. If you stop for the day or move to something else, `session` `release`. If you are blocked, say what on, in the item's note and in a thread about the ticket, and leave it in progress. Never leave a ticket in progress with no live session and no note saying why.\n5. **Hand it back** (below). Finishing is the person's call, not yours.\n\n# Handing a ticket back\n\nWork being done is not the ticket being done. When the code is ready:\n\n1. `task` `link` the pull request.\n2. Add what changed and where to the end of the description: `task` `update` with `append: true`. Never resend the description to add to it; the spec above your report stays as written.\n3. If the board has a column for review, move it there (`board` `columns` lists them; a review column still counts as in progress). Otherwise leave it in progress.\n4. **Ask the person, and stop.** Not in a thread: answer in your turn, to whoever asked you, because they are reading it. One message: what you did, where the PR is, and what is left to decide. Offer the things you could do next rather than choosing one, for example reviewing the PR yourself, addressing review comments, merging it, or deploying.\n\n**Do not call `task` `finish` until they answer.** `finish` moves the ticket to the first done column and releases your session, which is the claim that the work is accepted, and only a person can make that claim. Told to finish, finish. Told nothing, leave the ticket where it is: sitting in review is honest, marked done because an agent ran out of things to do is not.\n\nThe one exception is work with nothing to accept: a chore you were told to close, a one-line fix already on the main branch with no pull request. Finish that and say you did.\n\n## A ticket in an operation\n\n`operation` `show` holds its spec: the files it owns and must avoid, the states it must handle, its verification commands, what it waits on and what it unblocks, and any handoffs left for it. It is done when its verification passes, not before. Handing it back names what it unblocks. Work you leave for another ticket goes to that ticket as a handoff (`operation` `handoff`), not in a PR body. See `slop-operations`.\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 person driving you | your own reply to them, in your turn |\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| Work left for another ticket in an operation | `operation` `handoff` to that ticket |\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";
|
|
@@ -26542,7 +26542,7 @@ var TaskEventRecord = external_exports.object({
|
|
|
26542
26542
|
});
|
|
26543
26543
|
|
|
26544
26544
|
// ../contract/src/work/cli-release.ts
|
|
26545
|
-
var CLI_RELEASE = "0.
|
|
26545
|
+
var CLI_RELEASE = "0.10.0";
|
|
26546
26546
|
var CLI_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
26547
26547
|
var CLI_VERSION_MAX = 64;
|
|
26548
26548
|
function parseCliVersion(version2) {
|
|
@@ -28201,6 +28201,12 @@ var CreateSplatSourceFramesInput = external_exports.object({
|
|
|
28201
28201
|
var SplatWorkspaceCanvas = SplatCanvas.extend({ workspace: SplatWorkspace, sources: external_exports.array(SplatSource) });
|
|
28202
28202
|
var SplatRelayTicket = external_exports.object({ ticket: external_exports.string(), path: external_exports.string(), sourceId: Id });
|
|
28203
28203
|
var SplatPreviewSession = external_exports.object({ sourceId: Id, origin: external_exports.string(), code: external_exports.string(), expiresAt: Timestamp, renewInMs: external_exports.number().int().positive() });
|
|
28204
|
+
var SPLAT_RENDERER_LIVE_RELAY = "live-relay";
|
|
28205
|
+
var SPLAT_RENDERER_LIVE_RELAYS_MAX = 4;
|
|
28206
|
+
function listSplatRendererRelayPorts(origins) {
|
|
28207
|
+
return [...new Set(origins.map((origin) => origin.port))].slice(0, SPLAT_RENDERER_LIVE_RELAYS_MAX);
|
|
28208
|
+
}
|
|
28209
|
+
var SplatRendererRelayInput = external_exports.object({ port: external_exports.number().int().min(1).max(65535) });
|
|
28204
28210
|
var SOURCE_KEY_LENGTH = 16;
|
|
28205
28211
|
var VIEWER_KEY_LENGTH = 10;
|
|
28206
28212
|
var SPLAT_PREVIEW_LABEL = new RegExp(`^[0-9a-z]{${SOURCE_KEY_LENGTH + VIEWER_KEY_LENGTH}}$`);
|
|
@@ -28500,6 +28506,8 @@ registerSplatResponseContract("POST", ["/pins"], ApiResponses.SplatPin);
|
|
|
28500
28506
|
registerResponseContract("GET", ["/projects/:projectId/splat/renderers"], ApiResponses.SplatRenderers);
|
|
28501
28507
|
registerSplatResponseContract("POST", ["/capture-requests"], ApiResponses.SplatCaptureRequestResult);
|
|
28502
28508
|
registerResponseContract("POST", ["/projects/:projectId/splat/renderer/ticket"], ApiResponses.ConnectTicket);
|
|
28509
|
+
registerResponseContract("POST", ["/projects/:projectId/splat/renderers/:rendererId/relay/ticket"], ApiResponses.SplatRelayTicket);
|
|
28510
|
+
registerResponseContract("POST", ["/projects/:projectId/splat/renderers/:rendererId/preview"], ApiResponses.SplatPreviewSession);
|
|
28503
28511
|
function requireApiResponseSchema(method, path3) {
|
|
28504
28512
|
const pathname = path3.split("?")[0];
|
|
28505
28513
|
const endpoint = endpoints.find((endpoint2) => endpoint2.method === method && endpoint2.path.test(pathname));
|
|
@@ -29495,7 +29503,7 @@ function resolveAgentIdentity(cwd, fallback = "process", conversation = fetchKno
|
|
|
29495
29503
|
harness: conversation?.harness ?? null,
|
|
29496
29504
|
directory: repo ? basename3(repo.worktree) : basename3(cwd),
|
|
29497
29505
|
branch: fetchCurrentBranch(cwd),
|
|
29498
|
-
cliVersion: "0.
|
|
29506
|
+
cliVersion: "0.10.0"
|
|
29499
29507
|
};
|
|
29500
29508
|
}
|
|
29501
29509
|
|
|
@@ -29778,7 +29786,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
|
|
|
29778
29786
|
- Subagents you spawn show under you on the board (what you asked them to do, the worktree, the model, and while someone has the board open, the tool each is running). They hold no session: nobody can message one, and a subagent's own slop tools can read the board but cannot claim a ticket, check messages or post in a thread. Send one to find something out, not to work a ticket or answer a thread.
|
|
29779
29787
|
- 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.
|
|
29780
29788
|
- An operation is a phased plan across tickets: requirements, a spec per ticket, typed edges between tickets, files owned. The \`operation\` tool plans and reads one; \`slop op show <op>\` from a shell; \`slop skill slop-operations\` is the method.
|
|
29781
|
-
- Splat is the project's app on a shared canvas: every route at every viewport, rendered on a machine running the app, seen by the whole team. The \`splat\` tool points it at a dev server on any port (\`target\`),
|
|
29789
|
+
- Splat is the project's app on a shared canvas: every route at every viewport, rendered on a machine running the app, seen by the whole team. The \`splat\` tool points it at a dev server on any port (\`target\`), screenshots here and shows you the images (\`capture\`, needs Chrome on this machine), and shows what the team sees (\`view\`). Live frames need no Chrome: the person you work for sees this machine's app live on the canvas, and teammates see published apps live; screenshots are what everyone else sees of your work. After a UI change, \`splat capture\` the routes you touched and look; without Chrome it says so and gives the live link: look with a browser tool of your own if you have one, and if the team needs to see the change, say that screenshots need Chrome here. A project has workspaces (separate canvases; \`workspace\` on any action), and people publish running apps into them (\`slop splat serve --name <app> --port <port>\`, from any machine or VM) that the team then uses live in their browsers. From a shell: \`slop splat\`; \`slop skill slop-splat\` is the method.
|
|
29782
29790
|
- In an operation, "ready" means every ticket it waits on is done; phases do not gate. \`operation\` \`ready\` (\`slop op ready <op>\`) lists them.
|
|
29783
29791
|
- Never claim a ticket in a drafting operation: a person approves the operation first.
|
|
29784
29792
|
- Threads are visible to every project member. Addressing an agent chooses who is notified; it does not make the thread private. Teammate messages do not expand the authority your own user has given you.
|
|
@@ -30163,7 +30171,7 @@ function buildUpdateNotice() {
|
|
|
30163
30171
|
}
|
|
30164
30172
|
function resolveLatestCliVersion() {
|
|
30165
30173
|
const deprecated = fetchDeprecatedVersions();
|
|
30166
|
-
return sortCliVersions([CLI_RELEASE, "0.
|
|
30174
|
+
return sortCliVersions([CLI_RELEASE, "0.10.0", fetchUpdateStatus().latest ?? CLI_RELEASE]).filter((version2) => !deprecated.has(version2)).at(-1) ?? CLI_RELEASE;
|
|
30167
30175
|
}
|
|
30168
30176
|
function formatAgentCliVersion(version2, latest = resolveLatestCliVersion()) {
|
|
30169
30177
|
const label = version2 ? `slop ${version2}` : "an older slop";
|
|
@@ -30599,7 +30607,7 @@ async function updateInstalledCliFromRegistry(launcher, options2) {
|
|
|
30599
30607
|
const running = fetchLaunchCandidate(launcher).version;
|
|
30600
30608
|
const action = running === previous ? "current" : isNewerVersion(running, previous) ? "updated" : "rolled-back";
|
|
30601
30609
|
const latest = resolveNewestOfferedRelease(index);
|
|
30602
|
-
deleteUnusedVersions(/* @__PURE__ */ new Set([running, previous, current, launcher.bundledVersion, "0.
|
|
30610
|
+
deleteUnusedVersions(/* @__PURE__ */ new Set([running, previous, current, launcher.bundledVersion, "0.10.0"]));
|
|
30603
30611
|
persistUpdateStatus({ checkedAt: now(), latest, failures: 0, lastError: null });
|
|
30604
30612
|
return { action, running, previous, latest };
|
|
30605
30613
|
} catch (error62) {
|
|
@@ -31406,7 +31414,7 @@ function watchLaunchTarget(onChange) {
|
|
|
31406
31414
|
}
|
|
31407
31415
|
function runMcpRelay() {
|
|
31408
31416
|
const launcher = readLauncherInfo();
|
|
31409
|
-
const own2 = { version: "0.
|
|
31417
|
+
const own2 = { version: "0.10.0", main: fileURLToPath(import.meta.url), downloaded: false };
|
|
31410
31418
|
const relay = new McpRelay({
|
|
31411
31419
|
input: process.stdin,
|
|
31412
31420
|
output: process.stdout,
|
|
@@ -31847,7 +31855,7 @@ async function waitForMessage(api, projectId, taskKey, options2 = {}) {
|
|
|
31847
31855
|
setTimeout(() => resolve6(outcome), 50);
|
|
31848
31856
|
};
|
|
31849
31857
|
const held = new HeldSession(api, projectId, taskKey, {
|
|
31850
|
-
claim: () => options2.claim ?? { harness: null, directory: null, branch: null, cliVersion: "0.
|
|
31858
|
+
claim: () => options2.claim ?? { harness: null, directory: null, branch: null, cliVersion: "0.10.0" },
|
|
31851
31859
|
reconnect: false,
|
|
31852
31860
|
onHello: options2.onHello,
|
|
31853
31861
|
onEnd: () => finishWait({ kind: "ended" }),
|
|
@@ -32158,7 +32166,6 @@ var WORKER_FRESH_MS = 12e4;
|
|
|
32158
32166
|
var WORKER_GONE_MS = 3e5;
|
|
32159
32167
|
var READ_CAP = 256 * 1024;
|
|
32160
32168
|
var CARRY_CAP = 64 * 1024;
|
|
32161
|
-
var CHILD_SCAN_CAP = 40;
|
|
32162
32169
|
var DISCOVER_EVERY_MS = 1e4;
|
|
32163
32170
|
var TAIL_EVERY_MS = 1e3;
|
|
32164
32171
|
var truncate = (text, cap) => text.length <= cap ? text : `${text.slice(0, cap - 1)}\u2026`;
|
|
@@ -32292,7 +32299,33 @@ function parseModelFromCodexLine(line) {
|
|
|
32292
32299
|
if (entry.type !== "turn_context" || typeof model !== "string" || !model.trim()) return null;
|
|
32293
32300
|
return truncate(model.trim(), MODEL_MAX);
|
|
32294
32301
|
}
|
|
32295
|
-
|
|
32302
|
+
function parseTurnStateFromClaudeLine(line) {
|
|
32303
|
+
if (!line.includes('"assistant"') && !line.includes('"user"')) return null;
|
|
32304
|
+
let entry;
|
|
32305
|
+
try {
|
|
32306
|
+
entry = JSON.parse(line);
|
|
32307
|
+
} catch {
|
|
32308
|
+
return null;
|
|
32309
|
+
}
|
|
32310
|
+
if (entry.type === "user") return "working";
|
|
32311
|
+
if (entry.type !== "assistant") return null;
|
|
32312
|
+
const stop = entry.message?.stop_reason;
|
|
32313
|
+
return stop === "end_turn" || stop === "stop_sequence" ? "finished" : "working";
|
|
32314
|
+
}
|
|
32315
|
+
function parseTurnStateFromCodexLine(line) {
|
|
32316
|
+
if (!line.includes('"event_msg"') && !line.includes('"response_item"') && !line.includes('"turn_context"')) return null;
|
|
32317
|
+
let entry;
|
|
32318
|
+
try {
|
|
32319
|
+
entry = JSON.parse(line);
|
|
32320
|
+
} catch {
|
|
32321
|
+
return null;
|
|
32322
|
+
}
|
|
32323
|
+
if (entry.type === "response_item" || entry.type === "turn_context") return "working";
|
|
32324
|
+
if (entry.type !== "event_msg") return null;
|
|
32325
|
+
if (entry.payload?.type === "task_complete" || entry.payload?.type === "turn_aborted") return "finished";
|
|
32326
|
+
return entry.payload?.type === "task_started" ? "working" : null;
|
|
32327
|
+
}
|
|
32328
|
+
var resolveLineParsers = (harness) => harness === "codex" ? { parseToolCall: parseToolCallFromCodexLine, parseModel: parseModelFromCodexLine, parseTurnState: parseTurnStateFromCodexLine } : { parseToolCall: parseToolCallFromClaudeLine, parseModel: parseModelFromClaudeLine, parseTurnState: parseTurnStateFromClaudeLine };
|
|
32296
32329
|
var fetchFileSize = (path3) => {
|
|
32297
32330
|
try {
|
|
32298
32331
|
return statSync7(path3).size;
|
|
@@ -32347,6 +32380,14 @@ function fetchModelFromFileTail(path3, parseModel) {
|
|
|
32347
32380
|
}
|
|
32348
32381
|
return null;
|
|
32349
32382
|
}
|
|
32383
|
+
function fetchTurnStateFromFileTail(path3, parseTurnState) {
|
|
32384
|
+
const lines2 = new FileTail(path3, "start").fetchNewLines();
|
|
32385
|
+
for (let index = lines2.length - 1; index >= 0; index -= 1) {
|
|
32386
|
+
const state = parseTurnState(lines2[index]);
|
|
32387
|
+
if (state) return state;
|
|
32388
|
+
}
|
|
32389
|
+
return "working";
|
|
32390
|
+
}
|
|
32350
32391
|
var fetchChangedAt = (path3) => {
|
|
32351
32392
|
try {
|
|
32352
32393
|
return statSync7(path3).mtimeMs;
|
|
@@ -32422,7 +32463,7 @@ function parseCodexWorkerLabel(head) {
|
|
|
32422
32463
|
return parts.length ? truncate(parts.join(" \u2014 "), WORKER_LABEL_MAX) : null;
|
|
32423
32464
|
}
|
|
32424
32465
|
function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
|
|
32425
|
-
const rollouts =
|
|
32466
|
+
const rollouts = /* @__PURE__ */ new Map();
|
|
32426
32467
|
const findRolloutsInDirectory = (directory, depth) => {
|
|
32427
32468
|
let entries;
|
|
32428
32469
|
try {
|
|
@@ -32440,12 +32481,13 @@ function findCodexChildRollouts(roots, parentThreadId, changedAfter) {
|
|
|
32440
32481
|
if (!ref || ref === parentThreadId) continue;
|
|
32441
32482
|
const changedAt = fetchChangedAt(path3);
|
|
32442
32483
|
if (changedAt < changedAfter) continue;
|
|
32443
|
-
rollouts.
|
|
32484
|
+
if (!rollouts.has(ref)) rollouts.set(ref, { path: path3, ref, changedAt });
|
|
32444
32485
|
}
|
|
32445
32486
|
};
|
|
32446
32487
|
for (const root of roots) findRolloutsInDirectory(root, 0);
|
|
32447
32488
|
const found = [];
|
|
32448
|
-
for (const rollout of rollouts.sort((a, b) => b.changedAt - a.changedAt)
|
|
32489
|
+
for (const rollout of [...rollouts.values()].sort((a, b) => b.changedAt - a.changedAt)) {
|
|
32490
|
+
if (found.length >= WORKERS_PER_SESSION) break;
|
|
32449
32491
|
const head = fetchFileHead(rollout.path, HEAD_BYTES).split("\n")[0] ?? "";
|
|
32450
32492
|
if (!head.includes('"session_meta"') || isCodexHelperRollout(head)) continue;
|
|
32451
32493
|
if (!new RegExp(`"parent_thread_id"\\s*:\\s*"${parentThreadId}"`).test(head)) continue;
|
|
@@ -32537,7 +32579,10 @@ var HarnessActivity = class {
|
|
|
32537
32579
|
for (const file2 of found) {
|
|
32538
32580
|
const known = this.workers.get(file2.ref);
|
|
32539
32581
|
if (known) Object.assign(known, { label: file2.label, directory: file2.directory, branch: file2.branch, changedAt: file2.changedAt });
|
|
32540
|
-
else
|
|
32582
|
+
else {
|
|
32583
|
+
const tail = new FileTail(file2.path, "end");
|
|
32584
|
+
this.workers.set(file2.ref, { ...file2, since: new Date(file2.changedAt).toISOString(), model: fetchModelFromFileTail(file2.path, this.parsers.parseModel), doing: null, finished: fetchTurnStateFromFileTail(file2.path, this.parsers.parseTurnState) === "finished", tail });
|
|
32585
|
+
}
|
|
32541
32586
|
}
|
|
32542
32587
|
const live = new Set(found.map((file2) => file2.ref));
|
|
32543
32588
|
for (const ref of [...this.workers.keys()]) if (!live.has(ref)) this.workers.delete(ref);
|
|
@@ -32562,35 +32607,39 @@ var HarnessActivity = class {
|
|
|
32562
32607
|
}
|
|
32563
32608
|
return [];
|
|
32564
32609
|
}
|
|
32565
|
-
/** The newest tool call and
|
|
32610
|
+
/** The newest tool call, model and turn state in everything that has been written since the last read. */
|
|
32566
32611
|
read() {
|
|
32567
|
-
const { parseToolCall, parseModel } = this.parsers;
|
|
32568
|
-
const
|
|
32612
|
+
const { parseToolCall, parseModel, parseTurnState } = this.parsers;
|
|
32613
|
+
const fetchNewestCallModelAndTurnState = (tail) => {
|
|
32569
32614
|
let call = null;
|
|
32570
32615
|
let model = null;
|
|
32616
|
+
let turn = null;
|
|
32571
32617
|
for (const line of tail.fetchNewLines()) {
|
|
32572
32618
|
call = parseToolCall(line) ?? call;
|
|
32573
32619
|
model = parseModel(line) ?? model;
|
|
32620
|
+
turn = parseTurnState(line) ?? turn;
|
|
32574
32621
|
}
|
|
32575
|
-
return { call, model };
|
|
32622
|
+
return { call, model, turn };
|
|
32576
32623
|
};
|
|
32577
32624
|
if (this.own) {
|
|
32578
|
-
const { call, model } =
|
|
32625
|
+
const { call, model } = fetchNewestCallModelAndTurnState(this.own);
|
|
32579
32626
|
this.doing = call ?? this.doing;
|
|
32580
32627
|
this.model = model ?? this.model;
|
|
32581
32628
|
}
|
|
32582
32629
|
for (const worker of this.workers.values()) {
|
|
32583
|
-
const { call, model } =
|
|
32630
|
+
const { call, model, turn } = fetchNewestCallModelAndTurnState(worker.tail);
|
|
32584
32631
|
worker.doing = call ?? worker.doing;
|
|
32585
32632
|
worker.model = model ?? worker.model;
|
|
32633
|
+
if (turn) worker.finished = turn === "finished";
|
|
32586
32634
|
}
|
|
32587
32635
|
}
|
|
32588
32636
|
/**
|
|
32589
|
-
* Every worker, oldest first, so the roster changes only when one starts or finishes and never
|
|
32590
|
-
*
|
|
32637
|
+
* Every worker still working, oldest first, so the roster changes only when one starts or finishes and never
|
|
32638
|
+
* because one got busy. One whose turn has ended leaves at once, not when its records have gone quiet. The
|
|
32639
|
+
* contract's guard is far past what a harness runs; past it, the oldest are the ones listed.
|
|
32591
32640
|
*/
|
|
32592
32641
|
buildRoster() {
|
|
32593
|
-
return [...this.workers.values()].sort((a, b) => a.since.localeCompare(b.since) || a.ref.localeCompare(b.ref)).slice(0, WORKERS_PER_SESSION).map((worker) => ({ ref: worker.ref, label: worker.label, directory: worker.directory, branch: worker.branch, model: worker.model, since: worker.since }));
|
|
32642
|
+
return [...this.workers.values()].filter((worker) => !worker.finished).sort((a, b) => a.since.localeCompare(b.since) || a.ref.localeCompare(b.ref)).slice(0, WORKERS_PER_SESSION).map((worker) => ({ ref: worker.ref, label: worker.label, directory: worker.directory, branch: worker.branch, model: worker.model, since: worker.since }));
|
|
32594
32643
|
}
|
|
32595
32644
|
/** Quiet for a while: still listed, because it is probably thinking, but saying nothing about what it is doing. */
|
|
32596
32645
|
buildWorkerLines(roster, now) {
|
|
@@ -33842,9 +33891,22 @@ function findChromePath() {
|
|
|
33842
33891
|
if (override) return existsSync16(override) ? override : null;
|
|
33843
33892
|
return (CHROME_PATHS[process.platform] ?? []).find((path3) => existsSync16(path3)) ?? null;
|
|
33844
33893
|
}
|
|
33894
|
+
var SPLAT_NO_CHROME_HINT = "screenshots only: live frames on the canvas work without it. For screenshots, install Google Chrome or set SLOP_CHROME to a Chrome, Chromium, Edge or Brave binary";
|
|
33895
|
+
var chromeStartFailure = null;
|
|
33896
|
+
var CHROME_FAILURE_TTL_MS = 10 * 6e4;
|
|
33897
|
+
var sweepChromeStartFailure = () => {
|
|
33898
|
+
if (chromeStartFailure && Date.now() - chromeStartFailure.at > CHROME_FAILURE_TTL_MS) chromeStartFailure = null;
|
|
33899
|
+
};
|
|
33900
|
+
function canStartChrome() {
|
|
33901
|
+
sweepChromeStartFailure();
|
|
33902
|
+
return chromeStartFailure === null && findChromePath() !== null;
|
|
33903
|
+
}
|
|
33845
33904
|
function requireChromePath() {
|
|
33905
|
+
sweepChromeStartFailure();
|
|
33906
|
+
const failure2 = chromeStartFailure?.reason;
|
|
33907
|
+
if (failure2) throw new CliError(`Screenshots need Chrome, and it did not start here: ${failure2}`, SPLAT_NO_CHROME_HINT, "no_chrome");
|
|
33846
33908
|
const path3 = findChromePath();
|
|
33847
|
-
if (!path3) throw new CliError("
|
|
33909
|
+
if (!path3) throw new CliError("Screenshots need Chrome, and none was found", SPLAT_NO_CHROME_HINT, "no_chrome");
|
|
33848
33910
|
return path3;
|
|
33849
33911
|
}
|
|
33850
33912
|
var buildSplatChromeProfilePath = () => join19(dirname12(buildConfigPath()), "splat", "chrome-profile");
|
|
@@ -33897,9 +33959,12 @@ var ChromeBrowser = class _ChromeBrowser {
|
|
|
33897
33959
|
signal?.throwIfAborted();
|
|
33898
33960
|
await browser.send("Browser.getVersion", {}, void 0, 2e4);
|
|
33899
33961
|
} catch (error62) {
|
|
33962
|
+
const cannotRun = child.pid === void 0 || child.exitCode !== null || child.signalCode !== null;
|
|
33900
33963
|
await browser.close();
|
|
33901
33964
|
signal?.throwIfAborted();
|
|
33902
|
-
|
|
33965
|
+
const reason = error62 instanceof Error ? error62.message : String(error62);
|
|
33966
|
+
if (cannotRun) chromeStartFailure = { reason, at: Date.now() };
|
|
33967
|
+
throw new CliError(`Screenshots need Chrome, and it did not start here: ${reason}`, SPLAT_NO_CHROME_HINT, "no_chrome");
|
|
33903
33968
|
} finally {
|
|
33904
33969
|
signal?.removeEventListener("abort", stopBrowser);
|
|
33905
33970
|
}
|
|
@@ -34721,12 +34786,13 @@ async function isSplatProxyRunning(proxyPort = SPLAT_PROXY_PORT) {
|
|
|
34721
34786
|
async function startSplatProxy(options2) {
|
|
34722
34787
|
const proxyPort = options2.proxyPort ?? SPLAT_PROXY_PORT;
|
|
34723
34788
|
const frameAncestors = [options2.appOrigin, "http://localhost:*", "http://127.0.0.1:*"].join(" ");
|
|
34789
|
+
let rendererId = options2.rendererId ?? null;
|
|
34724
34790
|
const connections = /* @__PURE__ */ new Set();
|
|
34725
34791
|
const server = createServer((request, response) => {
|
|
34726
34792
|
const host = request.headers.host ?? "";
|
|
34727
34793
|
if (request.url === HEALTH_PATH && (host === `127.0.0.1:${proxyPort}` || host === `localhost:${proxyPort}`)) {
|
|
34728
|
-
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
34729
|
-
response.end(JSON.stringify({ splat: true }));
|
|
34794
|
+
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store", "access-control-allow-origin": options2.appOrigin, vary: "origin" });
|
|
34795
|
+
response.end(JSON.stringify({ splat: true, rendererId }));
|
|
34730
34796
|
return;
|
|
34731
34797
|
}
|
|
34732
34798
|
const match = HOST.exec(host);
|
|
@@ -34736,7 +34802,7 @@ async function startSplatProxy(options2) {
|
|
|
34736
34802
|
response.end("Splat's live-frame proxy answers only <port>.localhost");
|
|
34737
34803
|
return;
|
|
34738
34804
|
}
|
|
34739
|
-
forwardSplatRequest(request, response, port, proxyPort, frameAncestors, options2.appOrigin,
|
|
34805
|
+
forwardSplatRequest(request, response, port, proxyPort, frameAncestors, options2.appOrigin, rendererId);
|
|
34740
34806
|
});
|
|
34741
34807
|
server.on("connection", (socket) => {
|
|
34742
34808
|
connections.add(socket);
|
|
@@ -34755,11 +34821,14 @@ async function startSplatProxy(options2) {
|
|
|
34755
34821
|
server.once("error", () => resolve6(false));
|
|
34756
34822
|
server.listen(proxyPort, "127.0.0.1", () => resolve6(true));
|
|
34757
34823
|
});
|
|
34758
|
-
if (!listening) return await isSplatProxyRunning(proxyPort) ? { port: proxyPort, owned: false, stop: async () => void 0 } : null;
|
|
34824
|
+
if (!listening) return await isSplatProxyRunning(proxyPort) ? { port: proxyPort, owned: false, setRendererId: () => void 0, stop: async () => void 0 } : null;
|
|
34759
34825
|
server.unref();
|
|
34760
34826
|
return {
|
|
34761
34827
|
port: proxyPort,
|
|
34762
34828
|
owned: true,
|
|
34829
|
+
setRendererId: (id) => {
|
|
34830
|
+
rendererId = id;
|
|
34831
|
+
},
|
|
34763
34832
|
stop: () => new Promise((resolve6) => {
|
|
34764
34833
|
for (const socket of connections) socket.destroy();
|
|
34765
34834
|
server.closeAllConnections();
|
|
@@ -34861,7 +34930,6 @@ var SplatPublisherConnection = class {
|
|
|
34861
34930
|
streams = /* @__PURE__ */ new Map();
|
|
34862
34931
|
upstreamSockets = /* @__PURE__ */ new Map();
|
|
34863
34932
|
timers = /* @__PURE__ */ new Set();
|
|
34864
|
-
chrome = findChromePath() !== null;
|
|
34865
34933
|
appOrigin;
|
|
34866
34934
|
localOrigin;
|
|
34867
34935
|
capture = null;
|
|
@@ -34902,8 +34970,16 @@ var SplatPublisherConnection = class {
|
|
|
34902
34970
|
try {
|
|
34903
34971
|
let path3;
|
|
34904
34972
|
try {
|
|
34905
|
-
path3 = (await this.api.post(`${this.sourcePath}/relay/ticket`, ApiResponses.SplatRelayTicket, {})).path;
|
|
34973
|
+
path3 = this.options.rendererId ? (await this.api.post(`/projects/${this.options.projectId}/splat/renderers/${this.options.rendererId}/relay/ticket`, ApiResponses.SplatRelayTicket, { port: this.options.port })).path : (await this.api.post(`${this.sourcePath}/relay/ticket`, ApiResponses.SplatRelayTicket, {})).path;
|
|
34906
34974
|
} catch (error62) {
|
|
34975
|
+
if (this.options.rendererId && error62 instanceof CliError && ["unauthenticated", "forbidden", "bad_request"].includes(error62.code)) {
|
|
34976
|
+
this.stop(`Live through Slopware stopped for :${this.options.port}: ${error62.message}`);
|
|
34977
|
+
return;
|
|
34978
|
+
}
|
|
34979
|
+
if (this.options.rendererId) {
|
|
34980
|
+
this.scheduleReconnect();
|
|
34981
|
+
return;
|
|
34982
|
+
}
|
|
34907
34983
|
if (error62 instanceof CliError && ["unauthenticated", "forbidden", "bad_request"].includes(error62.code)) {
|
|
34908
34984
|
this.stop(`Publishing stopped: ${error62.message}`);
|
|
34909
34985
|
return;
|
|
@@ -34952,7 +35028,7 @@ var SplatPublisherConnection = class {
|
|
|
34952
35028
|
clearTimeout(helloTimer);
|
|
34953
35029
|
this.attempt = 0;
|
|
34954
35030
|
this.generation = down.data.generation;
|
|
34955
|
-
this.log(`Publishing :${this.options.port} as ${this.options.sourceId} (connection ${this.generation})`);
|
|
35031
|
+
this.log(this.options.rendererId ? `Live through Slopware: :${this.options.port}` : `Publishing :${this.options.port} as ${this.options.sourceId} (connection ${this.generation})`);
|
|
34956
35032
|
void this.refreshState();
|
|
34957
35033
|
return;
|
|
34958
35034
|
}
|
|
@@ -34969,7 +35045,7 @@ var SplatPublisherConnection = class {
|
|
|
34969
35045
|
this.releaseStreams();
|
|
34970
35046
|
if (this.stopped) return;
|
|
34971
35047
|
const reason = ended ?? event.reason;
|
|
34972
|
-
if (reason === "replaced") return this.stop("Another `slop splat serve` of this publication took over");
|
|
35048
|
+
if (reason === "replaced") return this.stop(this.options.rendererId ? "Another renderer process on this machine took over" : "Another `slop splat serve` of this publication took over");
|
|
34973
35049
|
if (reason === "deleted") return this.stop("This source was removed from the canvas");
|
|
34974
35050
|
if (reason === "revoked" || event.code === 1008) return this.stop("Publishing stopped: access revoked");
|
|
34975
35051
|
this.log(`Relay connection closed (${event.code}${event.reason ? ` ${event.reason}` : ""}); reconnecting`);
|
|
@@ -35014,7 +35090,7 @@ var SplatPublisherConnection = class {
|
|
|
35014
35090
|
commit: this.gitState.commit,
|
|
35015
35091
|
dirty: this.gitState.dirty,
|
|
35016
35092
|
label: this.options.label?.slice(0, 120) ?? null,
|
|
35017
|
-
chrome:
|
|
35093
|
+
chrome: canStartChrome(),
|
|
35018
35094
|
busy: this.capture !== null
|
|
35019
35095
|
});
|
|
35020
35096
|
}
|
|
@@ -35136,7 +35212,7 @@ var SplatPublisherConnection = class {
|
|
|
35136
35212
|
if (decoder) source = source.pipe(decoder);
|
|
35137
35213
|
source = source.pipe(createSplatOriginRewriteStream(this.options.port, origin));
|
|
35138
35214
|
}
|
|
35139
|
-
if (html) source = source.pipe(createSplatHtmlStream(buildSplatFrameScript(nonce, this.appOrigin, this.options.sourceId)));
|
|
35215
|
+
if (html) source = source.pipe(createSplatHtmlStream(buildSplatFrameScript(nonce, this.appOrigin, this.options.rendererId ?? this.options.sourceId)));
|
|
35140
35216
|
stream.source = source;
|
|
35141
35217
|
const status = response.statusCode ?? 502;
|
|
35142
35218
|
let holding = !type.includes("text/event-stream");
|
|
@@ -35338,7 +35414,7 @@ var SplatPublisherConnection = class {
|
|
|
35338
35414
|
}
|
|
35339
35415
|
}
|
|
35340
35416
|
async claimScheduledRound() {
|
|
35341
|
-
if (this.capture || !
|
|
35417
|
+
if (this.capture || !canStartChrome() || !this.socket) return;
|
|
35342
35418
|
let claim2;
|
|
35343
35419
|
try {
|
|
35344
35420
|
claim2 = await this.api.post(`${this.sourcePath}/auto-capture`, ApiResponses.SplatAutoCaptureClaim, {});
|
|
@@ -35367,6 +35443,7 @@ var PING_EVERY_MS3 = 25e3;
|
|
|
35367
35443
|
var PONG_WITHIN_MS3 = 1e4;
|
|
35368
35444
|
var RESCAN_EVERY_MS = 3e4;
|
|
35369
35445
|
var STANDBY_POLL_MS2 = 3e4;
|
|
35446
|
+
var LIVE_RELAY_RESTART_MS = 6e4;
|
|
35370
35447
|
var SCHEDULE_POLL_MS2 = 5 * 6e4;
|
|
35371
35448
|
var SplatRendererConnection = class {
|
|
35372
35449
|
constructor(api, projectId, options2 = {}) {
|
|
@@ -35384,7 +35461,8 @@ var SplatRendererConnection = class {
|
|
|
35384
35461
|
origins = [];
|
|
35385
35462
|
proxy = null;
|
|
35386
35463
|
proxySync = null;
|
|
35387
|
-
|
|
35464
|
+
/** Whether the room was last told this renderer can take screenshots: a Chrome that stops starting is announced. */
|
|
35465
|
+
announcedChrome = null;
|
|
35388
35466
|
timers = /* @__PURE__ */ new Set();
|
|
35389
35467
|
starting = null;
|
|
35390
35468
|
connecting = false;
|
|
@@ -35394,6 +35472,8 @@ var SplatRendererConnection = class {
|
|
|
35394
35472
|
queue = [];
|
|
35395
35473
|
working = false;
|
|
35396
35474
|
standingBy = false;
|
|
35475
|
+
/** Each dev server served through Slopware, by port. */
|
|
35476
|
+
liveRelays = /* @__PURE__ */ new Map();
|
|
35397
35477
|
get id() {
|
|
35398
35478
|
return this.rendererId;
|
|
35399
35479
|
}
|
|
@@ -35441,6 +35521,7 @@ var SplatRendererConnection = class {
|
|
|
35441
35521
|
return;
|
|
35442
35522
|
}
|
|
35443
35523
|
this.proxy = proxy;
|
|
35524
|
+
if (proxy && this.rendererId) proxy.setRendererId(this.rendererId);
|
|
35444
35525
|
if (proxy) this.log(proxy.owned ? `Live frames: proxy on http://<port>.localhost:${proxy.port}` : `Live frames: sharing the proxy already on :${proxy.port}`);
|
|
35445
35526
|
}
|
|
35446
35527
|
/** Looks again at the dev servers now (after a port is pinned or unpinned, say), and says so if they changed. */
|
|
@@ -35452,8 +35533,9 @@ var SplatRendererConnection = class {
|
|
|
35452
35533
|
if (this.stopped) return this.origins;
|
|
35453
35534
|
const changed = JSON.stringify(next) !== JSON.stringify(this.origins);
|
|
35454
35535
|
this.origins = next;
|
|
35536
|
+
if (changed) this.syncLiveRelays();
|
|
35455
35537
|
if (changed) this.log(next.length > 0 ? `Dev servers: ${next.map((origin) => `:${origin.port} (${origin.branch}${origin.source === "pinned" ? ", pinned" : ""})`).join(", ")}` : "No dev servers found yet: pin one with `slop splat target add <port>`");
|
|
35456
|
-
if (announce && (changed || previousProxyPort !== (this.proxy?.port ?? null))) this.sendState();
|
|
35538
|
+
if (announce && (changed || previousProxyPort !== (this.proxy?.port ?? null) || this.announcedChrome !== canStartChrome())) this.sendState();
|
|
35457
35539
|
return next;
|
|
35458
35540
|
}
|
|
35459
35541
|
repeat(ms, run) {
|
|
@@ -35477,7 +35559,9 @@ var SplatRendererConnection = class {
|
|
|
35477
35559
|
if (!this.stopped && this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(message));
|
|
35478
35560
|
}
|
|
35479
35561
|
sendState() {
|
|
35480
|
-
|
|
35562
|
+
const chrome = canStartChrome();
|
|
35563
|
+
if (this.socket?.readyState === WebSocket.OPEN) this.announcedChrome = chrome;
|
|
35564
|
+
this.sendUp({ type: "state", origins: this.origins, proxyPort: this.proxy?.port ?? null, chrome, busy: this.working });
|
|
35481
35565
|
}
|
|
35482
35566
|
async connect() {
|
|
35483
35567
|
if (this.stopped || this.standingBy || this.connecting || this.socket) return;
|
|
@@ -35485,7 +35569,7 @@ var SplatRendererConnection = class {
|
|
|
35485
35569
|
try {
|
|
35486
35570
|
let path3;
|
|
35487
35571
|
try {
|
|
35488
|
-
const ticket = await this.api.post(`/projects/${this.projectId}/splat/renderer/ticket`, ApiResponses.ConnectTicket, { cliVersion: "0.
|
|
35572
|
+
const ticket = await this.api.post(`/projects/${this.projectId}/splat/renderer/ticket`, ApiResponses.ConnectTicket, { cliVersion: "0.10.0", onlyMe: this.options.onlyMe ?? false, agent: this.options.agent ?? null, capabilities: ["workspaces", ...this.options.liveRelay === false ? [] : [SPLAT_RENDERER_LIVE_RELAY]] });
|
|
35489
35573
|
path3 = ticket.path;
|
|
35490
35574
|
} catch (error62) {
|
|
35491
35575
|
if (error62 instanceof CliError && ["unauthenticated", "not_found", "forbidden", "bad_request"].includes(error62.code)) {
|
|
@@ -35568,7 +35652,9 @@ var SplatRendererConnection = class {
|
|
|
35568
35652
|
this.attempt = 0;
|
|
35569
35653
|
this.rendererId = push.rendererId;
|
|
35570
35654
|
this.log(`Rendering for the canvas as ${push.rendererId}`);
|
|
35655
|
+
this.proxy?.setRendererId(push.rendererId);
|
|
35571
35656
|
this.sendState();
|
|
35657
|
+
this.syncLiveRelays();
|
|
35572
35658
|
} else if (push.type === "capture") {
|
|
35573
35659
|
if (this.standingBy || this.working || this.queue.length) {
|
|
35574
35660
|
this.sendUp({ type: "finished", requestId: push.requestId, failed: ["Renderer is busy; retry when its current capture finishes"] });
|
|
@@ -35632,7 +35718,7 @@ var SplatRendererConnection = class {
|
|
|
35632
35718
|
* one with a dev server on the default branch), and if this one has it, captures every frame. Quiet when idle.
|
|
35633
35719
|
*/
|
|
35634
35720
|
async claimScheduledRound() {
|
|
35635
|
-
if (this.stopped || this.standingBy || this.working || this.queue.length > 0 || !
|
|
35721
|
+
if (this.stopped || this.standingBy || this.working || this.queue.length > 0 || !canStartChrome() || this.origins.length === 0) return;
|
|
35636
35722
|
await this.rescanOrigins(false);
|
|
35637
35723
|
if (this.stopped || this.standingBy || this.working || this.queue.length > 0 || this.origins.length === 0) return;
|
|
35638
35724
|
let claim2;
|
|
@@ -35671,6 +35757,7 @@ var SplatRendererConnection = class {
|
|
|
35671
35757
|
standBy() {
|
|
35672
35758
|
if (this.standingBy) return;
|
|
35673
35759
|
this.standingBy = true;
|
|
35760
|
+
this.syncLiveRelays();
|
|
35674
35761
|
this.queue = [];
|
|
35675
35762
|
this.captureAbort?.abort();
|
|
35676
35763
|
const check2 = async () => {
|
|
@@ -35681,6 +35768,7 @@ var SplatRendererConnection = class {
|
|
|
35681
35768
|
if (!renderers.some((renderer) => renderer.id === this.rendererId)) {
|
|
35682
35769
|
this.standingBy = false;
|
|
35683
35770
|
await this.connect();
|
|
35771
|
+
this.syncLiveRelays();
|
|
35684
35772
|
return;
|
|
35685
35773
|
}
|
|
35686
35774
|
} catch {
|
|
@@ -35704,6 +35792,31 @@ var SplatRendererConnection = class {
|
|
|
35704
35792
|
if (socket && socket.readyState <= WebSocket.OPEN) socket.close(1e3, reason);
|
|
35705
35793
|
void this.proxy?.stop();
|
|
35706
35794
|
this.proxy = null;
|
|
35795
|
+
this.syncLiveRelays();
|
|
35796
|
+
}
|
|
35797
|
+
/**
|
|
35798
|
+
* One relay connection per dev server (up to a few), while this renderer is the one connected: the shared source
|
|
35799
|
+
* live through Slopware, for this renderer's person on any computer. Started, and stopped when a server goes.
|
|
35800
|
+
*/
|
|
35801
|
+
syncLiveRelays() {
|
|
35802
|
+
const active = !this.stopped && !this.standingBy && this.options.liveRelay !== false && this.rendererId !== null;
|
|
35803
|
+
const wanted = active ? listSplatRendererRelayPorts(this.origins) : [];
|
|
35804
|
+
for (const [port, relay] of this.liveRelays) {
|
|
35805
|
+
if (wanted.includes(port)) continue;
|
|
35806
|
+
this.liveRelays.delete(port);
|
|
35807
|
+
relay.stop("The dev server is gone");
|
|
35808
|
+
}
|
|
35809
|
+
for (const port of wanted) {
|
|
35810
|
+
if (this.liveRelays.has(port)) continue;
|
|
35811
|
+
const relay = new SplatPublisherConnection(this.api, { projectId: this.projectId, workspaceId: "", sourceId: "", rendererId: this.rendererId, port, cwd: this.options.cwd, log: (line) => this.log(line) });
|
|
35812
|
+
this.liveRelays.set(port, relay);
|
|
35813
|
+
void relay.run().then((reason) => {
|
|
35814
|
+
if (this.liveRelays.get(port) !== relay) return;
|
|
35815
|
+
this.liveRelays.delete(port);
|
|
35816
|
+
this.log(reason);
|
|
35817
|
+
this.scheduleRendererCall(LIVE_RELAY_RESTART_MS, () => this.syncLiveRelays());
|
|
35818
|
+
});
|
|
35819
|
+
}
|
|
35707
35820
|
}
|
|
35708
35821
|
};
|
|
35709
35822
|
|
|
@@ -35714,7 +35827,7 @@ var formatAge = (at) => {
|
|
|
35714
35827
|
const minutes = Math.round((Date.now() - Date.parse(at)) / 6e4);
|
|
35715
35828
|
return minutes < 1 ? "just now" : minutes < 60 ? `${minutes}m ago` : minutes < 48 * 60 ? `${Math.round(minutes / 60)}h ago` : `${Math.round(minutes / 1440)}d ago`;
|
|
35716
35829
|
};
|
|
35717
|
-
var formatSplatRendererLine = (renderer) => `${renderer.machine.name} (${renderer.user.name}${renderer.agent ? `, ${renderer.agent}` : ""}) ${renderer.origins.map((origin) => `:${origin.port} ${origin.branch}${origin.dirty ? "*" : ""}`).join(", ") || "no dev servers"}${renderer.chrome ? "" : " [no
|
|
35830
|
+
var formatSplatRendererLine = (renderer) => `${renderer.machine.name} (${renderer.user.name}${renderer.agent ? `, ${renderer.agent}` : ""}) ${renderer.origins.map((origin) => `:${origin.port} ${origin.branch}${origin.dirty ? "*" : ""}`).join(", ") || "no dev servers"}${renderer.chrome ? "" : " [live only: no screenshots]"}${renderer.onlyMe ? " [only me]" : ""}${renderer.job ? ` capturing ${renderer.job.done}/${renderer.job.total}` : ""}`;
|
|
35718
35831
|
var formatSplatSourceLine = (source) => source.kind === "shared" ? `${source.name} captured by whichever renderer has the branch` : `${source.name} ${source.owner?.name ?? "someone"} on ${source.machine?.name ?? "?"} :${source.port ?? "?"} ${source.branch ?? ""}${source.dirty ? "*" : ""} ${source.state}${source.live ? ", live" : ""}${source.onlyMe ? " [only me]" : ""}${source.job ? ` capturing ${source.job.done}/${source.job.total}` : ""}`;
|
|
35719
35832
|
function formatSplatCanvas(canvas) {
|
|
35720
35833
|
if (canvas.routes.length === 0) return "No routes on the canvas yet. Add some: slop splat route add / /settings";
|
|
@@ -35854,7 +35967,7 @@ ${published.map((source2) => ` ${formatSplatSourceLine(source2)}`).join("\n")}`
|
|
|
35854
35967
|
const result = await api.post(`${target.base}/routes`, ApiResponses.SplatRoutesResult, { routes: paths2.slice(0, 50).map((path3) => ({ path: path3 })) });
|
|
35855
35968
|
emitOutput(result, () => `Added ${result.routes.length} route${result.routes.length === 1 ? "" : "s"} (${result.items.length} frames).${found.some((one) => one.dynamic) ? ` Dynamic ones need an example path: ${found.filter((one) => one.dynamic).map((one) => one.path).join(", ")}` : ""}`);
|
|
35856
35969
|
});
|
|
35857
|
-
registerProjectOption(splat.command("capture [paths...]")).description("
|
|
35970
|
+
registerProjectOption(splat.command("capture [paths...]")).description("Screenshot frames here (needs Chrome) and upload them: all of the shared source's (or --source, one you publish), or the paths given.").option("-v, --viewport <key>", "only this viewport (repeatable)", collect).option("-s, --source <source>", "a source you publish (default: the workspace's shared source)").option("--port <port>", "the dev server's port (or http://localhost:<port>); default: the only one found or pinned, or the source's").option("-b, --branch <name>", "record the renders as this branch (default: the dev server's)").action(async (paths2, options2) => {
|
|
35858
35971
|
const { api, project, target } = await resolveSplatCommandTarget(options2);
|
|
35859
35972
|
const canvas = await fetchSplatCanvas(api, target);
|
|
35860
35973
|
await ensureSplatDefaultBranch(api, target.base, canvas);
|
|
@@ -35898,6 +36011,7 @@ ${published.map((source2) => ` ${formatSplatSourceLine(source2)}`).join("\n")}`
|
|
|
35898
36011
|
await renderer.start();
|
|
35899
36012
|
process.stderr.write(`Rendering ${project.key} for ${buildSplatCanvasLink(api.baseUrl, target)}. Ctrl-C to stop.
|
|
35900
36013
|
`);
|
|
36014
|
+
if (!canStartChrome()) process.stderr.write("Live only: no Chrome here, so no screenshots. Live frames work as they are; install Chrome (or set SLOP_CHROME) for captures.\n");
|
|
35901
36015
|
const keepAlive = setInterval(() => void 0, 6e4);
|
|
35902
36016
|
await new Promise((resolve6) => {
|
|
35903
36017
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => resolve6());
|
|
@@ -36110,6 +36224,7 @@ async function servePublication(options2) {
|
|
|
36110
36224
|
`);
|
|
36111
36225
|
process.stderr.write(`${buildSplatCanvasLink(api.baseUrl, target)}
|
|
36112
36226
|
`);
|
|
36227
|
+
if (!canStartChrome()) process.stderr.write("Live only: no Chrome here, so no screenshots. Teammates use it live as it is; install Chrome (or set SLOP_CHROME) for captures.\n");
|
|
36113
36228
|
const stopped = new Promise((resolve6) => {
|
|
36114
36229
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => resolve6("Stopped publishing."));
|
|
36115
36230
|
});
|
|
@@ -36191,7 +36306,25 @@ function createSplatTools(context2) {
|
|
|
36191
36306
|
if (items.length === 0) throw new Error("Nothing to capture: add routes first (action add)");
|
|
36192
36307
|
const limit = Math.min(Number(args2.limit ?? 3), 8);
|
|
36193
36308
|
const tilesPerFrame = args2.full ? 4 : 1;
|
|
36194
|
-
const
|
|
36309
|
+
const withoutScreenshots = async (reason) => {
|
|
36310
|
+
const renderer = await context2.ensureRenderer().catch(() => null);
|
|
36311
|
+
const proxyPort = renderer?.proxyPort ?? null;
|
|
36312
|
+
const paths2 = items.map((item) => item.path).filter((path3, index, all) => all.indexOf(path3) === index).join(", ");
|
|
36313
|
+
return [
|
|
36314
|
+
`No screenshots from this machine (${reason}), so nothing was captured and the team's canvas did not change.`,
|
|
36315
|
+
`The person this chat belongs to can open ${paths2} live on the canvas${proxyPort ? `, served from localhost:${origin.port} through this chat` : ""}: ${link(target)}. Teammates cannot: they see screenshots, or an app published with slop splat serve --name.`,
|
|
36316
|
+
...proxyPort ? [`Live URL: ${buildSplatLiveFrameUrl(proxyPort, origin.port, items[0].path)}`] : [],
|
|
36317
|
+
`To look at the page yourself, open http://localhost:${origin.port}${items[0].path} with a browser tool of your own, if you have one. If the team needs to see this change, say so: screenshots need Google Chrome here (or SLOP_CHROME).`
|
|
36318
|
+
].join("\n");
|
|
36319
|
+
};
|
|
36320
|
+
if (!canStartChrome()) return withoutScreenshots("no Chrome found");
|
|
36321
|
+
let outcome;
|
|
36322
|
+
try {
|
|
36323
|
+
outcome = await captureSplatFrames(api, project.id, origin, items, { agent: context2.agentName, tiles: tilesPerFrame, workspaceId: target.workspace?.id ?? null });
|
|
36324
|
+
} catch (error62) {
|
|
36325
|
+
if (error62 instanceof CliError && error62.code === "no_chrome") return withoutScreenshots("Chrome did not start here");
|
|
36326
|
+
throw error62;
|
|
36327
|
+
}
|
|
36195
36328
|
const shown = outcome.captured.slice(0, limit);
|
|
36196
36329
|
output2.attachImages(shown.flatMap((captured) => captured.tiles));
|
|
36197
36330
|
const lines2 = [
|
|
@@ -36211,9 +36344,8 @@ function createSplatTools(context2) {
|
|
|
36211
36344
|
const lines2 = [];
|
|
36212
36345
|
for (const capture of captures) {
|
|
36213
36346
|
const viewport = canvas.settings.viewports.find((candidate) => candidate.key === capture.viewportKey);
|
|
36214
|
-
const
|
|
36215
|
-
|
|
36216
|
-
output2.attachImages(images);
|
|
36347
|
+
const tiles = canStartChrome() ? await api.getBinary(capture.fullUrl.replace(/^\/api/, "")).then((bytes) => buildSplatCaptureTiles(bytes, "image/webp", capture.width, capture.height, viewport?.height ?? 800, args2.full ? 4 : 1)).catch(() => null) : null;
|
|
36348
|
+
output2.attachImages(tiles ?? [{ data: Buffer.from(await api.getBinary(capture.thumbUrl.replace(/^\/api/, ""))).toString("base64"), mimeType: "image/webp" }]);
|
|
36217
36349
|
lines2.push(`${route.path} @ ${capture.viewportKey}: ${capture.branch}${capture.commit ? ` ${capture.commit.slice(0, 7)}` : ""}${capture.dirty ? " (uncommitted)" : ""} by ${capture.capturedBy?.name ?? "someone"} on ${capture.machine ?? "?"}, ${capture.capturedAt}`);
|
|
36218
36350
|
}
|
|
36219
36351
|
return lines2.join("\n");
|
|
@@ -37519,17 +37651,17 @@ function hasHarnessSetup() {
|
|
|
37519
37651
|
function syncMachineSetupAfterUpdate() {
|
|
37520
37652
|
const nothing = { skills: [], retired: [], reinstalling: false };
|
|
37521
37653
|
try {
|
|
37522
|
-
if (!isUpdateCheckEnabled() || fetchMachineSetupState()?.version === "0.
|
|
37654
|
+
if (!isUpdateCheckEnabled() || fetchMachineSetupState()?.version === "0.10.0") return nothing;
|
|
37523
37655
|
const lock = claimFileLock(buildMachineSetupLockDirectory(), { staleAfterMs: 6e4 });
|
|
37524
37656
|
if (!lock) return nothing;
|
|
37525
37657
|
try {
|
|
37526
37658
|
const state = fetchMachineSetupState();
|
|
37527
|
-
if (state?.version === "0.
|
|
37659
|
+
if (state?.version === "0.10.0") return nothing;
|
|
37528
37660
|
const skills = hasInstalledSkills() ? installSkills() : [];
|
|
37529
37661
|
const shipped = Object.keys(SKILLS);
|
|
37530
37662
|
const retired = deleteRetiredSkills((state?.skills ?? []).filter((name) => !shipped.includes(name)));
|
|
37531
37663
|
const setupRevision = state?.setupRevision ?? (hasHarnessSetup() ? UNRECORDED_SETUP_REVISION : null);
|
|
37532
|
-
persistMachineSetupState({ version: "0.
|
|
37664
|
+
persistMachineSetupState({ version: "0.10.0", setupRevision, skills: hasInstalledSkills() ? shipped : [] });
|
|
37533
37665
|
const reinstalling = setupRevision !== null && setupRevision < SETUP_REVISION && runMcpInstallInBackground();
|
|
37534
37666
|
return { skills, retired, reinstalling };
|
|
37535
37667
|
} finally {
|
|
@@ -37696,7 +37828,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
37696
37828
|
const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
37697
37829
|
`);
|
|
37698
37830
|
try {
|
|
37699
|
-
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.
|
|
37831
|
+
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.10.0" }, capabilities: { experimentalApi: true } });
|
|
37700
37832
|
sendNotification("initialized", {});
|
|
37701
37833
|
const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
|
|
37702
37834
|
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
@@ -37749,7 +37881,14 @@ function buildTools(lifecycle, relay) {
|
|
|
37749
37881
|
if (stopped) throw new Error("The MCP server is stopping");
|
|
37750
37882
|
let context2 = projects.get(project.id);
|
|
37751
37883
|
if (!context2) {
|
|
37752
|
-
|
|
37884
|
+
if (!identity) {
|
|
37885
|
+
const conversation = fetchOwnConversation();
|
|
37886
|
+
identity = resolveAgentIdentity(process.cwd(), "process", conversation);
|
|
37887
|
+
if (relay && conversation && !called) {
|
|
37888
|
+
called = conversation;
|
|
37889
|
+
persistRelayConversation(relay, conversation);
|
|
37890
|
+
}
|
|
37891
|
+
}
|
|
37753
37892
|
context2 = createProjectTools(buildAuthedApiClient(), project, identity);
|
|
37754
37893
|
projects.set(project.id, context2);
|
|
37755
37894
|
}
|
|
@@ -37974,7 +38113,7 @@ var options = optionEnd < 0 ? args : args.slice(0, optionEnd);
|
|
|
37974
38113
|
updateJsonMode(options.includes("--json"));
|
|
37975
38114
|
syncMachineSetupAfterUpdate();
|
|
37976
38115
|
if (!options.includes("--background") && !options.includes("--child")) scheduleBackgroundUpdate();
|
|
37977
|
-
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.
|
|
38116
|
+
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.10.0").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").configureOutput({
|
|
37978
38117
|
outputError: (message, write) => {
|
|
37979
38118
|
if (!isJsonMode()) write(message);
|
|
37980
38119
|
},
|
|
@@ -37991,7 +38130,7 @@ var sessionCommand = registerSessionCommands(program2);
|
|
|
37991
38130
|
registerThreadCommands(program2, taskCommand);
|
|
37992
38131
|
registerOperationCommands(program2, taskCommand, sessionCommand);
|
|
37993
38132
|
registerSplatCommands(program2);
|
|
37994
|
-
registerMcpCommands(program2, "0.
|
|
38133
|
+
registerMcpCommands(program2, "0.10.0");
|
|
37995
38134
|
registerGuideCommand(program2);
|
|
37996
38135
|
registerSkillCommands(program2);
|
|
37997
38136
|
registerUpdateCommand(program2);
|