workflowskill 0.1.0 → 0.2.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/skill/SKILL.md CHANGED
@@ -406,7 +406,7 @@ steps:
406
406
 
407
407
  - id: fetch_details
408
408
  type: tool
409
- tool: http.request
409
+ tool: api.get_item # platform-specific tool; use web.scrape for HTML pages
410
410
  each: $steps.get_ids.output.ids # iterate over ids array
411
411
  on_error: ignore # skip failed fetches, continue
412
412
  inputs:
@@ -432,9 +432,9 @@ outputs:
432
432
  value: $steps.fetch_details.output # the collected array of per-iteration results
433
433
  ```
434
434
 
435
- **Pattern: List → Fetch Details Filter → Summarize**
435
+ **Pattern: List → SliceFetch Details**
436
436
 
437
- Full example for "fetch Hacker News top stories":
437
+ Full example using generic tool names (substitute platform-specific tools as needed):
438
438
 
439
439
  ```yaml
440
440
  inputs:
@@ -443,19 +443,19 @@ inputs:
443
443
  default: 10
444
444
  base_url:
445
445
  type: string
446
- default: "https://hacker-news.firebaseio.com/v0/item/"
446
+ default: "https://api.example.com/item/"
447
447
 
448
448
  outputs:
449
- stories:
449
+ items:
450
450
  type: array
451
- value: $steps.fetch_stories.output
451
+ value: $steps.fetch_details.output
452
452
 
453
453
  steps:
454
- - id: get_top_ids
454
+ - id: get_ids
455
455
  type: tool
456
- tool: http.request
456
+ tool: api.list_items
457
457
  inputs:
458
- url: { type: string, value: "https://hacker-news.firebaseio.com/v0/topstories.json" }
458
+ url: { type: string, value: "https://api.example.com/items" }
459
459
  outputs:
460
460
  ids: { type: array, value: $result.body }
461
461
 
@@ -464,14 +464,16 @@ steps:
464
464
  operation: filter
465
465
  where: $index < $inputs.count # cap iteration count to avoid rate limiting
466
466
  inputs:
467
- items: { type: array, value: $steps.get_top_ids.output.ids }
467
+ items: { type: array, value: $steps.get_ids.output.ids }
468
468
  outputs:
469
469
  ids: { type: array }
470
470
 
471
- - id: fetch_stories
471
+ - id: fetch_details
472
472
  type: tool
473
- tool: http.request
473
+ tool: api.get_item
474
474
  each: $steps.slice_ids.output.ids
475
+ delay: "2s"
476
+ retry: { max: 3, delay: "2s", backoff: 1.5 }
475
477
  on_error: ignore
476
478
  inputs:
477
479
  url:
@@ -500,8 +502,9 @@ Use plain text output (`value: $result`) for the LLM step, then zip the LLM resu
500
502
  steps:
501
503
  - id: fetch_items
502
504
  type: tool
503
- tool: http.request
505
+ tool: api.get_item # platform-specific; use web.scrape for HTML pages
504
506
  each: $steps.get_ids.output.ids
507
+ delay: "2s"
505
508
  on_error: ignore
506
509
  inputs:
507
510
  url:
@@ -594,42 +597,69 @@ outputs:
594
597
 
595
598
  When a workflow fetches HTML and extracts structured data, follow this recipe:
596
599
 
597
- ### Step pattern: fetch → guard → extract
600
+ ### Step pattern: scrape → guard
601
+
602
+ `web.scrape` fetches the URL and applies CSS selectors in one step:
598
603
 
599
604
  ```yaml
600
605
  steps:
601
- - id: fetch_page
606
+ - id: scrape_data
602
607
  type: tool
603
- tool: http.request
608
+ tool: web.scrape
604
609
  retry: { max: 3, delay: "2s", backoff: 1.5 }
605
610
  inputs:
606
611
  url: { type: string, value: "https://example.com/search" }
607
- method: { type: string, value: "GET" }
608
612
  headers:
609
613
  type: object
610
614
  value: { "User-Agent": "Mozilla/5.0", "Accept": "text/html" }
615
+ selector: { type: string, value: "li.result-item" }
616
+ fields:
617
+ type: object
618
+ value:
619
+ title: "h3.title"
620
+ url: "a.link @href"
621
+ id: "@data-pid"
622
+ limit: { type: int, value: 50 }
611
623
  outputs:
612
- html: { type: string, value: $result.body }
624
+ items: { type: array, value: $result.results }
613
625
 
614
626
  - id: guard_empty
615
627
  type: exit
616
- condition: $steps.fetch_page.output.html == ""
628
+ condition: $steps.scrape_data.output.items.length == 0
617
629
  status: success
618
630
  output: { results: [] }
631
+ inputs: {}
632
+ outputs: {}
633
+ ```
619
634
 
