@workser/cli 0.6.6 → 0.6.8

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/AGENTS.md CHANGED
@@ -71,10 +71,11 @@ workser workflow list | create <name> [--body <json>] | get <id>
71
71
  workser workflow activate <id> | deactivate <id> | run <id> [--wait] [--body <json>]
72
72
  workser workflow runs <id> # past executions of a workflow
73
73
  workser workflow nodes [query] # search the node-type catalog
74
- workser app list [--toolkit <slug>] # connectable + connected third-party apps
75
- workser app connect <toolkit> | disconnect <connectionId>
76
- workser app tools <toolkit> # a connected app's callable actions
77
- workser app run <toolSlug> [--body <json>] # execute one action (e.g. GOOGLESHEETS_APPEND_ROW)
74
+ workser connection list [--toolkit <slug>] # connectable + connected third-party apps
75
+ workser connection search "<query>" [--toolkit <slug>] [--limit N] # full-text search across every toolkit's actions
76
+ workser connection connect <toolkit> | disconnect <connectionId>
77
+ workser connection tools <toolkit> # browse one connected toolkit's callable actions
78
+ workser connection run <toolSlug> [--body <json>] # execute one action (e.g. GOOGLESHEETS_APPEND_ROW)
78
79
 
79
80
  workser tool list # computer-use tools available to you now
80
81
  workser tool run <name> [--body <json>] # filesystem/shell/screenshot/input/clipboard/browser
@@ -87,8 +88,27 @@ workser artifact add <path> [--kind <k>] [-d <text>] # record a finished delive
87
88
  workser artifact add --url <url> --kind app # record a deployed app
88
89
  workser artifact run # which task you're attached to
89
90
 
91
+ workser search "<query>" [-n <maxResults>] # Google-grounded web search
92
+
93
+ workser image generate "<prompt>" [-r <url>...] [-o <path>] # generate an image, get a public URL
94
+ workser image understand "<query>" [--url <u>|--file <p>] [-t <task>] # describe/caption/answer questions about an image
95
+ workser video understand "<query>" [--url <u>|--file <p>] [-t <task>] # summarize/describe a video (URL also accepts YouTube)
96
+ workser audio understand "<query>" [--url <u>|--file <p>] [-t <task>] # transcribe/describe audio (URL also accepts YouTube)
97
+ # image/video/audio understand: the fallback for a text-only model, or media you have no other
98
+ # way to see/hear. --url is fetched server-side (no size ceiling); --file reads a small local
99
+ # file and sends it inline — for anything bigger, `workser storage upload` it and pass --url.
100
+
90
101
  workser ask "<question>" [--type <t>] [--option <o>] # ask the user, WAIT for the answer
91
102
 
103
+ # Tasks and subtasks (the plan shown in Orbit)
104
+ workser task show [id] # inspect the task and its current plan
105
+ workser task subtask list [taskId] # list the task's subtasks
106
+ workser task subtask add "<title>" --role <role> # add one proposed subtask
107
+ workser task subtask update <id> --role <role> # fix its teammate after creation
108
+ workser task subtask update <id> --title "…" --note "…" --scope <paths...>
109
+ workser task subtask remove <id> # remove it before work starts
110
+ workser task start [id] # starts only an already-approved plan
111
+
92
112
  # owner-only (will return owner_only / exit 6 — ask the user to do these in Orbit):
93
113
  # project create, project use, env rm, domain set
94
114
  ```
@@ -108,12 +128,12 @@ workser doc create "<title>" --markdown <text> | doc update <id> --markdown <tex
108
128
  already doing and what this project chose on purpose, so you don't re-file work or
109
129
  quietly reverse a decision.
110
130
 
111
- **A plan with phases goes on the Board before you build it.** The moment you split a
112
- task into more than one phase: one `board create` card per phase (only the one you're
113
- doing goes to `in-progress`), the plan itself as one `doc create --markdown` with
114
- **no `--work-item`**, because a linked doc is shown on its card and hidden from the
115
- Docs panel — and a `decision create` if the plan settled a real tradeoff. A plan that
116
- lives only in your reply is gone as soon as the conversation scrolls.
131
+ **A plan with phases goes on this task's Subtasks list, not the Board.** Use one
132
+ `task subtask add` per phase. If you spot a bad title, role, note, or file scope after
133
+ creation, correct that existing row with `task subtask update <id>`; do not say it is
134
+ locked, create a replacement, or duplicate the plan. Write the plan narrative once
135
+ as `doc create --markdown` with no `--work-item`, and record a real tradeoff with
136
+ `decision create`.
117
137
 
118
138
  **Keep it true as you work.** `board move <id> in-progress` when you pick it up,
119
139
  `in-review` when it's ready to look at, `board close <id>` when it's done and
@@ -215,11 +235,14 @@ done: `workser workflow create` builds an event-driven, multi-step automation (t
215
235
  same engine Workser's own web Workflow tab uses) — nodes, connections, and triggers go
216
236
  in `--body` as JSON; browse `workser workflow nodes` first to see what's available.
217
237
  Before an automation can use a third-party app (Gmail, Slack, Stripe, Google Sheets,
218
- ...), the user connects it once via `workser app connect <toolkit>` (this opens an
219
- OAuth link — ask the user to complete it, then continue); after that, `workser app
220
- run <toolSlug> --body '{"...":...}'` calls any of its actions directly, and a workflow
221
- node can call the same toolkit. Use `workser app list` to see what's already
222
- connected before assuming you need to ask the user to connect something new.
238
+ ...), the user connects it once via `workser connection connect <toolkit>` (this opens
239
+ an OAuth link — ask the user to complete it, then continue); after that, `workser
240
+ connection run <toolSlug> --body '{"...":...}'` calls any of its actions directly, and
241
+ a workflow node can call the same toolkit. Use `workser connection list` to see what's
242
+ already connected before assuming you need to ask the user to connect something new.
243
+ Don't know the exact action slug? `workser connection search "<what you want to do>"`
244
+ searches across every toolkit's actions at once — cheaper than browsing toolkit by
245
+ toolkit with `workser connection tools <toolkit>`.
223
246
 
224
247
  ## Delegate to roles
225
248
  The user can configure **roles** — named specialists each backed by a local CLI agent
package/dist/index.js CHANGED
@@ -3794,7 +3794,7 @@ and the spec is the difference.
3794
3794
  topic: "automation",
3795
3795
  title: "Workflows & connected apps",
3796
3796
  summary: "Build automations that outlive the run; use Gmail, Slack, Stripe, Sheets.",
3797
- commands: ["workflow", "app"],
3797
+ commands: ["workflow", "connection"],
3798
3798
  source: "skills/workser/reference/automation.md",
3799
3799
  body: `# Workflows & connected apps
