slopwaresystems 0.1.2 → 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/dist/index.js +188 -54
- package/package.json +1 -1
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\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 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
|
}
|
|
@@ -23305,6 +23305,44 @@ var UpdateItemInput = external_exports.object({ title, note, done: external_expo
|
|
|
23305
23305
|
var ReorderItemsInput = external_exports.object({ ids: external_exports.array(Id).min(1) });
|
|
23306
23306
|
var ItemProgress = external_exports.object({ done: external_exports.number(), total: external_exports.number() });
|
|
23307
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
|
+
|
|
23308
23346
|
// ../contract/src/work/tasks.ts
|
|
23309
23347
|
var TaskStatus = StatusKey;
|
|
23310
23348
|
var TASK_SEVERITIES = ["low", "medium", "high", "critical"];
|
|
@@ -23415,8 +23453,12 @@ var AskInput = external_exports.object({ body: external_exports.string().trim().
|
|
|
23415
23453
|
var SessionPush = external_exports.discriminatedUnion("type", [
|
|
23416
23454
|
external_exports.object({ type: external_exports.literal("hello"), session: Session }),
|
|
23417
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 }),
|
|
23418
23458
|
/** The room is closing this session (someone ran `slop session end`). */
|
|
23419
|
-
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() })
|
|
23420
23462
|
]);
|
|
23421
23463
|
|
|
23422
23464
|
// ../contract/src/work/bindings.ts
|
|
@@ -23490,16 +23532,16 @@ var CliError = class extends Error {
|
|
|
23490
23532
|
|
|
23491
23533
|
// src/platform/api.ts
|
|
23492
23534
|
function createApi(options) {
|
|
23493
|
-
async function request(method, path3,
|
|
23535
|
+
async function request(method, path3, body2) {
|
|
23494
23536
|
const headers = { accept: "application/json" };
|
|
23495
|
-
if (
|
|
23537
|
+
if (body2 !== void 0) headers["content-type"] = "application/json";
|
|
23496
23538
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
23497
23539
|
let response;
|
|
23498
23540
|
try {
|
|
23499
23541
|
response = await fetch(`${options.baseUrl}/api${path3}`, {
|
|
23500
23542
|
method,
|
|
23501
23543
|
headers,
|
|
23502
|
-
body:
|
|
23544
|
+
body: body2 === void 0 ? void 0 : JSON.stringify(body2),
|
|
23503
23545
|
signal: AbortSignal.timeout(2e4)
|
|
23504
23546
|
});
|
|
23505
23547
|
} catch (error62) {
|
|
@@ -23522,9 +23564,9 @@ function createApi(options) {
|
|
|
23522
23564
|
return {
|
|
23523
23565
|
baseUrl: options.baseUrl,
|
|
23524
23566
|
get: (path3) => request("GET", path3),
|
|
23525
|
-
post: (path3,
|
|
23526
|
-
patch: (path3,
|
|
23527
|
-
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),
|
|
23528
23570
|
delete: (path3) => request("DELETE", path3)
|
|
23529
23571
|
};
|
|
23530
23572
|
}
|
|
@@ -23652,20 +23694,20 @@ function registerAuthCommands(program3) {
|
|
|
23652
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) => {
|
|
23653
23695
|
const baseUrl = (options.url || process.env.SLOP_URL || readConfig()?.baseUrl || DEFAULT_URL).replace(/\/+$/, "");
|
|
23654
23696
|
const api = createApi({ baseUrl, token: null });
|
|
23655
|
-
const
|
|
23697
|
+
const started2 = await api.post("/cli/auth/start", { machine: machineInfo() });
|
|
23656
23698
|
const remote = headless();
|
|
23657
23699
|
process.stderr.write(`
|
|
23658
23700
|
Approve this login in a browser${remote ? " on any device (this machine has no browser; your laptop or phone is fine)" : ""}:
|
|
23659
|
-
${
|
|
23701
|
+
${started2.verifyUrl}
|
|
23660
23702
|
|
|
23661
|
-
Confirm the code matches: ${
|
|
23703
|
+
Confirm the code matches: ${started2.userCode}
|
|
23662
23704
|
|
|
23663
23705
|
`);
|
|
23664
|
-
if (options.browser && !remote) openBrowser(
|
|
23665
|
-
const deadline = Date.now() +
|
|
23706
|
+
if (options.browser && !remote) openBrowser(started2.verifyUrl);
|
|
23707
|
+
const deadline = Date.now() + started2.expiresIn * 1e3;
|
|
23666
23708
|
while (Date.now() < deadline) {
|
|
23667
|
-
await sleep(
|
|
23668
|
-
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 });
|
|
23669
23711
|
if (poll.status === "pending") continue;
|
|
23670
23712
|
if (poll.status === "denied") throw new CliError("The login was denied in the web app");
|
|
23671
23713
|
if (poll.status === "expired") break;
|
|
@@ -23704,7 +23746,7 @@ This repository is linked to a Slopware Tasks board. Use the \`slop\` CLI to kee
|
|
|
23704
23746
|
- Before starting tracked work: \`slop task list\`, then \`slop task start <KEY>\`.
|
|
23705
23747
|
- When it is finished and verified: \`slop task done <KEY>\`.
|
|
23706
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 -\`.
|
|
23707
|
-
- 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\`.
|
|
23708
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.
|
|
23709
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.
|
|
23710
23752
|
- Add \`--json\` to any command for machine-readable output. \`slop task --help\` lists everything.
|
|
@@ -23908,14 +23950,91 @@ linked to ${link.projectKey} ${link.projectName} (${link.baseUrl})`
|
|
|
23908
23950
|
}
|
|
23909
23951
|
|
|
23910
23952
|
// src/commands/sessions.ts
|
|
23911
|
-
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.`;
|
|
23912
24031
|
|
|
23913
24032
|
// src/commands/tasks.ts
|
|
23914
|
-
import { readFileSync as
|
|
24033
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
23915
24034
|
|
|
23916
24035
|
// src/commands/items.ts
|
|
23917
|
-
import { readFileSync as
|
|
23918
|
-
var
|
|
24036
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
24037
|
+
var readText2 = (value) => value === "-" ? readFileSync6(0, "utf8").trim() : value;
|
|
23919
24038
|
var path2 = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}/items`;
|
|
23920
24039
|
function renderItems(items) {
|
|
23921
24040
|
if (items.length === 0) return "No plan yet. Write one: slop task plan <KEY> -";
|
|
@@ -23942,7 +24061,7 @@ function registerItemCommands(task, withProject) {
|
|
|
23942
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) => {
|
|
23943
24062
|
const api = authedApi();
|
|
23944
24063
|
const project = await resolveProject(api, options.project);
|
|
23945
|
-
const source = lines.length === 1 && lines[0] === "-" ?
|
|
24064
|
+
const source = lines.length === 1 && lines[0] === "-" ? readFileSync6(0, "utf8") : lines.join("\n");
|
|
23946
24065
|
const titles = source.split("\n").map((line) => line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "").trim()).filter(Boolean);
|
|
23947
24066
|
if (titles.length === 0) throw new CliError("Nothing to add", 'give items as arguments, or "-" and one per line on stdin');
|
|
23948
24067
|
const items = await api.post(path2(project.id, key), { items: titles.map((title3) => ({ title: title3 })) });
|
|
@@ -23952,7 +24071,7 @@ function registerItemCommands(task, withProject) {
|
|
|
23952
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) => {
|
|
23953
24072
|
const api = authedApi();
|
|
23954
24073
|
const project = await resolveProject(api, options.project);
|
|
23955
|
-
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) : "" });
|
|
23956
24075
|
emit(items, () => renderItems(items));
|
|
23957
24076
|
});
|
|
23958
24077
|
for (const [name, done, help] of [
|
|
@@ -23971,7 +24090,7 @@ function registerItemCommands(task, withProject) {
|
|
|
23971
24090
|
const api = authedApi();
|
|
23972
24091
|
const project = await resolveProject(api, options.project);
|
|
23973
24092
|
const target = await itemAt(api, project.id, key, ref);
|
|
23974
|
-
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) });
|
|
23975
24094
|
emit(items, () => renderItems(items));
|
|
23976
24095
|
});
|
|
23977
24096
|
withProject(item.command("rename <key> <item> <title>")).description("Retitle an item.").action(async (key, ref, title3, options) => {
|
|
@@ -23999,7 +24118,7 @@ function oneOf(value, allowed, what) {
|
|
|
23999
24118
|
if (allowed.includes(normalized)) return normalized;
|
|
24000
24119
|
throw new CliError(`Unknown ${what} "${value}"`, `use one of: ${allowed.join(", ")}`);
|
|
24001
24120
|
}
|
|
24002
|
-
var readDescription = (value) => value === "-" ?
|
|
24121
|
+
var readDescription = (value) => value === "-" ? readFileSync7(0, "utf8").trim() : value;
|
|
24003
24122
|
var taskUrl = (baseUrl, projectId, task) => `${baseUrl}/projects/${projectId}?task=${task.key}`;
|
|
24004
24123
|
var loadProject = (api, id) => api.get(`/projects/${id}`);
|
|
24005
24124
|
function resolveStatus(project, ref) {
|
|
@@ -24014,17 +24133,17 @@ function statusInCategory(project, category) {
|
|
|
24014
24133
|
return status.key;
|
|
24015
24134
|
}
|
|
24016
24135
|
var ASSIGNEE_HELP = 'an email, a name, "me", or "none"';
|
|
24017
|
-
async function resolveAssignee(api, projectId,
|
|
24018
|
-
if (
|
|
24019
|
-
if (
|
|
24020
|
-
if (
|
|
24021
|
-
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();
|
|
24022
24141
|
const members2 = await listMembers(api, projectId);
|
|
24023
24142
|
const exact = members2.filter((member) => member.email.toLowerCase() === needle || member.name.toLowerCase() === needle);
|
|
24024
24143
|
const matches = exact.length > 0 ? exact : members2.filter((member) => member.email.toLowerCase().includes(needle) || member.name.toLowerCase().includes(needle));
|
|
24025
24144
|
if (matches.length === 1) return matches[0].userId;
|
|
24026
|
-
if (matches.length === 0) throw new CliError(`Nobody in this project matches "${
|
|
24027
|
-
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");
|
|
24028
24147
|
}
|
|
24029
24148
|
function registerTaskCommands(program3) {
|
|
24030
24149
|
const task = program3.command("task").alias("tasks").description("Create, read and move tasks in the linked project.");
|
|
@@ -24097,10 +24216,10 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24097
24216
|
const updated = await api.patch(`/projects/${project.id}/tasks/${encodeURIComponent(key)}`, { status: resolveStatus(project, column) });
|
|
24098
24217
|
emit(updated, () => `${updated.key} is now in ${statusLabel(project.statuses, updated.status)}.`);
|
|
24099
24218
|
});
|
|
24100
|
-
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) => {
|
|
24101
24220
|
const api = authedApi();
|
|
24102
24221
|
const project = await resolveProject(api, options.project);
|
|
24103
|
-
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) });
|
|
24104
24223
|
emit(updated, () => `${updated.key} is assigned to ${updated.assignee?.name ?? "nobody"}.`);
|
|
24105
24224
|
});
|
|
24106
24225
|
for (const [name, category] of [["start", "in_progress"], ["done", "done"]]) {
|
|
@@ -24151,11 +24270,10 @@ ${taskUrl(api.baseUrl, project.id, created)}`);
|
|
|
24151
24270
|
|
|
24152
24271
|
// src/commands/sessions.ts
|
|
24153
24272
|
var taskPath = (projectId, key) => `/projects/${projectId}/tasks/${encodeURIComponent(key)}`;
|
|
24154
|
-
var readText2 = (value) => value === "-" ? readFileSync7(0, "utf8").trim() : value;
|
|
24155
24273
|
var sessionLine = (session) => [session.taskKey, session.user.name, session.machine.name, session.startedAt];
|
|
24156
24274
|
function messageLine(record2) {
|
|
24157
|
-
const
|
|
24158
|
-
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}`;
|
|
24159
24277
|
}
|
|
24160
24278
|
function registerSessionCommands(program3, task) {
|
|
24161
24279
|
const withProject = (command) => command.option("-p, --project <key>", "project key (default: the project this repository is linked to)");
|
|
@@ -24165,10 +24283,19 @@ function registerSessionCommands(program3, task) {
|
|
|
24165
24283
|
const project = await loadProject(api, (await resolveProject(api, options.project)).id);
|
|
24166
24284
|
const assigneeId = await resolveAssignee(api, project.id, "me");
|
|
24167
24285
|
const updated = await api.patch(taskPath(project.id, key), { assigneeId, status: statusInCategory(project, "in_progress") });
|
|
24168
|
-
|
|
24169
|
-
|
|
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
|
+
});
|
|
24170
24297
|
});
|
|
24171
|
-
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) => {
|
|
24172
24299
|
const api = authedApi();
|
|
24173
24300
|
const project = await resolveProject(api, options.project);
|
|
24174
24301
|
const ticket = await api.post(`${taskPath(project.id, key)}/session/ticket`);
|
|
@@ -24190,6 +24317,16 @@ Go live (in the background, and relaunch it after each message): slop session w
|
|
|
24190
24317
|
resolve2({ ended: true });
|
|
24191
24318
|
return;
|
|
24192
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;
|
|
24193
24330
|
resolve2({ record: push.record });
|
|
24194
24331
|
});
|
|
24195
24332
|
socket.addEventListener("close", (event) => {
|
|
@@ -24202,8 +24339,9 @@ Go live (in the background, and relaunch it after each message): slop session w
|
|
|
24202
24339
|
resolve2({ closed: "socket error" });
|
|
24203
24340
|
});
|
|
24204
24341
|
});
|
|
24205
|
-
if ("record" in outcome) {
|
|
24206
|
-
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));
|
|
24207
24345
|
socket.close(1e3, "handled");
|
|
24208
24346
|
await new Promise((resolve2) => {
|
|
24209
24347
|
socket.addEventListener("close", () => resolve2(), { once: true });
|
|
@@ -24230,12 +24368,6 @@ Go live (in the background, and relaunch it after each message): slop session w
|
|
|
24230
24368
|
const sessions = await api.get(`/projects/${project.id}/sessions`);
|
|
24231
24369
|
emit(sessions, () => sessions.length === 0 ? "Nobody is live." : table([["TASK", "WHO", "MACHINE", "SINCE"], ...sessions.map(sessionLine)]));
|
|
24232
24370
|
});
|
|
24233
|
-
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) => {
|
|
24234
|
-
const api = authedApi();
|
|
24235
|
-
const project = await resolveProject(api, options.project);
|
|
24236
|
-
const result = await api.post(`${taskPath(project.id, key)}/ask`, { body: readText2(text) });
|
|
24237
|
-
emit(result, () => result.delivered ? `Delivered to ${result.delivered} live session${result.delivered === 1 ? "" : "s"}.` : "Nobody is live on it; left on the timeline.");
|
|
24238
|
-
});
|
|
24239
24371
|
}
|
|
24240
24372
|
|
|
24241
24373
|
// src/commands/skills.ts
|
|
@@ -24260,10 +24392,12 @@ ${written.map((path3) => ` ${path3}`).join("\n")}`);
|
|
|
24260
24392
|
}
|
|
24261
24393
|
|
|
24262
24394
|
// src/index.ts
|
|
24263
|
-
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)");
|
|
24264
24396
|
registerAuthCommands(program2);
|
|
24265
24397
|
registerProjectCommands(program2);
|
|
24266
|
-
|
|
24398
|
+
var taskCommand = registerTaskCommands(program2);
|
|
24399
|
+
registerSessionCommands(program2, taskCommand);
|
|
24400
|
+
registerThreadCommands(program2, taskCommand);
|
|
24267
24401
|
registerGuideCommand(program2);
|
|
24268
24402
|
registerSkillCommands(program2);
|
|
24269
24403
|
program2.parseAsync().catch(fail);
|