opencode-image 0.1.2 → 2.0.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
@@ -7,9 +7,31 @@
7
7
  - 兼容任何 OpenAI 风格的 `/images/generations` 与 `/images/edits` 接口
8
8
  - 跨平台缩放(基于 [sharp](https://sharp.pixelplumbing.com)),不依赖 `sips` 等系统命令
9
9
 
10
+ ## 版本与兼容(重要)
11
+
12
+ | 版本 | 目标 opencode | 插件 API | npm 安装 |
13
+ |---|---|---|---|
14
+ | **2.x**(当前线,`next` tag) | opencode **2.0+** | `@opencode/plugin@2`(`Plugin.define` + `ctx.tool` / `ctx.session`) | `opencode-image@next` |
15
+ | 1.x(`latest` tag) | opencode **1.x** | `@opencode-ai/plugin` | `opencode-image@latest` |
16
+
17
+ - v2 的插件运行时**只认** `export default { id, effect | setup }`,v1 形态的插件在 v2 里加载不了,所以这是**大版本切换**。
18
+ - 本仓库 `publishConfig.tag` 固定为 `next`,直接 `npm publish` **不会**顶掉 `latest`,因此 **v1 老用户不会被自动升级到 2.x**。
19
+ - v1 线要出新补丁:`bun run release:legacy`(打 patch 到 `latest`)。
20
+
10
21
  ## 安装
11
22
 
12
- 在 `opencode.json` / `opencode.jsonc` 里配置:
23
+ opencode **2.0+**(v2 插件 API):
24
+
25
+ ```jsonc
26
+ {
27
+ "$schema": "https://opencode.ai/config.json",
28
+ "plugin": [
29
+ ["opencode-image@next", { "apiKey": "sk-..." }]
30
+ ]
31
+ }
32
+ ```
33
+
34
+ opencode **1.x**(保持老版本):
13
35
 
14
36
  ```jsonc
15
37
  {
@@ -20,7 +42,8 @@
20
42
  }
21
43
  ```
22
44
 
23
- opencode 启动时会自动从 npm 安装插件到 `~/.cache/opencode/packages/opencode-image/node_modules/`。
45
+ opencode 启动时会自动从 npm 安装插件到 `~/.cache/opencode/packages/opencode-image.../node_modules/`。
46
+
24
47
 
25
48
  ## 配置项
26
49
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { type Plugin } from "@opencode-ai/plugin";
2
- export declare const OpenCodeImagePlugin: Plugin;
1
+ import { Plugin } from "@opencode/plugin";
2
+ export declare const OpenCodeImagePlugin: Plugin.Plugin;
3
3
  export default OpenCodeImagePlugin;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
- import { tool } from "@opencode-ai/plugin";
2
+ import { Plugin } from "@opencode/plugin";
3
+ import { Schema } from "effect";
3
4
  import * as fs5 from "node:fs";
4
5
  import * as path5 from "node:path";
5
6
 
@@ -295,6 +296,7 @@ function resolveOptions(options) {
295
296
  serverPort: opts.serverPort ?? DEFAULT_SERVER_PORT,
296
297
  serverIdleMs: opts.serverIdleMs ?? DEFAULT_SERVER_IDLE_MS,
297
298
  updateCheck: opts.updateCheck ?? true,
299
+ injectPrompt: opts.injectPrompt ?? true,
298
300
  cacheDir: opts.cacheDir ?? DEFAULT_CACHE_DIR,
299
301
  cacheLimit: opts.cacheLimit ?? DEFAULT_CACHE_LIMIT,
300
302
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
@@ -406,9 +408,9 @@ class ImageServer {
406
408
  return Buffer.from(relative2.split(path4.sep).join("/"), "utf8").toString("base64url");
407
409
  }
408
410
  }
409
- for (const [id2, registered] of this.extra) {
411
+ for (const [id, registered] of this.extra) {
410
412
  if (registered === file)
411
- return id2;
413
+ return id;
412
414
  }
413
415
  const id = randomUUID().replace(/-/g, "").slice(0, 16);
414
416
  this.extra.set(id, file);
@@ -460,9 +462,9 @@ class ImageServer {
460
462
  const port = this.configuredPort || derivePort(path4.resolve(opts.outDir));
461
463
  const server = http.createServer((req, res) => void this.handle(req, res));
462
464
  server.unref();
463
- const failure = await new Promise((resolve3) => {
464
- server.once("error", resolve3);
465
- server.listen(port, "127.0.0.1", () => resolve3(undefined));
465
+ const failure = await new Promise((resolve) => {
466
+ server.once("error", resolve);
467
+ server.listen(port, "127.0.0.1", () => resolve(undefined));
466
468
  });
467
469
  if (!failure) {
468
470
  this.server = server;
@@ -514,15 +516,15 @@ class ImageServer {
514
516
  const root = this.root;
515
517
  if (!root)
516
518
  return;
517
- let relative2;
519
+ let relative;
518
520
  try {
519
- relative2 = Buffer.from(id, "base64url").toString("utf8");
521
+ relative = Buffer.from(id, "base64url").toString("utf8");
520
522
  } catch {
521
523
  return;
522
524
  }
523
- if (!relative2 || relative2.includes("\x00") || path4.isAbsolute(relative2))
525
+ if (!relative || relative.includes("\x00") || path4.isAbsolute(relative))
524
526
  return;
525
- const target = path4.resolve(root, relative2);
527
+ const target = path4.resolve(root, relative);
526
528
  if (target === root || !target.startsWith(root + path4.sep))
527
529
  return;
528
530
  try {
@@ -573,9 +575,25 @@ class ImageServer {
573
575
  }
574
576
 
575
577
  // src/index.ts
576
- var z = tool.schema;
577
- function resolveDestination(filename, outDir, ctx) {
578
- const dir = resolveOutDir(ctx.directory, outDir);
578
+ var PLUGIN_ID = "opencode-image";
579
+ var GENERATE_DESCRIPTION = "Generate an image from a text prompt. This is the only supported way to create images in this session: " + "the endpoint and API key are already configured inside this plugin, so NEVER call the image API yourself " + "(no curl, no scripts) and never read the API key out of opencode.json. " + "The first line of the output is a Markdown image line (plus a separate 查看原图 link when displayMaxSize > 0); " + "you MUST copy those lines verbatim into your reply text, otherwise the user cannot see the picture. " + "If the output contains a line starting with [请转述给用户], relay that line to the user as well. " + "To refine a result, call image_edit with the returned path instead of generating a new image.";
580
+ var EDIT_DESCRIPTION = "Edit an existing image with a text instruction (image-to-image). Prefer this over generating a new image " + "when the user wants to adjust a picture: pass the exact path returned by a previous image_generate or " + "image_edit call to keep refining the same one. " + "Same rules: never call the image API yourself and never read the API key; copy the Markdown image line " + "(and the 查看原图 link, if present) from the output verbatim into your reply, and relay any line starting " + "with [请转述给用户].";
581
+ var CACHE_DESCRIPTION = "Inspect or clear the preview-thumbnail cache used for rendering images in the conversation. " + 'Use action="status" to report the cache directory, file count, total size and age of the oldest entry; ' + 'action="clean" to delete every cached preview (the generated images in outDir are NOT touched). ' + "This returns text only — do not turn it into a Markdown image line.";
582
+ var SYSTEM_RULE = "Image generation/editing: always use the `image_generate` / `image_edit` tools provided by the " + "opencode-image plugin — the endpoint and API key are already configured there. Never call the image API " + "directly, never read the API key from config, and do not use the imagegen skill or its CLI. Copy the " + "Markdown image line from that tool's output verbatim into your reply so the user can see the picture.";
583
+ var GenerateInput = Schema.Struct({
584
+ prompt: Schema.String,
585
+ filename: Schema.optional(Schema.String)
586
+ });
587
+ var EditInput = Schema.Struct({
588
+ image: Schema.String,
589
+ prompt: Schema.String,
590
+ filename: Schema.optional(Schema.String)
591
+ });
592
+ var CacheInput = Schema.Struct({
593
+ action: Schema.Literals(["status", "clean"])
594
+ });
595
+ function resolveDestination(filename, options, directory) {
596
+ const dir = resolveOutDir(directory, options.outDir);
579
597
  if (filename) {
580
598
  const target = path5.isAbsolute(filename) ? filename : path5.join(dir, filename);
581
599
  return `${target.replace(/\.[A-Za-z0-9]+$/, "")}.png`;
@@ -585,123 +603,119 @@ function resolveDestination(filename, outDir, ctx) {
585
603
  return path5.join(dir, `image-${stamp}-${suffix}.png`);
586
604
  }
587
605
  function asAttachment(file) {
588
- return [
589
- {
590
- type: "file",
591
- mime: "image/png",
592
- filename: path5.basename(file),
593
- url: `data:image/png;base64,${fs5.readFileSync(file).toString("base64")}`
594
- }
595
- ];
606
+ return {
607
+ type: "file",
608
+ mime: "image/png",
609
+ name: path5.basename(file),
610
+ uri: `data:image/png;base64,${fs5.readFileSync(file).toString("base64")}`
611
+ };
596
612
  }
597
- var OpenCodeImagePlugin = async (_input, rawOptions) => {
598
- const options = resolveOptions(rawOptions);
599
- const cache = new PreviewCache(options.cacheDir, options.cacheLimit);
600
- const server = new ImageServer(options.serverIdleMs, options.serverPort, cache);
601
- const selfVersion = readOwnVersion();
602
- let updateHint;
603
- let updateChecked = false;
604
- const upgradeNotice = async () => {
605
- if (!options.updateCheck || updateChecked || !selfVersion)
613
+ var OpenCodeImagePlugin = Plugin.define({
614
+ id: PLUGIN_ID,
615
+ setup: async (ctx) => {
616
+ const options = resolveOptions(ctx.options);
617
+ const directory = ctx.location.directory;
618
+ const outDir = resolveOutDir(directory, options.outDir);
619
+ const cache = new PreviewCache(options.cacheDir, options.cacheLimit);
620
+ const server = new ImageServer(options.serverIdleMs, options.serverPort, cache);
621
+ const selfVersion = readOwnVersion();
622
+ let updateHint;
623
+ let updateChecked = false;
624
+ const upgradeNotice = async () => {
625
+ if (!options.updateCheck || updateChecked || !selfVersion)
626
+ return updateHint;
627
+ updateChecked = true;
628
+ updateHint = await checkForUpdate(selfVersion);
606
629
  return updateHint;
607
- updateChecked = true;
608
- updateHint = await checkForUpdate(selfVersion);
609
- return updateHint;
610
- };
611
- const result = async (label, prompt, file, ctx, usage) => {
612
- const { width, height } = await imageDimensions(file);
613
- let display;
614
- try {
615
- await server.start({ outDir: resolveOutDir(ctx.directory, options.outDir) });
616
- const alt = prompt.slice(0, 60);
617
- const lines = [`![${alt}](${server.url(file, options.displayMaxSize)})`];
618
- if (options.displayMaxSize > 0)
619
- lines.push(`[查看原图](${server.url(file, 0)})`);
620
- display = lines.join(`
630
+ };
631
+ const render = async (label, prompt, file, progress, usage) => {
632
+ const { width, height } = await imageDimensions(file);
633
+ let display;
634
+ try {
635
+ await server.start({ outDir });
636
+ const alt = prompt.slice(0, 60);
637
+ const lines = [`![${alt}](${server.url(file, options.displayMaxSize)})`];
638
+ if (options.displayMaxSize > 0)
639
+ lines.push(`[查看原图](${server.url(file, 0)})`);
640
+ display = lines.join(`
621
641
  `);
622
- } catch (error) {
623
- display = `[opencode-image: local image server unavailable, cannot render inline: ${String(error)}]`;
624
- }
625
- const notice = await upgradeNotice();
626
- const output = [
627
- display,
628
- `${label} saved to ${file} (${width}x${height})`,
629
- `Refine it by calling image_edit with image="${file}" and a prompt describing the change.`,
630
- `IMPORTANT: copy the Markdown image line above verbatim into your reply text, otherwise the user cannot see the picture.`
631
- ];
632
- if (notice)
633
- output.push(notice);
634
- const cacheNotice = server.consumeCacheNotice();
635
- if (cacheNotice)
636
- output.push(cacheNotice);
637
- return {
638
- title: `${label}: ${prompt.slice(0, 60)}`,
639
- output: output.join(`
640
- `),
641
- metadata: { file, prompt, usage },
642
- attachments: asAttachment(file)
642
+ } catch (error) {
643
+ display = `[opencode-image: local image server unavailable, cannot render inline: ${String(error)}]`;
644
+ }
645
+ const notice = await upgradeNotice();
646
+ const output = [
647
+ display,
648
+ `${label} saved to ${file} (${width}x${height})`,
649
+ `Refine it by calling image_edit with image="${file}" and a prompt describing the change.`,
650
+ `IMPORTANT: copy the Markdown image line above verbatim into your reply text, otherwise the user cannot see the picture.`
651
+ ];
652
+ if (notice)
653
+ output.push(notice);
654
+ const cacheNotice = server.consumeCacheNotice();
655
+ if (cacheNotice)
656
+ output.push(cacheNotice);
657
+ progress(`${label}: ${prompt.slice(0, 60)}`);
658
+ return {
659
+ content: [{ type: "text", text: output.join(`
660
+ `) }, asAttachment(file)],
661
+ metadata: { file, prompt, usage }
662
+ };
643
663
  };
644
- };
645
- const generate = async (args, ctx) => {
646
- ctx.metadata({ title: `Generating image: ${args.prompt.slice(0, 60)}` });
647
- const { signal, clear } = withTimeout(ctx.abort, options.timeoutMs);
648
- try {
649
- const remote = await generateImage(options, args.prompt, signal);
650
- const raw = await fetchImageBytes(remote, signal);
651
- const destination = resolveDestination(args.filename, options.outDir, ctx);
652
- await persistImage(raw, destination, options.maxSize);
653
- return result("Image generated", args.prompt, destination, ctx, remote.usage);
654
- } finally {
655
- clear();
656
- }
657
- };
658
- const edit = async (args, ctx) => {
659
- const source = path5.isAbsolute(args.image) ? args.image : path5.resolve(ctx.directory, args.image);
660
- if (!fs5.existsSync(source))
661
- throw new Error(`image_edit: source image not found: ${source}`);
662
- ctx.metadata({ title: `Editing image: ${args.prompt.slice(0, 60)}` });
663
- const { signal, clear } = withTimeout(ctx.abort, options.timeoutMs);
664
- try {
665
- const upload = await toPngBuffer(source, options.maxSize);
666
- const remote = await editImage(options, upload, `${path5.basename(source, path5.extname(source))}.png`, args.prompt, signal);
667
- const raw = await fetchImageBytes(remote, signal);
668
- const destination = resolveDestination(args.filename, options.outDir, ctx);
669
- await persistImage(raw, destination, options.maxSize);
670
- return result("Image edited", args.prompt, destination, ctx, remote.usage);
671
- } finally {
672
- clear();
673
- }
674
- };
675
- return {
676
- tool: {
677
- image_generate: tool({
678
- description: "Generate a new image from a text prompt using an OpenAI-compatible image API. " + "The result starts with a Markdown image line (plus a separate 查看原图 link when displayMaxSize is set) that you MUST copy verbatim into your reply text, " + "If the output contains a line starting with [请转述给用户], relay that line to the user in your reply. " + "otherwise the user cannot see the picture. To iterate on a result, call image_edit with the returned path.",
679
- args: {
680
- prompt: z.string().describe("Detailed description of the image to generate"),
681
- filename: z.string().optional().describe("Optional output file name or path; defaults to a timestamped name")
682
- },
683
- execute: generate
684
- }),
685
- image_edit: tool({
686
- description: "Edit an existing image with a text instruction (image-to-image). Pass the exact path returned by a previous " + "image_generate or image_edit call to keep refining the same picture. " + "The result starts with a Markdown image line (plus a separate 查看原图 link when displayMaxSize is set) that you MUST copy verbatim into your reply text, " + "If the output contains a line starting with [请转述给用户], relay that line to the user in your reply. " + "otherwise the user cannot see the picture.",
687
- args: {
688
- image: z.string().describe("Path to the source image, normally the file returned by image_generate/image_edit"),
689
- prompt: z.string().describe("What to change, e.g. 'make the hat red, keep everything else identical'"),
690
- filename: z.string().optional().describe("Optional output file name or path; defaults to a timestamped name")
691
- },
692
- execute: edit
693
- }),
694
- image_cache: tool({
695
- description: "Inspect or clear the preview-thumbnail cache used for rendering images in the conversation. " + 'Use action="status" to report the cache directory, file count, total size and age of the oldest entry; ' + 'action="clean" to delete every cached preview (the generated images in outDir are NOT touched). ' + "This returns text only — do not turn it into a Markdown image line.",
696
- args: {
697
- action: z.enum(["status", "clean"]).describe("status = 查看缓存;clean = 清空缓存")
698
- },
664
+ await ctx.tool.transform((tools) => {
665
+ tools.add({
666
+ name: "image_generate",
667
+ description: GENERATE_DESCRIPTION,
668
+ input: GenerateInput,
669
+ options: { codemode: false },
670
+ execute: async (args, toolCtx) => {
671
+ const title = `Generating image: ${args.prompt.slice(0, 60)}`;
672
+ await toolCtx.progress({ title });
673
+ const { signal, clear } = withTimeout(toolCtx.signal, options.timeoutMs);
674
+ try {
675
+ const remote = await generateImage(options, args.prompt, signal);
676
+ const raw = await fetchImageBytes(remote, signal);
677
+ const destination = resolveDestination(args.filename, options, directory);
678
+ await persistImage(raw, destination, options.maxSize);
679
+ return await render("Image generated", args.prompt, destination, (m) => void toolCtx.progress({ title: m }), remote.usage);
680
+ } finally {
681
+ clear();
682
+ }
683
+ }
684
+ });
685
+ tools.add({
686
+ name: "image_edit",
687
+ description: EDIT_DESCRIPTION,
688
+ input: EditInput,
689
+ options: { codemode: false },
690
+ execute: async (args, toolCtx) => {
691
+ const source = path5.isAbsolute(args.image) ? args.image : path5.resolve(directory, args.image);
692
+ if (!fs5.existsSync(source))
693
+ throw new Error(`image_edit: source image not found: ${source}`);
694
+ const title = `Editing image: ${args.prompt.slice(0, 60)}`;
695
+ await toolCtx.progress({ title });
696
+ const { signal, clear } = withTimeout(toolCtx.signal, options.timeoutMs);
697
+ try {
698
+ const upload = await toPngBuffer(source, options.maxSize);
699
+ const remote = await editImage(options, upload, `${path5.basename(source, path5.extname(source))}.png`, args.prompt, signal);
700
+ const raw = await fetchImageBytes(remote, signal);
701
+ const destination = resolveDestination(args.filename, options, directory);
702
+ await persistImage(raw, destination, options.maxSize);
703
+ return await render("Image edited", args.prompt, destination, (m) => void toolCtx.progress({ title: m }), remote.usage);
704
+ } finally {
705
+ clear();
706
+ }
707
+ }
708
+ });
709
+ tools.add({
710
+ name: "image_cache",
711
+ description: CACHE_DESCRIPTION,
712
+ input: CacheInput,
713
+ options: { codemode: false },
699
714
  execute: async (args) => {
700
715
  if (args.action === "clean") {
701
716
  const removed = server.cleanCache();
702
717
  return {
703
- title: `已清理图片缓存(${removed.count} 个 / ${humanBytes(removed.bytes)})`,
704
- output: [
718
+ content: [
705
719
  `已清理 ${removed.count} 个预览文件,共 ${humanBytes(removed.bytes)}。`,
706
720
  `缓存目录:${removed.dir}`,
707
721
  `注意:这不影响 outDir 里的原图,会话里的图片仍能正常显示(需要时会重新生成缩略图)。`
@@ -712,8 +726,7 @@ var OpenCodeImagePlugin = async (_input, rawOptions) => {
712
726
  }
713
727
  const stats = server.cacheStats();
714
728
  return {
715
- title: `图片缓存:${stats.count} 个 / ${humanBytes(stats.bytes)}`,
716
- output: [
729
+ content: [
717
730
  `缓存目录:${stats.dir}`,
718
731
  `文件数量:${stats.count}`,
719
732
  `占用大小:${humanBytes(stats.bytes)}`,
@@ -724,15 +737,17 @@ var OpenCodeImagePlugin = async (_input, rawOptions) => {
724
737
  metadata: stats
725
738
  };
726
739
  }
727
- })
728
- },
729
- dispose: async () => {
730
- server.stop();
731
- }
732
- };
733
- };
740
+ });
741
+ });
742
+ if (options.injectPrompt)
743
+ await ctx.session.hook("context", (event) => {
744
+ event.system.push({ type: "text", text: SYSTEM_RULE });
745
+ });
746
+ return () => server.stop();
747
+ }
748
+ });
734
749
  var src_default = OpenCodeImagePlugin;
735
750
  export {
736
- src_default as default,
737
- OpenCodeImagePlugin
751
+ OpenCodeImagePlugin,
752
+ src_default as default
738
753
  };
package/dist/options.d.ts CHANGED
@@ -30,6 +30,8 @@ export type PluginOptions = {
30
30
  serverIdleMs?: number;
31
31
  /** 首次出图时查一次 npm 最新版,落后则在工具输出末尾附升级提示。默认开启 */
32
32
  updateCheck?: boolean;
33
+ /** 往系统提示里注入一条"用哪个工具生图"的规则。默认开启 */
34
+ injectPrompt?: boolean;
33
35
  /** 对话里展示用的缩略图缓存目录,默认 <系统临时目录>/opencode-image */
34
36
  cacheDir?: string;
35
37
  /** 缩略图缓存最多保留多少个文件,超出按最久未访问淘汰;0 表示不限制 */
@@ -56,6 +58,7 @@ export type ResolvedOptions = {
56
58
  serverPort: number;
57
59
  serverIdleMs: number;
58
60
  updateCheck: boolean;
61
+ injectPrompt: boolean;
59
62
  cacheDir: string;
60
63
  cacheLimit: number;
61
64
  timeoutMs: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-image",
3
- "version": "0.1.2",
4
- "description": "Image generation and editing tools for opencode, with inline rendering in the conversation.",
3
+ "version": "2.0.0",
4
+ "description": "Image generation and editing tools for opencode (v2 plugin API), with inline rendering in the conversation.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -23,7 +23,8 @@
23
23
  },
24
24
  "publishConfig": {
25
25
  "registry": "https://registry.npmjs.org/",
26
- "access": "public"
26
+ "access": "public",
27
+ "tag": "next"
27
28
  },
28
29
  "exports": {
29
30
  ".": {
@@ -39,25 +40,31 @@
39
40
  ],
40
41
  "scripts": {
41
42
  "build": "bun run build:js && bun run build:types",
42
- "build:js": "bun build src/index.ts --target=node --format=esm --external sharp --external @opencode-ai/plugin --outfile=dist/index.js",
43
+ "build:js": "bun build src/index.ts --target=node --format=esm --external sharp --external @opencode/plugin --external effect --outfile=dist/index.js",
43
44
  "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
44
45
  "typecheck": "tsc --noEmit",
45
46
  "test": "bun test",
46
47
  "prepublishOnly": "bun run build && bun test",
47
- "release": "npm version patch && npm publish && rm -rf ~/.cache/opencode/packages/opencode-image*"
48
+ "release": "npm publish && rm -rf ~/.cache/opencode/packages/opencode-image*",
49
+ "release:legacy": "npm version patch && npm publish --tag latest"
48
50
  },
49
51
  "engines": {
50
52
  "node": ">=18"
51
53
  },
52
54
  "dependencies": {
53
- "@opencode-ai/plugin": "^1.18.30",
54
55
  "sharp": "^0.35.4"
55
56
  },
57
+ "peerDependencies": {
58
+ "@opencode/plugin": ">=2.0.27",
59
+ "effect": "^4.0.0"
60
+ },
56
61
  "devDependencies": {
62
+ "@opencode/plugin": "2.0.27",
57
63
  "@types/node": "^24.12.2",
64
+ "effect": "4.0.1",
58
65
  "typescript": "^5.8.2"
59
66
  },
60
67
  "bin": {
61
- "opencode-image": "./bin/cli.js"
68
+ "opencode-image": "bin/cli.js"
62
69
  }
63
70
  }