slopwaresystems 0.1.1 → 0.2.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/README.md +1 -1
- package/dist/index.js +217 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The [Slopware Tasks](https://tasks.slopware.systems) board from the terminal. Bu
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm i -g slopwaresystems
|
|
7
|
-
slop login # approve the machine in your browser
|
|
7
|
+
slop login # approve the machine in your browser (on a VM or over SSH: open the printed link on any device)
|
|
8
8
|
cd your-repo && slop link WEB
|
|
9
9
|
slop task create "Retry storms on 429" -s high -d -
|
|
10
10
|
slop skills install # puts the agent skills into .claude/skills/
|
package/dist/index.js
CHANGED
|
@@ -3379,16 +3379,16 @@ import { homedir } from "os";
|
|
|
3379
3379
|
import { join } from "path";
|
|
3380
3380
|
|
|
3381
3381
|
// skills/slop-cli.md
|
|
3382
|
-
var slop_cli_default = '---\nname: slop-cli\ndescription: How to use the slop CLI to read and update the task board this repository is linked to. Load when asked to create, update, plan, comment on or finish tasks.\n---\n\n# slop: the board from the terminal\n\nThe repository you are in is linked to a project on Slopware Tasks. Everything a person can do on the board, you can do with `slop`. Add `--json` to any command for machine-readable output.\n\n## Orientation\n\n```bash\nslop status # which project this checkout is linked to\nslop statuses # the board\'s columns (key, label, category)\nslop members # who tasks can be assigned to\nslop task list [--all] [--status <column>] [--assignee me]\nslop task show <KEY> # title, status, severity, assignee, links, plan, description\nslop task history <KEY> # everything that happened to it, in order\n```\n\nTask keys look like `WEB-12`; the bare number works inside the linked project.\n\n## Creating and changing tasks\n\n```bash\nslop task create "Short imperative title" -d "What, where, why" -s <low|medium|high|critical> [--status <column>] [-a <email|me|none>]\nslop task update <KEY> [--title \u2026] [-d \u2026] [-s \u2026] [--status \u2026] [-a \u2026]\nslop task move <KEY> "<column label or key>"\nslop task start <KEY> # first "in progress" column\nslop task done <KEY> # first "done" column\nslop task assign <KEY> <email|me|none>\nslop task link <KEY> <url> # a PR, a design, a log; GitHub PRs are recognised\nslop task notes <KEY> [text|-] # the scratchpad under the links: read it, or replace it whole\n\n## Sessions (see the `slop-sessions` skill)\n\n```bash\nslop session start <KEY> # claim: assign me, move to in progress\nslop session wait <KEY> # be live until someone says something; run in the background\nslop session end <KEY>\nslop sessions # who is live on what\nslop task ask <KEY> "\u2026" #
|
|
3382
|
+
var slop_cli_default = '---\nname: slop-cli\ndescription: How to use the slop CLI to read and update the task board this repository is linked to. Load when asked to create, update, plan, comment on or finish tasks.\n---\n\n# slop: the board from the terminal\n\nThe repository you are in is linked to a project on Slopware Tasks. Everything a person can do on the board, you can do with `slop`. Add `--json` to any command for machine-readable output.\n\n## Orientation\n\n```bash\nslop status # which project this checkout is linked to\nslop statuses # the board\'s columns (key, label, category)\nslop members # who tasks can be assigned to\nslop task list [--all] [--status <column>] [--assignee me]\nslop task show <KEY> # title, status, severity, assignee, links, plan, description\nslop task history <KEY> # everything that happened to it, in order\n```\n\nTask keys look like `WEB-12`; the bare number works inside the linked project.\n\nA link belongs to the clone: every git worktree of it (including agent sandboxes made with `git worktree add`) is linked without doing anything. On a machine with no browser, `slop login` prints a link and a code; open it on any device.\n\n## Creating and changing tasks\n\n```bash\nslop task create "Short imperative title" -d "What, where, why" -s <low|medium|high|critical> [--status <column>] [-a <email|me|none>]\nslop task update <KEY> [--title \u2026] [-d \u2026] [-s \u2026] [--status \u2026] [-a \u2026]\nslop task move <KEY> "<column label or key>"\nslop task start <KEY> # first "in progress" column\nslop task done <KEY> # first "done" column\nslop task assign <KEY> <email|me|none>\nslop task link <KEY> <url> # a PR, a design, a log; GitHub PRs are recognised\nslop task notes <KEY> [text|-] # the scratchpad under the links: read it, or replace it whole\n\n## Sessions (see the `slop-sessions` skill)\n\n```bash\nslop session start <KEY> # claim: assign me, move to in progress\nslop session wait <KEY> # be live until someone says something; run in the background\nslop session end <KEY>\nslop sessions # who is live on what\nslop threads [--task KEY] [--general] [--unread] # the project\'s threads\nslop thread show <id> | reply <id> "\u2026" | new "Subject" -m "\u2026" [--task KEY]\nslop task ask <KEY> "\u2026" # a new thread about the task, to whoever is live on it\n```\nslop task archive <KEY> # off the board, history kept\n```\n\nDescriptions are Markdown and are shown in full. Long text: `-d -` reads stdin.\n\n## Plans (see the `slop-planning` skill)\n\n```bash\nslop task items <KEY> # the plan, numbered, with notes\nslop task plan <KEY> - <<\'EOF\' # one item per line, appended in order\nInventory the Intercom data model\nWrite the export script\nEOF\nslop task item add <KEY> "title" [-n "note"]\nslop task item done <KEY> <n> # by number from `items`\nslop task item reopen <KEY> <n>\nslop task item note <KEY> <n> "what is true now"\nslop task item rename <KEY> <n> "new title"\nslop task item remove <KEY> <n>\n```\n\n## Projects\n\n```bash\nslop projects\nslop project create "Name" [--key KEY]\nslop project update [--name \u2026] [--description \u2026]\nslop project status add "In review" -c in_progress # columns: add | rename | remove | order\n```\n\n## Rules of the road\n\n- Do not delete tasks you did not create. Archive finished work instead.\n- Keep titles short and imperative; put the reasoning in the description or the plan\'s notes. There is no chat: the plan\'s notes are the running state, the description holds decisions.\n- When you open a pull request for a task, link it.\n';
|
|
3383
3383
|
|
|
3384
3384
|
// skills/slop-planning.md
|
|
3385
3385
|
var slop_planning_default = '---\nname: slop-planning\ndescription: How to break long or multi-session work into a plan on its task, keep each item\'s note current, and pick up where you left off. Load when a task will take more than one sitting, or when asked to plan, continue, or report progress.\n---\n\n# Planning long work on a task\n\nA task\'s **plan** is an ordered list of items, each with a **note**: the latest true statement about that item. The plan plus the task\'s timeline is your memory between sessions. Write to it as you work, not at the end.\n\n## Starting\n\n1. Read the task: `slop task show <KEY>`. If it has a plan, read the notes; they are what the last session knew.\n2. If there is no plan, write one. Items are concrete, checkable, and ordered by what unlocks what. Eight to fifteen is typical; more means the task should be split.\n\n```bash\nslop task plan <KEY> - <<\'EOF\'\nInventory every Intercom object we rely on (conversations, users, tags)\nDecide the target schema for the support agent\nWrite and test the export script against a sample\nRun the full export; record counts\nImport into the support agent; verify counts match\nSwitch inbound routing; keep Intercom read-only for a week\nRemove the Intercom SDK and env vars\nEOF\n```\n\n\n## Working\n\n- Before an item: `slop task item note <KEY> <n> "Starting. Approach: \u2026"`\n- As facts arrive, replace the note. Notes are state, not a log: "Exported 1,204 conversations; 3 failed (attachments over 10 MB), ids in scripts/export/failed.json".\n- When an item is done: `slop task item done <KEY> <n>`. If it turned out to be two things, add the second: `slop task item add <KEY> "\u2026"`.\n- Blocked? Put the blocker in the note, then move on to an item that is not blocked. Do not mark blocked items done.\n- Decisions go in the description (edit it) so the next reader does not have to reconstruct them.\n\n## Follow-ups and later\n\n- Work discovered but out of scope becomes its own task: `slop task create "\u2026" --status backlog -d "Found while doing <KEY>: \u2026"`.\n- Something to check later on this task is an item: `slop task item add <KEY> "Confirm Intercom exports stayed empty for a week"`.\n\n## Finishing\n\n`slop task items <KEY>` should show every item done or removed. Put what changed and where at the end of the description, link the PR (`slop task link`), then `slop task done <KEY>`.\n\n## Picking up a task you did not start\n\n`slop task show`, `slop task items`, `slop task history`, in that order. The notes tell you the present; the history tells you how it got there. Then continue from the first item that is not done.\n';
|
|
3386
3386
|
|
|
3387
3387
|
// skills/slop-sessions.md
|
|
3388
|
-
var slop_sessions_default =
|
|
3388
|
+
var slop_sessions_default = '---\nname: slop-sessions\ndescription: How an agent claims a task, stays reachable on it (a live session), sees who else is live, and talks to other agents and people through the project\'s message board. Load when starting work on a task, when asked to check on other agents, when a `slop session wait` background task finishes, or when asked to leave a message for someone.\n---\n\n# Sessions and the board\n\nA **session** is you, live on one task. It exists while `slop session wait` is running: when that process ends (you exit, or your thread closes), the session ends with it. Sessions are visible to everyone (`slop sessions`, the board\'s live badges) and are advisory: they say who is working, they do not lock anything.\n\n**Threads** are how agents and people talk across sessions. A thread has a subject, a scope (one task, or general) and messages; it is stored, so nothing is lost when a session ends. **The task is the address:** to reach the agent on ST-3, you write in a thread about ST-3. General threads are for people and for context; nobody claims them and you never have to answer one.\n\n## What wakes you, and what waits\n\n- A message in a thread **about the task you are live on** reaches your waiter at once. Nothing else does: another task\'s thread, or a general one, never interrupts you.\n- **Everything else waits.** `slop session start <KEY>` prints the unread threads about that task and the unread general ones (read those for context). `slop threads` lists them any time; `slop thread show <id>` reads one.\n- Your own messages never wake your own waiter.\n\n## The loop\n\n1. **Claim:** `slop session start <KEY>`. Assigns the task to you, moves it to in progress, and prints what is waiting for you. Read it before touching anything.\n2. **Go live:** run `slop session wait <KEY>` **in the background** (Claude Code: a background Bash task; Codex: a background command). It blocks until someone writes in a thread about your task, prints the message with its thread id, and exits. **The exit is the message.** If your harness can watch a stream instead, `slop session wait <KEY> --follow` stays up and prints each message as it comes.\n3. **Work.** Keep the plan\'s notes current (`slop-planning`).\n4. **When the wait exits with a message:** read it, act or answer with `slop thread reply <id> "\u2026"`, then start `slop session wait <KEY>` in the background **immediately**. Coming back within two minutes continues the same session; not coming back means you have gone dark.\n5. **Finish:** `slop task done <KEY>`, then `slop session end <KEY>` (or just let the wait exit).\n\nExit codes of `wait`: 0 a message was printed; 2 the session was ended on purpose (someone ran `slop session end`); 3 `--timeout` elapsed with nothing said; 4 the connection dropped. On 2 do not relaunch. On 3 or 4 relaunch if you are still working.\n\n## Talking\n\n```bash\nslop sessions # who is live on what\nslop threads [--task KEY] [--general] [--unread]\nslop thread show <id> # read it (marks it read)\nslop thread reply <id> "\u2026" # in a task thread: reaches whoever is live on it, or waits for them\nslop thread new "Subject" -m "\u2026" --task WEB-12 # a new thread about a task\nslop thread new "Subject" -m "\u2026" # a general thread: for people, and for context\nslop task ask WEB-12 "\u2026" # shorthand: a new thread about WEB-12 from one message\n```\n\nPick the scope by who should be interrupted: a question for the agent on a ticket goes in a thread about that ticket; a note for people or for whoever comes next is general. Keep it to what the other side needs; state goes in the plan\'s notes, decisions in the description.\n\n## Etiquette\n\n- One live session per agent. Do not `session start` a task that has someone else\'s live session; write in a thread about it first.\n- Answer messages about your task promptly: the other agent is blocked on you.\n- Report progress in the plan\'s notes, not in threads. Threads are for talking to someone.\n- If you stop working on a task, end the session so nobody waits on you.\n';
|
|
3389
3389
|
|
|
3390
3390
|
// skills/slop-working.md
|
|
3391
|
-
var slop_working_default =
|
|
3391
|
+
var slop_working_default = '---\nname: slop-working\ndescription: The working loop for tasks on the board (claim, do, report, finish) and what to record where. Load at the start of any coding session in a repository linked with slop.\n---\n\n# The loop\n\n1. **What is there:** `slop task list --assignee me` (or `slop task list`). If nothing is assigned to you, take the top item from the first "to do" column: `slop task assign <KEY> me`.\n2. **Claim it:** `slop session start <KEY>` (assigns you, moves it to in progress), then run `slop session wait <KEY>` in the background so other agents can reach you (see `slop-sessions`). Read `slop task show <KEY>` fully, including the plan and its notes.\n3. **Do it.** Long work: follow `slop-planning`. Short work: just do it.\n4. **Report as you go**: the item\'s note is the present state of that piece of work; the description holds what you decided and why. Write in sentences a teammate can act on.\n5. **Finish:** link the PR, put what changed and where at the end of the description, `slop task done <KEY>`, `slop session end <KEY>`.\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 |\n| A decision and its reason | the description (edit it) |\n| A question for the agent on a ticket | a thread about that ticket (`slop task ask KEY "\u2026"`) |\n| A note for people, or for whoever comes next | a general thread (`slop post "\u2026"`) |\n| A discovery, a hand-off | the description, or a new task if it is work |\n| A pull request, a design, a log | a link |\n| Loose working notes nobody needs to act on | the task\'s notes (`slop task notes`) |\n| Work you found but will not do now | a new task in backlog |\n\n# Etiquette\n\n- One task in progress at a time per agent. `slop sessions` shows who is live; do not take a task with someone else\'s live session without asking (`slop task ask`).\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';
|
|
3392
3392
|
|
|
3393
3393
|
// src/platform/skills.ts
|
|
3394
3394
|
var SKILLS = { "slop-cli": slop_cli_default, "slop-planning": slop_planning_default, "slop-sessions": slop_sessions_default, "slop-working": slop_working_default };
|
|
@@ -17722,7 +17722,7 @@ function generateRecordCheck(doc, ctx, schema, accessor) {
|
|
|
17722
17722
|
}, `return INVALID;`);
|
|
17723
17723
|
return outputVar;
|
|
17724
17724
|
}
|
|
17725
|
-
function emitOwnKeys(doc, ctx, accessor, kVar,
|
|
17725
|
+
function emitOwnKeys(doc, ctx, accessor, kVar, body2, onSymbol) {
|
|
17726
17726
|
const propIsEnumerableConst = addConstant(ctx, Object.prototype.propertyIsEnumerable);
|
|
17727
17727
|
const symsVar = newVar(ctx);
|
|
17728
17728
|
const keysVar = newVar(ctx);
|
|
@@ -17733,7 +17733,7 @@ function emitOwnKeys(doc, ctx, accessor, kVar, body, onSymbol) {
|
|
|
17733
17733
|
doc.indented((d) => {
|
|
17734
17734
|
d.write(`const ${kVar} = ${keysVar}[${iVar}];`);
|
|
17735
17735
|
d.write(`if (${kVar} === "__proto__" || !${propIsEnumerableConst}.call(${accessor}, ${kVar})) continue;`);
|
|
17736
|
-
|
|
17736
|
+
body2(d);
|
|
17737
17737
|
});
|
|
17738
17738
|
doc.write(`}`);
|
|
17739
17739
|
doc.write(`for (let ${iVar} = 0; ${iVar} < ${symsVar}.length; ${iVar}++) {`);
|
|
@@ -17743,7 +17743,7 @@ function emitOwnKeys(doc, ctx, accessor, kVar, body, onSymbol) {
|
|
|
17743
17743
|
if (onSymbol)
|
|
17744
17744
|
d.write(onSymbol);
|
|
17745
17745
|
else
|
|
17746
|
-
|
|
17746
|
+
body2(d);
|
|
17747
17747
|
});
|
|
17748
17748
|
doc.write(`}`);
|
|
17749
17749
|
}
|
|
@@ -23207,9 +23207,21 @@ var Invitation = external_exports.object({
|
|
|
23207
23207
|
role: OrgRole,
|
|
23208
23208
|
createdAt: Timestamp
|
|
23209
23209
|
});
|
|
23210
|
+
var InviteLink = external_exports.object({
|
|
23211
|
+
id: Id,
|
|
23212
|
+
role: OrgRole,
|
|
23213
|
+
createdAt: Timestamp,
|
|
23214
|
+
expiresAt: Timestamp.nullable(),
|
|
23215
|
+
uses: external_exports.number()
|
|
23216
|
+
});
|
|
23217
|
+
var CreateInviteLinkInput = external_exports.object({ role: OrgRole.default("member"), expiresInHours: external_exports.number().int().min(1).max(24 * 30).nullable().default(24) });
|
|
23218
|
+
var CreatedInviteLink = InviteLink.extend({ url: external_exports.string() });
|
|
23219
|
+
var JoinPreview = external_exports.object({ org: external_exports.object({ id: Id, name: external_exports.string() }), role: OrgRole, alreadyMember: external_exports.boolean() });
|
|
23210
23220
|
var MembersResult = external_exports.object({
|
|
23211
23221
|
members: external_exports.array(Member),
|
|
23212
|
-
invitations: external_exports.array(Invitation)
|
|
23222
|
+
invitations: external_exports.array(Invitation),
|
|
23223
|
+
/** Active links only. Admins see them; members get an empty list. */
|
|
23224
|
+
links: external_exports.array(InviteLink)
|
|
23213
23225
|
});
|
|
23214
23226
|
var InviteMemberInput = external_exports.object({ email: Email, role: OrgRole.default("member") });
|
|
23215
23227
|
var UpdateMemberInput = external_exports.object({ role: OrgRole });
|
|
@@ -23241,8 +23253,6 @@ function findStatus(statuses, ref) {
|
|
|
23241
23253
|
|
|
23242
23254
|
// ../contract/src/work/projects.ts
|
|
23243
23255
|
var ProjectKey = external_exports.string().trim().toUpperCase().pipe(external_exports.string().regex(/^[A-Z][A-Z0-9]{1,7}$/, "2\u20138 letters or digits, starting with a letter"));
|
|
23244
|
-
var PROJECT_THEMES = ["slate", "stone", "zinc", "mist", "sand", "rose", "violet", "emerald"];
|
|
23245
|
-
var ProjectTheme = external_exports.enum(PROJECT_THEMES);
|
|
23246
23256
|
var TaskCounts = external_exports.record(external_exports.string(), external_exports.number());
|
|
23247
23257
|
var Project = external_exports.object({
|
|
23248
23258
|
id: Id,
|
|
@@ -23250,8 +23260,6 @@ var Project = external_exports.object({
|
|
|
23250
23260
|
key: external_exports.string(),
|
|
23251
23261
|
name: external_exports.string(),
|
|
23252
23262
|
description: external_exports.string(),
|
|
23253
|
-
/** Null: no theme of its own; the viewer's personal theme applies. */
|
|
23254
|
-
theme: ProjectTheme.nullable(),
|
|
23255
23263
|
/** The board's columns, in order. */
|
|
23256
23264
|
statuses: external_exports.array(ProjectStatus),
|
|
23257
23265
|
createdAt: Timestamp,
|
|
@@ -23266,8 +23274,7 @@ var CreateProjectInput = external_exports.object({
|
|
|
23266
23274
|
});
|
|
23267
23275
|
var UpdateProjectInput = external_exports.object({
|
|
23268
23276
|
name: external_exports.string().trim().min(1).max(80),
|
|
23269
|
-
description: external_exports.string().trim().max(2e3)
|
|
23270
|
-
theme: ProjectTheme.nullable()
|
|
23277
|
+
description: external_exports.string().trim().max(2e3)
|
|
23271
23278
|
}).partial();
|
|
23272
23279
|
function suggestProjectKey(name) {
|
|
23273
23280
|
const words = name.toUpperCase().replace(/[^A-Z0-9 ]+/g, " ").split(/\s+/).filter(Boolean);
|
|
@@ -23298,6 +23305,44 @@ var UpdateItemInput = external_exports.object({ title, note, done: external_expo
|
|
|
23298
23305
|
var ReorderItemsInput = external_exports.object({ ids: external_exports.array(Id).min(1) });
|
|
23299
23306
|
var ItemProgress = external_exports.object({ done: external_exports.number(), total: external_exports.number() });
|
|
23300
23307
|
|
|
23308
|
+
// ../contract/src/work/threads.ts
|
|
23309
|
+
var MessageVia = external_exports.enum(["person", "agent"]);
|
|
23310
|
+
var ThreadMessage = external_exports.object({
|
|
23311
|
+
id: Id,
|
|
23312
|
+
threadId: Id,
|
|
23313
|
+
author: UserRef,
|
|
23314
|
+
/** A person on the web, or an agent through the CLI (with its machine). */
|
|
23315
|
+
via: MessageVia,
|
|
23316
|
+
machine: external_exports.string().nullable(),
|
|
23317
|
+
body: external_exports.string(),
|
|
23318
|
+
createdAt: Timestamp
|
|
23319
|
+
});
|
|
23320
|
+
var Thread = external_exports.object({
|
|
23321
|
+
id: Id,
|
|
23322
|
+
projectId: Id,
|
|
23323
|
+
/** Null: general. */
|
|
23324
|
+
taskId: Id.nullable(),
|
|
23325
|
+
taskKey: external_exports.string().nullable(),
|
|
23326
|
+
subject: external_exports.string(),
|
|
23327
|
+
createdBy: UserRef,
|
|
23328
|
+
createdAt: Timestamp,
|
|
23329
|
+
lastMessageAt: Timestamp,
|
|
23330
|
+
messageCount: external_exports.number(),
|
|
23331
|
+
/** Messages the viewer has not read. Their own count as read. */
|
|
23332
|
+
unread: external_exports.number(),
|
|
23333
|
+
last: external_exports.object({ author: UserRef, via: MessageVia, body: external_exports.string() }).nullable()
|
|
23334
|
+
});
|
|
23335
|
+
var ThreadDetail = Thread.extend({ messages: external_exports.array(ThreadMessage) });
|
|
23336
|
+
var body = external_exports.string().trim().min(1, "Say something").max(8e3);
|
|
23337
|
+
var CreateThreadInput = external_exports.object({
|
|
23338
|
+
subject: external_exports.string().trim().min(1, "Give it a subject").max(200),
|
|
23339
|
+
/** A task key or number; absent means general. */
|
|
23340
|
+
task: external_exports.string().trim().min(1).optional(),
|
|
23341
|
+
body
|
|
23342
|
+
});
|
|
23343
|
+
var ReplyInput = external_exports.object({ body });
|
|
23344
|
+
var ProjectThreadSummary = external_exports.object({ projectId: Id, unread: external_exports.number(), threads: external_exports.array(Thread) });
|
|
23345
|
+
|
|
23301
23346
|
// ../contract/src/work/tasks.ts
|
|
23302
23347
|
var TaskStatus = StatusKey;
|
|
23303
23348
|
var TASK_SEVERITIES = ["low", "medium", "high", "critical"];
|
|
@@ -23408,8 +23453,12 @@ var AskInput = external_exports.object({ body: external_exports.string().trim().
|
|
|
23408
23453
|
var SessionPush = external_exports.discriminatedUnion("type", [
|
|
23409
23454
|
external_exports.object({ type: external_exports.literal("hello"), session: Session }),
|
|
23410
23455
|
external_exports.object({ type: external_exports.literal("event"), record: TaskEventRecord }),
|
|
23456
|
+
/** A message in a thread about the task this session is on. */
|
|
23457
|
+
external_exports.object({ type: external_exports.literal("thread"), thread: external_exports.object({ id: Id, subject: external_exports.string(), taskKey: external_exports.string().nullable() }), message: ThreadMessage }),
|
|
23411
23458
|
/** The room is closing this session (someone ran `slop session end`). */
|
|
23412
|
-
external_exports.object({ type: external_exports.literal("end") })
|
|
23459
|
+
external_exports.object({ type: external_exports.literal("end") }),
|
|
23460
|
+
/** To board watchers: something in the project changed; refetch. */
|
|
23461
|
+
external_exports.object({ type: external_exports.literal("changed"), scope: external_exports.string() })
|
|
23413
23462
|
]);
|
|
23414
23463
|
|
|
23415
23464
|
// ../contract/src/work/bindings.ts
|
|
@@ -23483,16 +23532,16 @@ var CliError = class extends Error {
|
|
|
23483
23532
|
|
|
23484
23533
|
// src/platform/api.ts
|
|
23485
23534
|
function createApi(options) {
|
|
23486
|
-
async function request(method, path3,
|
|
23535
|
+
async function request(method, path3, body2) {
|
|
23487
23536
|
const headers = { accept: "application/json" };
|
|
23488
|
-
if (
|
|
23537
|
+
if (body2 !== void 0) headers["content-type"] = "application/json";
|
|
23489
23538
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
23490
23539
|
let response;
|
|
23491
23540
|
try {
|
|
23492
23541
|
response = await fetch(`${options.baseUrl}/api${path3}`, {
|
|
23493
23542
|
method,
|
|
23494
23543
|
headers,
|
|
23495
|
-
body:
|
|
23544
|
+
body: body2 === void 0 ? void 0 : JSON.stringify(body2),
|
|
23496
23545
|
signal: AbortSignal.timeout(2e4)
|
|
23497
23546
|
});
|
|
23498
23547
|
} catch (error62) {
|
|
@@ -23515,9 +23564,9 @@ function createApi(options) {
|
|
|
23515
23564
|
return {
|
|
23516
23565
|
baseUrl: options.baseUrl,
|
|
23517
23566
|
get: (path3) => request("GET", path3),
|
|
23518
|
-
post: (path3,
|
|
23519
|
-
patch: (path3,
|
|
23520
|
-
put: (path3,
|
|
23567
|
+
post: (path3, body2) => request("POST", path3, body2 ?? {}),
|
|
23568
|
+
patch: (path3, body2) => request("PATCH", path3, body2),
|
|
23569
|
+
put: (path3, body2) => request("PUT", path3, body2),
|
|
23521
23570
|
delete: (path3) => request("DELETE", path3)
|
|
23522
23571
|
};
|
|
23523
23572
|
}
|
|
@@ -23640,23 +23689,25 @@ function openBrowser(url2) {
|
|
|
23640
23689
|
}
|
|
23641
23690
|
}
|
|
23642
23691
|
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
23692
|
+
var headless = () => Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY) || platform2() === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
23643
23693
|
function registerAuthCommands(program3) {
|
|
23644
|
-
program3.command("login").description("Sign this machine in. Opens the web app, where you approve the login.").option("--url <url>", "the Slopware Tasks instance to sign in to").option("--no-browser", "print the link instead of opening it").action(async (options) => {
|
|
23694
|
+
program3.command("login").description("Sign this machine in. Opens the web app, where you approve the login. On a VM or over SSH, open the printed link on any device.").option("--url <url>", "the Slopware Tasks instance to sign in to").option("--no-browser", "print the link instead of opening it").action(async (options) => {
|
|
23645
23695
|
const baseUrl = (options.url || process.env.SLOP_URL || readConfig()?.baseUrl || DEFAULT_URL).replace(/\/+$/, "");
|
|
23646
23696
|
const api = createApi({ baseUrl, token: null });
|
|
23647
|
-
const
|
|
23697
|
+
const started2 = await api.post("/cli/auth/start", { machine: machineInfo() });
|
|
23698
|
+
const remote = headless();
|
|
23648
23699
|
process.stderr.write(`
|
|
23649
|
-
Approve this login in your
|
|
23650
|
-
${
|
|
23700
|
+
Approve this login in a browser${remote ? " on any device (this machine has no browser; your laptop or phone is fine)" : ""}:
|
|
23701
|
+
${started2.verifyUrl}
|
|
23651
23702
|
|
|
23652
|
-
Confirm the code matches: ${
|
|
23703
|
+
Confirm the code matches: ${started2.userCode}
|
|
23653
23704
|
|
|
23654
23705
|
`);
|
|
23655
|
-
if (options.browser) openBrowser(
|
|
23656
|
-
const deadline = Date.now() +
|
|
23706
|
+
if (options.browser && !remote) openBrowser(started2.verifyUrl);
|
|
23707
|
+
const deadline = Date.now() + started2.expiresIn * 1e3;
|
|
23657
23708
|
while (Date.now() < deadline) {
|
|
23658
|
-
await sleep(
|
|
23659
|
-
const poll = await api.post("/cli/auth/poll", { deviceCode:
|
|
23709
|
+
await sleep(started2.interval * 1e3);
|
|
23710
|
+
const poll = await api.post("/cli/auth/poll", { deviceCode: started2.deviceCode });
|
|
23660
23711
|
if (poll.status === "pending") continue;
|
|
23661
23712
|
if (poll.status === "denied") throw new CliError("The login was denied in the web app");
|
|
23662
23713
|
if (poll.status === "expired") break;
|
|
@@ -23695,7 +23746,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
|
|
|
23695
23746
|
- Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
|
|
23696
23747
|
- When it is finished and verified: \`slop task done <KEY>\`.
|
|
23697
23748
|
- Descriptions are Markdown and are shown in full on the board. Write real ones: context, file paths, what done means. Pipe long ones in with \`-d -\`.
|
|
23698
|
-
- Claim a task with \`slop session start <KEY>\` and run \`slop session wait <KEY>\` in the background so other agents can reach you
|
|
23749
|
+
- Claim a task with \`slop session start <KEY>\` (it prints what was said to you) and run \`slop session wait <KEY>\` in the background so other agents can reach you. 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\`.
|
|
23699
23750
|
- 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.
|
|
23700
23751
|
- Everything is editable: \`slop task update <KEY> --title \u2026 --severity \u2026 --status \u2026 --assignee \u2026\`. Assign with \`slop task assign <KEY> <email|me|none>\`; see who exists with \`slop members\`. \`slop task list --mine\` shows your user's tasks.
|
|
23701
23752
|
- Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
|
|
@@ -23721,8 +23772,9 @@ function git(args, cwd = process.cwd()) {
|
|
|
23721
23772
|
function findRepo() {
|
|
23722
23773
|
const commonDir = git(["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
23723
23774
|
if (!commonDir) return null;
|
|
23724
|
-
const
|
|
23725
|
-
|
|
23775
|
+
const worktree = git(["rev-parse", "--show-toplevel"]);
|
|
23776
|
+
const root = basename(commonDir) === ".git" ? resolve(commonDir, "..") : worktree ?? commonDir;
|
|
23777
|
+
return { root, worktree: worktree ?? root, linkPath: join4(commonDir, "slop.json") };
|
|
23726
23778
|
}
|
|
23727
23779
|
function requireRepo() {
|
|
23728
23780
|
const repo = findRepo();
|
|
@@ -23845,17 +23897,13 @@ function registerProjectCommands(program3) {
|
|
|
23845
23897
|
const statuses = await api.put(`/projects/${target.id}/statuses/order`, { ids: refs.map((ref) => columnOf(target, ref).id) });
|
|
23846
23898
|
emit(statuses, () => list(statuses));
|
|
23847
23899
|
});
|
|
23848
|
-
project.command("update").description("Change the linked project's name
|
|
23900
|
+
project.command("update").description("Change the linked project's name or description.").option("-p, --project <key>", "project key (default: the linked project)").option("-n, --name <name>").option("-d, --description <text>").action(async (options) => {
|
|
23849
23901
|
const api = authedApi();
|
|
23850
23902
|
const target = await resolveProject(api, options.project);
|
|
23851
|
-
|
|
23852
|
-
|
|
23853
|
-
throw new CliError(`Unknown theme "${theme}"`, `use one of: ${PROJECT_THEMES.join(", ")}, none`);
|
|
23903
|
+
if (options.name === void 0 && options.description === void 0) {
|
|
23904
|
+
throw new CliError("Nothing to change", "pass --name or --description");
|
|
23854
23905
|
}
|
|
23855
|
-
|
|
23856
|
-
throw new CliError("Nothing to change", "pass --name, --description or --theme");
|
|
23857
|
-
}
|
|
23858
|
-
const updated = await api.patch(`/projects/${target.id}`, { name: options.name, description: options.description, theme });
|
|
23906
|
+
const updated = await api.patch(`/projects/${target.id}`, { name: options.name, description: options.description });
|
|
23859
23907
|
emit(updated, () => `Updated ${updated.key} ${updated.name}`);
|
|
23860
23908
|
});
|
|
23861
23909
|
project.command("create <name>").description("Create a project.").option("-k, --key <key>", "task key prefix, e.g. WEB (default: derived from the name)").option("-d, --description <text>", "what the project is").option("--org <slug-or-id>", "which org, if you belong to several").action(async (name, options) => {
|
|
@@ -23891,24 +23939,102 @@ Try: slop task create "My first task"`);
|
|
|
23891
23939
|
program3.command("status").description("Show what this repository is linked to.").action(async () => {
|
|
23892
23940
|
const repo = findRepo();
|
|
23893
23941
|
const link = repo ? readLink(repo) : null;
|
|
23942
|
+
const where = !repo ? "" : repo.worktree === repo.root ? repo.root : `${repo.worktree} (worktree of ${repo.root})`;
|
|
23894
23943
|
emit(
|
|
23895
|
-
{ repo: repo?.root ?? null, link },
|
|
23896
|
-
() => !repo ? "Not inside a git repository." : !link ? `${
|
|
23897
|
-
not linked. Run: slop link <PROJECT_KEY>` : `${
|
|
23944
|
+
{ repo: repo?.root ?? null, worktree: repo?.worktree ?? null, link },
|
|
23945
|
+
() => !repo ? "Not inside a git repository." : !link ? `${where}
|
|
23946
|
+
not linked. Run: slop link <PROJECT_KEY>` : `${where}
|
|
23898
23947
|
linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
23899
23948
|
);
|
|
23900
23949
|
});
|
|
23901
23950
|
}
|
|
23902
23951
|
|
|
23903
23952
|
// src/commands/sessions.ts
|
|
23904
|
-
import { readFileSync as
|
|
23953
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
23954
|
+
|
|
23955
|
+
// src/commands/threads.ts
|
|
23956
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
23957
|
+
var readText = (value) => value === "-" ? readFileSync5(0, "utf8").trim() : value;
|
|
23958
|
+
var when = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
23959
|
+
var who = (message) => `${message.author.name}${message.via === "agent" ? ` (agent${message.machine ? ` \xB7 ${message.machine}` : ""})` : ""}`;
|
|
23960
|
+
function threadLine(thread) {
|
|
23961
|
+
const scope = thread.taskKey ?? "general";
|
|
23962
|
+
const unread = thread.unread > 0 ? ` ${thread.unread} unread` : "";
|
|
23963
|
+
const last = thread.last ? `
|
|
23964
|
+
${who(thread.last)}: ${thread.last.body.split("\n")[0]}` : "";
|
|
23965
|
+
return `${thread.id} [${scope}] ${thread.subject} (${thread.messageCount} msg, ${when(thread.lastMessageAt)})${unread}${last}`;
|
|
23966
|
+
}
|
|
23967
|
+
function messageBlock(message) {
|
|
23968
|
+
return `${who(message)} ${when(message.createdAt)}
|
|
23969
|
+
${message.body.replace(/^/gm, " ")}`;
|
|
23970
|
+
}
|
|
23971
|
+
function threadBlock(thread) {
|
|
23972
|
+
return [`${thread.subject} [${thread.taskKey ?? "general"}] ${thread.id}`, "", ...thread.messages.map(messageBlock)].join("\n\n").replace("\n\n\n", "\n\n");
|
|
23973
|
+
}
|
|
23974
|
+
function pushBlock(push) {
|
|
23975
|
+
return `Thread ${push.thread.id} [${push.thread.taskKey ?? "general"}] ${push.thread.subject}
|
|
23976
|
+
${messageBlock(push.message)}
|
|
23977
|
+
Reply: slop thread reply ${push.thread.id} "\u2026"`;
|
|
23978
|
+
}
|
|
23979
|
+
function registerThreadCommands(program3, task) {
|
|
23980
|
+
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
23981
|
+
withProject(program3.command("threads")).description("List the project's threads, newest activity first.").option("-t, --task <key>", "only threads about this task").option("--general", "only general threads").option("--unread", "only threads with messages you have not read").action(async (options) => {
|
|
23982
|
+
const api = authedApi();
|
|
23983
|
+
const project = await resolveProject(api, options.project);
|
|
23984
|
+
const query = new URLSearchParams();
|
|
23985
|
+
if (options.task) query.set("task", options.task);
|
|
23986
|
+
if (options.general) query.set("scope", "general");
|
|
23987
|
+
if (options.unread) query.set("unread", "1");
|
|
23988
|
+
const threads = await api.get(`/projects/${project.id}/threads${query.size ? `?${query}` : ""}`);
|
|
23989
|
+
emit(threads, () => threads.length === 0 ? "No threads." : threads.map(threadLine).join("\n"));
|
|
23990
|
+
});
|
|
23991
|
+
const thread = program3.command("thread").description("One thread: start, read, reply.");
|
|
23992
|
+
withProject(thread.command("new <subject>")).description('Start a thread. General by default; --task makes it about a task (and reaches whoever is live on it). The first message is -m, or "-" for stdin.').requiredOption("-m, --message <text>", 'the first message; "-" reads stdin').option("-t, --task <key>", "about this task").action(async (subject, options) => {
|
|
23993
|
+
const api = authedApi();
|
|
23994
|
+
const project = await resolveProject(api, options.project);
|
|
23995
|
+
const result = await api.post(`/projects/${project.id}/threads`, { subject, task: options.task, body: readText(options.message) });
|
|
23996
|
+
emit(result, () => started(result));
|
|
23997
|
+
});
|
|
23998
|
+
withProject(thread.command("show <id>")).description("Read a thread (and mark it read).").action(async (id, options) => {
|
|
23999
|
+
const api = authedApi();
|
|
24000
|
+
const project = await resolveProject(api, options.project);
|
|
24001
|
+
const detail = await api.get(`/projects/${project.id}/threads/${id}`);
|
|
24002
|
+
emit(detail, () => threadBlock(detail));
|
|
24003
|
+
});
|
|
24004
|
+
withProject(thread.command("reply <id> <text>")).description('Reply in a thread. About a task: reaches whoever is live on it. "-" reads stdin.').action(async (id, text, options) => {
|
|
24005
|
+
const api = authedApi();
|
|
24006
|
+
const project = await resolveProject(api, options.project);
|
|
24007
|
+
const result = await api.post(`/projects/${project.id}/threads/${id}/messages`, { body: readText(text) });
|
|
24008
|
+
emit(result, () => result.delivered ? `Replied; reached ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : "Replied. Nobody is live on it; they see it when they start.");
|
|
24009
|
+
});
|
|
24010
|
+
withProject(program3.command("post <text>")).description('Start a general thread from one message (the first line is the subject). For a task, use --task. "-" reads stdin.').option("-t, --task <key>", "about this task").action(async (text, options) => {
|
|
24011
|
+
const api = authedApi();
|
|
24012
|
+
const project = await resolveProject(api, options.project);
|
|
24013
|
+
const body2 = readText(text);
|
|
24014
|
+
const result = await api.post(`/projects/${project.id}/threads`, { subject: subjectOf(body2), task: options.task, body: body2 });
|
|
24015
|
+
emit(result, () => started(result));
|
|
24016
|
+
});
|
|
24017
|
+
withProject(task.command("ask <key> <text>")).description('Start a thread about the task and say something to whoever is live on it. "-" reads stdin.').action(async (key, text, options) => {
|
|
24018
|
+
const api = authedApi();
|
|
24019
|
+
const project = await resolveProject(api, options.project);
|
|
24020
|
+
const body2 = readText(text);
|
|
24021
|
+
const result = await api.post(`/projects/${project.id}/threads`, { subject: subjectOf(body2), task: key, body: body2 });
|
|
24022
|
+
emit(result, () => started(result));
|
|
24023
|
+
});
|
|
24024
|
+
}
|
|
24025
|
+
var subjectOf = (body2) => {
|
|
24026
|
+
const line = body2.split("\n")[0]?.trim() ?? "";
|
|
24027
|
+
if (!line) throw new CliError("Say something", "the first line becomes the subject");
|
|
24028
|
+
return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
|
|
24029
|
+
};
|
|
24030
|
+
var started = (result) => result.thread.taskKey ? result.delivered ? `Started ${result.thread.id} on ${result.thread.taskKey}; reached ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : `Started ${result.thread.id} on ${result.thread.taskKey}. Nobody is live on it; they see it when they start.` : `Started ${result.thread.id} (general). People see it on the web; agents at their next session start.`;
|
|
23905
24031
|
|
|
23906
24032
|
// src/commands/tasks.ts
|
|
23907
|
-
import { readFileSync as
|
|
24033
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
23908
24034
|
|
|
23909
24035
|
// src/commands/items.ts
|
|
23910
|
-
import { readFileSync as
|
|
23911
|
-
var
|
|
24036
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
24037
|
+
var readText2 = (value) => value === "-" ? readFileSync6(0, "utf8").trim() : value;
|
|
23912
24038
|
var path2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
23913
24039
|
function renderItems(items) {
|
|
23914
24040
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -23935,7 +24061,7 @@ function registerItemCommands(task, withProject) {
|
|
|
23935
24061
|
withProject(task.command("plan <key> [lines...]")).description('Append items to the plan, one per argument; "-" reads one per line from stdin (list markers are fine).').action(async (key, lines, options) => {
|
|
23936
24062
|
const api = authedApi();
|
|
23937
24063
|
const project = await resolveProject(api, options.project);
|
|
23938
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
24064
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync6(0, "utf8") : lines.join("\n");
|
|
23939
24065
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
23940
24066
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
23941
24067
|
const items = await api.post(path2(project.id, key), { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -23945,7 +24071,7 @@ function registerItemCommands(task, withProject) {
|
|
|
23945
24071
|
withProject(item.command("add <key> <title>")).description("Add an item to the end of the plan.").option("-n, --note <text>", 'what is true about it now; "-" reads stdin').action(async (key, title3, options) => {
|
|
23946
24072
|
const api = authedApi();
|
|
23947
24073
|
const project = await resolveProject(api, options.project);
|
|
23948
|
-
const items = await api.post(path2(project.id, key), { title: title3, note: options.note ?
|
|
24074
|
+
const items = await api.post(path2(project.id, key), { title: title3, note: options.note ? readText2(options.note) : "" });
|
|
23949
24075
|
emit(items, () => renderItems(items));
|
|
23950
24076
|
});
|
|
23951
24077
|
for (const [name, done, help] of [
|
|
@@ -23964,7 +24090,7 @@ function registerItemCommands(task, withProject) {
|
|
|
23964
24090
|
const api = authedApi();
|
|
23965
24091
|
const project = await resolveProject(api, options.project);
|
|
23966
24092
|
const target = await itemAt(api, project.id, key, ref);
|
|
23967
|
-
const items = await api.patch(`${path2(project.id, key)}/${target.id}`, { note:
|
|
24093
|
+
const items = await api.patch(`${path2(project.id, key)}/${target.id}`, { note: readText2(text) });
|
|
23968
24094
|
emit(items, () => renderItems(items));
|
|
23969
24095
|
});
|
|
23970
24096
|
withProject(item.command("rename <key> <item> <title>")).description("Retitle an item.").action(async (key, ref, title3, options) => {
|
|
@@ -23992,7 +24118,7 @@ function oneOf(value, allowed, what) {
|
|
|
23992
24118
|
if (allowed.includes(normalized)) return normalized;
|
|
23993
24119
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
23994
24120
|
}
|
|
23995
|
-
var readDescription = (value) => value === "-" ?
|
|
24121
|
+
var readDescription = (value) => value === "-" ? readFileSync7(0, "utf8").trim() : value;
|
|
23996
24122
|
var taskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
23997
24123
|
var loadProject = (api, id) => api.get(`/projects/${id}`);
|
|
23998
24124
|
function resolveStatus(project, ref) {
|
|
@@ -24007,17 +24133,17 @@ function statusInCategory(project, category) {
|
|
|
24007
24133
|
return status.key;
|
|
24008
24134
|
}
|
|
24009
24135
|
var ASSIGNEE_HELP = 'an email, a name, "me", or "none"';
|
|
24010
|
-
async function resolveAssignee(api, projectId,
|
|
24011
|
-
if (
|
|
24012
|
-
if (
|
|
24013
|
-
if (
|
|
24014
|
-
const needle =
|
|
24136
|
+
async function resolveAssignee(api, projectId, who2) {
|
|
24137
|
+
if (who2 === void 0) return void 0;
|
|
24138
|
+
if (who2.toLowerCase() === "none") return null;
|
|
24139
|
+
if (who2.toLowerCase() === "me") return (await api.get("/me")).user.id;
|
|
24140
|
+
const needle = who2.toLowerCase();
|
|
24015
24141
|
const members2 = await listMembers(api, projectId);
|
|
24016
24142
|
const exact = members2.filter((member) => member.email.toLowerCase() === needle || member.name.toLowerCase() === needle);
|
|
24017
24143
|
const matches = exact.length > 0 ? exact : members2.filter((member) => member.email.toLowerCase().includes(needle) || member.name.toLowerCase().includes(needle));
|
|
24018
24144
|
if (matches.length === 1) return matches[0].userId;
|
|
24019
|
-
if (matches.length === 0) throw new CliError(`Nobody in this project matches "${
|
|
24020
|
-
throw new CliError(`"${
|
|
24145
|
+
if (matches.length === 0) throw new CliError(`Nobody in this project matches "${who2}"`, "slop members");
|
|
24146
|
+
throw new CliError(`"${who2}" matches ${matches.map((member) => member.email).join(", ")}`, "use the full email");
|
|
24021
24147
|
}
|
|
24022
24148
|
function registerTaskCommands(program3) {
|
|
24023
24149
|
const task = program3.command("task").alias("tasks").description("Create, read and move tasks in the linked project.");
|
|
@@ -24090,10 +24216,10 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24090
24216
|
const updated = await api.patch(`/projects/${project.id}/tasks/${encodeURIComponent(key)}`, { status: resolveStatus(project, column) });
|
|
24091
24217
|
emit(updated, () => `${updated.key} is now in ${statusLabel(project.statuses, updated.status)}.`);
|
|
24092
24218
|
});
|
|
24093
|
-
withProject(task.command("assign <key> <who>")).description(`Assign a task to ${ASSIGNEE_HELP}.`).action(async (key,
|
|
24219
|
+
withProject(task.command("assign <key> <who>")).description(`Assign a task to ${ASSIGNEE_HELP}.`).action(async (key, who2, options) => {
|
|
24094
24220
|
const api = authedApi();
|
|
24095
24221
|
const project = await resolveProject(api, options.project);
|
|
24096
|
-
const updated = await api.patch(`/projects/${project.id}/tasks/${encodeURIComponent(key)}`, { assigneeId: await resolveAssignee(api, project.id,
|
|
24222
|
+
const updated = await api.patch(`/projects/${project.id}/tasks/${encodeURIComponent(key)}`, { assigneeId: await resolveAssignee(api, project.id, who2) });
|
|
24097
24223
|
emit(updated, () => `${updated.key} is assigned to ${updated.assignee?.name ?? "nobody"}.`);
|
|
24098
24224
|
});
|
|
24099
24225
|
for (const [name, category] of [["start", "in_progress"], ["done", "done"]]) {
|
|
@@ -24144,11 +24270,10 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24144
24270
|
|
|
24145
24271
|
// src/commands/sessions.ts
|
|
24146
24272
|
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24147
|
-
var readText2 = (value) => value === "-" ? readFileSync7(0, "utf8").trim() : value;
|
|
24148
24273
|
var sessionLine = (session) => [session.taskKey, session.user.name, session.machine.name, session.startedAt];
|
|
24149
24274
|
function messageLine(record2) {
|
|
24150
|
-
const
|
|
24151
|
-
return `${record2.actor.name} (${record2.createdAt}): ${
|
|
24275
|
+
const body2 = record2.event.kind === "message" ? record2.event.body : JSON.stringify(record2.event);
|
|
24276
|
+
return `${record2.actor.name} (${record2.createdAt}): ${body2}`;
|
|
24152
24277
|
}
|
|
24153
24278
|
function registerSessionCommands(program3, task) {
|
|
24154
24279
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
@@ -24158,10 +24283,19 @@ function registerSessionCommands(program3, task) {
|
|
|
24158
24283
|
const project = await loadProject(api, (await resolveProject(api, options.project)).id);
|
|
24159
24284
|
const assigneeId = await resolveAssignee(api, project.id, "me");
|
|
24160
24285
|
const updated = await api.patch(taskPath(project.id, key), { assigneeId, status: statusInCategory(project, "in_progress") });
|
|
24161
|
-
|
|
24162
|
-
|
|
24286
|
+
const [about, general] = await Promise.all([
|
|
24287
|
+
api.get(`/projects/${project.id}/threads?task=${encodeURIComponent(updated.key)}&unread=1&read=1`),
|
|
24288
|
+
api.get(`/projects/${project.id}/threads?scope=general&unread=1&read=1`)
|
|
24289
|
+
]);
|
|
24290
|
+
emit({ task: updated, threads: { task: about, general } }, () => {
|
|
24291
|
+
const lines = [`Claimed ${updated.key} ${updated.title}`];
|
|
24292
|
+
if (about.length) lines.push("", `Unread about ${updated.key} (read one with slop thread show <id>):`, ...about.map(threadLine));
|
|
24293
|
+
if (general.length) lines.push("", `Unread general threads (for context; nobody has to answer):`, ...general.map(threadLine));
|
|
24294
|
+
lines.push("", `Go live (in the background, and relaunch it after each message): slop session wait ${updated.key}`);
|
|
24295
|
+
return lines.join("\n");
|
|
24296
|
+
});
|
|
24163
24297
|
});
|
|
24164
|
-
withProject(session.command("wait <key>")).description("Hold a live session on the task until someone says something; print it and exit. Exit 2 if the session was ended, 3 on timeout, 4 if the connection dropped (relaunch).").option("--timeout <seconds>", "give up after this long (default: never)").action(async (key, options) => {
|
|
24298
|
+
withProject(session.command("wait <key>")).description("Hold a live session on the task until someone says something in a thread about it; print it and exit. Exit 2 if the session was ended, 3 on timeout, 4 if the connection dropped (relaunch). --follow stays up and prints each message as it comes.").option("--timeout <seconds>", "give up after this long (default: never)").option("--follow", "keep the session and print every message instead of exiting on the first").action(async (key, options) => {
|
|
24165
24299
|
const api = authedApi();
|
|
24166
24300
|
const project = await resolveProject(api, options.project);
|
|
24167
24301
|
const ticket = await api.post(`${taskPath(project.id, key)}/session/ticket`);
|
|
@@ -24183,6 +24317,16 @@ Go live (in the background, and relaunch it after each message): slop session w
|
|
|
24183
24317
|
resolve2({ ended: true });
|
|
24184
24318
|
return;
|
|
24185
24319
|
}
|
|
24320
|
+
if (push.type === "thread") {
|
|
24321
|
+
if (options.follow) {
|
|
24322
|
+
emit({ thread: push.thread, message: push.message }, () => `${pushBlock(push)}
|
|
24323
|
+
`);
|
|
24324
|
+
return;
|
|
24325
|
+
}
|
|
24326
|
+
resolve2({ push });
|
|
24327
|
+
return;
|
|
24328
|
+
}
|
|
24329
|
+
if (push.type !== "event") return;
|
|
24186
24330
|
resolve2({ record: push.record });
|
|
24187
24331
|
});
|
|
24188
24332
|
socket.addEventListener("close", (event) => {
|
|
@@ -24195,8 +24339,9 @@ Go live (in the background, and relaunch it after each message): slop session w
|
|
|
24195
24339
|
resolve2({ closed: "socket error" });
|
|
24196
24340
|
});
|
|
24197
24341
|
});
|
|
24198
|
-
if ("record" in outcome) {
|
|
24199
|
-
emit(outcome.
|
|
24342
|
+
if ("record" in outcome || "push" in outcome) {
|
|
24343
|
+
if ("push" in outcome) emit(outcome.push, () => pushBlock(outcome.push));
|
|
24344
|
+
else emit(outcome.record, () => messageLine(outcome.record));
|
|
24200
24345
|
socket.close(1e3, "handled");
|
|
24201
24346
|
await new Promise((resolve2) => {
|
|
24202
24347
|
socket.addEventListener("close", () => resolve2(), { once: true });
|
|
@@ -24223,12 +24368,6 @@ Go live (in the background, and relaunch it after each message): slop session w
|
|
|
24223
24368
|
const sessions = await api.get(`/projects/${project.id}/sessions`);
|
|
24224
24369
|
emit(sessions, () => sessions.length === 0 ? "Nobody is live." : table([["TASK", "WHO", "MACHINE", "SINCE"], ...sessions.map(sessionLine)]));
|
|
24225
24370
|
});
|
|
24226
|
-
withProject(task.command("ask <key> <text>")).description('Say something to whoever is live on the task; "-" reads stdin. It also goes on the timeline.').action(async (key, text, options) => {
|
|
24227
|
-
const api = authedApi();
|
|
24228
|
-
const project = await resolveProject(api, options.project);
|
|
24229
|
-
const result = await api.post(`${taskPath(project.id, key)}/ask`, { body: readText2(text) });
|
|
24230
|
-
emit(result, () => result.delivered ? `Delivered to ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : "Nobody is live on it; left on the timeline.");
|
|
24231
|
-
});
|
|
24232
24371
|
}
|
|
24233
24372
|
|
|
24234
24373
|
// src/commands/skills.ts
|
|
@@ -24253,10 +24392,12 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
|
|
|
24253
24392
|
}
|
|
24254
24393
|
|
|
24255
24394
|
// src/index.ts
|
|
24256
|
-
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.
|
|
24395
|
+
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.2.0").option("--json", "machine-readable output: JSON on stdout, errors as JSON on stderr").hook("preAction", (_, action) => setJsonMode(Boolean(action.optsWithGlobals().json))).showHelpAfterError("(run with --help for usage)");
|
|
24257
24396
|
registerAuthCommands(program2);
|
|
24258
24397
|
registerProjectCommands(program2);
|
|
24259
|
-
|
|
24398
|
+
var taskCommand = registerTaskCommands(program2);
|
|
24399
|
+
registerSessionCommands(program2, taskCommand);
|
|
24400
|
+
registerThreadCommands(program2, taskCommand);
|
|
24260
24401
|
registerGuideCommand(program2);
|
|
24261
24402
|
registerSkillCommands(program2);
|
|
24262
24403
|
program2.parseAsync().catch(fail);
|