620
- - id: extract_data
635
+ ### Multi-page scraping with `each`
636
+
637
+ When scraping multiple pages, combine `web.scrape` with `each`:
638
+
639
+ ```yaml
640
+ steps:
641
+ - id: get_page_list
642
+ type: tool
643
+ tool: api.get_sitemap # returns list of page URLs to scrape
644
+ outputs:
645
+ pages:
646
+ type: array
647
+
648
+ - id: scrape_pages
621
649
  type: tool
622
- tool: html.select
650
+ tool: web.scrape
651
+ each: $steps.get_page_list.output.pages
652
+ delay: "2s"
653
+ retry: { max: 3, delay: "2s", backoff: 1.5 }
654
+ on_error: ignore
623
655
  inputs:
624
- html: { type: string, value: $steps.fetch_page.output.html }
625
- selector: { type: string, value: "li.result-item" }
656
+ url: { type: string, value: $item }
657
+ selector: { type: string, value: "article.post" }
626
658
  fields:
627
659
  type: object
628
660
  value:
629
- title: "h3.title"
630
- url: "a.link @href"
631
- id: "@data-pid"
632
- limit: { type: int, value: 50 }
661
+ title: "h1.title"
662
+ body: "div.content"
633
663
  outputs:
634
664
  items: { type: array, value: $result.results }
635
665
  ```
@@ -645,7 +675,7 @@ Follow the Research protocol (Authoring Process, Phase 2) before writing selecto
645
675
  3. **Always declare inputs and outputs.** They enable validation and composability.
646
676
  4. **Use `value` on workflow outputs** to explicitly map step results to workflow outputs. Use `$steps.<id>.output.<field>` expressions. This is preferred over exit steps for producing output.
647
677
  5. **Use `value` on step outputs** to map fields from the raw executor result using `$result`. Required for LLM steps (which return raw text/JSON). Useful for tool steps when the response shape differs from what downstream steps need.
648
- 6. **Use `each` for per-item processing** on both tool and LLM steps. Always include `delay` on every `each` loop that calls an external service — `delay: "2s"` for HTTP tool steps, `delay: "1s"` for LLM steps. See *Iteration Patterns*.
678
+ 6. **Use `each` for per-item processing** on both tool and LLM steps. Always include `delay` on every `each` loop that calls an external service — `delay: "2s"` for tool steps (including `web.scrape`), `delay: "1s"` for LLM steps. See *Iteration Patterns*.
649
679
  7. **Add `on_error: ignore` for non-critical steps** like notifications.
650
680
  8. **Add `retry` for external API calls** (tool steps that might fail transiently).
651
681
  9. **Use `condition` guards for early exits** rather than letting empty data flow through.
@@ -657,7 +687,7 @@ Follow the Research protocol (Authoring Process, Phase 2) before writing selecto
657
687
  15. **Use `map` with `$index` for cross-array merging.** When multiple steps produce parallel arrays, use a `map` transform with bracket indexing (`$steps.other.output.field[$index]`) to zip them into structured objects. Never use an LLM step for pure data restructuring.
