@workser/cli 0.6.6 → 0.6.7

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,6 +88,16 @@ 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
 
92
103
  # owner-only (will return owner_only / exit 6 — ask the user to do these in Orbit):
@@ -215,11 +226,14 @@ done: `workser workflow create` builds an event-driven, multi-step automation (t
215
226
  same engine Workser's own web Workflow tab uses) — nodes, connections, and triggers go
216
227
  in `--body` as JSON; browse `workser workflow nodes` first to see what's available.
217
228
  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.
229
+ ...), the user connects it once via `workser connection connect <toolkit>` (this opens
230
+ an OAuth link — ask the user to complete it, then continue); after that, `workser
231
+ connection run <toolSlug> --body '{"...":...}'` calls any of its actions directly, and
232
+ a workflow node can call the same toolkit. Use `workser connection list` to see what's
233
+ already connected before assuming you need to ask the user to connect something new.
234
+ Don't know the exact action slug? `workser connection search "<what you want to do>"`
235
+ searches across every toolkit's actions at once — cheaper than browsing toolkit by
236
+ toolkit with `workser connection tools <toolkit>`.
223
237
 
224
238
  ## Delegate to roles
225
239
  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 = [
@@ -9841,7 +10000,7 @@ function requireApp(app) {
9841
10000
  const value = typeof app === "string" ? app.trim() : "";
9842
10001
  if (value) return value;
9843
10002
  throw new WorkserError(
9844
- "Which service? Pass --app <webAppId>; `workser app list` shows them.",
10003
+ "Which service? Pass --app <webAppId>; `workser project apps` shows them.",
9845
10004
  { code: "bad_request" }
9846
10005
  );
9847
10006
  }
@@ -10632,7 +10791,7 @@ function colour(d) {
10632
10791
 
10633
10792
  // src/index.ts
10634
10793
  var pkg = {
10635
- version: true ? "0.6.6" : "0.0.0-dev"
10794
+ version: true ? "0.6.7" : "0.0.0-dev"
10636
10795
  };
10637
10796
  var program2 = new Command();
10638
10797
  program2.name("workser").description(
@@ -10675,12 +10834,14 @@ registerUsage(program2);
10675
10834
  registerCheckpoint(program2);
10676
10835
  registerSync(program2);
10677
10836
  registerWorkflow(program2);
10678
- registerApp(program2);
10837
+ registerConnection(program2);
10679
10838
  registerTool(program2);
10680
10839
  registerMemory(program2);
10681
10840
  registerBusiness(program2);
10682
10841
  registerArtifact(program2);
10683
10842
  registerImage(program2);
10843
+ registerVideo(program2);
10844
+ registerAudio(program2);
10684
10845
  registerAsk(program2);
10685
10846
  registerSearch(program2);
10686
10847
  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.7",
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",
@@ -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.