@youdotcom-oss/api 0.0.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 +551 -0
- package/bin/cli.js +5483 -0
- package/package.json +68 -0
- package/src/cli.ts +98 -0
- package/src/commands/contents.ts +52 -0
- package/src/commands/express.ts +52 -0
- package/src/commands/search.ts +52 -0
- package/src/contents/contents.schemas.ts +46 -0
- package/src/contents/contents.utils.ts +98 -0
- package/src/contents/tests/contents.utils.spec.ts +78 -0
- package/src/express/express.schemas.ts +77 -0
- package/src/express/express.utils.ts +113 -0
- package/src/express/tests/express.utils.spec.ts +83 -0
- package/src/main.ts +22 -0
- package/src/search/search.schemas.ts +110 -0
- package/src/search/search.utils.ts +80 -0
- package/src/search/tests/search.utils.spec.ts +135 -0
- package/src/shared/api.constants.ts +10 -0
- package/src/shared/api.types.ts +1 -0
- package/src/shared/check-response-for-errors.ts +13 -0
- package/src/shared/generate-error-report-link.ts +35 -0
- package/src/shared/tests/check-response-for-errors.spec.ts +31 -0
- package/src/shared/tests/format-search-results-text.spec.ts +85 -0
- package/src/shared/use-get-user-agents.ts +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@youdotcom-oss/api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "You.com API client with bundled CLI for agents supporting Agent Skills",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18",
|
|
8
|
+
"bun": ">= 1.2.21"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/youdotcom-oss/dx-toolkit.git",
|
|
13
|
+
"directory": "packages/api"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/youdotcom-oss/dx-toolkit/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api#readme",
|
|
19
|
+
"author": "You.com (https://you.com)",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"you.com",
|
|
22
|
+
"api",
|
|
23
|
+
"cli",
|
|
24
|
+
"search",
|
|
25
|
+
"ai",
|
|
26
|
+
"agent",
|
|
27
|
+
"agent-skills",
|
|
28
|
+
"bash",
|
|
29
|
+
"web-search",
|
|
30
|
+
"crawling",
|
|
31
|
+
"content-extraction"
|
|
32
|
+
],
|
|
33
|
+
"bin": {
|
|
34
|
+
"ydc": "./bin/cli.js"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"main": "./src/main.ts",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": "./src/main.ts"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"./src/**",
|
|
43
|
+
"!./src/**/tests/*",
|
|
44
|
+
"!./src/**/*.spec.ts",
|
|
45
|
+
"bin/cli.js"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "bun build ./src/cli.ts --outfile ./bin/cli.js --target=node",
|
|
52
|
+
"check": "bun run check:biome && bun run check:types && bun run check:package",
|
|
53
|
+
"check:biome": "biome check",
|
|
54
|
+
"check:package": "format-package --check",
|
|
55
|
+
"check:types": "tsc --noEmit",
|
|
56
|
+
"check:write": "biome check --write && bun run format:package",
|
|
57
|
+
"dev": "bun src/cli.ts",
|
|
58
|
+
"format:package": "format-package --write",
|
|
59
|
+
"prepublishOnly": "bun run build",
|
|
60
|
+
"test": "bun test",
|
|
61
|
+
"test:coverage": "bun test --coverage",
|
|
62
|
+
"test:coverage:watch": "bun test --coverage --watch",
|
|
63
|
+
"test:watch": "bun test --watch"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"zod": "^4.3.5"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ydc - You.com API CLI
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* search <query> - Search the web with You.com
|
|
7
|
+
* express <input> - Get AI answers with web context
|
|
8
|
+
* contents <url> [url...] - Extract content from URLs
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --api-key <key> - You.com API key (overrides YDC_API_KEY)
|
|
12
|
+
* --client <name> - Client name for tracking (overrides YDC_CLIENT)
|
|
13
|
+
* --output <json|text> - Output format (default: json)
|
|
14
|
+
* --help, -h - Show help
|
|
15
|
+
*/
|
|
16
|
+
import { parseArgs } from 'node:util'
|
|
17
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
18
|
+
import { contentsCommand } from './commands/contents.ts'
|
|
19
|
+
import { expressCommand } from './commands/express.ts'
|
|
20
|
+
import { searchCommand } from './commands/search.ts'
|
|
21
|
+
import { generateErrorReportLink } from './shared/generate-error-report-link.ts'
|
|
22
|
+
import { useGetUserAgent } from './shared/use-get-user-agents.ts'
|
|
23
|
+
|
|
24
|
+
// Extract command and args (allows flags anywhere)
|
|
25
|
+
const rawArgs = process.argv.slice(2)
|
|
26
|
+
const command = rawArgs.find((arg) => !arg.startsWith('-')) || ''
|
|
27
|
+
const commandIndex = rawArgs.indexOf(command)
|
|
28
|
+
const args = commandIndex >= 0 ? rawArgs.slice(commandIndex + 1) : []
|
|
29
|
+
|
|
30
|
+
// Check for help
|
|
31
|
+
if (rawArgs.includes('--help') || rawArgs.includes('-h') || !command) {
|
|
32
|
+
console.log(`ydc v${packageJson.version} - You.com API CLI
|
|
33
|
+
|
|
34
|
+
Usage: ydc <command> --json <json> [options]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
search Search the web with You.com
|
|
38
|
+
express Get AI answers with web context
|
|
39
|
+
contents Extract content from URLs
|
|
40
|
+
|
|
41
|
+
Global Options:
|
|
42
|
+
--json <json> JSON string with command parameters (required)
|
|
43
|
+
--api-key <key> You.com API key (overrides YDC_API_KEY)
|
|
44
|
+
--client <name> Client name for tracking and debugging
|
|
45
|
+
--schema Output JSON schema for what can be passed to --json
|
|
46
|
+
--help, -h Show this help
|
|
47
|
+
|
|
48
|
+
Environment Variables:
|
|
49
|
+
YDC_API_KEY You.com API key (required)
|
|
50
|
+
|
|
51
|
+
Output Format:
|
|
52
|
+
Success: API response on stdout (exit 0)
|
|
53
|
+
Error: { success: false, error: {...} } on stderr (exit 1)
|
|
54
|
+
Invalid args: Error message on stderr (exit 2)
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
ydc search "AI developments" --client Openclaw
|
|
58
|
+
ydc search "AI" --client Openclaw | jq '.data.results.web[0].title'
|
|
59
|
+
ydc express "What happened today?" --client MyAgent --tools web_search
|
|
60
|
+
ydc contents https://example.com --formats markdown
|
|
61
|
+
ydc search --schema # Get JSON schema for search --json input
|
|
62
|
+
|
|
63
|
+
More info: https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/api
|
|
64
|
+
`)
|
|
65
|
+
process.exit(command ? 0 : 2)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
switch (command) {
|
|
70
|
+
case 'search':
|
|
71
|
+
await searchCommand(args)
|
|
72
|
+
break
|
|
73
|
+
case 'express':
|
|
74
|
+
await expressCommand(args)
|
|
75
|
+
break
|
|
76
|
+
case 'contents':
|
|
77
|
+
await contentsCommand(args)
|
|
78
|
+
break
|
|
79
|
+
default:
|
|
80
|
+
console.error(`Unknown command: ${command}`)
|
|
81
|
+
console.error(`Run 'ydc --help' for usage`)
|
|
82
|
+
process.exit(2)
|
|
83
|
+
}
|
|
84
|
+
process.exit(0)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error)
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
88
|
+
const { values } = parseArgs({
|
|
89
|
+
args,
|
|
90
|
+
options: {
|
|
91
|
+
client: { type: 'string' },
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
const getUserAgent = useGetUserAgent(values.client || process.env.YDC_CLIENT)
|
|
95
|
+
console.error('\nTo report this error, share this mailto link with the user:')
|
|
96
|
+
console.error(generateErrorReportLink({ errorMessage: message, tool: command, clientInfo: getUserAgent() }))
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import * as z from 'zod'
|
|
3
|
+
import { ContentsQuerySchema } from '../contents/contents.schemas.ts'
|
|
4
|
+
import { fetchContents } from '../contents/contents.utils.ts'
|
|
5
|
+
import { useGetUserAgent } from '../shared/use-get-user-agents.ts'
|
|
6
|
+
|
|
7
|
+
export const contentsCommand = async (args: string[]) => {
|
|
8
|
+
// Handle --schema flag
|
|
9
|
+
if (args.includes('--schema')) {
|
|
10
|
+
console.log(JSON.stringify(z.toJSONSchema(ContentsQuerySchema)))
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Parse flags with Node's built-in parseArgs
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args,
|
|
17
|
+
options: {
|
|
18
|
+
json: { type: 'string' },
|
|
19
|
+
'api-key': { type: 'string' },
|
|
20
|
+
client: { type: 'string' },
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// --json is required
|
|
25
|
+
if (!values.json) {
|
|
26
|
+
throw new Error('--json flag is required')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse JSON and validate with schema
|
|
30
|
+
const query = JSON.parse(values.json)
|
|
31
|
+
const apiKey = values['api-key']
|
|
32
|
+
const client = values.client || process.env.YDC_CLIENT
|
|
33
|
+
|
|
34
|
+
// Get API key from options or environment
|
|
35
|
+
const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
|
|
36
|
+
if (!YDC_API_KEY) {
|
|
37
|
+
throw new Error('YDC_API_KEY environment variable is required')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate with schema (includes urls validation)
|
|
41
|
+
const contentsQuery = ContentsQuerySchema.parse(query)
|
|
42
|
+
|
|
43
|
+
// Fetch contents
|
|
44
|
+
const response = await fetchContents({
|
|
45
|
+
contentsQuery,
|
|
46
|
+
YDC_API_KEY,
|
|
47
|
+
getUserAgent: useGetUserAgent(client),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Output response to stdout (success)
|
|
51
|
+
console.log(JSON.stringify(response))
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import * as z from 'zod'
|
|
3
|
+
import { ExpressAgentInputSchema } from '../express/express.schemas.ts'
|
|
4
|
+
import { callExpressAgent } from '../express/express.utils.ts'
|
|
5
|
+
import { useGetUserAgent } from '../shared/use-get-user-agents.ts'
|
|
6
|
+
|
|
7
|
+
export const expressCommand = async (args: string[]) => {
|
|
8
|
+
// Handle --schema flag
|
|
9
|
+
if (args.includes('--schema')) {
|
|
10
|
+
console.log(JSON.stringify(z.toJSONSchema(ExpressAgentInputSchema)))
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Parse flags with Node's built-in parseArgs
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args,
|
|
17
|
+
options: {
|
|
18
|
+
json: { type: 'string' },
|
|
19
|
+
'api-key': { type: 'string' },
|
|
20
|
+
client: { type: 'string' },
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// --json is required
|
|
25
|
+
if (!values.json) {
|
|
26
|
+
throw new Error('--json flag is required')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse JSON and validate with schema
|
|
30
|
+
const input = JSON.parse(values.json)
|
|
31
|
+
const apiKey = values['api-key']
|
|
32
|
+
const client = values.client || process.env.YDC_CLIENT
|
|
33
|
+
|
|
34
|
+
// Get API key from options or environment
|
|
35
|
+
const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
|
|
36
|
+
if (!YDC_API_KEY) {
|
|
37
|
+
throw new Error('YDC_API_KEY environment variable is required')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate with schema (includes input validation)
|
|
41
|
+
const agentInput = ExpressAgentInputSchema.parse(input)
|
|
42
|
+
|
|
43
|
+
// Call agent
|
|
44
|
+
const response = await callExpressAgent({
|
|
45
|
+
agentInput,
|
|
46
|
+
YDC_API_KEY,
|
|
47
|
+
getUserAgent: useGetUserAgent(client),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Output response to stdout (success)
|
|
51
|
+
console.log(JSON.stringify(response))
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import * as z from 'zod'
|
|
3
|
+
import { SearchQuerySchema } from '../search/search.schemas.ts'
|
|
4
|
+
import { fetchSearchResults } from '../search/search.utils.ts'
|
|
5
|
+
import { useGetUserAgent } from '../shared/use-get-user-agents.ts'
|
|
6
|
+
|
|
7
|
+
export const searchCommand = async (args: string[]) => {
|
|
8
|
+
// Handle --schema flag
|
|
9
|
+
if (args.includes('--schema')) {
|
|
10
|
+
console.log(JSON.stringify(z.toJSONSchema(SearchQuerySchema)))
|
|
11
|
+
process.exit(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Parse flags with Node's built-in parseArgs
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args,
|
|
17
|
+
options: {
|
|
18
|
+
json: { type: 'string' },
|
|
19
|
+
'api-key': { type: 'string' },
|
|
20
|
+
client: { type: 'string' },
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// --json is required
|
|
25
|
+
if (!values.json) {
|
|
26
|
+
throw new Error('--json flag is required')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse JSON and validate with schema
|
|
30
|
+
const query = JSON.parse(values.json)
|
|
31
|
+
const apiKey = values['api-key']
|
|
32
|
+
const client = values.client || process.env.YDC_CLIENT
|
|
33
|
+
|
|
34
|
+
// Get API key from options or environment
|
|
35
|
+
const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
|
|
36
|
+
if (!YDC_API_KEY) {
|
|
37
|
+
throw new Error('YDC_API_KEY environment variable is required')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate with schema (includes query validation)
|
|
41
|
+
const searchQuery = SearchQuerySchema.parse(query)
|
|
42
|
+
|
|
43
|
+
// Fetch results
|
|
44
|
+
const response = await fetchSearchResults({
|
|
45
|
+
searchQuery,
|
|
46
|
+
YDC_API_KEY,
|
|
47
|
+
getUserAgent: useGetUserAgent(client),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Output response to stdout (success)
|
|
51
|
+
console.log(JSON.stringify(response))
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as z from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input schema for the you-contents tool
|
|
5
|
+
* Accepts an array of URLs, optional formats array (or legacy format string), and optional crawl timeout
|
|
6
|
+
*/
|
|
7
|
+
export const ContentsQuerySchema = z.object({
|
|
8
|
+
urls: z
|
|
9
|
+
.array(z.string().url())
|
|
10
|
+
.min(1)
|
|
11
|
+
.describe('Array of webpage URLs to extract content from (e.g., ["https://example.com"])'),
|
|
12
|
+
formats: z
|
|
13
|
+
.array(z.enum(['markdown', 'html', 'metadata']))
|
|
14
|
+
.optional()
|
|
15
|
+
.describe('Output formats: array of "markdown" (text), "html" (layout), or "metadata" (structured data)'),
|
|
16
|
+
format: z.enum(['markdown', 'html']).optional().describe('(Deprecated) Output format - use formats array instead'),
|
|
17
|
+
crawl_timeout: z.number().min(1).max(60).optional().describe('Optional timeout in seconds (1-60) for page crawling'),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export type ContentsQuery = z.infer<typeof ContentsQuerySchema>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Schema for a single content item in the API response
|
|
24
|
+
*/
|
|
25
|
+
const ContentsItemSchema = z.object({
|
|
26
|
+
url: z.string().describe('URL'),
|
|
27
|
+
title: z.string().optional().describe('Title'),
|
|
28
|
+
html: z.string().optional().describe('HTML content'),
|
|
29
|
+
markdown: z.string().optional().describe('Markdown content'),
|
|
30
|
+
metadata: z
|
|
31
|
+
.object({
|
|
32
|
+
jsonld: z.array(z.record(z.string(), z.unknown())).optional().describe('JSON-LD structured data (Schema.org)'),
|
|
33
|
+
opengraph: z.record(z.string(), z.string()).optional().describe('OpenGraph meta tags'),
|
|
34
|
+
twitter: z.record(z.string(), z.string()).optional().describe('Twitter Card metadata'),
|
|
35
|
+
})
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Structured metadata when available'),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* API response schema from You.com Contents API
|
|
42
|
+
* Validates the full response array
|
|
43
|
+
*/
|
|
44
|
+
export const ContentsApiResponseSchema = z.array(ContentsItemSchema)
|
|
45
|
+
|
|
46
|
+
export type ContentsApiResponse = z.infer<typeof ContentsApiResponseSchema>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { CONTENTS_API_URL } from '../shared/api.constants.ts'
|
|
2
|
+
import type { GetUserAgent } from '../shared/api.types.ts'
|
|
3
|
+
import { checkResponseForErrors } from '../shared/check-response-for-errors.ts'
|
|
4
|
+
import { type ContentsApiResponse, ContentsApiResponseSchema, type ContentsQuery } from './contents.schemas.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetch content from You.com Contents API
|
|
8
|
+
* The API accepts multiple URLs in a single request and returns all results
|
|
9
|
+
* @param contentsQuery - Query parameters including URLs and format
|
|
10
|
+
* @param YDC_API_KEY - You.com API key
|
|
11
|
+
* @param getUserAgent - Function to get User-Agent string
|
|
12
|
+
* @returns Parsed and validated API response
|
|
13
|
+
*/
|
|
14
|
+
export const fetchContents = async ({
|
|
15
|
+
contentsQuery: { urls, formats, format, crawl_timeout },
|
|
16
|
+
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
17
|
+
getUserAgent,
|
|
18
|
+
}: {
|
|
19
|
+
contentsQuery: ContentsQuery
|
|
20
|
+
YDC_API_KEY?: string
|
|
21
|
+
getUserAgent: GetUserAgent
|
|
22
|
+
}): Promise<ContentsApiResponse> => {
|
|
23
|
+
if (!YDC_API_KEY) {
|
|
24
|
+
throw new Error('YDC_API_KEY is required for Contents API')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle backward compatibility: prefer formats array, fallback to format string, default to ['markdown']
|
|
28
|
+
const requestFormats = formats || (format ? [format] : ['markdown'])
|
|
29
|
+
|
|
30
|
+
// Build request body
|
|
31
|
+
const requestBody: {
|
|
32
|
+
urls: string[]
|
|
33
|
+
formats: string[]
|
|
34
|
+
crawl_timeout?: number
|
|
35
|
+
} = {
|
|
36
|
+
urls,
|
|
37
|
+
formats: requestFormats,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (crawl_timeout !== undefined) {
|
|
41
|
+
requestBody.crawl_timeout = crawl_timeout
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Make single API call with all URLs
|
|
45
|
+
const options = {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: new Headers({
|
|
48
|
+
'X-API-Key': YDC_API_KEY,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'User-Agent': getUserAgent(),
|
|
51
|
+
}),
|
|
52
|
+
body: JSON.stringify(requestBody),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await fetch(CONTENTS_API_URL, options)
|
|
56
|
+
|
|
57
|
+
// Handle HTTP errors
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const errorCode = response.status
|
|
60
|
+
|
|
61
|
+
// Try to parse error response body
|
|
62
|
+
let errorDetail = `Failed to fetch contents. HTTP ${errorCode}`
|
|
63
|
+
try {
|
|
64
|
+
const errorBody = await response.json()
|
|
65
|
+
if (errorBody && typeof errorBody === 'object' && 'detail' in errorBody) {
|
|
66
|
+
errorDetail = String(errorBody.detail)
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// If parsing fails, use default error message
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle specific error codes
|
|
73
|
+
if (errorCode === 401) {
|
|
74
|
+
throw new Error(`Authentication failed: ${errorDetail}. Please check your You.com API key.`)
|
|
75
|
+
}
|
|
76
|
+
if (errorCode === 403) {
|
|
77
|
+
throw new Error(`Forbidden: ${errorDetail}. Your API key may not have access to the Contents API.`)
|
|
78
|
+
}
|
|
79
|
+
if (errorCode === 429) {
|
|
80
|
+
throw new Error('Rate limited by You.com API. Please try again later.')
|
|
81
|
+
}
|
|
82
|
+
if (errorCode >= 500) {
|
|
83
|
+
throw new Error(`You.com API server error: ${errorDetail}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(errorDetail)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const results = await response.json()
|
|
90
|
+
|
|
91
|
+
// Check for error field in 200 responses
|
|
92
|
+
checkResponseForErrors(results)
|
|
93
|
+
|
|
94
|
+
// Validate schema
|
|
95
|
+
const parsedResults = ContentsApiResponseSchema.parse(results)
|
|
96
|
+
|
|
97
|
+
return parsedResults
|
|
98
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { fetchContents } from '../contents.utils.ts'
|
|
3
|
+
|
|
4
|
+
const getUserAgent = () => 'API/test (You.com;TEST)'
|
|
5
|
+
|
|
6
|
+
// NOTE: The following tests require a You.com API key with access to the Contents API
|
|
7
|
+
// Using example.com/example.org as test URLs since You.com blocks self-scraping
|
|
8
|
+
describe('fetchContents', () => {
|
|
9
|
+
test(
|
|
10
|
+
'returns valid response structure for single URL',
|
|
11
|
+
async () => {
|
|
12
|
+
const result = await fetchContents({
|
|
13
|
+
contentsQuery: {
|
|
14
|
+
urls: ['https://documentation.you.com/developer-resources/mcp-server'],
|
|
15
|
+
format: 'markdown',
|
|
16
|
+
},
|
|
17
|
+
getUserAgent,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
expect(Array.isArray(result)).toBe(true)
|
|
21
|
+
expect(result.length).toBeGreaterThan(0)
|
|
22
|
+
|
|
23
|
+
const firstItem = result[0]
|
|
24
|
+
expect(firstItem).toBeDefined()
|
|
25
|
+
|
|
26
|
+
// Should have markdown content
|
|
27
|
+
expect(firstItem?.markdown).toBeDefined()
|
|
28
|
+
expect(typeof firstItem?.markdown).toBe('string')
|
|
29
|
+
},
|
|
30
|
+
{ retry: 2 },
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
test(
|
|
34
|
+
'handles multiple URLs',
|
|
35
|
+
async () => {
|
|
36
|
+
const result = await fetchContents({
|
|
37
|
+
contentsQuery: {
|
|
38
|
+
urls: [
|
|
39
|
+
'https://documentation.you.com/developer-resources/mcp-server',
|
|
40
|
+
'https://documentation.you.com/developer-resources/python-sdk',
|
|
41
|
+
],
|
|
42
|
+
format: 'markdown',
|
|
43
|
+
},
|
|
44
|
+
getUserAgent,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(Array.isArray(result)).toBe(true)
|
|
48
|
+
expect(result.length).toBe(2)
|
|
49
|
+
|
|
50
|
+
for (const item of result) {
|
|
51
|
+
expect(item).toHaveProperty('url')
|
|
52
|
+
expect(item.markdown).toBeDefined()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{ retry: 2 },
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
test(
|
|
59
|
+
'handles html format',
|
|
60
|
+
async () => {
|
|
61
|
+
const result = await fetchContents({
|
|
62
|
+
contentsQuery: {
|
|
63
|
+
urls: ['https://documentation.you.com/developer-resources/mcp-server'],
|
|
64
|
+
format: 'html',
|
|
65
|
+
},
|
|
66
|
+
getUserAgent,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(Array.isArray(result)).toBe(true)
|
|
70
|
+
const firstItem = result[0]
|
|
71
|
+
expect(firstItem).toBeDefined()
|
|
72
|
+
|
|
73
|
+
expect(firstItem?.html).toBeDefined()
|
|
74
|
+
expect(typeof firstItem?.html).toBe('string')
|
|
75
|
+
},
|
|
76
|
+
{ retry: 2 },
|
|
77
|
+
)
|
|
78
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as z from 'zod'
|
|
2
|
+
|
|
3
|
+
export const ExpressAgentInputSchema = z.object({
|
|
4
|
+
input: z.string().min(1, 'Input is required').describe('Query or prompt'),
|
|
5
|
+
tools: z
|
|
6
|
+
.array(
|
|
7
|
+
z.object({
|
|
8
|
+
type: z.enum(['web_search']).describe('Tool type'),
|
|
9
|
+
}),
|
|
10
|
+
)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Tools (web search only)'),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export type ExpressAgentInput = z.infer<typeof ExpressAgentInputSchema>
|
|
16
|
+
|
|
17
|
+
// API Response Schema - Validates the full response from You.com API
|
|
18
|
+
|
|
19
|
+
// Search result content item from web_search.results
|
|
20
|
+
// Note: thumbnail_url, source_type, and provider are API-only pass-through fields not used in MCP output
|
|
21
|
+
const ApiSearchResultItemSchema = z.object({
|
|
22
|
+
source_type: z.string().optional(),
|
|
23
|
+
citation_uri: z.string().optional(), // Used as fallback for url in transformation
|
|
24
|
+
url: z.string(),
|
|
25
|
+
title: z.string(),
|
|
26
|
+
snippet: z.string(),
|
|
27
|
+
thumbnail_url: z.string().optional(), // API-only, not transformed to MCP output
|
|
28
|
+
provider: z.any().optional(), // API-only, not transformed to MCP output
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Union of possible output item types from API
|
|
32
|
+
const ExpressAgentApiOutputItemSchema = z.union([
|
|
33
|
+
// web_search.results type - has content array, no text
|
|
34
|
+
z.object({
|
|
35
|
+
type: z.literal('web_search.results'),
|
|
36
|
+
content: z.array(ApiSearchResultItemSchema),
|
|
37
|
+
}),
|
|
38
|
+
// message.answer type - has text, no content
|
|
39
|
+
z.object({
|
|
40
|
+
type: z.literal('message.answer'),
|
|
41
|
+
text: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
export const ExpressAgentApiResponseSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
output: z.array(ExpressAgentApiOutputItemSchema),
|
|
48
|
+
agent: z.string().optional().describe('Agent identifier'),
|
|
49
|
+
mode: z.string().optional().describe('Agent mode'),
|
|
50
|
+
input: z.array(z.any()).optional().describe('Input messages'),
|
|
51
|
+
})
|
|
52
|
+
.passthrough()
|
|
53
|
+
|
|
54
|
+
export type ExpressAgentApiResponse = z.infer<typeof ExpressAgentApiResponseSchema>
|
|
55
|
+
|
|
56
|
+
// MCP Output Schema - Defines what we return to the MCP client (answer + optional search results, token efficient)
|
|
57
|
+
|
|
58
|
+
// Search result item for MCP output
|
|
59
|
+
const McpSearchResultItemSchema = z.object({
|
|
60
|
+
url: z.string().describe('URL'),
|
|
61
|
+
title: z.string().describe('Title'),
|
|
62
|
+
snippet: z.string().describe('Snippet'),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// MCP response structure: answer (always) + results (optional when web_search used)
|
|
66
|
+
const ExpressAgentMcpResponseSchema = z.object({
|
|
67
|
+
answer: z.string().describe('AI answer'),
|
|
68
|
+
results: z
|
|
69
|
+
.object({
|
|
70
|
+
web: z.array(McpSearchResultItemSchema).describe('Web results'),
|
|
71
|
+
})
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Search results'),
|
|
74
|
+
agent: z.string().optional().describe('Agent ID'),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export type ExpressAgentMcpResponse = z.infer<typeof ExpressAgentMcpResponseSchema>
|