pi-qtools 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ Requires a `qwen-token-plan*` provider authenticated in pi (`/login`).
19
19
  | `qwen_image_search` | `/responses` `web_search_image` | text → image URLs. **qwen3.8-max only** |
20
20
  | `qwen_reverse_image_search` | `/responses` `image_search` | image URL or local file → similar images; accepts a local path and inlines it as a data URL (4 MB cap) |
21
21
  | `qwen_deep_search` | chat `search_strategy: "agent"` | multi-step research; very expensive (~340k prompt tokens observed) |
22
- | `qwen_generate_image` | native `multimodal-generation` | `wan2.7-image`, `wan2.7-image-pro`, `qwen-image-3.0-pro`; sync; `save: true` writes into `<cwd>/out/qtools/` |
22
+ | `qwen_generate_image` | native `multimodal-generation` | `wan2.7-image`, `wan2.7-image-pro`, `qwen-image-3.0-pro`; sync; `image` edits a reference picture; `save: true` writes into `<cwd>/out/qtools/` |
23
23
  | `qwen_generate_video` | native `video-generation` + task polling | `happyhorse-1.1-t2v` / `-i2v` / `-r2v`; async, ~90-120s; `save: true` writes the mp4 |
24
24
 
25
25
  ## Passive main-loop search
@@ -40,10 +40,31 @@ sends one. That is why those two capabilities are standalone tools instead.
40
40
  ## Commands
41
41
 
42
42
  - `/qtools-config` — settings TUI (search mode, model defaults, capability matrix)
43
- - `/qimage <prompt> [-m model] [-s WxH]` — saves into `out/qtools/`
43
+ - `/qimage <prompt> [-i img] [-m model] [-s WxH]` — saves into `out/qtools/`
44
44
  - `/qvideo <prompt> [-i <path|url>] [-m model] [-s WxH]` — `-i` selects image-to-video; saves into `out/qtools/`
45
45
 
46
- ### Getting parameter hints
46
+ ### Image editing (measured)
47
+
48
+ Passing a reference image turns the same endpoint from text-to-image into
49
+ image+prompt editing. The shape is one extra content part:
50
+
51
+ ```json
52
+ { "input": { "messages": [ { "role": "user", "content": [
53
+ { "image": "https://... or data:image/png;base64,..." },
54
+ { "text": "turn this into a red panda, keep the flat vector style" }
55
+ ] } ] } }
56
+ ```
57
+
58
+ Verified: a flat-vector pig + that prompt came back a red panda **in the same
59
+ style**, and a local file inlined as a data URL worked too. Two reference images
60
+ in one message are accepted.
61
+
62
+ The trap here is that a wrong request shape still returns a valid image, just
63
+ generated from the text alone. Every claim above was checked by asking a vision
64
+ model (`qwen3.8-max`, which has real image input on this plan) to describe the
65
+ output, not by trusting the HTTP 200.
66
+
67
+ ## Getting parameter hints
47
68
 
48
69
  pi exposes no `argumentHint` for extension commands, so `/qimage` and `/qvideo` make
49
70
  themselves discoverable three ways:
