@cozeclaw/coze-openclaw-plugin 0.1.0 → 0.1.1

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
@@ -119,6 +119,7 @@ node {baseDir}/scripts/gen.mjs --prompt "A futuristic city at sunset"
119
119
  - The plugin requires `apiKey` in plugin config and does not fall back to `COZE_*` environment variables
120
120
  - Skill visibility depends on `plugins.entries.coze-openclaw-plugin.config.apiKey`
121
121
  - `coze_web_fetch` fetches URLs sequentially
122
+ - Run `npm test` to execute the maintained unit tests under `./src`
122
123
 
123
124
  ## License
124
125
 
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@cozeclaw/coze-openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenClaw Coze tools and bundled skills",
5
5
  "type": "module",
6
+ "scripts": {
7
+ "test": "vitest run src/config.test.ts src/skill-cli.test.ts src/tools/web-search.test.ts src/tools/web-fetch.test.ts"
8
+ },
6
9
  "devDependencies": {
7
- "openclaw": "v2026.2.6"
10
+ "openclaw": "v2026.2.6",
11
+ "vitest": "^3.2.4"
8
12
  },
9
13
  "dependencies": {
10
14
  "@sinclair/typebox": "0.34.48",
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const hoisted = vi.hoisted(() => ({
4
+ loadCozePluginConfigFromOpenClawConfig: vi.fn(),
5
+ resolveCozeClientConfig: vi.fn(),
6
+ synthesizeSpeech: vi.fn(),
7
+ transcribeSpeech: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("./config.js", async () => {
11
+ const actual = await vi.importActual<typeof import("./config.js")>("./config.js");
12
+ return {
13
+ ...actual,
14
+ loadCozePluginConfigFromOpenClawConfig: (...args: unknown[]) =>
15
+ hoisted.loadCozePluginConfigFromOpenClawConfig(...args),
16
+ resolveCozeClientConfig: (...args: unknown[]) => hoisted.resolveCozeClientConfig(...args),
17
+ };
18
+ });
19
+
20
+ vi.mock("./shared/tts.js", () => ({
21
+ synthesizeSpeech: (...args: unknown[]) => hoisted.synthesizeSpeech(...args),
22
+ }));
23
+
24
+ vi.mock("./shared/asr.js", () => ({
25
+ transcribeSpeech: (...args: unknown[]) => hoisted.transcribeSpeech(...args),
26
+ }));
27
+
28
+ const { runAsrCli, runTtsCli } = await import("./skill-cli.js");
29
+
30
+ function createIo() {
31
+ return {
32
+ logs: [] as string[],
33
+ errors: [] as string[],
34
+ log(message: string) {
35
+ this.logs.push(message);
36
+ },
37
+ error(message: string) {
38
+ this.errors.push(message);
39
+ },
40
+ };
41
+ }
42
+
43
+ describe("skill cli", () => {
44
+ it("rejects missing values for single-value flags", async () => {
45
+ const io = createIo();
46
+
47
+ const code = await runAsrCli(["--file", "--url", "https://example.com/audio.mp3"], {}, io);
48
+
49
+ expect(code).toBe(1);
50
+ expect(io.errors).toContain("Error: --file requires a value");
51
+ expect(hoisted.transcribeSpeech).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it("rejects missing values in tts options before calling the sdk", async () => {
55
+ hoisted.loadCozePluginConfigFromOpenClawConfig.mockResolvedValue({});
56
+ hoisted.resolveCozeClientConfig.mockReturnValue({ apiKey: "test-key" });
57
+ const io = createIo();
58
+
59
+ const code = await runTtsCli(["--text", "Hello", "--speaker", "--format", "mp3"], {}, io);
60
+
61
+ expect(code).toBe(1);
62
+ expect(io.errors).toContain("Error: --speaker requires a value");
63
+ expect(hoisted.synthesizeSpeech).not.toHaveBeenCalled();
64
+ });
65
+ });
package/src/skill-cli.ts CHANGED
@@ -36,7 +36,14 @@ async function requireConfig(env: NodeJS.ProcessEnv): Promise<CozeConfig> {
36
36
 
37
37
  function readArg(args: string[], name: string): string | undefined {
38
38
  const index = args.indexOf(name);
39
- return index >= 0 ? args[index + 1] : undefined;
39
+ if (index < 0) {
40
+ return undefined;
41
+ }
42
+ const value = args[index + 1];
43
+ if (!value || value.startsWith("-")) {
44
+ return undefined;
45
+ }
46
+ return value;
40
47
  }
41
48
 
42
49
  function readTrailingValues(args: string[], name: string): string[] {
@@ -64,11 +71,35 @@ function readNumberArg(args: string[], name: string): number | undefined {
64
71
  return Number.isFinite(parsed) ? parsed : undefined;
65
72
  }
66
73
 
74
+ function findMissingValueFlag(args: string[], names: string[]): string | undefined {
75
+ for (const name of names) {
76
+ const index = args.indexOf(name);
77
+ if (index < 0) {
78
+ continue;
79
+ }
80
+ const value = args[index + 1];
81
+ if (!value || value.startsWith("-")) {
82
+ return name;
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+
67
88
  export async function runImageCli(
68
89
  args: string[],
69
90
  env: NodeJS.ProcessEnv = process.env,
70
91
  io: SkillIo = createDefaultIo(),
71
92
  ): Promise<number> {
93
+ const missingValueFlag = findMissingValueFlag(args, [
94
+ "--prompt",
95
+ "--count",
96
+ "--size",
97
+ "--max-sequential",
98
+ ]);
99
+ if (missingValueFlag) {
100
+ io.error(`Error: ${missingValueFlag} requires a value`);
101
+ return 1;
102
+ }
72
103
  const prompt = readArg(args, "--prompt");
73
104
  if (!prompt) {
74
105
  io.error("Error: --prompt is required");
@@ -104,6 +135,19 @@ export async function runTtsCli(
104
135
  env: NodeJS.ProcessEnv = process.env,
105
136
  io: SkillIo = createDefaultIo(),
106
137
  ): Promise<number> {
138
+ const missingValueFlag = findMissingValueFlag(args, [
139
+ "--text",
140
+ "--texts",
141
+ "--speaker",
142
+ "--format",
143
+ "--sample-rate",
144
+ "--speech-rate",
145
+ "--loudness-rate",
146
+ ]);
147
+ if (missingValueFlag) {
148
+ io.error(`Error: ${missingValueFlag} requires a value`);
149
+ return 1;
150
+ }
107
151
  const text = readArg(args, "--text");
108
152
  const texts = readTrailingValues(args, "--texts");
109
153
  const mergedTexts = text ? [text] : texts;
@@ -150,6 +194,11 @@ export async function runAsrCli(
150
194
  env: NodeJS.ProcessEnv = process.env,
151
195
  io: SkillIo = createDefaultIo(),
152
196
  ): Promise<number> {
197
+ const missingValueFlag = findMissingValueFlag(args, ["--url", "-u", "--file", "-f"]);
198
+ if (missingValueFlag) {
199
+ io.error(`Error: ${missingValueFlag} requires a value`);
200
+ return 1;
201
+ }
153
202
  const url = readArg(args, "--url") ?? readArg(args, "-u");
154
203
  const file = readArg(args, "--file") ?? readArg(args, "-f");
155
204
  if (!url && !file) {
@@ -103,4 +103,38 @@ describe("createCozeWebSearchTool", () => {
103
103
  });
104
104
  expect(getTextContent(result)).toContain("https://example.com/image.png");
105
105
  });
106
+
107
+ it("renders extracted content when needContent is enabled", async () => {
108
+ hoisted.searchWeb.mockResolvedValueOnce({
109
+ query: "OpenClaw",
110
+ type: "web",
111
+ items: [
112
+ {
113
+ title: "OpenClaw",
114
+ url: "https://openclaw.ai",
115
+ snippet: "Short summary",
116
+ content: "Long-form extracted page content.",
117
+ },
118
+ ],
119
+ });
120
+
121
+ const tool = createCozeWebSearchTool({
122
+ pluginConfig: { apiKey: "test-key" },
123
+ logger: {
124
+ debug() {},
125
+ info() {},
126
+ warn() {},
127
+ error() {},
128
+ },
129
+ env: {},
130
+ });
131
+
132
+ const result = await tool.execute("call-3", {
133
+ query: "OpenClaw",
134
+ type: "web",
135
+ needContent: true,
136
+ });
137
+
138
+ expect(getTextContent(result)).toContain("Long-form extracted page content.");
139
+ });
106
140
  });
@@ -53,7 +53,10 @@ const SearchToolSchema = Type.Object(
53
53
 
54
54
  type SearchToolParams = Static<typeof SearchToolSchema>;
55
55
 
56
- function buildSearchText(params: Awaited<ReturnType<typeof searchWeb>>): string {
56
+ function buildSearchText(
57
+ params: Awaited<ReturnType<typeof searchWeb>>,
58
+ options: { includeContent: boolean },
59
+ ): string {
57
60
  const lines = [`Coze web search: ${params.query}`];
58
61
  if (params.summary) {
59
62
  lines.push("", `Summary: ${params.summary}`);
@@ -76,6 +79,10 @@ function buildSearchText(params: Awaited<ReturnType<typeof searchWeb>>): string
76
79
  if (item.snippet) {
77
80
  lines.push(` ${item.snippet}`);
78
81
  }
82
+ if (options.includeContent && item.content) {
83
+ lines.push(" Content:");
84
+ lines.push(` ${item.content}`);
85
+ }
79
86
  }
80
87
  return lines.join("\n");
81
88
  }
@@ -107,7 +114,12 @@ export function createCozeWebSearchTool(params: {
107
114
  try {
108
115
  const result = await searchWeb(toolParams, clientConfig);
109
116
  return {
110
- content: [{ type: "text", text: buildSearchText(result) }],
117
+ content: [
118
+ {
119
+ type: "text",
120
+ text: buildSearchText(result, { includeContent: toolParams.needContent === true }),
121
+ },
122
+ ],
111
123
  details: {
112
124
  query: result.query,
113
125
  type: result.type,