slopwaresystems 0.9.6 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/main.js +220 -50
- 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.1",
|
|
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.1";
|
|
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.1"
|
|
29499
29507
|
};
|
|
29500
29508
|
}
|
|
29501
29509
|
|
|
@@ -29775,10 +29783,10 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
|
|
|
29775
29783
|
- Shell project commands for people and scripts: \`slop project bindings --project <KEY>\` lists repository bindings, including other checkouts; \`slop project binding delete <ID> --project <KEY>\` removes one if you created it or are an org admin. \`slop session reachable <AGENT|SESSION_ID> on|off\` changes visibility for one of your live sessions. \`slop task move <KEY> <COLUMN> --before <NEIGHBOR>\` places the task immediately before that neighbor; \`--after <NEIGHBOR>\` places it immediately after. Use both only for adjacent neighbors: \`--after <PREVIOUS> --before <NEXT>\`. With neither, the task goes to the end of the column. \`slop task delete <KEY...>\` accepts explicit task keys; delete only tasks you created. Each deletion is separate, so a later server rejection can follow earlier successful deletions.
|
|
29776
29784
|
- Other shell project actions for people and scripts: \`slop task unlink <KEY> <LINK>\` removes one task link by ID, title or URL; \`slop task archive-done\` archives every task in a done column; \`slop project delete <KEY|ID>\` permanently deletes a project and its tasks (admin only, no undo).
|
|
29777
29785
|
- Agents use the eight slop MCP tools for everything on the board (\`slop mcp install\` once per machine): \`board\`, \`task\`, \`plan\`, \`session\`, \`threads\`, \`say\`, \`operation\`, \`splat\`, 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. slop updates itself (the CLI, the MCP server and the skills) and open chats switch to a new version on their own: never update or restart for it. 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\`.
|
|
29778
|
-
- 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.
|
|
29786
|
+
- Subagents you spawn show under you on the board, a workflow's agents too (what you asked them to do, or what the workflow calls them, 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.1", 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.1"]));
|
|
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.1", 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.1" },
|
|
31851
31859
|
reconnect: false,
|
|
31852
31860
|
onHello: options2.onHello,
|
|
31853
31861
|
onEnd: () => finishWait({ kind: "ended" }),
|
|
@@ -32299,7 +32307,7 @@ function parseTurnStateFromClaudeLine(line) {
|
|
|
32299
32307
|
} catch {
|
|
32300
32308
|
return null;
|
|
32301
32309
|
}
|
|
32302
|
-
if (entry.type === "user") return "working";
|
|
32310
|
+
if (entry.type === "user") return entry.toolEndsTurn === true ? "finished" : "working";
|
|
32303
32311
|
if (entry.type !== "assistant") return null;
|
|
32304
32312
|
const stop = entry.message?.stop_reason;
|
|
32305
32313
|
return stop === "end_turn" || stop === "stop_sequence" ? "finished" : "working";
|
|
@@ -32413,7 +32421,44 @@ function parseClaudeWorkerLabel(text) {
|
|
|
32413
32421
|
return parts.length ? truncate(parts.join(" \u2014 "), WORKER_LABEL_MAX) : null;
|
|
32414
32422
|
}
|
|
32415
32423
|
function findClaudeWorkerFiles(transcriptPath, conversation, changedAfter) {
|
|
32416
|
-
const
|
|
32424
|
+
const chat = join18(dirname11(transcriptPath), conversation);
|
|
32425
|
+
const directory = join18(chat, "subagents");
|
|
32426
|
+
const found = findClaudeWorkerFilesInDirectory(directory, changedAfter, (ref) => {
|
|
32427
|
+
try {
|
|
32428
|
+
return parseClaudeWorkerLabel(readFileSync17(join18(directory, `agent-${ref}.meta.json`), "utf8"));
|
|
32429
|
+
} catch {
|
|
32430
|
+
return null;
|
|
32431
|
+
}
|
|
32432
|
+
});
|
|
32433
|
+
let runs = [];
|
|
32434
|
+
try {
|
|
32435
|
+
runs = readdirSync10(join18(directory, "workflows"));
|
|
32436
|
+
} catch {
|
|
32437
|
+
}
|
|
32438
|
+
for (const runId of runs) {
|
|
32439
|
+
const runDirectory = join18(directory, "workflows", runId);
|
|
32440
|
+
const agents = findClaudeWorkerFilesInDirectory(runDirectory, changedAfter, () => null);
|
|
32441
|
+
if (agents.length === 0) continue;
|
|
32442
|
+
let journal = "";
|
|
32443
|
+
try {
|
|
32444
|
+
journal = readFileSync17(join18(runDirectory, "journal.jsonl"), "utf8");
|
|
32445
|
+
} catch {
|
|
32446
|
+
}
|
|
32447
|
+
let record2 = "";
|
|
32448
|
+
try {
|
|
32449
|
+
record2 = readFileSync17(join18(chat, "workflows", `${runId}.json`), "utf8");
|
|
32450
|
+
} catch {
|
|
32451
|
+
}
|
|
32452
|
+
const journaled = parseClaudeWorkflowFinishedAgents(journal);
|
|
32453
|
+
const run = parseClaudeWorkflowRecord(record2);
|
|
32454
|
+
for (const agent of agents) {
|
|
32455
|
+
const label = run.labels.get(agent.ref) ?? parseClaudeWorkerPromptLabel(fetchFileHead(agent.path, 16 * 1024).split("\n")[0] ?? "");
|
|
32456
|
+
found.push({ ...agent, label: label ? truncate(label, WORKER_LABEL_MAX) : "workflow agent", finished: run.over || journaled.has(agent.ref) || run.finished.has(agent.ref) });
|
|
32457
|
+
}
|
|
32458
|
+
}
|
|
32459
|
+
return found.sort((a, b) => b.changedAt - a.changedAt);
|
|
32460
|
+
}
|
|
32461
|
+
function findClaudeWorkerFilesInDirectory(directory, changedAfter, labelFor) {
|
|
32417
32462
|
let names;
|
|
32418
32463
|
try {
|
|
32419
32464
|
names = readdirSync10(directory);
|
|
@@ -32427,16 +32472,56 @@ function findClaudeWorkerFiles(transcriptPath, conversation, changedAfter) {
|
|
|
32427
32472
|
const path3 = join18(directory, name);
|
|
32428
32473
|
const changedAt = fetchChangedAt(path3);
|
|
32429
32474
|
if (changedAt < changedAfter) continue;
|
|
32430
|
-
|
|
32475
|
+
const first = fetchFileHead(path3, 8 * 1024).split("\n")[0] ?? "";
|
|
32476
|
+
const where = parseWhereFromJson(first);
|
|
32477
|
+
found.push({ ref, label: labelFor(ref) ?? "subagent", path: path3, directory: where.directory, branch: where.branch, changedAt });
|
|
32478
|
+
}
|
|
32479
|
+
return found;
|
|
32480
|
+
}
|
|
32481
|
+
function parseClaudeWorkflowFinishedAgents(journal) {
|
|
32482
|
+
const finished = /* @__PURE__ */ new Set();
|
|
32483
|
+
for (const line of journal.split("\n")) {
|
|
32484
|
+
if (!line.includes('"result"')) continue;
|
|
32431
32485
|
try {
|
|
32432
|
-
|
|
32486
|
+
const entry = JSON.parse(line);
|
|
32487
|
+
if (entry.type === "result" && typeof entry.agentId === "string") finished.add(entry.agentId);
|
|
32433
32488
|
} catch {
|
|
32434
32489
|
}
|
|
32435
|
-
const first = fetchFileHead(path3, 8 * 1024).split("\n")[0] ?? "";
|
|
32436
|
-
const where = parseWhereFromJson(first);
|
|
32437
|
-
found.push({ ref, label: label ?? "subagent", path: path3, directory: where.directory, branch: where.branch, changedAt });
|
|
32438
32490
|
}
|
|
32439
|
-
return
|
|
32491
|
+
return finished;
|
|
32492
|
+
}
|
|
32493
|
+
var WORKFLOW_RUN_LIVE = /* @__PURE__ */ new Set(["running", "pending", "queued", "starting"]);
|
|
32494
|
+
var WORKFLOW_AGENT_DONE = /* @__PURE__ */ new Set(["done", "error", "failed", "cancelled", "skipped"]);
|
|
32495
|
+
function parseClaudeWorkflowRecord(record2) {
|
|
32496
|
+
const parsed = { labels: /* @__PURE__ */ new Map(), finished: /* @__PURE__ */ new Set(), over: false };
|
|
32497
|
+
let run;
|
|
32498
|
+
try {
|
|
32499
|
+
run = JSON.parse(record2);
|
|
32500
|
+
} catch {
|
|
32501
|
+
return parsed;
|
|
32502
|
+
}
|
|
32503
|
+
parsed.over = typeof run.status === "string" && !WORKFLOW_RUN_LIVE.has(run.status);
|
|
32504
|
+
if (!Array.isArray(run.workflowProgress)) return parsed;
|
|
32505
|
+
const workflow = typeof run.workflowName === "string" && run.workflowName.trim() ? run.workflowName.trim() : null;
|
|
32506
|
+
for (const step of run.workflowProgress) {
|
|
32507
|
+
if (step?.type !== "workflow_agent" || typeof step.agentId !== "string") continue;
|
|
32508
|
+
if (typeof step.state === "string" && WORKFLOW_AGENT_DONE.has(step.state)) parsed.finished.add(step.agentId);
|
|
32509
|
+
if (typeof step.label === "string" && step.label.trim()) parsed.labels.set(step.agentId, workflow ? `${workflow} \u2014 ${step.label.trim()}` : step.label.trim());
|
|
32510
|
+
}
|
|
32511
|
+
return parsed;
|
|
32512
|
+
}
|
|
32513
|
+
function parseClaudeWorkerPromptLabel(line) {
|
|
32514
|
+
let entry;
|
|
32515
|
+
try {
|
|
32516
|
+
entry = JSON.parse(line);
|
|
32517
|
+
} catch {
|
|
32518
|
+
return null;
|
|
32519
|
+
}
|
|
32520
|
+
if (entry.type !== "user") return null;
|
|
32521
|
+
const content = entry.message?.content;
|
|
32522
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => typeof part?.text === "string" ? part.text : "").join(" ") : "";
|
|
32523
|
+
const first = text.split("\n").map((one) => one.trim()).find((one) => one !== "");
|
|
32524
|
+
return first ?? null;
|
|
32440
32525
|
}
|
|
32441
32526
|
function parseWhereFromJson(line) {
|
|
32442
32527
|
try {
|
|
@@ -32570,10 +32655,12 @@ var HarnessActivity = class {
|
|
|
32570
32655
|
const found = this.findWorkerFiles(now - WORKER_GONE_MS);
|
|
32571
32656
|
for (const file2 of found) {
|
|
32572
32657
|
const known = this.workers.get(file2.ref);
|
|
32573
|
-
if (known)
|
|
32574
|
-
|
|
32658
|
+
if (known) {
|
|
32659
|
+
Object.assign(known, { label: file2.label, directory: file2.directory, branch: file2.branch, changedAt: file2.changedAt });
|
|
32660
|
+
if (file2.finished) known.finished = true;
|
|
32661
|
+
} else {
|
|
32575
32662
|
const tail = new FileTail(file2.path, "end");
|
|
32576
|
-
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 });
|
|
32663
|
+
this.workers.set(file2.ref, { ...file2, since: new Date(file2.changedAt).toISOString(), model: fetchModelFromFileTail(file2.path, this.parsers.parseModel), doing: null, finished: file2.finished === true || fetchTurnStateFromFileTail(file2.path, this.parsers.parseTurnState) === "finished", tail });
|
|
32577
32664
|
}
|
|
32578
32665
|
}
|
|
32579
32666
|
const live = new Set(found.map((file2) => file2.ref));
|
|
@@ -33883,9 +33970,22 @@ function findChromePath() {
|
|
|
33883
33970
|
if (override) return existsSync16(override) ? override : null;
|
|
33884
33971
|
return (CHROME_PATHS[process.platform] ?? []).find((path3) => existsSync16(path3)) ?? null;
|
|
33885
33972
|
}
|
|
33973
|
+
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";
|
|
33974
|
+
var chromeStartFailure = null;
|
|
33975
|
+
var CHROME_FAILURE_TTL_MS = 10 * 6e4;
|
|
33976
|
+
var sweepChromeStartFailure = () => {
|
|
33977
|
+
if (chromeStartFailure && Date.now() - chromeStartFailure.at > CHROME_FAILURE_TTL_MS) chromeStartFailure = null;
|
|
33978
|
+
};
|
|
33979
|
+
function canStartChrome() {
|
|
33980
|
+
sweepChromeStartFailure();
|
|
33981
|
+
return chromeStartFailure === null && findChromePath() !== null;
|
|
33982
|
+
}
|
|
33886
33983
|
function requireChromePath() {
|
|
33984
|
+
sweepChromeStartFailure();
|
|
33985
|
+
const failure2 = chromeStartFailure?.reason;
|
|
33986
|
+
if (failure2) throw new CliError(`Screenshots need Chrome, and it did not start here: ${failure2}`, SPLAT_NO_CHROME_HINT, "no_chrome");
|
|
33887
33987
|
const path3 = findChromePath();
|
|
33888
|
-
if (!path3) throw new CliError("
|
|
33988
|
+
if (!path3) throw new CliError("Screenshots need Chrome, and none was found", SPLAT_NO_CHROME_HINT, "no_chrome");
|
|
33889
33989
|
return path3;
|
|
33890
33990
|
}
|
|
33891
33991
|
var buildSplatChromeProfilePath = () => join19(dirname12(buildConfigPath()), "splat", "chrome-profile");
|
|
@@ -33938,9 +34038,12 @@ var ChromeBrowser = class _ChromeBrowser {
|
|
|
33938
34038
|
signal?.throwIfAborted();
|
|
33939
34039
|
await browser.send("Browser.getVersion", {}, void 0, 2e4);
|
|
33940
34040
|
} catch (error62) {
|
|
34041
|
+
const cannotRun = child.pid === void 0 || child.exitCode !== null || child.signalCode !== null;
|
|
33941
34042
|
await browser.close();
|
|
33942
34043
|
signal?.throwIfAborted();
|
|
33943
|
-
|
|
34044
|
+
const reason = error62 instanceof Error ? error62.message : String(error62);
|
|
34045
|
+
if (cannotRun) chromeStartFailure = { reason, at: Date.now() };
|
|
34046
|
+
throw new CliError(`Screenshots need Chrome, and it did not start here: ${reason}`, SPLAT_NO_CHROME_HINT, "no_chrome");
|
|
33944
34047
|
} finally {
|
|
33945
34048
|
signal?.removeEventListener("abort", stopBrowser);
|
|
33946
34049
|
}
|
|
@@ -34762,12 +34865,13 @@ async function isSplatProxyRunning(proxyPort = SPLAT_PROXY_PORT) {
|
|
|
34762
34865
|
async function startSplatProxy(options2) {
|
|
34763
34866
|
const proxyPort = options2.proxyPort ?? SPLAT_PROXY_PORT;
|
|
34764
34867
|
const frameAncestors = [options2.appOrigin, "http://localhost:*", "http://127.0.0.1:*"].join(" ");
|
|
34868
|
+
let rendererId = options2.rendererId ?? null;
|
|
34765
34869
|
const connections = /* @__PURE__ */ new Set();
|
|
34766
34870
|
const server = createServer((request, response) => {
|
|
34767
34871
|
const host = request.headers.host ?? "";
|
|
34768
34872
|
if (request.url === HEALTH_PATH && (host === `127.0.0.1:${proxyPort}` || host === `localhost:${proxyPort}`)) {
|
|
34769
|
-
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
34770
|
-
response.end(JSON.stringify({ splat: true }));
|
|
34873
|
+
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store", "access-control-allow-origin": options2.appOrigin, vary: "origin" });
|
|
34874
|
+
response.end(JSON.stringify({ splat: true, rendererId }));
|
|
34771
34875
|
return;
|
|
34772
34876
|
}
|
|
34773
34877
|
const match = HOST.exec(host);
|
|
@@ -34777,7 +34881,7 @@ async function startSplatProxy(options2) {
|
|
|
34777
34881
|
response.end("Splat's live-frame proxy answers only <port>.localhost");
|
|
34778
34882
|
return;
|
|
34779
34883
|
}
|
|
34780
|
-
forwardSplatRequest(request, response, port, proxyPort, frameAncestors, options2.appOrigin,
|
|
34884
|
+
forwardSplatRequest(request, response, port, proxyPort, frameAncestors, options2.appOrigin, rendererId);
|
|
34781
34885
|
});
|
|
34782
34886
|
server.on("connection", (socket) => {
|
|
34783
34887
|
connections.add(socket);
|
|
@@ -34796,11 +34900,14 @@ async function startSplatProxy(options2) {
|
|
|
34796
34900
|
server.once("error", () => resolve6(false));
|
|
34797
34901
|
server.listen(proxyPort, "127.0.0.1", () => resolve6(true));
|
|
34798
34902
|
});
|
|
34799
|
-
if (!listening) return await isSplatProxyRunning(proxyPort) ? { port: proxyPort, owned: false, stop: async () => void 0 } : null;
|
|
34903
|
+
if (!listening) return await isSplatProxyRunning(proxyPort) ? { port: proxyPort, owned: false, setRendererId: () => void 0, stop: async () => void 0 } : null;
|
|
34800
34904
|
server.unref();
|
|
34801
34905
|
return {
|
|
34802
34906
|
port: proxyPort,
|
|
34803
34907
|
owned: true,
|
|
34908
|
+
setRendererId: (id) => {
|
|
34909
|
+
rendererId = id;
|
|
34910
|
+
},
|
|
34804
34911
|
stop: () => new Promise((resolve6) => {
|
|
34805
34912
|
for (const socket of connections) socket.destroy();
|
|
34806
34913
|
server.closeAllConnections();
|
|
@@ -34902,7 +35009,6 @@ var SplatPublisherConnection = class {
|
|
|
34902
35009
|
streams = /* @__PURE__ */ new Map();
|
|
34903
35010
|
upstreamSockets = /* @__PURE__ */ new Map();
|
|
34904
35011
|
timers = /* @__PURE__ */ new Set();
|
|
34905
|
-
chrome = findChromePath() !== null;
|
|
34906
35012
|
appOrigin;
|
|
34907
35013
|
localOrigin;
|
|
34908
35014
|
capture = null;
|
|
@@ -34943,8 +35049,16 @@ var SplatPublisherConnection = class {
|
|
|
34943
35049
|
try {
|
|
34944
35050
|
let path3;
|
|
34945
35051
|
try {
|
|
34946
|
-
path3 = (await this.api.post(`${this.sourcePath}/relay/ticket`, ApiResponses.SplatRelayTicket, {})).path;
|
|
35052
|
+
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;
|
|
34947
35053
|
} catch (error62) {
|
|
35054
|
+
if (this.options.rendererId && error62 instanceof CliError && ["unauthenticated", "forbidden", "bad_request"].includes(error62.code)) {
|
|
35055
|
+
this.stop(`Live through Slopware stopped for :${this.options.port}: ${error62.message}`);
|
|
35056
|
+
return;
|
|
35057
|
+
}
|
|
35058
|
+
if (this.options.rendererId) {
|
|
35059
|
+
this.scheduleReconnect();
|
|
35060
|
+
return;
|
|
35061
|
+
}
|
|
34948
35062
|
if (error62 instanceof CliError && ["unauthenticated", "forbidden", "bad_request"].includes(error62.code)) {
|
|
34949
35063
|
this.stop(`Publishing stopped: ${error62.message}`);
|
|
34950
35064
|
return;
|
|
@@ -34993,7 +35107,7 @@ var SplatPublisherConnection = class {
|
|
|
34993
35107
|
clearTimeout(helloTimer);
|
|
34994
35108
|
this.attempt = 0;
|
|
34995
35109
|
this.generation = down.data.generation;
|
|
34996
|
-
this.log(`Publishing :${this.options.port} as ${this.options.sourceId} (connection ${this.generation})`);
|
|
35110
|
+
this.log(this.options.rendererId ? `Live through Slopware: :${this.options.port}` : `Publishing :${this.options.port} as ${this.options.sourceId} (connection ${this.generation})`);
|
|
34997
35111
|
void this.refreshState();
|
|
34998
35112
|
return;
|
|
34999
35113
|
}
|
|
@@ -35010,7 +35124,7 @@ var SplatPublisherConnection = class {
|
|
|
35010
35124
|
this.releaseStreams();
|
|
35011
35125
|
if (this.stopped) return;
|
|
35012
35126
|
const reason = ended ?? event.reason;
|
|
35013
|
-
if (reason === "replaced") return this.stop("Another `slop splat serve` of this publication took over");
|
|
35127
|
+
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");
|
|
35014
35128
|
if (reason === "deleted") return this.stop("This source was removed from the canvas");
|
|
35015
35129
|
if (reason === "revoked" || event.code === 1008) return this.stop("Publishing stopped: access revoked");
|
|
35016
35130
|
this.log(`Relay connection closed (${event.code}${event.reason ? ` ${event.reason}` : ""}); reconnecting`);
|
|
@@ -35055,7 +35169,7 @@ var SplatPublisherConnection = class {
|
|
|
35055
35169
|
commit: this.gitState.commit,
|
|
35056
35170
|
dirty: this.gitState.dirty,
|
|
35057
35171
|
label: this.options.label?.slice(0, 120) ?? null,
|
|
35058
|
-
chrome:
|
|
35172
|
+
chrome: canStartChrome(),
|
|
35059
35173
|
busy: this.capture !== null
|
|
35060
35174
|
});
|
|
35061
35175
|
}
|
|
@@ -35177,7 +35291,7 @@ var SplatPublisherConnection = class {
|
|
|
35177
35291
|
if (decoder) source = source.pipe(decoder);
|
|
35178
35292
|
source = source.pipe(createSplatOriginRewriteStream(this.options.port, origin));
|
|
35179
35293
|
}
|
|
35180
|
-
if (html) source = source.pipe(createSplatHtmlStream(buildSplatFrameScript(nonce, this.appOrigin, this.options.sourceId)));
|
|
35294
|
+
if (html) source = source.pipe(createSplatHtmlStream(buildSplatFrameScript(nonce, this.appOrigin, this.options.rendererId ?? this.options.sourceId)));
|
|
35181
35295
|
stream.source = source;
|
|
35182
35296
|
const status = response.statusCode ?? 502;
|
|
35183
35297
|
let holding = !type.includes("text/event-stream");
|
|
@@ -35379,7 +35493,7 @@ var SplatPublisherConnection = class {
|
|
|
35379
35493
|
}
|
|
35380
35494
|
}
|
|
35381
35495
|
async claimScheduledRound() {
|
|
35382
|
-
if (this.capture || !
|
|
35496
|
+
if (this.capture || !canStartChrome() || !this.socket) return;
|
|
35383
35497
|
let claim2;
|
|
35384
35498
|
try {
|
|
35385
35499
|
claim2 = await this.api.post(`${this.sourcePath}/auto-capture`, ApiResponses.SplatAutoCaptureClaim, {});
|
|
@@ -35408,6 +35522,7 @@ var PING_EVERY_MS3 = 25e3;
|
|
|
35408
35522
|
var PONG_WITHIN_MS3 = 1e4;
|
|
35409
35523
|
var RESCAN_EVERY_MS = 3e4;
|
|
35410
35524
|
var STANDBY_POLL_MS2 = 3e4;
|
|
35525
|
+
var LIVE_RELAY_RESTART_MS = 6e4;
|
|
35411
35526
|
var SCHEDULE_POLL_MS2 = 5 * 6e4;
|
|
35412
35527
|
var SplatRendererConnection = class {
|
|
35413
35528
|
constructor(api, projectId, options2 = {}) {
|
|
@@ -35425,7 +35540,8 @@ var SplatRendererConnection = class {
|
|
|
35425
35540
|
origins = [];
|
|
35426
35541
|
proxy = null;
|
|
35427
35542
|
proxySync = null;
|
|
35428
|
-
|
|
35543
|
+
/** Whether the room was last told this renderer can take screenshots: a Chrome that stops starting is announced. */
|
|
35544
|
+
announcedChrome = null;
|
|
35429
35545
|
timers = /* @__PURE__ */ new Set();
|
|
35430
35546
|
starting = null;
|
|
35431
35547
|
connecting = false;
|
|
@@ -35435,6 +35551,8 @@ var SplatRendererConnection = class {
|
|
|
35435
35551
|
queue = [];
|
|
35436
35552
|
working = false;
|
|
35437
35553
|
standingBy = false;
|
|
35554
|
+
/** Each dev server served through Slopware, by port. */
|
|
35555
|
+
liveRelays = /* @__PURE__ */ new Map();
|
|
35438
35556
|
get id() {
|
|
35439
35557
|
return this.rendererId;
|
|
35440
35558
|
}
|
|
@@ -35482,6 +35600,7 @@ var SplatRendererConnection = class {
|
|
|
35482
35600
|
return;
|
|
35483
35601
|
}
|
|
35484
35602
|
this.proxy = proxy;
|
|
35603
|
+
if (proxy && this.rendererId) proxy.setRendererId(this.rendererId);
|
|
35485
35604
|
if (proxy) this.log(proxy.owned ? `Live frames: proxy on http://<port>.localhost:${proxy.port}` : `Live frames: sharing the proxy already on :${proxy.port}`);
|
|
35486
35605
|
}
|
|
35487
35606
|
/** Looks again at the dev servers now (after a port is pinned or unpinned, say), and says so if they changed. */
|
|
@@ -35493,8 +35612,9 @@ var SplatRendererConnection = class {
|
|
|
35493
35612
|
if (this.stopped) return this.origins;
|
|
35494
35613
|
const changed = JSON.stringify(next) !== JSON.stringify(this.origins);
|
|
35495
35614
|
this.origins = next;
|
|
35615
|
+
if (changed) this.syncLiveRelays();
|
|
35496
35616
|
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>`");
|
|
35497
|
-
if (announce && (changed || previousProxyPort !== (this.proxy?.port ?? null))) this.sendState();
|
|
35617
|
+
if (announce && (changed || previousProxyPort !== (this.proxy?.port ?? null) || this.announcedChrome !== canStartChrome())) this.sendState();
|
|
35498
35618
|
return next;
|
|
35499
35619
|
}
|
|
35500
35620
|
repeat(ms, run) {
|
|
@@ -35518,7 +35638,9 @@ var SplatRendererConnection = class {
|
|
|
35518
35638
|
if (!this.stopped && this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(message));
|
|
35519
35639
|
}
|
|
35520
35640
|
sendState() {
|
|
35521
|
-
|
|
35641
|
+
const chrome = canStartChrome();
|
|
35642
|
+
if (this.socket?.readyState === WebSocket.OPEN) this.announcedChrome = chrome;
|
|
35643
|
+
this.sendUp({ type: "state", origins: this.origins, proxyPort: this.proxy?.port ?? null, chrome, busy: this.working });
|
|
35522
35644
|
}
|
|
35523
35645
|
async connect() {
|
|
35524
35646
|
if (this.stopped || this.standingBy || this.connecting || this.socket) return;
|
|
@@ -35526,7 +35648,7 @@ var SplatRendererConnection = class {
|
|
|
35526
35648
|
try {
|
|
35527
35649
|
let path3;
|
|
35528
35650
|
try {
|
|
35529
|
-
const ticket = await this.api.post(`/projects/${this.projectId}/splat/renderer/ticket`, ApiResponses.ConnectTicket, { cliVersion: "0.
|
|
35651
|
+
const ticket = await this.api.post(`/projects/${this.projectId}/splat/renderer/ticket`, ApiResponses.ConnectTicket, { cliVersion: "0.10.1", onlyMe: this.options.onlyMe ?? false, agent: this.options.agent ?? null, capabilities: ["workspaces", ...this.options.liveRelay === false ? [] : [SPLAT_RENDERER_LIVE_RELAY]] });
|
|
35530
35652
|
path3 = ticket.path;
|
|
35531
35653
|
} catch (error62) {
|
|
35532
35654
|
if (error62 instanceof CliError && ["unauthenticated", "not_found", "forbidden", "bad_request"].includes(error62.code)) {
|
|
@@ -35609,7 +35731,9 @@ var SplatRendererConnection = class {
|
|
|
35609
35731
|
this.attempt = 0;
|
|
35610
35732
|
this.rendererId = push.rendererId;
|
|
35611
35733
|
this.log(`Rendering for the canvas as ${push.rendererId}`);
|
|
35734
|
+
this.proxy?.setRendererId(push.rendererId);
|
|
35612
35735
|
this.sendState();
|
|
35736
|
+
this.syncLiveRelays();
|
|
35613
35737
|
} else if (push.type === "capture") {
|
|
35614
35738
|
if (this.standingBy || this.working || this.queue.length) {
|
|
35615
35739
|
this.sendUp({ type: "finished", requestId: push.requestId, failed: ["Renderer is busy; retry when its current capture finishes"] });
|
|
@@ -35673,7 +35797,7 @@ var SplatRendererConnection = class {
|
|
|
35673
35797
|
* one with a dev server on the default branch), and if this one has it, captures every frame. Quiet when idle.
|
|
35674
35798
|
*/
|
|
35675
35799
|
async claimScheduledRound() {
|
|
35676
|
-
if (this.stopped || this.standingBy || this.working || this.queue.length > 0 || !
|
|
35800
|
+
if (this.stopped || this.standingBy || this.working || this.queue.length > 0 || !canStartChrome() || this.origins.length === 0) return;
|
|
35677
35801
|
await this.rescanOrigins(false);
|
|
35678
35802
|
if (this.stopped || this.standingBy || this.working || this.queue.length > 0 || this.origins.length === 0) return;
|
|
35679
35803
|
let claim2;
|
|
@@ -35712,6 +35836,7 @@ var SplatRendererConnection = class {
|
|
|
35712
35836
|
standBy() {
|
|
35713
35837
|
if (this.standingBy) return;
|
|
35714
35838
|
this.standingBy = true;
|
|
35839
|
+
this.syncLiveRelays();
|
|
35715
35840
|
this.queue = [];
|
|
35716
35841
|
this.captureAbort?.abort();
|
|
35717
35842
|
const check2 = async () => {
|
|
@@ -35722,6 +35847,7 @@ var SplatRendererConnection = class {
|
|
|
35722
35847
|
if (!renderers.some((renderer) => renderer.id === this.rendererId)) {
|
|
35723
35848
|
this.standingBy = false;
|
|
35724
35849
|
await this.connect();
|
|
35850
|
+
this.syncLiveRelays();
|
|
35725
35851
|
return;
|
|
35726
35852
|
}
|
|
35727
35853
|
} catch {
|
|
@@ -35745,6 +35871,31 @@ var SplatRendererConnection = class {
|
|
|
35745
35871
|
if (socket && socket.readyState <= WebSocket.OPEN) socket.close(1e3, reason);
|
|
35746
35872
|
void this.proxy?.stop();
|
|
35747
35873
|
this.proxy = null;
|
|
35874
|
+
this.syncLiveRelays();
|
|
35875
|
+
}
|
|
35876
|
+
/**
|
|
35877
|
+
* One relay connection per dev server (up to a few), while this renderer is the one connected: the shared source
|
|
35878
|
+
* live through Slopware, for this renderer's person on any computer. Started, and stopped when a server goes.
|
|
35879
|
+
*/
|
|
35880
|
+
syncLiveRelays() {
|
|
35881
|
+
const active = !this.stopped && !this.standingBy && this.options.liveRelay !== false && this.rendererId !== null;
|
|
35882
|
+
const wanted = active ? listSplatRendererRelayPorts(this.origins) : [];
|
|
35883
|
+
for (const [port, relay] of this.liveRelays) {
|
|
35884
|
+
if (wanted.includes(port)) continue;
|
|
35885
|
+
this.liveRelays.delete(port);
|
|
35886
|
+
relay.stop("The dev server is gone");
|
|
35887
|
+
}
|
|
35888
|
+
for (const port of wanted) {
|
|
35889
|
+
if (this.liveRelays.has(port)) continue;
|
|
35890
|
+
const relay = new SplatPublisherConnection(this.api, { projectId: this.projectId, workspaceId: "", sourceId: "", rendererId: this.rendererId, port, cwd: this.options.cwd, log: (line) => this.log(line) });
|
|
35891
|
+
this.liveRelays.set(port, relay);
|
|
35892
|
+
void relay.run().then((reason) => {
|
|
35893
|
+
if (this.liveRelays.get(port) !== relay) return;
|
|
35894
|
+
this.liveRelays.delete(port);
|
|
35895
|
+
this.log(reason);
|
|
35896
|
+
this.scheduleRendererCall(LIVE_RELAY_RESTART_MS, () => this.syncLiveRelays());
|
|
35897
|
+
});
|
|
35898
|
+
}
|
|
35748
35899
|
}
|
|
35749
35900
|
};
|
|
35750
35901
|
|
|
@@ -35755,7 +35906,7 @@ var formatAge = (at) => {
|
|
|
35755
35906
|
const minutes = Math.round((Date.now() - Date.parse(at)) / 6e4);
|
|
35756
35907
|
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`;
|
|
35757
35908
|
};
|
|
35758
|
-
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
|
|
35909
|
+
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}` : ""}`;
|
|
35759
35910
|
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}` : ""}`;
|
|
35760
35911
|
function formatSplatCanvas(canvas) {
|
|
35761
35912
|
if (canvas.routes.length === 0) return "No routes on the canvas yet. Add some: slop splat route add / /settings";
|
|
@@ -35895,7 +36046,7 @@ ${published.map((source2) => ` ${formatSplatSourceLine(source2)}`).join("\n")}`
|
|
|
35895
36046
|
const result = await api.post(`${target.base}/routes`, ApiResponses.SplatRoutesResult, { routes: paths2.slice(0, 50).map((path3) => ({ path: path3 })) });
|
|
35896
36047
|
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(", ")}` : ""}`);
|
|
35897
36048
|
});
|
|
35898
|
-
registerProjectOption(splat.command("capture [paths...]")).description("
|
|
36049
|
+
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) => {
|
|
35899
36050
|
const { api, project, target } = await resolveSplatCommandTarget(options2);
|
|
35900
36051
|
const canvas = await fetchSplatCanvas(api, target);
|
|
35901
36052
|
await ensureSplatDefaultBranch(api, target.base, canvas);
|
|
@@ -35939,6 +36090,7 @@ ${published.map((source2) => ` ${formatSplatSourceLine(source2)}`).join("\n")}`
|
|
|
35939
36090
|
await renderer.start();
|
|
35940
36091
|
process.stderr.write(`Rendering ${project.key} for ${buildSplatCanvasLink(api.baseUrl, target)}. Ctrl-C to stop.
|
|
35941
36092
|
`);
|
|
36093
|
+
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");
|
|
35942
36094
|
const keepAlive = setInterval(() => void 0, 6e4);
|
|
35943
36095
|
await new Promise((resolve6) => {
|
|
35944
36096
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => resolve6());
|
|
@@ -36151,6 +36303,7 @@ async function servePublication(options2) {
|
|
|
36151
36303
|
`);
|
|
36152
36304
|
process.stderr.write(`${buildSplatCanvasLink(api.baseUrl, target)}
|
|
36153
36305
|
`);
|
|
36306
|
+
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");
|
|
36154
36307
|
const stopped = new Promise((resolve6) => {
|
|
36155
36308
|
for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => resolve6("Stopped publishing."));
|
|
36156
36309
|
});
|
|
@@ -36232,7 +36385,25 @@ function createSplatTools(context2) {
|
|
|
36232
36385
|
if (items.length === 0) throw new Error("Nothing to capture: add routes first (action add)");
|
|
36233
36386
|
const limit = Math.min(Number(args2.limit ?? 3), 8);
|
|
36234
36387
|
const tilesPerFrame = args2.full ? 4 : 1;
|
|
36235
|
-
const
|
|
36388
|
+
const withoutScreenshots = async (reason) => {
|
|
36389
|
+
const renderer = await context2.ensureRenderer().catch(() => null);
|
|
36390
|
+
const proxyPort = renderer?.proxyPort ?? null;
|
|
36391
|
+
const paths2 = items.map((item) => item.path).filter((path3, index, all) => all.indexOf(path3) === index).join(", ");
|
|
36392
|
+
return [
|
|
36393
|
+
`No screenshots from this machine (${reason}), so nothing was captured and the team's canvas did not change.`,
|
|
36394
|
+
`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.`,
|
|
36395
|
+
...proxyPort ? [`Live URL: ${buildSplatLiveFrameUrl(proxyPort, origin.port, items[0].path)}`] : [],
|
|
36396
|
+
`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).`
|
|
36397
|
+
].join("\n");
|
|
36398
|
+
};
|
|
36399
|
+
if (!canStartChrome()) return withoutScreenshots("no Chrome found");
|
|
36400
|
+
let outcome;
|
|
36401
|
+
try {
|
|
36402
|
+
outcome = await captureSplatFrames(api, project.id, origin, items, { agent: context2.agentName, tiles: tilesPerFrame, workspaceId: target.workspace?.id ?? null });
|
|
36403
|
+
} catch (error62) {
|
|
36404
|
+
if (error62 instanceof CliError && error62.code === "no_chrome") return withoutScreenshots("Chrome did not start here");
|
|
36405
|
+
throw error62;
|
|
36406
|
+
}
|
|
36236
36407
|
const shown = outcome.captured.slice(0, limit);
|
|
36237
36408
|
output2.attachImages(shown.flatMap((captured) => captured.tiles));
|
|
36238
36409
|
const lines2 = [
|
|
@@ -36252,9 +36423,8 @@ function createSplatTools(context2) {
|
|
|
36252
36423
|
const lines2 = [];
|
|
36253
36424
|
for (const capture of captures) {
|
|
36254
36425
|
const viewport = canvas.settings.viewports.find((candidate) => candidate.key === capture.viewportKey);
|
|
36255
|
-
const
|
|
36256
|
-
|
|
36257
|
-
output2.attachImages(images);
|
|
36426
|
+
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;
|
|
36427
|
+
output2.attachImages(tiles ?? [{ data: Buffer.from(await api.getBinary(capture.thumbUrl.replace(/^\/api/, ""))).toString("base64"), mimeType: "image/webp" }]);
|
|
36258
36428
|
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}`);
|
|
36259
36429
|
}
|
|
36260
36430
|
return lines2.join("\n");
|
|
@@ -37560,17 +37730,17 @@ function hasHarnessSetup() {
|
|
|
37560
37730
|
function syncMachineSetupAfterUpdate() {
|
|
37561
37731
|
const nothing = { skills: [], retired: [], reinstalling: false };
|
|
37562
37732
|
try {
|
|
37563
|
-
if (!isUpdateCheckEnabled() || fetchMachineSetupState()?.version === "0.
|
|
37733
|
+
if (!isUpdateCheckEnabled() || fetchMachineSetupState()?.version === "0.10.1") return nothing;
|
|
37564
37734
|
const lock = claimFileLock(buildMachineSetupLockDirectory(), { staleAfterMs: 6e4 });
|
|
37565
37735
|
if (!lock) return nothing;
|
|
37566
37736
|
try {
|
|
37567
37737
|
const state = fetchMachineSetupState();
|
|
37568
|
-
if (state?.version === "0.
|
|
37738
|
+
if (state?.version === "0.10.1") return nothing;
|
|
37569
37739
|
const skills = hasInstalledSkills() ? installSkills() : [];
|
|
37570
37740
|
const shipped = Object.keys(SKILLS);
|
|
37571
37741
|
const retired = deleteRetiredSkills((state?.skills ?? []).filter((name) => !shipped.includes(name)));
|
|
37572
37742
|
const setupRevision = state?.setupRevision ?? (hasHarnessSetup() ? UNRECORDED_SETUP_REVISION : null);
|
|
37573
|
-
persistMachineSetupState({ version: "0.
|
|
37743
|
+
persistMachineSetupState({ version: "0.10.1", setupRevision, skills: hasInstalledSkills() ? shipped : [] });
|
|
37574
37744
|
const reinstalling = setupRevision !== null && setupRevision < SETUP_REVISION && runMcpInstallInBackground();
|
|
37575
37745
|
return { skills, retired, reinstalling };
|
|
37576
37746
|
} finally {
|
|
@@ -37737,7 +37907,7 @@ async function trustCodexHooks(codexHome) {
|
|
|
37737
37907
|
const sendNotification = (method, params) => server.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}
|
|
37738
37908
|
`);
|
|
37739
37909
|
try {
|
|
37740
|
-
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.
|
|
37910
|
+
await callCodexMethod("initialize", { clientInfo: { name: "slop", version: "0.10.1" }, capabilities: { experimentalApi: true } });
|
|
37741
37911
|
sendNotification("initialized", {});
|
|
37742
37912
|
const listed = await callCodexMethod("hooks/list", { cwds: [process.cwd()] });
|
|
37743
37913
|
const ours = listed.data.flatMap((entry) => entry.hooks).filter((hook) => hook.command?.startsWith("slop hook "));
|
|
@@ -38022,7 +38192,7 @@ var options = optionEnd < 0 ? args : args.slice(0, optionEnd);
|
|
|
38022
38192
|
updateJsonMode(options.includes("--json"));
|
|
38023
38193
|
syncMachineSetupAfterUpdate();
|
|
38024
38194
|
if (!options.includes("--background") && !options.includes("--child")) scheduleBackgroundUpdate();
|
|
38025
|
-
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.
|
|
38195
|
+
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.1").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").configureOutput({
|
|
38026
38196
|
outputError: (message, write) => {
|
|
38027
38197
|
if (!isJsonMode()) write(message);
|
|
38028
38198
|
},
|
|
@@ -38039,7 +38209,7 @@ var sessionCommand = registerSessionCommands(program2);
|
|
|
38039
38209
|
registerThreadCommands(program2, taskCommand);
|
|
38040
38210
|
registerOperationCommands(program2, taskCommand, sessionCommand);
|
|
38041
38211
|
registerSplatCommands(program2);
|
|
38042
|
-
registerMcpCommands(program2, "0.
|
|
38212
|
+
registerMcpCommands(program2, "0.10.1");
|
|
38043
38213
|
registerGuideCommand(program2);
|
|
38044
38214
|
registerSkillCommands(program2);
|
|
38045
38215
|
registerUpdateCommand(program2);
|