3800
3800
 
@@ -3807,10 +3807,11 @@ workser workflow activate <id> | deactivate <id> | run <id> [--wait] [--body <js
3807
3807
  workser workflow runs <id> # past executions of a workflow
3808
3808
  workser workflow nodes [query] # search the node-type catalog
3809
3809
 
3810
- workser app list [--toolkit <slug>] # connectable + connected third-party apps
3811
- workser app connect <toolkit> | disconnect <connectionId>
3812
- workser app tools <toolkit> # a connected app's callable actions
3813
- workser app run <toolSlug> [--body <json>] # execute one action
3810
+ workser connection list [--toolkit <slug>] # connectable + connected third-party apps
3811
+ workser connection search "<query>" [--toolkit <slug>] [--limit N] # find an action across every toolkit
3812
+ workser connection connect <toolkit> | disconnect <connectionId>
3813
+ workser connection tools <toolkit> # browse one connected toolkit's actions
3814
+ workser connection run <toolSlug> [--body <json>] # execute one action
3814
3815
  \`\`\`
3815
3816
 
3816
3817
  ## Building a workflow
@@ -3826,12 +3827,13 @@ Created workflows start inactive: \`workser workflow activate <id>\` when it's r
3826
3827
 
3827
3828
  ## Using a connected app
3828
3829
 
3829
- 1. \`workser app list\` \u2014 check what's already connected before asking for anything.
3830
- 2. If it isn't: \`workser app connect <toolkit>\` returns an OAuth link. The **user**
3830
+ 1. \`workser connection list\` \u2014 check what's already connected before asking for anything.
3831
+ 2. If it isn't: \`workser connection connect <toolkit>\` returns an OAuth link. The **user**
3831
3832
  must open it; you cannot complete OAuth on their behalf. Wait, then continue.
3832
- 3. \`workser app tools <toolkit>\` \u2014 read the argument schema rather than guessing
3833
- field names.
3834
- 4. \`workser app run <toolSlug> --body '{"\u2026":\u2026}'\` \u2014 e.g. \`GOOGLESHEETS_APPEND_ROW\`,
3833
+ 3. Don't know the exact action? \`workser connection search "<query>"\` finds it across
3834
+ every toolkit; \`workser connection tools <toolkit>\` browses one toolkit you already
3835
+ know. Either way, read the argument schema rather than guessing field names.
3836
+ 4. \`workser connection run <toolSlug> --body '{"\u2026":\u2026}'\` \u2014 e.g. \`GOOGLESHEETS_APPEND_ROW\`,
3835
3837
  \`GMAIL_SEND_EMAIL\`.
3836
3838
 
3837
3839
  **A \`run\` is a real side effect in someone's real account.** Sending an email or
@@ -4566,11 +4568,11 @@ up front \u2014 most of the apps a goal will touch don't exist when it's propose
4566
4568
  },
4567
4569
  {
4568
4570
  topic: "images",
4569
- title: "Image generation",
4570
- summary: "Generate images from a prompt, optionally conditioned on existing images.",
4571
- commands: ["image"],
4571
+ title: "Image generation & media understanding",
4572
+ summary: "Generate images from a prompt; describe/transcribe an image, video, or audio clip you can't natively see or hear.",
4573
+ commands: ["image", "video", "audio"],
4572
4574
  source: "skills/workser/reference/images.md",
4573
- body: `# Image generation
4575
+ body: `# Image generation & media understanding
4574
4576
 
4575
4577
  \`\`\`
4576
4578
  workser image generate "<prompt>" # alias: workser image gen
@@ -4586,7 +4588,7 @@ workser image generate "flat illustration of a farm delivery van, brand colors"
4586
4588
  workser image gen "same van, from the side" -r https://\u2026 -o ./public/van.png --json
4587
4589
  \`\`\`
4588
4590
 
4589
- ## Notes that matter
4591
+ ## Notes that matter (generation)
4590
4592
 
4591
4593
  - **Reference images are image-to-image conditioning**, not attachments. Up to 4;
4592
4594
  anything beyond that is dropped.
@@ -4598,6 +4600,42 @@ workser image gen "same van, from the side" -r https://\u2026 -o ./public/van.pn
4598
4600
  exist only as URLs.
4599
4601
  - **Placeholder art is not a deliverable.** Generating a hero image to unblock a
4600
4602
  layout is fine; shipping it as the user's brand asset without asking is not.
4603
+
4604
+ ## Understanding media you can't natively see or hear
4605
+
4606
+ The fallback for a text-only model, or media you have no other way to reach: describe
4607
+ an image, summarize/transcribe a video, transcribe/describe audio. Runs server-side
4608
+ (Gemini) \u2014 you never need a model key.
4609
+
4610
+ \`\`\`
4611
+ workser image understand "<query>" [--url <u> | --file <p>] [-t <task>]
4612
+ workser video understand "<query>" [--url <u> | --file <p>] [-t <task>]
4613
+ workser audio understand "<query>" [--url <u> | --file <p>] [-t <task>]
4614
+ \`\`\`
4615
+
4616
+ \`\`\`bash
4617
+ workser image understand "what's wrong with this layout?" --url https://\u2026/screenshot.png --json
4618
+ workser video understand "what happens at the end?" --url https://youtu.be/\u2026 -t timestamp_analysis --json
4619
+ workser audio understand "transcribe this" --file ./voicemail.m4a -t transcribe --json
4620
+ \`\`\`
4621
+
4622
+ ## Notes that matter (understanding)
4623
+
4624
+ - **\`--url\` vs \`--file\`**: \`--url\` is fetched server-side with no size ceiling \u2014 the
4625
+ right choice for anything already hosted (a project's own storage bucket, a public
4626
+ link, a YouTube URL for video/audio). \`--file\` is read and sent inline by the CLI
4627
+ itself, so it's bounded by the daemon's own request-size limit \u2014 for a small local
4628
+ file only (a screenshot, a short voice memo). Something bigger: \`workser storage
4629
+ upload\` it first, then pass the returned URL with \`--url\`.
4630
+ - **\`-t/--task\` shapes the answer, it doesn't gate what you can ask** \u2014 \`general\` (the
4631
+ default) takes any free-form \`<query>\`. The other values just bias the prompt
4632
+ toward a specific shape: \`caption\`/\`visual_qa\`/\`object_detection\`/\`segmentation\`
4633
+ for images; \`summarize\`/\`describe\`/\`visual_qa\`/\`timestamp_analysis\` for video;
4634
+ \`transcribe\`/\`describe\`/\`audio_qa\`/\`speaker_diarization\`/\`emotion_detection\` for
4635
+ audio.
4636
+ - **This is billed to the project's organization**, same as image generation \u2014 it's
4637
+ a real provider call, not free introspection. Don't loop it over every file in a
4638
+ folder "just in case"; use it when you actually need to know what's in one.
4601
4639
  `
4602
4640
  },