@@ -643,18 +643,40 @@ function extractImageUrls(body: any): string[] {
643
643
  /** Shared core so the tool and the /qimage command behave identically. */
644
644
  async function generateImage(
645
645
  ctx: ExtensionContext,
646
- opts: { prompt: string; model?: string; negativePrompt?: string; size?: string; signal?: AbortSignal },
647
- ): Promise<{ model: string; urls: string[]; raw: unknown }> {
646
+ opts: {
647
+ prompt: string;
648
+ model?: string;
649
+ negativePrompt?: string;
650
+ size?: string;
651
+ /** Reference image to edit: local path, http(s) URL, or data: URL. */
652
+ image?: string;
653
+ signal?: AbortSignal;
654
+ },
655
+ ): Promise<{ model: string; urls: string[]; raw: unknown; edited?: string }> {
648
656
  const model = opts.model && IMAGE_MODELS.includes(opts.model) ? opts.model : config.imageModel;
649
657
  const parameters: Record<string, unknown> = { result_format: "message" };
650
658
  if (opts.size) parameters.size = opts.size;
651
659
  if (opts.negativePrompt) parameters.negative_prompt = opts.negativePrompt;
660
+
661
+ // Verified shape: an extra {image} content part next to the text switches this
662
+ // endpoint from text-to-image to image+prompt editing. Confirmed by vision:
663
+ // a flat-vector pig + "turn this into a red panda" came back a red panda in the
664
+ // same style, and a local file inlined as a data URL worked too.
665
+ const content: Array<Record<string, unknown>> = [];
666
+ let edited: string | undefined;
667
+ if (opts.image?.trim()) {
668
+ const ref = toImageUrl(opts.image.trim());
669
+ content.push({ image: ref });
670
+ edited = ref.startsWith("data:") ? `inline image (${Math.round(ref.length / 1024)} KB base64)` : ref;
671
+ }
672
+ content.push({ text: opts.prompt });
673
+
652
674
  const r = await nativeCall(ctx, MG_PATH, {
653
675
  model,
654
- input: { messages: [{ role: "user", content: [{ text: opts.prompt }] }] },
676
+ input: { messages: [{ role: "user", content }] },
655
677
  parameters,
656
678
  }, { signal: opts.signal });
657
- return { model, urls: extractImageUrls(r.body), raw: r.body };
679
+ return { model, urls: extractImageUrls(r.body), raw: r.body, edited };
658
680
  }
659
681
 
660
682
  /**
@@ -761,9 +783,11 @@ function genUsage(kind: "image" | "video", cwd: string): string {
761
783
  const outDir = join(cwd, "out", "qtools");
762
784
  if (kind === "image") {
763
785
  return [
764
- "/qimage <prompt> [-m model] [-s WxH]",
786
+ "/qimage <prompt> [-i img] [-m model] [-s WxH]",
765
787
  "",
766
788
  " prompt what to draw. No quotes needed: flags are stripped, the rest is the prompt.",
789
+ " -i reference image to edit (local path, http(s) or data URL).",
790
+ " With -i this is image+prompt editing, not text-to-image.",
767
791
  ` -m model: ${IMAGE_MODELS.join(", ")} (default: ${config.imageModel})`,
768
792
  " -s e.g. 1024x1024. Must be 589824-16777216 total pixels (768x768 .. 4096x4096).",
769
793
  " --help this text",
@@ -1342,14 +1366,23 @@ export default function (pi: ExtensionAPI): void {
1342
1366
  description:
1343
1367
  "Generate images with the Qwen plan's image models (wan2.7-image, wan2.7-image-pro, qwen-image-3.0-pro) " +
1344
1368
  "through the native multimodal-generation endpoint. These are not chat models, so they are unreachable " +
1345
- "from pi's normal model list. Returns temporary hosted image URLs.",
1346
- promptSnippet: "Generate images with Qwen/Wan image models",
1369
+ "from pi's normal model list. Pass `image` to edit an existing picture instead of drawing from scratch: " +
1370
+ "the reference plus the prompt produce a new image that keeps the source's style and subject layout. " +
1371
+ "Returns temporary hosted image URLs.",
1372
+ promptSnippet: "Generate or edit images with Qwen/Wan image models",
1347
1373
  promptGuidelines: [
1348
1374
  "Use qwen_generate_image when the user asks to create or draw an image; report the returned URLs as links and note they are temporary hosted files.",
1375
+ "Use qwen_generate_image with the image parameter when the user wants an existing picture restyled, edited, or used as a reference.",
1349
1376
  "qwen_generate_image cannot render images in the terminal, so hand the user the URL rather than describing pixels you cannot see.",
1350
1377
  ],
1351
1378
  parameters: Type.Object({
1352
- prompt: Type.String({ description: "What to draw." }),
1379
+ prompt: Type.String({ description: "What to draw, or how to change the reference image." }),
1380
+ image: Type.Optional(
1381
+ Type.String({
1382
+ description:
1383
+ "Reference image to edit: http(s) URL, data: URL, or local file path. Omit for text-to-image.",
1384
+ }),
1385
+ ),
1353
1386
  model: Type.Optional(Type.String({ description: `Override model. One of: ${IMAGE_MODELS.join(", ")}.` })),
1354
1387
  negativePrompt: Type.Optional(Type.String({ description: "What to avoid." })),
1355
1388
  size: Type.Optional(Type.String({ description: "Pixel size, e.g. 1024*1024. Must be 589824-16777216 total pixels (768² to 4096²)." })),
@@ -1366,7 +1399,7 @@ export default function (pi: ExtensionAPI): void {
1366
1399
  files = await Promise.all(r.urls.map((u) => saveMedia(ctx, u, r.model, signal)));
1367
1400
  text += `\n\nSaved:\n${files.map((f) => ` ${f}`).join("\n")}`;
1368
1401
  }
1369
- return { content: [{ type: "text", text }], details: { model: r.model, urls: r.urls, files } };
1402
+ return { content: [{ type: "text", text }], details: { model: r.model, urls: r.urls, files, edited: r.edited } };
1370
1403
  },
1371
1404
  });
1372
1405
 
@@ -1422,20 +1455,20 @@ export default function (pi: ExtensionAPI): void {
1422
1455
 
1423
1456
  // ---------------------------------------------------------------- commands
1424
1457
  pi.registerCommand("qimage", {
1425
- description: `Generate an image — /qimage <prompt> [-m model] [-s WxH] (default: ${config.imageModel})`,
1426
- getArgumentCompletions: (prefix) => genCompletions(prefix, { models: IMAGE_MODELS, sizes: [], image: false }),
1458
+ description: `Generate or edit an image — /qimage <prompt> [-i img] [-m model] [-s WxH]`,
1459
+ getArgumentCompletions: (prefix) => genCompletions(prefix, { models: IMAGE_MODELS, sizes: [], image: true }),
1427
1460
  handler: async (args, ctx) => {
1428
1461
  if (wantsHelp(args)) {
1429
1462
  ctx.ui.notify(genUsage("image", ctx.cwd), "info");
1430
1463
  return;
1431
1464
  }
1432
- const { prompt, model, size } = parseGenArgs(args, IMAGE_MODELS);
1465
+ const { prompt, model, size, image } = parseGenArgs(args, IMAGE_MODELS);
1433
1466
  if (!prompt) {
1434
1467
  ctx.ui.notify(genUsage("image", ctx.cwd), "error");
1435
1468
  return;
1436
1469
  }
1437
- ctx.ui.notify(`Generating image with ${model || config.imageModel}...`, "info");
1438
- const r = await generateImage(ctx, { prompt, model, size });
1470
+ ctx.ui.notify(`${image ? "Editing" : "Generating"} image with ${model || config.imageModel}...`, "info");
1471
+ const r = await generateImage(ctx, { prompt, model, size, image });
1439
1472
  if (!r.urls.length) {
1440
1473
  ctx.ui.notify(`No image URL from ${r.model}`, "error");
1441
1474
  return;
package/package.json CHANGED
@@ -1,9 +1,21 @@
1
1
  {
2
2
  "name": "pi-qtools",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Qwen Token Plan bridge for pi: web search, scraping, code interpreter, image search, and image/video generation via DashScope built-in tools and native task endpoints.",
5
5
  "type": "module",
6
- "keywords": ["pi-package", "pi", "pi-coding-agent", "qwen", "dashscope", "web-search", "web-scraping", "code-interpreter", "image-search", "image-generation", "video-generation"],
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-coding-agent",
10
+ "qwen",
11
+ "dashscope",
12
+ "web-search",
13
+ "web-scraping",
14
+ "code-interpreter",
15
+ "image-search",
16
+ "image-generation",
17
+ "video-generation"
18
+ ],
7
19
  "license": "MIT",
8
20
  "publishConfig": {
9
21
  "access": "public"
@@ -17,18 +29,30 @@
17
29
  "bugs": {
18
30
  "url": "https://github.com/YasribProyas/pi-qtools/issues"
19
31
  },
20
- "files": ["extensions", "README.md", "LICENSE"],
32
+ "files": [
33
+ "extensions",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
21
37
  "peerDependencies": {
22
38
  "@earendil-works/pi-coding-agent": "*",
23
39
  "@earendil-works/pi-tui": "*",
24
40
  "typebox": "*"
25
41
  },
26
42
  "peerDependenciesMeta": {
27
- "@earendil-works/pi-coding-agent": { "optional": true },
28
- "@earendil-works/pi-tui": { "optional": true },
29
- "typebox": { "optional": true }
43
+ "@earendil-works/pi-coding-agent": {
44
+ "optional": true
45
+ },
46
+ "@earendil-works/pi-tui": {
47
+ "optional": true
48
+ },
49
+ "typebox": {
50
+ "optional": true
51
+ }
30
52
  },
31
53
  "pi": {
32
- "extensions": ["./extensions/qtools.ts"]
54
+ "extensions": [
55
+ "./extensions/qtools.ts"
56
+ ]
33
57
  }
34
58
  }