658
688
  16. **When JSON output is used, LLM prompts must say "raw JSON only — no markdown fences, no commentary."** Models default to wrapping JSON in ``` fences. The runtime parses the raw text with `JSON.parse`, which rejects fenced output. Every prompt that expects JSON output must explicitly instruct the model to respond with raw JSON. Put this instruction **last** in the prompt, after all data and task description, immediately before the model generates. This exploits recency bias — the last instruction the model sees is the most influential, especially when data references expand to large content that can push earlier instructions out of focus. Also describe the exact expected shape (e.g., "Your entire response must be a valid JSON object starting with { and ending with }").
659
689
  17. **Guard expensive steps behind deterministic exits.** Pattern: fetch → filter → exit guard → LLM. Use deterministic expressions (e.g., `$item.department == "Engineering"` or `$item.title contains "Product Manager"`) in `transform filter` steps before any LLM call. See *Patterns*.
660
- 18. **Prefer bulk endpoints over per-item iteration.** When `each` + `http.request` is unavoidable, always add `delay: "2s"` (minimum), cap iteration count, and add `retry` with `backoff`. `delay` is not optional — external APIs rate-limit without warning and delays are free. Same applies to `each` + `llm` steps: always add `delay: "1s"`. See *Iteration Patterns*.
690
+ 18. **Prefer bulk endpoints over per-item iteration.** When per-item `each` + tool calls (including `web.scrape`) are unavoidable, always add `delay: "2s"` (minimum), cap iteration count, and add `retry` with `backoff`. `delay` is not optional — external sites and APIs rate-limit without warning and delays are free. Same applies to `each` + `llm` steps: always add `delay: "1s"`. See *Iteration Patterns*.
661
691
  19. **Prefer plain text LLM output over JSON.** For single-value tasks (summarization, classification, scoring), use `value: $result` and instruct the model to return plain text. Reserve JSON (`value: $result.field`) for multi-field output where every field requires LLM reasoning. In `each` + LLM patterns, always use plain text + a `map` transform to zip LLM output with structural data from the source array. See *Iteration Patterns*.
662
692
 
663
693
  ## Output Format
@@ -720,6 +750,6 @@ After writing the file, always validate it against the runtime. The validation c
720
750
  - [ ] LLM prompts expecting JSON include "raw JSON only — no markdown fences" instruction
721
751
  - [ ] LLM steps with `each` prefer plain text output (`value: $result`) over JSON (`value: $result.field`)
722
752
  - [ ] Every `each` loop that calls an external service has `delay` (tool steps: `"2s"` minimum; LLM steps: `"1s"` minimum)
723
- - [ ] `each` + `http.request` steps are bounded (preceded by a cap) and have `retry` with `backoff`
753
+ - [ ] `each` + `web.scrape` steps are bounded (preceded by a cap) and have `retry` with `backoff`
724
754
 
725
755
  If validation fails, fix the errors and revalidate.
@@ -1,209 +0,0 @@
1
- import { gmail } from "@googleapis/gmail";
2
-
3
- //#region src/dev-tools/tools/gmail.ts
4
- const searchDescriptor = {
5
- name: "gmail.search",
6
- description: "Search Gmail messages matching a query.",
7
- inputSchema: {
8
- type: "object",
9
- properties: {
10
- query: {
11
- type: "string",
12
- description: "Gmail search query (same syntax as Gmail search bar)"
13
- },
14
- max_results: {
15
- type: "number",
16
- description: "Maximum messages to return (default: 10)"
17
- }
18
- },
19
- required: ["query"]
20
- },
21
- outputSchema: {
22
- type: "object",
23
- properties: { messages: {
24
- type: "array",
25
- items: {
26
- type: "object",
27
- properties: {
28
- id: { type: "string" },
29
- from: { type: "string" },
30
- to: { type: "string" },
31
- subject: { type: "string" },
32
- snippet: { type: "string" },
33
- date: { type: "string" }
34
- }
35
- }
36
- } }
37
- }
38
- };
39
- const readDescriptor = {
40
- name: "gmail.read",
41
- description: "Read a full Gmail message by ID.",
42
- inputSchema: {
43
- type: "object",
44
- properties: { message_id: {
45
- type: "string",
46
- description: "Gmail message ID"
47
- } },
48
- required: ["message_id"]
49
- },
50
- outputSchema: {
51
- type: "object",
52
- properties: {
53
- id: { type: "string" },
54
- from: { type: "string" },
55
- to: { type: "string" },
56
- subject: { type: "string" },
57
- body: { type: "string" },
58
- date: { type: "string" }
59
- }
60
- }
61
- };
62
- const sendDescriptor = {
63
- name: "gmail.send",
64
- description: "Send an email via Gmail.",
65
- inputSchema: {
66
- type: "object",
67
- properties: {
68
- to: {
69
- type: "string",
70
- description: "Recipient email address"
71
- },
72
- subject: {
73
- type: "string",
74
- description: "Email subject"
75
- },
76
- body: {
77
- type: "string",
78
- description: "Email body (plain text)"
79
- }
80
- },
81
- required: [
82
- "to",
83
- "subject",
84
- "body"
85
- ]
86
- },
87
- outputSchema: {
88
- type: "object",
89
- properties: { message_id: { type: "string" } }
90
- }
91
- };
92
- function getHeader(headers, name) {
93
- return headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? "";
94
- }
95
- function decodeBody(message) {
96
- const parts = message.payload?.parts;
97
- if (parts) {
98
- const textPart = parts.find((p) => p.mimeType === "text/plain");
99
- if (textPart?.body?.data) return Buffer.from(textPart.body.data, "base64url").toString("utf-8");
100
- }
101
- if (message.payload?.body?.data) return Buffer.from(message.payload.body.data, "base64url").toString("utf-8");
102
- return message.snippet ?? "";
103
- }
104
- function createHandlers(auth) {
105
- const client = gmail({
106
- version: "v1",
107
- auth
108
- });
109
- async function search(args) {
110
- const query = args.query;
111
- const maxResults = args.max_results ?? 10;
112
- if (!query || typeof query !== "string") return {
113
- output: null,
114
- error: "gmail.search: \"query\" is required"
115
- };
116
- try {
117
- const messageIds = (await client.users.messages.list({
118
- userId: "me",
119
- q: query,
120
- maxResults
121
- })).data.messages ?? [];
122
- return { output: { messages: await Promise.all(messageIds.map(async (m) => {
123
- const msg = await client.users.messages.get({
124
- userId: "me",
125
- id: m.id,
126
- format: "metadata",
127
- metadataHeaders: [
128
- "From",
129
- "To",
130
- "Subject",
131
- "Date"
132
- ]
133
- });
134
- const headers = msg.data.payload?.headers;
135
- return {
136
- id: msg.data.id ?? "",
137
- from: getHeader(headers, "From"),
138
- to: getHeader(headers, "To"),
139
- subject: getHeader(headers, "Subject"),
140
- snippet: msg.data.snippet ?? "",
141
- date: getHeader(headers, "Date")
142
- };
143
- })) } };
144
- } catch (err) {
145
- return {
146
- output: null,
147
- error: `gmail.search failed: ${err instanceof Error ? err.message : String(err)}`
148
- };
149
- }
150
- }
151
- async function read(args) {
152
- const messageId = args.message_id;
153
- if (!messageId || typeof messageId !== "string") return {
154
- output: null,
155
- error: "gmail.read: \"message_id\" is required"
156
- };
157
- try {
158
- const msg = await client.users.messages.get({
159
- userId: "me",
160
- id: messageId,
161
- format: "full"
162
- });
163
- const headers = msg.data.payload?.headers;
164
- return { output: {
165
- id: msg.data.id ?? "",
166
- from: getHeader(headers, "From"),
167
- to: getHeader(headers, "To"),
168
- subject: getHeader(headers, "Subject"),
169
- body: decodeBody(msg.data),
170
- date: getHeader(headers, "Date")
171
- } };
172
- } catch (err) {
173
- return {
174
- output: null,
175
- error: `gmail.read failed: ${err instanceof Error ? err.message : String(err)}`
176
- };
177
- }
178
- }
179
- async function send(args) {
180
- const to = args.to;
181
- const subject = args.subject;
182
- const body = args.body;
183
- if (!to || !subject || !body) return {
184
- output: null,
185
- error: "gmail.send: \"to\", \"subject\", and \"body\" are required"
186
- };
187
- try {
188
- const raw = Buffer.from(`To: ${to}\r\nSubject: ${subject}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body}`).toString("base64url");
189
- return { output: { message_id: (await client.users.messages.send({
190
- userId: "me",
191
- requestBody: { raw }
192
- })).data.id ?? "" } };
193
- } catch (err) {
194
- return {
195
- output: null,
196
- error: `gmail.send failed: ${err instanceof Error ? err.message : String(err)}`
197
- };
198
- }
199
- }
200
- return {
201
- search,
202
- read,
203
- send
204
- };
205
- }
206
-
207
- //#endregion
208
- export { createHandlers, readDescriptor, searchDescriptor, sendDescriptor };
209
- //# sourceMappingURL=gmail-CYBJ6Dxa.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"gmail-CYBJ6Dxa.mjs","names":[],"sources":["../src/dev-tools/tools/gmail.ts"],"sourcesContent":["// Tools: gmail.search, gmail.read, gmail.send — Gmail integration via googleapis.\n\nimport { gmail, gmail_v1 } from '@googleapis/gmail';\nimport type { OAuth2Client } from 'google-auth-library';\nimport type { ToolDescriptor, ToolResult } from '../../types/index.js';\n\n// ─── Tool descriptors ────────────────────────────────────────────────────────\n\nexport const searchDescriptor: ToolDescriptor = {\n name: 'gmail.search',\n description: 'Search Gmail messages matching a query.',\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Gmail search query (same syntax as Gmail search bar)' },\n max_results: { type: 'number', description: 'Maximum messages to return (default: 10)' },\n },\n required: ['query'],\n },\n outputSchema: {\n type: 'object',\n properties: {\n messages: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n from: { type: 'string' },\n to: { type: 'string' },\n subject: { type: 'string' },\n snippet: { type: 'string' },\n date: { type: 'string' },\n },\n },\n },\n },\n },\n};\n\nexport const readDescriptor: ToolDescriptor = {\n name: 'gmail.read',\n description: 'Read a full Gmail message by ID.',\n inputSchema: {\n type: 'object',\n properties: {\n message_id: { type: 'string', description: 'Gmail message ID' },\n },\n required: ['message_id'],\n },\n outputSchema: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n from: { type: 'string' },\n to: { type: 'string' },\n subject: { type: 'string' },\n body: { type: 'string' },\n date: { type: 'string' },\n },\n },\n};\n\nexport const sendDescriptor: ToolDescriptor = {\n name: 'gmail.send',\n description: 'Send an email via Gmail.',\n inputSchema: {\n type: 'object',\n properties: {\n to: { type: 'string', description: 'Recipient email address' },\n subject: { type: 'string', description: 'Email subject' },\n body: { type: 'string', description: 'Email body (plain text)' },\n },\n required: ['to', 'subject', 'body'],\n },\n outputSchema: {\n type: 'object',\n properties: {\n message_id: { type: 'string' },\n },\n },\n};\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\nfunction getHeader(\n headers: gmail_v1.Schema$MessagePartHeader[] | undefined,\n name: string,\n): string {\n return headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? '';\n}\n\nfunction decodeBody(message: gmail_v1.Schema$Message): string {\n // Try to get plain text body from parts\n const parts = message.payload?.parts;\n if (parts) {\n const textPart = parts.find((p) => p.mimeType === 'text/plain');\n if (textPart?.body?.data) {\n return Buffer.from(textPart.body.data, 'base64url').toString('utf-8');\n }\n }\n // Fallback to payload body\n if (message.payload?.body?.data) {\n return Buffer.from(message.payload.body.data, 'base64url').toString('utf-8');\n }\n return message.snippet ?? '';\n}\n\n// ─── Handlers ────────────────────────────────────────────────────────────────\n\nexport function createHandlers(auth: OAuth2Client) {\n const client = gmail({ version: 'v1', auth });\n\n async function search(args: Record<string, unknown>): Promise<ToolResult> {\n const query = args.query as string | undefined;\n const maxResults = (args.max_results as number | undefined) ?? 10;\n\n if (!query || typeof query !== 'string') {\n return { output: null, error: 'gmail.search: \"query\" is required' };\n }\n\n try {\n const listRes = await client.users.messages.list({\n userId: 'me',\n q: query,\n maxResults,\n });\n\n const messageIds = listRes.data.messages ?? [];\n const messages = await Promise.all(\n messageIds.map(async (m: gmail_v1.Schema$Message) => {\n const msg = await client.users.messages.get({\n userId: 'me',\n id: m.id!,\n format: 'metadata',\n metadataHeaders: ['From', 'To', 'Subject', 'Date'],\n });\n const headers = msg.data.payload?.headers;\n return {\n id: msg.data.id ?? '',\n from: getHeader(headers, 'From'),\n to: getHeader(headers, 'To'),\n subject: getHeader(headers, 'Subject'),\n snippet: msg.data.snippet ?? '',\n date: getHeader(headers, 'Date'),\n };\n }),\n );\n\n return { output: { messages } };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { output: null, error: `gmail.search failed: ${message}` };\n }\n }\n\n async function read(args: Record<string, unknown>): Promise<ToolResult> {\n const messageId = args.message_id as string | undefined;\n\n if (!messageId || typeof messageId !== 'string') {\n return { output: null, error: 'gmail.read: \"message_id\" is required' };\n }\n\n try {\n const msg = await client.users.messages.get({\n userId: 'me',\n id: messageId,\n format: 'full',\n });\n\n const headers = msg.data.payload?.headers;\n return {\n output: {\n id: msg.data.id ?? '',\n from: getHeader(headers, 'From'),\n to: getHeader(headers, 'To'),\n subject: getHeader(headers, 'Subject'),\n body: decodeBody(msg.data),\n date: getHeader(headers, 'Date'),\n },\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { output: null, error: `gmail.read failed: ${message}` };\n }\n }\n\n async function send(args: Record<string, unknown>): Promise<ToolResult> {\n const to = args.to as string | undefined;\n const subject = args.subject as string | undefined;\n const body = args.body as string | undefined;\n\n if (!to || !subject || !body) {\n return { output: null, error: 'gmail.send: \"to\", \"subject\", and \"body\" are required' };\n }\n\n try {\n const raw = Buffer.from(\n `To: ${to}\\r\\nSubject: ${subject}\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\n${body}`,\n ).toString('base64url');\n\n const res = await client.users.messages.send({\n userId: 'me',\n requestBody: { raw },\n });\n\n return { output: { message_id: res.data.id ?? '' } };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { output: null, error: `gmail.send failed: ${message}` };\n }\n }\n\n return { search, read, send };\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"html-select-BZPYAr6k.mjs","names":[],"sources":["../src/dev-tools/tools/html-select.ts"],"sourcesContent":["// Tool: html.select — extract data from HTML using CSS selectors.\n\nimport * as cheerio from 'cheerio';\nimport type { ToolDescriptor, ToolResult } from '../../types/index.js';\n\nexport interface HtmlSelectArgs {\n html: string;\n selector: string;\n attribute?: string;\n limit?: number;\n fields?: Record<string, string>;\n}\n\nexport const descriptor: ToolDescriptor = {\n name: 'html.select',\n description:\n 'Extract data from HTML using CSS selectors. Returns text content, attribute values, or structured objects.',\n inputSchema: {\n type: 'object',\n properties: {\n html: { type: 'string', description: 'HTML content to search' },\n selector: { type: 'string', description: 'CSS selector to match elements' },\n attribute: {\n type: 'string',\n description: 'Attribute to extract instead of text content (ignored when fields is present)',\n },\n limit: { type: 'number', description: 'Maximum number of results to return' },\n fields: {\n type: 'object',\n description:\n 'Map of field names to sub-selectors for structured extraction. Each value is a CSS selector scoped to the matched parent. Append \" @attr\" to extract an attribute (e.g. \"a @href\", \"img @src\"). Bare \"@attr\" extracts from the parent itself. When present, returns array of objects instead of strings.',\n },\n },\n required: ['html', 'selector'],\n },\n outputSchema: {\n type: 'object',\n properties: {\n results: {\n type: 'array',\n description: 'Extracted values: strings without fields, objects with fields',\n },\n },\n },\n};\n\n/**\n * Parse a field spec string into a sub-selector and optional attribute name.\n * Formats: \"h3.title\" → text, \"a.link @href\" → attribute, \"@data-id\" → parent attribute.\n */\nfunction parseFieldSpec(spec: string): { subSelector: string | null; attr: string | null } {\n const atIndex = spec.lastIndexOf(' @');\n if (atIndex >= 0) {\n // \"sub-selector @attr\"\n const subSelector = spec.slice(0, atIndex).trim() || null;\n const attr = spec.slice(atIndex + 2).trim();\n return { subSelector, attr: attr || null };\n }\n if (spec.startsWith('@')) {\n // Bare \"@attr\" — extract attribute from parent\n return { subSelector: null, attr: spec.slice(1) };\n }\n // Plain sub-selector — extract text\n return { subSelector: spec, attr: null };\n}\n\nexport async function handler(args: Record<string, unknown>): Promise<ToolResult> {\n const { html, selector, attribute, limit, fields } = args as unknown as HtmlSelectArgs;\n\n if (!html || typeof html !== 'string') {\n return { output: null, error: 'html.select: \"html\" is required and must be a string' };\n }\n if (!selector || typeof selector !== 'string') {\n return { output: null, error: 'html.select: \"selector\" is required and must be a string' };\n }\n\n const $ = cheerio.load(html);\n const elements = $(selector);\n\n if (fields && typeof fields === 'object') {\n // Structured extraction: return array of objects\n const parsedFields = Object.entries(fields).map(([name, spec]) => ({\n name,\n ...parseFieldSpec(spec),\n }));\n\n let results: Array<Record<string, string | null>> = [];\n elements.each((_i, el) => {\n const row: Record<string, string | null> = {};\n for (const { name, subSelector, attr } of parsedFields) {\n const target = subSelector ? $(el).find(subSelector).first() : $(el);\n if (target.length === 0) {\n row[name] = null;\n } else if (attr) {\n row[name] = target.attr(attr) ?? null;\n } else {\n row[name] = target.text().trim();\n }\n }\n results.push(row);\n });\n\n if (limit !== undefined && limit > 0) {\n results = results.slice(0, limit);\n }\n\n return { output: { results } };\n }\n\n // Simple extraction: return array of strings (original behavior)\n let results: string[] = [];\n elements.each((_i, el) => {\n if (attribute) {\n const val = $(el).attr(attribute);\n if (val !== undefined) {\n results.push(val);\n }\n } else {\n results.push($(el).text().trim());\n }\n });\n\n if (limit !== undefined && limit > 0) {\n results = results.slice(0, limit);\n }\n\n return { output: { results } };\n}\n"],"mappings":";;;AAaA,MAAa,aAA6B;CACxC,MAAM;CACN,aACE;CACF,aAAa;EACX,MAAM;EACN,YAAY;GACV,MAAM;IAAE,MAAM;IAAU,aAAa;IAA0B;GAC/D,UAAU;IAAE,MAAM;IAAU,aAAa;IAAkC;GAC3E,WAAW;IACT,MAAM;IACN,aAAa;IACd;GACD,OAAO;IAAE,MAAM;IAAU,aAAa;IAAuC;GAC7E,QAAQ;IACN,MAAM;IACN,aACE;IACH;GACF;EACD,UAAU,CAAC,QAAQ,WAAW;EAC/B;CACD,cAAc;EACZ,MAAM;EACN,YAAY,EACV,SAAS;GACP,MAAM;GACN,aAAa;GACd,EACF;EACF;CACF;;;;;AAMD,SAAS,eAAe,MAAmE;CACzF,MAAM,UAAU,KAAK,YAAY,KAAK;AACtC,KAAI,WAAW,EAIb,QAAO;EAAE,aAFW,KAAK,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI;EAE/B,MADT,KAAK,MAAM,UAAU,EAAE,CAAC,MAAM,IACP;EAAM;AAE5C,KAAI,KAAK,WAAW,IAAI,CAEtB,QAAO;EAAE,aAAa;EAAM,MAAM,KAAK,MAAM,EAAE;EAAE;AAGnD,QAAO;EAAE,aAAa;EAAM,MAAM;EAAM;;AAG1C,eAAsB,QAAQ,MAAoD;CAChF,MAAM,EAAE,MAAM,UAAU,WAAW,OAAO,WAAW;AAErD,KAAI,CAAC,QAAQ,OAAO,SAAS,SAC3B,QAAO;EAAE,QAAQ;EAAM,OAAO;EAAwD;AAExF,KAAI,CAAC,YAAY,OAAO,aAAa,SACnC,QAAO;EAAE,QAAQ;EAAM,OAAO;EAA4D;CAG5F,MAAM,IAAI,QAAQ,KAAK,KAAK;CAC5B,MAAM,WAAW,EAAE,SAAS;AAE5B,KAAI,UAAU,OAAO,WAAW,UAAU;EAExC,MAAM,eAAe,OAAO,QAAQ,OAAO,CAAC,KAAK,CAAC,MAAM,WAAW;GACjE;GACA,GAAG,eAAe,KAAK;GACxB,EAAE;EAEH,IAAI,UAAgD,EAAE;AACtD,WAAS,MAAM,IAAI,OAAO;GACxB,MAAM,MAAqC,EAAE;AAC7C,QAAK,MAAM,EAAE,MAAM,aAAa,UAAU,cAAc;IACtD,MAAM,SAAS,cAAc,EAAE,GAAG,CAAC,KAAK,YAAY,CAAC,OAAO,GAAG,EAAE,GAAG;AACpE,QAAI,OAAO,WAAW,EACpB,KAAI,QAAQ;aACH,KACT,KAAI,QAAQ,OAAO,KAAK,KAAK,IAAI;QAEjC,KAAI,QAAQ,OAAO,MAAM,CAAC,MAAM;;AAGpC,WAAQ,KAAK,IAAI;IACjB;AAEF,MAAI,UAAU,UAAa,QAAQ,EACjC,WAAU,QAAQ,MAAM,GAAG,MAAM;AAGnC,SAAO,EAAE,QAAQ,EAAE,SAAS,EAAE;;CAIhC,IAAI,UAAoB,EAAE;AAC1B,UAAS,MAAM,IAAI,OAAO;AACxB,MAAI,WAAW;GACb,MAAM,MAAM,EAAE,GAAG,CAAC,KAAK,UAAU;AACjC,OAAI,QAAQ,OACV,SAAQ,KAAK,IAAI;QAGnB,SAAQ,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC;GAEnC;AAEF,KAAI,UAAU,UAAa,QAAQ,EACjC,WAAU,QAAQ,MAAM,GAAG,MAAM;AAGnC,QAAO,EAAE,QAAQ,EAAE,SAAS,EAAE"}
@@ -1,91 +0,0 @@
1
- //#region src/dev-tools/tools/http-request.ts
2
- const descriptor = {
3
- name: "http.request",
4
- description: "Make an HTTP request and return the response status, headers, and body.",
5
- inputSchema: {
6
- type: "object",
7
- properties: {
8
- url: {
9
- type: "string",
10
- description: "The URL to request"
11
- },
12
- method: {
13
- type: "string",
14
- description: "HTTP method (default: GET)"
15
- },
16
- headers: {
17
- type: "object",
18
- description: "Request headers as key-value pairs"
19
- },
20
- body: {
21
- type: "string",
22
- description: "Request body (for POST/PUT/PATCH)"
23
- },
24
- timeout: {
25
- type: "number",
26
- description: "Request timeout in milliseconds (default: 30000)"
27
- }
28
- },
29
- required: ["url"]
30
- },
31
- outputSchema: {
32
- type: "object",
33
- properties: {
34
- status: {
35
- type: "number",
36
- description: "HTTP status code"
37
- },
38
- headers: {
39
- type: "object",
40
- description: "Response headers"
41
- },
42
- body: {
43
- type: "object",
44
- description: "Response body — parsed object when content-type is application/json, raw string otherwise"
45
- }
46
- }
47
- }
48
- };
49
- async function handler(args) {
50
- const { url, method, headers, body: requestBody, timeout } = args;
51
- if (!url || typeof url !== "string") return {
52
- output: null,
53
- error: "http.request: \"url\" is required and must be a string"
54
- };
55
- try {
56
- const controller = new AbortController();
57
- const timeoutMs = timeout ?? 3e4;
58
- const timer = setTimeout(() => controller.abort(), timeoutMs);
59
- const response = await fetch(url, {
60
- method: method ?? "GET",
61
- headers,
62
- body: requestBody,
63
- signal: controller.signal
64
- });
65
- clearTimeout(timer);
66
- const responseText = await response.text();
67
- const responseHeaders = {};
68
- response.headers.forEach((value, key) => {
69
- responseHeaders[key] = value;
70
- });
71
- const contentType = response.headers.get("content-type") ?? "";
72
- let responseBody = responseText;
73
- if (contentType.includes("application/json")) try {
74
- responseBody = JSON.parse(responseText);
75
- } catch {}
76
- return { output: {
77
- status: response.status,
78
- headers: responseHeaders,
79
- body: responseBody
80
- } };
81
- } catch (err) {
82
- return {
83
- output: null,
84
- error: `http.request failed: ${err instanceof Error ? err.message : String(err)}`
85
- };
86
- }
87
- }
88
-
89
- //#endregion
90
- export { descriptor, handler };
91
- //# sourceMappingURL=http-request-k9bp5joL.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"http-request-k9bp5joL.mjs","names":[],"sources":["../src/dev-tools/tools/http-request.ts"],"sourcesContent":["// Tool: http.request — make HTTP requests using Node built-in fetch.\n\nimport type { ToolDescriptor, ToolResult } from '../../types/index.js';\n\nexport interface HttpRequestArgs {\n url: string;\n method?: string;\n headers?: Record<string, string>;\n body?: string;\n timeout?: number;\n}\n\nexport const descriptor: ToolDescriptor = {\n name: 'http.request',\n description: 'Make an HTTP request and return the response status, headers, and body.',\n inputSchema: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The URL to request' },\n method: { type: 'string', description: 'HTTP method (default: GET)' },\n headers: { type: 'object', description: 'Request headers as key-value pairs' },\n body: { type: 'string', description: 'Request body (for POST/PUT/PATCH)' },\n timeout: { type: 'number', description: 'Request timeout in milliseconds (default: 30000)' },\n },\n required: ['url'],\n },\n outputSchema: {\n type: 'object',\n properties: {\n status: { type: 'number', description: 'HTTP status code' },\n headers: { type: 'object', description: 'Response headers' },\n body: {\n type: 'object',\n description:\n 'Response body — parsed object when content-type is application/json, raw string otherwise',\n },\n },\n },\n};\n\nexport async function handler(args: Record<string, unknown>): Promise<ToolResult> {\n const { url, method, headers, body: requestBody, timeout } = args as unknown as HttpRequestArgs;\n\n if (!url || typeof url !== 'string') {\n return { output: null, error: 'http.request: \"url\" is required and must be a string' };\n }\n\n try {\n const controller = new AbortController();\n const timeoutMs = timeout ?? 30000;\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const response = await fetch(url, {\n method: method ?? 'GET',\n headers: headers,\n body: requestBody,\n signal: controller.signal,\n });\n\n clearTimeout(timer);\n\n const responseText = await response.text();\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n // Auto-parse JSON responses so workflows can access fields directly\n const contentType = response.headers.get('content-type') ?? '';\n let responseBody: unknown = responseText;\n if (contentType.includes('application/json')) {\n try {\n responseBody = JSON.parse(responseText);\n } catch {\n // Keep raw text if JSON parsing fails\n }\n }\n\n return {\n output: {\n status: response.status,\n headers: responseHeaders,\n body: responseBody,\n },\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return { output: null, error: `http.request failed: ${message}` };\n }\n}\n"],"mappings":";AAYA,MAAa,aAA6B;CACxC,MAAM;CACN,aAAa;CACb,aAAa;EACX,MAAM;EACN,YAAY;GACV,KAAK;IAAE,MAAM;IAAU,aAAa;IAAsB;GAC1D,QAAQ;IAAE,MAAM;IAAU,aAAa;IAA8B;GACrE,SAAS;IAAE,MAAM;IAAU,aAAa;IAAsC;GAC9E,MAAM;IAAE,MAAM;IAAU,aAAa;IAAqC;GAC1E,SAAS;IAAE,MAAM;IAAU,aAAa;IAAoD;GAC7F;EACD,UAAU,CAAC,MAAM;EAClB;CACD,cAAc;EACZ,MAAM;EACN,YAAY;GACV,QAAQ;IAAE,MAAM;IAAU,aAAa;IAAoB;GAC3D,SAAS;IAAE,MAAM;IAAU,aAAa;IAAoB;GAC5D,MAAM;IACJ,MAAM;IACN,aACE;IACH;GACF;EACF;CACF;AAED,eAAsB,QAAQ,MAAoD;CAChF,MAAM,EAAE,KAAK,QAAQ,SAAS,MAAM,aAAa,YAAY;AAE7D,KAAI,CAAC,OAAO,OAAO,QAAQ,SACzB,QAAO;EAAE,QAAQ;EAAM,OAAO;EAAwD;AAGxF,KAAI;EACF,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,WAAW;EAC7B,MAAM,QAAQ,iBAAiB,WAAW,OAAO,EAAE,UAAU;EAE7D,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ,UAAU;GACT;GACT,MAAM;GACN,QAAQ,WAAW;GACpB,CAAC;AAEF,eAAa,MAAM;EAEnB,MAAM,eAAe,MAAM,SAAS,MAAM;EAC1C,MAAM,kBAA0C,EAAE;AAClD,WAAS,QAAQ,SAAS,OAAO,QAAQ;AACvC,mBAAgB,OAAO;IACvB;EAGF,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe,IAAI;EAC5D,IAAI,eAAwB;AAC5B,MAAI,YAAY,SAAS,mBAAmB,CAC1C,KAAI;AACF,kBAAe,KAAK,MAAM,aAAa;UACjC;AAKV,SAAO,EACL,QAAQ;GACN,QAAQ,SAAS;GACjB,SAAS;GACT,MAAM;GACP,EACF;UACM,KAAK;AAEZ,SAAO;GAAE,QAAQ;GAAM,OAAO,wBADd,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;GACC"}