4603
4641
  {
@@ -7498,11 +7536,11 @@ function registerWorkflow(program3) {
7498
7536
  );
7499
7537
  }
7500
7538
 
7501
- // src/commands/app.ts
7539
+ // src/commands/connection.ts
7502
7540
  var import_picocolors20 = __toESM(require_picocolors(), 1);
7503
- function registerApp(program3) {
7504
- const appCmd = program3.command("app").description("Connect and use third-party app integrations (Gmail, Slack, Stripe, ...)");
7505
- appCmd.command("list").description("List connectable toolkits and this project's existing connections").option("--toolkit <slug>", "filter connections to one toolkit").action(
7541
+ function registerConnection(program3) {
7542
+ const connection = program3.command("connection").description("Connect and use third-party app connections (Gmail, Slack, Stripe, ...)");
7543
+ connection.command("list").description("List connectable toolkits and this project's existing connections").option("--toolkit <slug>", "filter connections to one toolkit").action(
7506
7544
  action(async ({ ctx, opts }) => {
7507
7545
  const projectId = requireProject(ctx);
7508
7546
  const [catalog, connections] = await Promise.all([
@@ -7518,7 +7556,21 @@ function registerApp(program3) {
7518
7556
  });
7519
7557
  })
7520
7558
  );
7521
- appCmd.command("connect <toolkit>").description("Start OAuth to connect a toolkit").option("--reference-user-id <id>", "connect on behalf of one of the app's own end-users").option("--redirect-url <url>", "where to send the user after OAuth completes").action(
7559
+ connection.command("search <query>").description('Full-text search for actions across every toolkit (or one, with --toolkit) \u2014 e.g. "send email"').option("--toolkit <slug>", "narrow the search to one toolkit").option("--limit <n>", "max results").action(
7560
+ action(async ({ ctx, args, opts }) => {
7561
+ const projectId = requireProject(ctx);
7562
+ const items = await api(ctx, `/v1/projects/${projectId}/integrations/search`, {
7563
+ query: { q: args[0], toolkit: opts.toolkit, limit: opts.limit }
7564
+ });
7565
+ ok(items, () => {
7566
+ if (!items?.length) return line(import_picocolors20.default.dim("No matching actions."));
7567
+ for (const t of items) {
7568
+ line(`${t.slug} ${import_picocolors20.default.dim(`[${t.toolkit}]`)} ${t.description ?? ""}`);
7569
+ }
7570
+ });
7571
+ })
7572
+ );
7573
+ connection.command("connect <toolkit>").description("Start OAuth to connect a toolkit").option("--reference-user-id <id>", "connect on behalf of one of the app's own end-users").option("--redirect-url <url>", "where to send the user after OAuth completes").action(
7522
7574
  action(async ({ ctx, args, opts }) => {
7523
7575
  const projectId = requireProject(ctx);
7524
7576
  const res = await api(ctx, `/v1/projects/${projectId}/integrations/connect`, {
@@ -7534,7 +7586,7 @@ function registerApp(program3) {
7534
7586
  );
7535
7587
  })
7536
7588
  );
7537
- appCmd.command("disconnect <connectionId>").description("Disconnect a connection").action(
7589
+ connection.command("disconnect <connectionId>").description("Disconnect a connection").action(
7538
7590
  action(async ({ ctx, args }) => {
7539
7591
  const projectId = requireProject(ctx);
7540
7592
  const res = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}`, {
@@ -7543,7 +7595,7 @@ function registerApp(program3) {
7543
7595
  ok(res, () => line("Disconnected."));
7544
7596
  })
7545
7597
  );
7546
- appCmd.command("tools <toolkit>").description("List a connected toolkit's callable tools + their arguments").action(
7598
+ connection.command("tools <toolkit>").description("List a connected toolkit's callable tools + their arguments").action(
7547
7599
  action(async ({ ctx, args }) => {
7548
7600
  const projectId = requireProject(ctx);
7549
7601
  const items = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}/tools`);
@@ -7553,7 +7605,7 @@ function registerApp(program3) {
7553
7605
  });
7554
7606
  })
7555
7607
  );
7556
- appCmd.command("run <toolSlug>").description("Execute one tool action (e.g. GOOGLESHEETS_APPEND_ROW)").option("--body <args>", "the tool's arguments as a JSON string", "{}").option("--reference-user-id <id>", "run on behalf of one of the app's own end-users").action(
7608
+ connection.command("run <toolSlug>").description("Execute one tool action (e.g. GOOGLESHEETS_APPEND_ROW)").option("--body <args>", "the tool's arguments as a JSON string", "{}").option("--reference-user-id <id>", "run on behalf of one of the app's own end-users").action(
7557
7609
  action(async ({ ctx, args, opts }) => {
7558
7610
  const projectId = requireProject(ctx);
7559
7611
  const res = await api(ctx, `/v1/projects/${projectId}/integrations/${args[0]}/execute`, {
@@ -7917,6 +7969,56 @@ function printRun(run) {
7917
7969
  // src/commands/image.ts
7918
7970
  import { writeFile as writeFile4, mkdir } from "fs/promises";
7919
7971
  import { dirname as dirname3, resolve as resolve4 } from "path";
7972
+
7973
+ // src/media-source.ts
7974
+ import { readFile as readFile4 } from "fs/promises";
7975
+ import { extname } from "path";
7976
+ var EXT_MIME = {
7977
+ ".jpg": "image/jpeg",
7978
+ ".jpeg": "image/jpeg",
7979
+ ".png": "image/png",
7980
+ ".webp": "image/webp",
7981
+ ".gif": "image/gif",
7982
+ ".heic": "image/heic",
7983
+ ".mp4": "video/mp4",
7984
+ ".mov": "video/quicktime",
7985
+ ".webm": "video/webm",
7986
+ ".avi": "video/x-msvideo",
7987
+ ".mp3": "audio/mp3",
7988
+ ".wav": "audio/wav",
7989
+ ".m4a": "audio/mp4",
7990
+ ".ogg": "audio/ogg",
7991
+ ".flac": "audio/flac",
7992
+ ".aac": "audio/aac"
7993
+ };
7994
+ async function resolveMediaSource(opts) {
7995
+ if (opts.url && opts.file) {
7996
+ throw new WorkserError("Pass --url or --file, not both.", {
7997
+ code: "bad_request"
7998
+ });
7999
+ }
8000
+ if (opts.url) {
8001
+ return { type: "url", url: opts.url };
8002
+ }
8003
+ if (opts.file) {
8004
+ let bytes;
8005
+ try {
8006
+ bytes = await readFile4(opts.file);
8007
+ } catch (e) {
8008
+ throw new WorkserError(
8009
+ `Could not read ${opts.file}: ${e instanceof Error ? e.message : String(e)}`,
8010
+ { code: "bad_request" }
8011
+ );
8012
+ }
8013
+ const mimeType = EXT_MIME[extname(opts.file).toLowerCase()] ?? "application/octet-stream";
8014
+ return { type: "base64", data: bytes.toString("base64"), mimeType };
8015
+ }
8016
+ throw new WorkserError("Pass --url <url> or --file <path> for the media to look at.", {
8017
+ code: "bad_request"
8018
+ });
8019
+ }
8020
+
8021
+ // src/commands/image.ts
7920
8022
  function registerImage(program3) {
7921
8023
  const image = program3.command("image").description("Generate images from a text prompt");
7922
8024
  image.command("generate <prompt>").alias("gen").description("Generate an image and return its public URL").option(
@@ -7969,6 +8071,23 @@ function registerImage(program3) {
7969
8071
  });
7970
8072
  })
7971
8073
  );
8074
+ image.command("understand <query>").description(
8075
+ "Describe/caption/answer questions about an image \u2014 the fallback for a text-only model or an image you have no other way to see"
8076
+ ).option("-u, --url <url>", "the image's URL (fetched server-side)").option("-f, --file <path>", "a local image file (read + sent inline; small files only)").option(
8077
+ "-t, --task <task>",
8078
+ "caption | visual_qa | object_detection | segmentation | general",
8079
+ "general"
8080
+ ).action(
8081
+ action(async ({ ctx, opts, args }) => {
8082
+ const projectId = requireProject(ctx);
8083
+ const source = await resolveMediaSource({ url: opts.url, file: opts.file });
8084
+ const res = await api(ctx, `/projects/${projectId}/images/understand`, {
8085
+ method: "POST",
8086
+ body: { source, query: args[0], task: opts.task }
8087
+ });
8088
+ ok(res, () => line(res?.answer ?? ""));
8089
+ })
8090
+ );
7972
8091
  }
7973
8092
  async function download(url, output) {
7974
8093
  const target = resolve4(output);
@@ -7984,6 +8103,46 @@ async function download(url, output) {
7984
8103
  return target;
7985
8104
  }
7986
8105
 
8106
+ // src/commands/video.ts
8107
+ function registerVideo(program3) {
8108
+ const video = program3.command("video").description("Understand video (the fallback for a model that can't watch it itself)");
8109
+ video.command("understand <query>").description("Summarize/describe/answer questions about a video").option("-u, --url <url>", "the video's URL (fetched server-side; also accepts a YouTube URL)").option("-f, --file <path>", "a local video file (read + sent inline; small clips only)").option(
8110
+ "-t, --task <task>",
8111
+ "summarize | describe | visual_qa | timestamp_analysis | general",
8112
+ "general"
8113
+ ).action(
8114
+ action(async ({ ctx, opts, args }) => {
8115
+ const projectId = requireProject(ctx);
8116
+ const source = await resolveMediaSource({ url: opts.url, file: opts.file });
8117
+ const res = await api(ctx, `/projects/${projectId}/video/understand`, {
8118
+ method: "POST",
8119
+ body: { source, query: args[0], task: opts.task }
8120
+ });
8121
+ ok(res, () => line(res?.answer ?? ""));
8122
+ })
8123
+ );
8124
+ }
8125
+
8126
+ // src/commands/audio.ts
8127
+ function registerAudio(program3) {
8128
+ const audio = program3.command("audio").description("Understand audio (the fallback for a model that can't hear it itself)");
8129
+ audio.command("understand <query>").description("Transcribe/describe/answer questions about audio").option("-u, --url <url>", "the audio's URL (fetched server-side; also accepts a YouTube URL)").option("-f, --file <path>", "a local audio file (read + sent inline; small clips only)").option(
8130
+ "-t, --task <task>",
8131
+ "transcribe | describe | audio_qa | speaker_diarization | emotion_detection | general",
8132
+ "general"
8133
+ ).action(
8134
+ action(async ({ ctx, opts, args }) => {
8135
+ const projectId = requireProject(ctx);
8136
+ const source = await resolveMediaSource({ url: opts.url, file: opts.file });
8137
+ const res = await api(ctx, `/projects/${projectId}/audio/understand`, {
8138
+ method: "POST",
8139
+ body: { source, query: args[0], task: opts.task }
8140
+ });
8141
+ ok(res, () => line(res?.answer ?? ""));
8142
+ })
8143
+ );
8144
+ }
8145
+
7987
8146
  // src/commands/ask.ts
7988
8147
  var import_picocolors25 = __toESM(require_picocolors(), 1);
7989
8148
  var TYPES = [
@@ -8335,15 +8494,33 @@ function collect2(value, previous) {
8335
8494
 
8336
8495
  // src/commands/task.ts
8337
8496
  var import_picocolors28 = __toESM(require_picocolors(), 1);
8338
- var STATUSES2 = ["todo", "working", "checking", "ready", "accepted", "archived"];
8497
+ var STATUSES2 = [
8498
+ "todo",
8499
+ "working",
8500
+ "checking",
8501
+ "ready",
8502
+ "accepted",
8503
+ "archived"
8504
+ ];
8339
8505
  var ROLES = ["pm", "architect", "web", "api", "automation", "qa"];
8340
- var KINDS2 = ["data_reports", "web", "mobile", "service", "automation", "docs"];
8506
+ var KINDS2 = [
8507
+ "data_reports",
8508
+ "web",
8509
+ "mobile",
8510
+ "service",
8511
+ "automation",
8512
+ "docs"
8513
+ ];
8341
8514
  function registerTask(program3) {
8342
8515
  const task = program3.command("task").description("The project's tasks and their subtasks (AI Tech Team)");
8343
- task.command("list").description("List the board's tasks \u2014 run this before starting anything").option("--status <value>", `only tasks in this status (${STATUSES2.join(" | ")})`).option("--label <value>", "only tasks carrying this label").option("--limit <n>", "cap the number of tasks returned").action(
8516
+ task.command("list").description("List the board's tasks \u2014 run this before starting anything").option(
8517
+ "--status <value>",
8518
+ `only tasks in this status (${STATUSES2.join(" | ")})`
8519
+ ).option("--label <value>", "only tasks carrying this label").option("--limit <n>", "cap the number of tasks returned").action(
8344
8520
  action(async ({ ctx, opts }) => {
8345
8521
  requireProject(ctx);
8346
- if (opts.status !== void 0) assertOneOf("--status", opts.status, STATUSES2);
8522
+ if (opts.status !== void 0)
8523
+ assertOneOf("--status", opts.status, STATUSES2);
8347
8524
  const rows = await api(ctx, "/v1/project-tasks", {
8348
8525
  query: {
8349
8526
  status: opts.status,
@@ -8365,7 +8542,10 @@ function registerTask(program3) {
8365
8542
  ).action(
8366
8543
  action(async ({ ctx, args }) => {
8367
8544
  const id = args[0] || ctx.parentTaskId || resolveTaskId(ctx);
8368
- const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
8545
+ const row = await api(
8546
+ ctx,
8547
+ `/v1/project-tasks/${encodeURIComponent(id)}`
8548
+ );
8369
8549
  const goalId = row.goal_id;
8370
8550
  const goal = goalId ? await api(
8371
8551
  ctx,
@@ -8395,8 +8575,14 @@ function registerTask(program3) {
8395
8575
  goalId: opts.goal,
8396
8576
  phase: opts.phase,
8397
8577
  targets: [
8398
- ...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
8399
- ...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
8578
+ ...(opts.app ?? []).map((appId) => ({
8579
+ kind: "app",
8580
+ appId
8581
+ })),
8582
+ ...(opts.infra ?? []).map((ref) => ({
8583
+ kind: "infra",
8584
+ ref
8585
+ }))
8400
8586
  ],
8401
8587
  ...hasChannelOrigin ? {
8402
8588
  channelId: ctx.projectChannelId,
@@ -8436,8 +8622,11 @@ function registerTask(program3) {
8436
8622
  ...channelMessageError ? { channelMessageError } : {}
8437
8623
  };
8438
8624
  ok(result, () => {
8439
- line(`${import_picocolors28.default.green("opened")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
8440
- if (channelMessage) line(import_picocolors28.default.dim("posted to the channel as Project Manager"));
8625
+ line(
8626
+ `${import_picocolors28.default.green("opened")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`
8627
+ );
8628
+ if (channelMessage)
8629
+ line(import_picocolors28.default.dim("posted to the channel as Project Manager"));
8441
8630
  if (channelMessageError) {
8442
8631
  warn(
8443
8632
  `Task opened, but its Project Manager card could not be posted: ${channelMessageError.message}`
@@ -8449,10 +8638,19 @@ function registerTask(program3) {
8449
8638
  const subtask = task.command("subtask").description("The subtasks a task is broken into");
8450
8639
  subtask.command("add <title>").description(
8451
8640
  'Add one subtask, e.g. `workser task subtask add "Build the upload screen" --role web`'
8452
- ).option("--task <id>", "the parent task (defaults to the task this run is inside)").option("--role <value>", `who does it (${ROLES.join(" | ")})`).option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--note <text>", "one sentence on what this subtask does").option(
8641
+ ).option(
8642
+ "--task <id>",
8643
+ "the parent task (defaults to the task this run is inside)"
8644
+ ).option("--role <value>", `who does it (${ROLES.join(" | ")})`).option("--kind <value>", `what it produces (${KINDS2.join(" | ")})`).option("--note <text>", "one sentence on what this subtask does").option(
8453
8645
  "--app <id...>",
8454
8646
  "the app this subtask is for \u2014 one id runs it inside that app's folder; leave it off and it runs at the project, seeing every app"
8455
- ).option("--infra <ref...>", "shared setup it touches (database | storage | auth | hosting | jobs)").option("--scope <path...>", "files or folders THIS subtask owns").option("--depends-on <id...>", "subtasks that must finish first (key or id)").action(
8647
+ ).option(
8648
+ "--infra <ref...>",
8649
+ "shared setup it touches (database | storage | auth | hosting | jobs)"
8650
+ ).option("--scope <path...>", "files or folders THIS subtask owns").option(
8651
+ "--depends-on <id...>",
8652
+ "subtasks that must finish first (key or id)"
8653
+ ).action(
8456
8654
  action(async ({ ctx, args, opts }) => {
8457
8655
  const parent = resolveTaskId(ctx, opts.task);
8458
8656
  if (opts.role !== void 0) assertOneOf("--role", opts.role, ROLES);
@@ -8468,13 +8666,21 @@ function registerTask(program3) {
8468
8666
  scopePaths: opts.scope,
8469
8667
  dependsOn,
8470
8668
  targets: [
8471
- ...(opts.app ?? []).map((appId) => ({ kind: "app", appId })),
8472
- ...(opts.infra ?? []).map((ref) => ({ kind: "infra", ref }))
8669
+ ...(opts.app ?? []).map((appId) => ({
8670
+ kind: "app",
8671
+ appId
8672
+ })),
8673
+ ...(opts.infra ?? []).map((ref) => ({
8674
+ kind: "infra",
8675
+ ref
8676
+ }))
8473
8677
  ]
8474
8678
  }
8475
8679
  });
8476
8680
  ok(row, () => {
8477
- line(`${import_picocolors28.default.green("added")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`);
8681
+ line(
8682
+ `${import_picocolors28.default.green("added")} ${import_picocolors28.default.bold(row.title)} ${import_picocolors28.default.dim(row.key ?? row.id)}`
8683
+ );
8478
8684
  if (row.role) line(import_picocolors28.default.dim(`role: ${row.role}`));
8479
8685
  });
8480
8686
  })
@@ -8482,7 +8688,10 @@ function registerTask(program3) {
8482
8688
  subtask.command("list [taskId]").description("The subtasks of a task, in order").action(
8483
8689
  action(async ({ ctx, args }) => {
8484
8690
  const id = resolveTaskId(ctx, args[0]);
8485
- const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}`);
8691
+ const row = await api(
8692
+ ctx,
8693
+ `/v1/project-tasks/${encodeURIComponent(id)}`
8694
+ );
8486
8695
  const rows = row.subtasks ?? [];
8487
8696
  ok(rows, () => {
8488
8697
  if (!rows.length) {
@@ -8522,7 +8731,9 @@ function registerTask(program3) {
8522
8731
  ok({ removed: args[0] }, () => line(import_picocolors28.default.green("removed")));
8523
8732
  })
8524
8733
  );
8525
- task.command("move <id> <status>").description(`Move a task or step along the board (${STATUSES2.join(" | ")})`).action(
8734
+ task.command("move <id> <status>").description(
8735
+ `Move a task or step along the board (${STATUSES2.join(" | ")})`
8736
+ ).action(
8526
8737
  action(async ({ ctx, args }) => {
8527
8738
  assertOneOf("<status>", args[1], STATUSES2);
8528
8739
  const row = await api(
@@ -8530,7 +8741,10 @@ function registerTask(program3) {
8530
8741
  `/v1/project-tasks/${encodeURIComponent(args[0])}/move`,
8531
8742
  { body: { status: args[1] } }
8532
8743
  );
8533
- ok(row, () => line(`${import_picocolors28.default.green("moved")} ${import_picocolors28.default.bold(row.title)} \u2192 ${args[1]}`));
8744
+ ok(
8745
+ row,
8746
+ () => line(`${import_picocolors28.default.green("moved")} ${import_picocolors28.default.bold(row.title)} \u2192 ${args[1]}`)
8747
+ );
8534
8748
  })
8535
8749
  );
8536
8750
  task.command("resume [id]").description(
@@ -8581,7 +8795,9 @@ function registerTask(program3) {
8581
8795
  });
8582
8796
  })
8583
8797
  );
8584
- task.command("can-start [id]").description("Ask whether work on this task may begin. Refuses until the owner approves.").action(
8798
+ task.command("can-start [id]").description(
8799
+ "Ask whether work on this task may begin. Refuses until the owner approves."
8800
+ ).action(
8585
8801
  action(async ({ ctx, args }) => {
8586
8802
  const id = resolveTaskId(ctx, args[0]);
8587
8803
  const row = await api(
@@ -8597,11 +8813,9 @@ function registerTask(program3) {
8597
8813
  ).action(
8598
8814
  action(async ({ ctx, args }) => {
8599
8815
  const id = resolveTaskId(ctx, args[0]);
8600
- const row = await api(
8601
- ctx,
8602
- `/v1/project-tasks/${encodeURIComponent(id)}/start`,
8603
- { method: "POST" }
8604
- );
8816
+ const row = await api(ctx, `/v1/project-tasks/${encodeURIComponent(id)}/start`, {
8817
+ method: "POST"
8818
+ });
8605
8819
  ok(row, () => {
8606
8820
  const started = row?.started ?? null;
8607
8821
  if (started && started > 0) {
@@ -8627,7 +8841,9 @@ function registerTask(program3) {
8627
8841
  );
8628
8842
  ok({ awaiting: row2.approval_state === "awaiting", task: row2 }, () => {
8629
8843
  line(
8630
- row2.approval_state === "awaiting" ? import_picocolors28.default.yellow("The plan is waiting on the owner. They see it in the task.") : `Already ${row2.approval_state}.`
8844
+ row2.approval_state === "awaiting" ? import_picocolors28.default.yellow(
8845
+ "The plan is waiting on the owner. They see it in the task."
8846
+ ) : `Already ${row2.approval_state}.`
8631
8847
  );
8632
8848
  });
8633
8849
  return;
@@ -8648,7 +8864,10 @@ function registerTask(program3) {
8648
8864
  }
8649
8865
  }
8650
8866
  );
8651
- ok(row, () => line(`${import_picocolors28.default.green(row.approval_state)} ${import_picocolors28.default.bold(row.title)}`));
8867
+ ok(
8868
+ row,
8869
+ () => line(`${import_picocolors28.default.green(row.approval_state)} ${import_picocolors28.default.bold(row.title)}`)
8870
+ );
8652
8871
  })
8653
8872
  );
8654
8873
  task.command("done [id]").description("Record what a subtask produced, and move it to ready").option("--summary <text>", "what changed, in the owner's words").action(
@@ -8777,7 +8996,11 @@ Nothing is running. The next part waiting is "${next.name}" \u2014 offer it to t
8777
8996
  )
8778
8997
  );
8779
8998
  } else if (!next) {
8780
- line(import_picocolors28.default.dim("\nEvery part of this plan is done. Say so, and offer to wrap it up."));
8999
+ line(
9000
+ import_picocolors28.default.dim(
9001
+ "\nEvery part of this plan is done. Say so, and offer to wrap it up."
9002
+ )
9003
+ );
8781
9004
  }
8782
9005
  line(
8783
9006
  import_picocolors28.default.dim(
@@ -9841,7 +10064,7 @@ function requireApp(app) {
9841
10064
  const value = typeof app === "string" ? app.trim() : "";
9842
10065
  if (value) return value;
9843
10066
  throw new WorkserError(
9844
- "Which service? Pass --app <webAppId>; `workser app list` shows them.",
10067
+ "Which service? Pass --app <webAppId>; `workser project apps` shows them.",
9845
10068
  { code: "bad_request" }
9846
10069
  );
9847
10070
  }
@@ -10632,7 +10855,7 @@ function colour(d) {
10632
10855
 
10633
10856
  // src/index.ts
10634
10857
  var pkg = {
10635
- version: true ? "0.6.6" : "0.0.0-dev"
10858
+ version: true ? "0.6.8" : "0.0.0-dev"
10636
10859
  };
10637
10860
  var program2 = new Command();
10638
10861
  program2.name("workser").description(
@@ -10675,12 +10898,14 @@ registerUsage(program2);
10675
10898
  registerCheckpoint(program2);
10676
10899
  registerSync(program2);
10677
10900
  registerWorkflow(program2);
10678
- registerApp(program2);
10901
+ registerConnection(program2);
10679
10902
  registerTool(program2);
10680
10903
  registerMemory(program2);
10681
10904
  registerBusiness(program2);
10682
10905
  registerArtifact(program2);
10683
10906
  registerImage(program2);
10907
+ registerVideo(program2);
10908
+ registerAudio(program2);
10684
10909
  registerAsk(program2);
10685
10910
  registerSearch(program2);
10686
10911
  registerBoard(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workser/cli",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Workser CLI — give your local AI agent native DevOps & infrastructure on Workser. The agent runs `workser …` to provision, deploy, and manage real apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,6 +77,9 @@ pick or switch it.
77
77
  3. **A phased plan goes on the subtask list, never the Board.** Phases are
78
78
  `workser task subtask add` (`workser help tasks`) — not `board create`,
79
79
  which makes a second, driftable "the plan" the task page never reads.
80
+ If a subtask's title, teammate, note, or scope is wrong after creation, fix
81
+ that same row with `workser task subtask update <id> --title … --role …
82
+ --note … --scope …`; never claim it is locked or create a duplicate.
80
83
  Write the narrative once as `doc create` (no `--work-item`, or it's hidden
81
84
  from the Docs panel), plus `decision create` for a real tradeoff. A plan
82
85
  in your reply alone is gone when the conversation scrolls. Details:
@@ -2,7 +2,7 @@
2
2
  topic: automation
3
3
  title: Workflows & connected apps
4
4
  summary: Build automations that outlive the run; use Gmail, Slack, Stripe, Sheets.
5
- commands: [workflow, app]
5
+ commands: [workflow, connection]
6
6
  ---
7
7
 
8
8
  # Workflows & connected apps
@@ -16,10 +16,11 @@ workser workflow activate <id> | deactivate <id> | run <id> [--wait] [--body <js
16
16
  workser workflow runs <id> # past executions of a workflow
17
17
  workser workflow nodes [query] # search the node-type catalog
18
18
 
19
- workser app list [--toolkit <slug>] # connectable + connected third-party apps
20
- workser app connect <toolkit> | disconnect <connectionId>
21
- workser app tools <toolkit> # a connected app's callable actions
22
- workser app run <toolSlug> [--body <json>] # execute one action
19
+ workser connection list [--toolkit <slug>] # connectable + connected third-party apps
20
+ workser connection search "<query>" [--toolkit <slug>] [--limit N] # find an action across every toolkit
21
+ workser connection connect <toolkit> | disconnect <connectionId>
22
+ workser connection tools <toolkit> # browse one connected toolkit's actions
23
+ workser connection run <toolSlug> [--body <json>] # execute one action
23
24
  ```
24
25
 
25
26
  ## Building a workflow
@@ -35,12 +36,13 @@ Created workflows start inactive: `workser workflow activate <id>` when it's rea
35
36
 
36
37
  ## Using a connected app
37
38
 
38
- 1. `workser app list` — check what's already connected before asking for anything.
39
- 2. If it isn't: `workser app connect <toolkit>` returns an OAuth link. The **user**
39
+ 1. `workser connection list` — check what's already connected before asking for anything.
40
+ 2. If it isn't: `workser connection connect <toolkit>` returns an OAuth link. The **user**
40
41
  must open it; you cannot complete OAuth on their behalf. Wait, then continue.
41
- 3. `workser app tools <toolkit>` read the argument schema rather than guessing
42
- field names.
43
- 4. `workser app run <toolSlug> --body '{"…":…}'` e.g. `GOOGLESHEETS_APPEND_ROW`,
42
+ 3. Don't know the exact action? `workser connection search "<query>"` finds it across
43
+ every toolkit; `workser connection tools <toolkit>` browses one toolkit you already
44
+ know. Either way, read the argument schema rather than guessing field names.
45
+ 4. `workser connection run <toolSlug> --body '{"…":…}'` — e.g. `GOOGLESHEETS_APPEND_ROW`,
44
46
  `GMAIL_SEND_EMAIL`.
45
47
 
46
48
  **A `run` is a real side effect in someone's real account.** Sending an email or
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  topic: images
3
- title: Image generation
4
- summary: Generate images from a prompt, optionally conditioned on existing images.
5
- commands: [image]
3
+ title: Image generation & media understanding
4
+ summary: Generate images from a prompt; describe/transcribe an image, video, or audio clip you can't natively see or hear.
5
+ commands: [image, video, audio]
6
6
  ---
7
7
 
8
- # Image generation
8
+ # Image generation & media understanding
9
9
 
10
10
  ```
11
11
  workser image generate "<prompt>" # alias: workser image gen
@@ -21,7 +21,7 @@ workser image generate "flat illustration of a farm delivery van, brand colors"
21
21
  workser image gen "same van, from the side" -r https://… -o ./public/van.png --json
22
22
  ```
23
23
 
24
- ## Notes that matter
24
+ ## Notes that matter (generation)
25
25
 
26
26
  - **Reference images are image-to-image conditioning**, not attachments. Up to 4;
27
27
  anything beyond that is dropped.
@@ -33,3 +33,39 @@ workser image gen "same van, from the side" -r https://… -o ./public/van.png -
33
33
  exist only as URLs.
34
34
  - **Placeholder art is not a deliverable.** Generating a hero image to unblock a
35
35
  layout is fine; shipping it as the user's brand asset without asking is not.
36
+
37
+ ## Understanding media you can't natively see or hear
38
+
39
+ The fallback for a text-only model, or media you have no other way to reach: describe
40
+ an image, summarize/transcribe a video, transcribe/describe audio. Runs server-side
41
+ (Gemini) — you never need a model key.
42
+
43
+ ```
44
+ workser image understand "<query>" [--url <u> | --file <p>] [-t <task>]
45
+ workser video understand "<query>" [--url <u> | --file <p>] [-t <task>]
46
+ workser audio understand "<query>" [--url <u> | --file <p>] [-t <task>]
47
+ ```
48
+
49
+ ```bash
50
+ workser image understand "what's wrong with this layout?" --url https://…/screenshot.png --json
51
+ workser video understand "what happens at the end?" --url https://youtu.be/… -t timestamp_analysis --json
52
+ workser audio understand "transcribe this" --file ./voicemail.m4a -t transcribe --json
53
+ ```
54
+
55
+ ## Notes that matter (understanding)
56
+
57
+ - **`--url` vs `--file`**: `--url` is fetched server-side with no size ceiling — the
58
+ right choice for anything already hosted (a project's own storage bucket, a public
59
+ link, a YouTube URL for video/audio). `--file` is read and sent inline by the CLI
60
+ itself, so it's bounded by the daemon's own request-size limit — for a small local
61
+ file only (a screenshot, a short voice memo). Something bigger: `workser storage
62
+ upload` it first, then pass the returned URL with `--url`.
63
+ - **`-t/--task` shapes the answer, it doesn't gate what you can ask** — `general` (the
64
+ default) takes any free-form `<query>`. The other values just bias the prompt
65
+ toward a specific shape: `caption`/`visual_qa`/`object_detection`/`segmentation`
66
+ for images; `summarize`/`describe`/`visual_qa`/`timestamp_analysis` for video;
67
+ `transcribe`/`describe`/`audio_qa`/`speaker_diarization`/`emotion_detection` for
68
+ audio.
69
+ - **This is billed to the project's organization**, same as image generation — it's
70
+ a real provider call, not free introspection. Don't loop it over every file in a
71
+ folder "just in case"; use it when you actually need to know what's in one.