@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 +38 -15
- package/dist/index.js +277 -52
- package/package.json +1 -1
- package/skills/workser/SKILL.md +3 -0
- package/skills/workser/reference/automation.md +12 -10
- package/skills/workser/reference/images.md +41 -5
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
|
|
75
|
-
workser
|
|
76
|
-
workser
|
|
77
|
-
workser
|
|
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
|
|
112
|
-
task
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
219
|
-
OAuth link — ask the user to complete it, then continue); after that, `workser
|
|
220
|
-
run <toolSlug> --body '{"...":...}'` calls any of its actions directly, and
|
|
221
|
-
node can call the same toolkit. Use `workser
|
|
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", "
|
|
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
|
|
3811
|
-
workser
|
|
3812
|
-
workser
|
|
3813
|
-
workser
|
|
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
|
|
3830
|
-
2. If it isn't: \`workser
|
|
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.
|
|
3833
|
-
|
|
3834
|
-
|
|
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,
|
|
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/
|
|
7539
|
+
// src/commands/connection.ts
|
|
7502
7540
|
var import_picocolors20 = __toESM(require_picocolors(), 1);
|
|
7503
|
-
function
|
|
7504
|
-
const
|
|
7505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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 = [
|
|
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(
|
|
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)
|
|
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(
|
|
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) => ({
|
|
8399
|
-
|
|
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(
|
|
8440
|
-
|
|
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(
|
|
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(
|
|
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) => ({
|
|
8472
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
8602
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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",
|
package/skills/workser/SKILL.md
CHANGED
|
@@ -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,
|
|
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
|
|
20
|
-
workser
|
|
21
|
-
workser
|
|
22
|
-
workser
|
|
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
|
|
39
|
-
2. If it isn't: `workser
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
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,
|
|
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.
|